diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 47db052bd8..c31551c123 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,6 +1,5 @@ blank_issues_enabled: false contact_links: - name: Support - url: https://stackoverflow.com/questions/tagged/mitmproxy - about: Please do not use GitHub for support requests. - If you have questions on how to use mitmproxy, please ask them on StackOverflow! + url: https://github.com/mitmproxy/mitmproxy/discussions + about: If you have questions on how to use mitmproxy, ask them on the discussions page! diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 701632e74d..cbe19b3654 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.10' - run: pip install tox - run: tox -e flake8 filename-matching: @@ -35,7 +35,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.10' - run: pip install tox - run: tox -e filename_matching mypy: @@ -46,7 +46,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.10' - run: pip install tox - run: tox -e mypy individual-coverage: @@ -58,7 +58,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.9' # there's a weird bug on 3.10 where some lines are not counted as covered. - run: pip install tox - run: tox -e individual_coverage test: @@ -67,15 +67,13 @@ jobs: matrix: include: - os: ubuntu-latest - py: '3.10.0-rc - 3.10' + py: "3.10" - os: windows-latest - py: 3.9 + py: "3.10" - os: macos-latest - py: 3.9 + py: "3.10" - os: ubuntu-latest py: 3.9 - - os: ubuntu-latest - py: 3.8 runs-on: ${{ matrix.os }} steps: - run: printenv @@ -88,6 +86,14 @@ jobs: python-version: ${{ matrix.py }} - run: pip install tox - run: tox -e py + if: matrix.os != 'ubuntu-latest' + - name: Run tox -e py (without internet) + run: | + # install dependencies (requires internet connectivity) + tox -e py --notest + # run tests with loopback only. We need to sudo for unshare, which means we need an absolute path for tox. + sudo unshare --net -- sh -c "ip link set lo up; $(which tox) -e py" + if: matrix.os == 'ubuntu-latest' - uses: codecov/codecov-action@a1ed4b322b4b38cb846afb5a0ebfa17086917d27 # mirrored below and at https://github.com/mitmproxy/mitmproxy/settings/actions with: @@ -118,7 +124,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.10' - if: matrix.platform == 'windows' uses: actions/cache@v2 with: @@ -169,10 +175,10 @@ jobs: persist-credentials: false - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.10' - run: | - wget -q https://github.com/gohugoio/hugo/releases/download/v0.88.1/hugo_extended_0.88.1_Linux-64bit.deb - echo "865ab9a930e0a9e4957e7dbfdf91c32847324f022e33271b5661d5717600bc2b hugo_extended_0.88.1_Linux-64bit.deb" | sha256sum -c + wget -q https://github.com/gohugoio/hugo/releases/download/v0.92.1/hugo_extended_0.92.1_Linux-64bit.deb + echo "a9440adfd3ecce40089def287dee4e42ffae252ba08c77d1ac575b880a079ce6 hugo_extended_0.92.1_Linux-64bit.deb" | sha256sum -c sudo dpkg -i hugo*.deb - run: pip install -e .[dev] - run: ./docs/build.py @@ -205,7 +211,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.10' - uses: actions/download-artifact@v2 with: name: binaries.linux @@ -239,7 +245,7 @@ jobs: persist-credentials: false - uses: actions/setup-python@v2 with: - python-version: '3.9' + python-version: '3.10' - run: sudo apt-get update - run: sudo apt-get install -y twine awscli - uses: actions/download-artifact@v2 diff --git a/.gitignore b/.gitignore index 0d5943d5ad..786f221855 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ mitmproxy/contrib/kaitaistruct/*.ksy .pytest_cache __pycache__ .hypothesis/ +.hugo_build.lock # UI diff --git a/CHANGELOG.md b/CHANGELOG.md index f7211e99b3..2e575e51d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,87 @@ ## Unreleased: mitmproxy next +## 28 June 2022: mitmproxy 8.1.1 + +* Support specifying the local address for outgoing connections + ([#5364](https://github.com/mitmproxy/mitmproxy/discussions/5364), @meitinger) +* Fix a bug where an excess empty chunk has been sent for chunked HEAD request. + ([#5372](https://github.com/mitmproxy/mitmproxy/discussions/5372), @jixunmoe) +* Drop pkg_resources dependency. + ([#5401](https://github.com/mitmproxy/mitmproxy/issues/5401), @PavelICS) +* Fix huge (>65kb) http2 responses corrupted. + ([#5428](https://github.com/mitmproxy/mitmproxy/issues/5428), @dhabensky) +* Remove overambitious assertions in the HTTP state machine, + fix some error handling. + ([#5383](https://github.com/mitmproxy/mitmproxy/issues/5383), @mhils) + +## 15 May 2022: mitmproxy 8.1.0 + +* DNS support + ([#5232](https://github.com/mitmproxy/mitmproxy/pull/5232), @meitinger) +* Mitmproxy now requires Python 3.9 or above. + ([#5233](https://github.com/mitmproxy/mitmproxy/issues/5233), @mhils) +* Fix a memory leak in mitmdump where flows were kept in memory. + ([#4786](https://github.com/mitmproxy/mitmproxy/issues/4786), @mhils) +* Replayed flows retain their current position in the flow list. + ([#5227](https://github.com/mitmproxy/mitmproxy/issues/5227), @mhils) +* Periodically send HTTP/2 ping frames to keep connections alive. + ([#5046](https://github.com/mitmproxy/mitmproxy/issues/5046), @EndUser509) +* Console Performance Improvements + ([#3427](https://github.com/mitmproxy/mitmproxy/issues/3427), @BkPHcgQL3V) +* Warn users if server side event responses are received without streaming. + ([#4469](https://github.com/mitmproxy/mitmproxy/issues/4469), @mhils) +* Add flatpak support to the browser addon + ([#5200](https://github.com/mitmproxy/mitmproxy/issues/5200), @pauloromeira) +* Add example addon to dump contents to files based on a filter expression + ([#5190](https://github.com/mitmproxy/mitmproxy/issues/5190), @redraw) +* Fix a bug where the wrong SNI is sent to an upstream HTTPS proxy + ([#5109](https://github.com/mitmproxy/mitmproxy/issues/5109), @mhils) +* Make sure that mitmproxy displays error messages on startup. + ([#5225](https://github.com/mitmproxy/mitmproxy/issues/5225), @mhils) +* Add example addon for domain fronting. + ([#5217](https://github.com/mitmproxy/mitmproxy/issues/5217), @randomstuff) +* Improve cut addon to better handle binary contents + ([#3965](https://github.com/mitmproxy/mitmproxy/issues/3965), @mhils) +* Fix text truncation for full-width characters + ([#4278](https://github.com/mitmproxy/mitmproxy/issues/4278), @kjy00302) +* Fix mitmweb export copy failed in non-secure domain. + ([#5264](https://github.com/mitmproxy/mitmproxy/issues/5264), @Pactortester) +* Add example script for manipulating cookies. + ([#5278](https://github.com/mitmproxy/mitmproxy/issues/5278), @WillahScott) +* When opening an external viewer for message contents, mailcap files are not considered anymore. + This preempts the upcoming deprecation of Python's `mailcap` module. + ([#5297](https://github.com/mitmproxy/mitmproxy/issues/5297), @KORraNpl) +* Fix hostname encoding for IDNA domains in upstream mode. + ([#5316](https://github.com/mitmproxy/mitmproxy/issues/5316), @nneonneo) +* Fix hot reloading of contentviews. + ([#5319](https://github.com/mitmproxy/mitmproxy/issues/5319), @nneonneo) +* Ignore HTTP/2 information responses instead of raising an error. + ([#5332](https://github.com/mitmproxy/mitmproxy/issues/5332), @mhils) +* Improve performance and memory usage by reusing OpenSSL contexts. + ([#5339](https://github.com/mitmproxy/mitmproxy/issues/5339), @mhils) +* Fix handling of multiple Cookie headers when proxying HTTP/2 to HTTP/1 + ([#5337](https://github.com/mitmproxy/mitmproxy/issues/5337), @rinsuki) + +## 19 March 2022: mitmproxy 8.0.0 + +### Major Changes + +* Major improvements to the web interface (@gorogoroumaru) +* Event hooks can now be async (@nneonneo, [#5106](https://github.com/mitmproxy/mitmproxy/issues/5106)) +* New [`tls_{established,failed}_{client,server}` event hooks](https://docs.mitmproxy.org/dev/api/events.html#TLSEvents) + to record negotiation success/failure (@mhils, [#4790](https://github.com/mitmproxy/mitmproxy/pull/4790)) + +### Security Fixes + +* [CVE-2022-24766](https://github.com/mitmproxy/mitmproxy/security/advisories/GHSA-gcx2-gvj7-pxv3): + Fix request smuggling vulnerability reported by @zeyu2001 (@mhils) + +### Full Changelog + * Support proxy authentication for SOCKS v5 mode (@starplanet) -* fix some responses not being decoded properly if the encoding was uppercase #4735 (@Mattwmaster58) +* Make it possible to ignore connections in the tls_clienthello event hook (@mhils) +* fix some responses not being decoded properly if the encoding was uppercase (#4735, @Mattwmaster58) * Trigger event hooks for flows with semantically invalid requests, for example invalid content-length headers (@mhils) * Improve error message on TLS version mismatch (@mhils) * Windows: Switch to Python's default asyncio event loop, which increases the number of sockets @@ -14,6 +93,28 @@ * Fix a crash caused when editing string option (#4852, @rbdixon) * Base container image bumped to Debian 11 Bullseye (@Kriechi) * Upstream replays don't do CONNECT on plaintext HTTP requests (#4876, @HoffmannP) +* Remove workarounds for old pyOpenSSL versions (#4831, @KarlParkinson) +* Add fonts to asset filter (~a) (#4928, @elespike) +* Fix bug that crashed when using `view.flows.resolve` (#4916, @rbdixon) +* Fix a bug where `running()` is invoked twice on startup (#3584, @mhils) +* Correct documentation example for User-Agent header modification (#4997, @jamesyale) +* Fix random connection stalls (#5040, @EndUser509) +* Add `n` new flow keybind to mitmweb (#5061, @ianklatzco) +* Fix compatibility with BoringSSL (@pmoulton) +* Added `WebSocketMessage.injected` flag (@Prinzhorn) +* Add example addon for saving streamed data to individual files (@EndUser509) +* Change connection event hooks to be blocking. + Processing will only resume once the event hook has finished. (@Prinzhorn) +* Reintroduce `Flow.live`, which signals if a flow belongs to a currently active connection. (#4207, @mhils) +* Speculative fix for some rare HTTP/2 connection stalls (#5158, @EndUser509) +* Add ability to specify custom ports with LDAP authentication (#5068, @demonoidvk) +* Add support for rotating saved streams every hour or day (@EndUser509) +* Console Improvements on Windows (@mhils) +* Fix processing of `--set` options (#5067, @marwinxxii) +* Lowercase user-added header names and emit a log message to notify the user when using HTTP/2 (#4746, @mhils) +* Exit early if there are errors on startup (#4544, @mhils) +* Fixed encoding guessing: only search for meta tags in HTML bodies (##4566, @Prinzhorn) +* Binaries are now built with Python 3.10 (@mhils) ## 28 September 2021: mitmproxy 7.0.4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df060f95d8..9f0496090a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ forward, please consider contributing in the following areas: - **Maintenance:** We are *incredibly* thankful for individuals who are stepping up and helping with maintenance. This includes (but is not limited to) triaging issues, reviewing pull requests and picking up stale ones, helping out other - users on [StackOverflow](https://stackoverflow.com/questions/tagged/mitmproxy), creating minimal, complete and + users on [GitHub Discussions](https://github.com/mitmproxy/mitmproxy/discussions), creating minimal, complete and verifiable examples or test cases for existing bug reports, updating documentation, or fixing minor bugs that have recently been reported. - **Code Contributions:** We actively mark issues that we consider are [good first contributions]( @@ -14,7 +14,7 @@ forward, please consider contributing in the following areas: ## Development Setup -To get started hacking on mitmproxy, please install a recent version of Python (we require at least Python 3.8). +To get started hacking on mitmproxy, please install a recent version of Python (we require at least Python 3.9). Then, do the following: ##### Linux / macOS diff --git a/README.md b/README.md index 2ffcb4be78..a92e0b0b76 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,9 @@ The documentation for mitmproxy is available on our website: [![mitmproxy documentation dev](https://shields.mitmproxy.org/badge/docs-dev-brightgreen.svg)](https://docs.mitmproxy.org/main/) If you have questions on how to use mitmproxy, please -ask them on StackOverflow! +use GitHub Discussions! -[![StackOverflow: mitmproxy](https://shields.mitmproxy.org/stackexchange/stackoverflow/t/mitmproxy?color=orange&label=stackoverflow%20questions)](https://stackoverflow.com/questions/tagged/mitmproxy) +[![mitmproxy discussions](https://shields.mitmproxy.org/badge/help-github%20discussions-orange.svg)](https://github.com/mitmproxy/mitmproxy/discussions) ## Contributing diff --git a/docs/build.py b/docs/build.py index a352f17aa2..f393901e93 100755 --- a/docs/build.py +++ b/docs/build.py @@ -10,7 +10,9 @@ print(f"Generating output for {script.name}...") out = subprocess.check_output(["python3", script.absolute()], cwd=here, text=True) if out: - (here / "src" / "generated" / f"{script.stem}.html").write_text(out, encoding="utf8") + (here / "src" / "generated" / f"{script.stem}.html").write_text( + out, encoding="utf8" + ) if (here / "public").exists(): shutil.rmtree(here / "public") diff --git a/docs/scripts/api-events.py b/docs/scripts/api-events.py index 80d91dae9b..0c23fbd86c 100644 --- a/docs/scripts/api-events.py +++ b/docs/scripts/api-events.py @@ -3,20 +3,17 @@ import inspect import textwrap from pathlib import Path -from typing import List, Type -import mitmproxy.addons.next_layer # noqa from mitmproxy import hooks, log, addonmanager from mitmproxy.proxy import server_hooks, layer -from mitmproxy.proxy.layers import http, modes, tcp, tls, websocket +from mitmproxy.proxy.layers import dns, http, modes, tcp, tls, websocket known = set() -def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: +def category(name: str, desc: str, hooks: list[type[hooks.Hook]]) -> None: all_params = [ - list(inspect.signature(hook.__init__).parameters.values())[1:] - for hook in hooks + list(inspect.signature(hook.__init__).parameters.values())[1:] for hook in hooks ] # slightly overengineered, but this was fun to write. ¯\_(ツ)_/¯ @@ -28,7 +25,9 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: mod = inspect.getmodule(param.annotation).__name__ if mod == "typing": # this is ugly, but can be removed once we are on Python 3.9+ only - imports.add(inspect.getmodule(param.annotation.__args__[0]).__name__) + imports.add( + inspect.getmodule(param.annotation.__args__[0]).__name__ + ) types.add(param.annotation._name) else: imports.add(mod) @@ -58,7 +57,9 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: print(f" def {hook.name}({', '.join(str(p) for p in ['self'] + params)}):") print(textwrap.indent(f'"""\n{doc}\n"""', " ")) if params: - print(f' ctx.log(f"{hook.name}: {" ".join("{" + p.name + "=}" for p in params)}")') + print( + f' ctx.log(f"{hook.name}: {" ".join("{" + p.name + "=}" for p in params)}")' + ) else: print(f' ctx.log("{hook.name}")') print("") @@ -77,7 +78,7 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: hooks.RunningHook, hooks.ConfigureHook, hooks.DoneHook, - ] + ], ) category( @@ -89,7 +90,7 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: server_hooks.ServerConnectHook, server_hooks.ServerConnectedHook, server_hooks.ServerDisconnectedHook, - ] + ], ) category( @@ -103,7 +104,17 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: http.HttpErrorHook, http.HttpConnectHook, http.HttpConnectUpstreamHook, - ] + ], + ) + + category( + "DNS", + "", + [ + dns.DnsRequestHook, + dns.DnsResponseHook, + dns.DnsErrorHook, + ], ) category( @@ -114,7 +125,7 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: tcp.TcpMessageHook, tcp.TcpEndHook, tcp.TcpErrorHook, - ] + ], ) category( @@ -124,7 +135,11 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: tls.TlsClienthelloHook, tls.TlsStartClientHook, tls.TlsStartServerHook, - ] + tls.TlsEstablishedClientHook, + tls.TlsEstablishedServerHook, + tls.TlsFailedClientHook, + tls.TlsFailedServerHook, + ], ) category( @@ -134,7 +149,7 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: websocket.WebsocketStartHook, websocket.WebsocketMessageHook, websocket.WebsocketEndHook, - ] + ], ) category( @@ -142,7 +157,7 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: "", [ modes.Socks5AuthHook, - ] + ], ) category( @@ -152,7 +167,7 @@ def category(name: str, desc: str, hooks: List[Type[hooks.Hook]]) -> None: layer.NextLayerHook, hooks.UpdateHook, log.AddLogHook, - ] + ], ) not_documented = set(hooks.all_hooks.keys()) - known diff --git a/docs/scripts/api-render.py b/docs/scripts/api-render.py index f7c58d091a..10299e7c3c 100644 --- a/docs/scripts/api-render.py +++ b/docs/scripts/api-render.py @@ -20,26 +20,26 @@ edit_url_map=edit_url_map, ) # We can't configure Hugo, but we can configure pdoc. -pdoc.render_helpers.formatter.cssclass = "chroma" +pdoc.render_helpers.formatter.cssclass = "chroma pdoc-code" modules = [ "mitmproxy.addonmanager", "mitmproxy.certs", "mitmproxy.connection", "mitmproxy.coretypes.multidict", + "mitmproxy.dns", "mitmproxy.flow", "mitmproxy.http", "mitmproxy.net.server_spec", + "mitmproxy.proxy.context", "mitmproxy.proxy.server_hooks", "mitmproxy.tcp", + "mitmproxy.tls", "mitmproxy.websocket", here / ".." / "src" / "generated" / "events.py", ] -pdoc.pdoc( - *modules, - output_directory=here / ".." / "src" / "generated" / "api" -) +pdoc.pdoc(*modules, output_directory=here / ".." / "src" / "generated" / "api") api_content = here / ".." / "src" / "content" / "api" if api_content.exists(): @@ -51,7 +51,9 @@ if isinstance(module, Path): continue filename = f"api/{module.replace('.', '/')}.html" - (api_content / f"{module}.md").write_text(textwrap.dedent(f""" + (api_content / f"{module}.md").write_text( + textwrap.dedent( + f""" --- title: "{module}" url: "{filename}" @@ -62,6 +64,8 @@ --- {{{{< readfile file="/generated/{filename}" >}}}} - """)) + """ + ) + ) (here / ".." / "src" / "content" / "addons-api.md").touch() diff --git a/docs/scripts/clirecording/clidirector.py b/docs/scripts/clirecording/clidirector.py index d0eb5cc904..db286b2b2a 100644 --- a/docs/scripts/clirecording/clidirector.py +++ b/docs/scripts/clirecording/clidirector.py @@ -1,13 +1,14 @@ import json +from typing import NamedTuple, Optional + import libtmux import random import subprocess import threading import time -import typing -class InstructionSpec(typing.NamedTuple): +class InstructionSpec(NamedTuple): instruction: str time_from: float time_to: float @@ -17,7 +18,7 @@ class CliDirector: def __init__(self): self.record_start = None self.pause_between_keys = 0.2 - self.instructions: typing.List[InstructionSpec] = [] + self.instructions: list[InstructionSpec] = [] def start(self, filename: str, width: int = 0, height: int = 0) -> libtmux.Session: self.start_session(width, height) @@ -26,7 +27,9 @@ def start(self, filename: str, width: int = 0, height: int = 0) -> libtmux.Sessi def start_session(self, width: int = 0, height: int = 0) -> libtmux.Session: self.tmux_server = libtmux.Server() - self.tmux_session = self.tmux_server.new_session(session_name="asciinema_recorder", kill_session=True) + self.tmux_session = self.tmux_server.new_session( + session_name="asciinema_recorder", kill_session=True + ) self.tmux_pane = self.tmux_session.attached_window.attached_pane self.tmux_version = self.tmux_pane.display_message("#{version}", True) if width and height: @@ -35,13 +38,26 @@ def start_session(self, width: int = 0, height: int = 0) -> libtmux.Session: return self.tmux_session def start_recording(self, filename: str) -> None: - self.asciinema_proc = subprocess.Popen([ - "asciinema", "rec", "-y", "--overwrite", "-c", "tmux attach -t asciinema_recorder", filename]) + self.asciinema_proc = subprocess.Popen( + [ + "asciinema", + "rec", + "-y", + "--overwrite", + "-c", + "tmux attach -t asciinema_recorder", + filename, + ] + ) self.pause(1.5) self.record_start = time.time() def resize_window(self, width: int, height: int) -> None: - subprocess.Popen(["resize", "-s", str(height), str(width)], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.Popen( + ["resize", "-s", str(height), str(width)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) def end(self) -> None: self.end_recording() @@ -56,7 +72,9 @@ def end_recording(self) -> None: def end_session(self) -> None: self.tmux_session.kill_session() - def press_key(self, keys: str, count=1, pause: typing.Optional[float] = None, target = None) -> None: + def press_key( + self, keys: str, count=1, pause: Optional[float] = None, target=None + ) -> None: if pause is None: pause = self.pause_between_keys if target is None: @@ -78,7 +96,7 @@ def press_key(self, keys: str, count=1, pause: typing.Optional[float] = None, ta real_pause += 2 * pause self.pause(real_pause) - def type(self, keys: str, pause: typing.Optional[float] = None, target = None) -> None: + def type(self, keys: str, pause: Optional[float] = None, target=None) -> None: if pause is None: pause = self.pause_between_keys if target is None: @@ -87,7 +105,7 @@ def type(self, keys: str, pause: typing.Optional[float] = None, target = None) - for key in keys: self.press_key(key, pause=pause, target=target) - def exec(self, keys: str, target = None) -> None: + def exec(self, keys: str, target=None) -> None: if target is None: target = self.tmux_pane self.type(keys, target=target) @@ -106,10 +124,18 @@ def pause(self, seconds: float) -> None: def run_external(self, command: str) -> None: subprocess.run(command, shell=True) - def message(self, msg: str, duration: typing.Optional[int] = None, add_instruction: bool = True, instruction_html: str = "") -> None: + def message( + self, + msg: str, + duration: Optional[int] = None, + add_instruction: bool = True, + instruction_html: str = "", + ) -> None: if duration is None: duration = len(msg) * 0.08 # seconds - self.tmux_session.set_option("display-time", int(duration * 1000)) # milliseconds + self.tmux_session.set_option( + "display-time", int(duration * 1000) + ) # milliseconds self.tmux_pane.display_message(" " + msg) if add_instruction or instruction_html: @@ -133,21 +159,25 @@ def close_popup(self, duration: float = 0) -> None: self.pause(duration) self.tmux_pane.cmd("display-popup", "-C") - def instruction(self, instruction: str, duration: float = 3, time_from: typing.Optional[float] = None) -> None: + def instruction( + self, instruction: str, duration: float = 3, time_from: Optional[float] = None + ) -> None: if time_from is None: time_from = self.current_time - self.instructions.append(InstructionSpec( - instruction = str(len(self.instructions) + 1) + ". " + instruction, - time_from = round(time_from, 1), - time_to = round(time_from + duration, 1) - )) + self.instructions.append( + InstructionSpec( + instruction=str(len(self.instructions) + 1) + ". " + instruction, + time_from=round(time_from, 1), + time_to=round(time_from + duration, 1), + ) + ) def save_instructions(self, output_path: str) -> None: instr_as_dicts = [] for instr in self.instructions: instr_as_dicts.append(instr._asdict()) - with open(output_path, 'w', encoding='utf-8') as f: + with open(output_path, "w", encoding="utf-8") as f: json.dump(instr_as_dicts, f, ensure_ascii=False, indent=4) @property diff --git a/docs/scripts/clirecording/record.py b/docs/scripts/clirecording/record.py index 8242cc501f..54ba1be2a7 100644 --- a/docs/scripts/clirecording/record.py +++ b/docs/scripts/clirecording/record.py @@ -4,7 +4,7 @@ import screenplays -if __name__ == '__main__': +if __name__ == "__main__": director = CliDirector() screenplays.record_user_interface(director) screenplays.record_intercept_requests(director) diff --git a/docs/scripts/clirecording/screenplays.py b/docs/scripts/clirecording/screenplays.py index 6c482ded75..ea871e7a7f 100644 --- a/docs/scripts/clirecording/screenplays.py +++ b/docs/scripts/clirecording/screenplays.py @@ -8,7 +8,9 @@ def record_user_interface(d: CliDirector): window = tmux.attached_window d.start_recording("recordings/mitmproxy_user_interface.cast") - d.message("Welcome to the mitmproxy tutorial. In this lesson we cover the user interface.") + d.message( + "Welcome to the mitmproxy tutorial. In this lesson we cover the user interface." + ) d.pause(1) d.exec("mitmproxy") d.pause(3) @@ -29,7 +31,7 @@ def record_user_interface(d: CliDirector): d.type(" --proxy http://127.0.0.1:8080") d.message("We use the text-based weather service `wttr.in`.") - d.exec(" \"http://wttr.in/Dunedin?0\"") + d.exec(' "http://wttr.in/Dunedin?0"') d.pause(2) d.press_key("Up") @@ -63,7 +65,9 @@ def record_user_interface(d: CliDirector): d.press_key("Right", count=2, pause=2.5) d.press_key("Left", count=2, pause=1) - d.message("Press `q` to exit the current view.",) + d.message( + "Press `q` to exit the current view.", + ) d.type("q") d.message("Press `?` to get a list of all available keyboard shortcuts.") @@ -90,7 +94,9 @@ def record_user_interface(d: CliDirector): d.message("Press `ENTER` to execute the command.") d.press_key("Enter") - d.message("Commands unleash the full power of mitmproxy, i.e., to configure interceptions.") + d.message( + "Commands unleash the full power of mitmproxy, i.e., to configure interceptions." + ) d.message("You now know basics of mitmproxy’s UI and how to control it.") d.pause(1) @@ -105,22 +111,32 @@ def record_intercept_requests(d: CliDirector): window = tmux.attached_window d.start_recording("recordings/mitmproxy_intercept_requests.cast") - d.message("Welcome to the mitmproxy tutorial. In this lesson we cover the interception of requests.") + d.message( + "Welcome to the mitmproxy tutorial. In this lesson we cover the interception of requests." + ) d.pause(1) d.exec("mitmproxy") d.pause(3) d.message("We first need to configure mitmproxy to intercept requests.") - d.message("Press `i` to prepopulate mitmproxy’s command prompt with `set intercept ''`.") + d.message( + "Press `i` to prepopulate mitmproxy’s command prompt with `set intercept ''`." + ) d.type("i") d.pause(2) - d.message("We use the flow filter expression `~u ` to only intercept specific URLs.") - d.message("Additionally, we use the filter `~q` to only intercept requests, but not responses.") + d.message( + "We use the flow filter expression `~u ` to only intercept specific URLs." + ) + d.message( + "Additionally, we use the filter `~q` to only intercept requests, but not responses." + ) d.message("We combine both flow filters using `&`.") - d.message("Enter `~u /Dunedin & ~q` between the quotes of the `set intercept` command and press `ENTER`.") + d.message( + "Enter `~u /Dunedin & ~q` between the quotes of the `set intercept` command and press `ENTER`." + ) d.exec("~u /Dunedin & ~q") d.message("The bottom bar shows that the interception has been configured.") @@ -133,14 +149,18 @@ def record_intercept_requests(d: CliDirector): d.focus_pane(pane_bottom) d.pause(2) - d.exec("curl --proxy http://127.0.0.1:8080 \"http://wttr.in/Dunedin?0\"") + d.exec('curl --proxy http://127.0.0.1:8080 "http://wttr.in/Dunedin?0"') d.pause(2) d.focus_pane(pane_top) d.message("You see a new line in in the list of flows.") - d.message("The new flow is displayed in red to indicate that it has been intercepted.") - d.message("Put the focus (`>>`) on the intercepted flow. This is already the case in our example.") + d.message( + "The new flow is displayed in red to indicate that it has been intercepted." + ) + d.message( + "Put the focus (`>>`) on the intercepted flow. This is already the case in our example." + ) d.message("Press `a` to resume this flow without making any changes.") d.type("a") d.pause(2) @@ -156,7 +176,9 @@ def record_intercept_requests(d: CliDirector): d.press_key("Down") d.pause(1) - d.message("Press `X` to kill this flow, i.e., discard it without forwarding it to its final destination `wttr.in`.") + d.message( + "Press `X` to kill this flow, i.e., discard it without forwarding it to its final destination `wttr.in`." + ) d.type("X") d.pause(3) @@ -170,13 +192,19 @@ def record_modify_requests(d: CliDirector): window = tmux.attached_window d.start_recording("recordings/mitmproxy_modify_requests.cast") - d.message("Welcome to the mitmproxy tutorial. In this lesson we cover the modification of intercepted requests.") + d.message( + "Welcome to the mitmproxy tutorial. In this lesson we cover the modification of intercepted requests." + ) d.pause(1) d.exec("mitmproxy") d.pause(3) - d.message("We configure and use the same interception rule as in the last tutorial.") - d.message("Press `i` to prepopulate mitmproxy’s command prompt, enter the flow filter `~u /Dunedin & ~q`, and press `ENTER`.") + d.message( + "We configure and use the same interception rule as in the last tutorial." + ) + d.message( + "Press `i` to prepopulate mitmproxy’s command prompt, enter the flow filter `~u /Dunedin & ~q`, and press `ENTER`." + ) d.type("i") d.pause(2) d.exec("~u /Dunedin & ~q") @@ -190,13 +218,15 @@ def record_modify_requests(d: CliDirector): d.focus_pane(pane_bottom) d.pause(2) - d.exec("curl --proxy http://127.0.0.1:8080 \"http://wttr.in/Dunedin?0\"") + d.exec('curl --proxy http://127.0.0.1:8080 "http://wttr.in/Dunedin?0"') d.pause(2) d.focus_pane(pane_top) d.message("We now want to modify the intercepted request.") - d.message("Put the focus (`>>`) on the intercepted flow. This is already the case in our example.") + d.message( + "Put the focus (`>>`) on the intercepted flow. This is already the case in our example." + ) d.message("Press `ENTER` to open the details view for the intercepted flow.") d.press_key("Enter") @@ -211,7 +241,9 @@ def record_modify_requests(d: CliDirector): d.pause(1) d.press_key("Enter") - d.message("mitmproxy shows all path components line by line, in our example its just `Dunedin`.") + d.message( + "mitmproxy shows all path components line by line, in our example its just `Dunedin`." + ) d.message("Press `ENTER` to modify the selected path component.") d.press_key("Down", pause=2) d.press_key("Enter") @@ -230,7 +262,9 @@ def record_modify_requests(d: CliDirector): d.type("a") d.pause(2) - d.message("You see that the request URL was modified and `wttr.in` replied with the weather report for `Innsbruck`.") + d.message( + "You see that the request URL was modified and `wttr.in` replied with the weather report for `Innsbruck`." + ) d.message("In the next lesson you will learn to replay flows.") d.save_instructions("recordings/mitmproxy_modify_requests_instructions.json") @@ -242,12 +276,16 @@ def record_replay_requests(d: CliDirector): window = tmux.attached_window d.start_recording("recordings/mitmproxy_replay_requests.cast") - d.message("Welcome to the mitmproxy tutorial. In this lesson we cover replaying requests.") + d.message( + "Welcome to the mitmproxy tutorial. In this lesson we cover replaying requests." + ) d.pause(1) d.exec("mitmproxy") d.pause(3) - d.message("Let’s generate a request that we can replay. We use `curl` in a separate terminal.") + d.message( + "Let’s generate a request that we can replay. We use `curl` in a separate terminal." + ) pane_top = d.current_pane pane_bottom = window.split_window(attach=True) @@ -256,23 +294,31 @@ def record_replay_requests(d: CliDirector): d.focus_pane(pane_bottom) d.pause(2) - d.exec("curl --proxy http://127.0.0.1:8080 \"http://wttr.in/Dunedin?0\"") + d.exec('curl --proxy http://127.0.0.1:8080 "http://wttr.in/Dunedin?0"') d.pause(2) d.focus_pane(pane_top) d.message("We now want to replay the this request.") - d.message("Put the focus (`>>`) on the request that should be replayed. This is already the case in our example.") + d.message( + "Put the focus (`>>`) on the request that should be replayed. This is already the case in our example." + ) d.message("Press `r` to replay the request.") d.type("r") - d.message("Note that no new rows are added for replayed flows, but the existing row is updated.") - d.message("Every time you press `r`, mitmproxy sends this request to the server again and updates the flow.") + d.message( + "Note that no new rows are added for replayed flows, but the existing row is updated." + ) + d.message( + "Every time you press `r`, mitmproxy sends this request to the server again and updates the flow." + ) d.press_key("r", count=4, pause=1) d.message("You can also modify a flow before replaying it.") d.message("It works as shown in the previous lesson, by pressing `e`.") - d.message("Congratulations! You have completed all lessons of the mitmproxy tutorial.") + d.message( + "Congratulations! You have completed all lessons of the mitmproxy tutorial." + ) d.save_instructions("recordings/mitmproxy_replay_requests_instructions.json") d.end() diff --git a/docs/scripts/examples.py b/docs/scripts/examples.py index cd69f6b051..4dd742d500 100755 --- a/docs/scripts/examples.py +++ b/docs/scripts/examples.py @@ -5,7 +5,7 @@ here = Path(__file__).absolute().parent example_dir = here / ".." / "src" / "examples" / "addons" -examples = example_dir.glob('*.py') +examples = example_dir.glob("*.py") overview = [] listings = [] @@ -14,7 +14,8 @@ code = example.read_text() slug = str(example.with_suffix("").relative_to(example_dir)) slug = re.sub(r"[^a-zA-Z]", "-", slug) - match = re.search(r''' + match = re.search( + r''' ^ (?:[#][^\n]*\n)? # there might be a shebang """ @@ -22,23 +23,27 @@ (.+?) \s* (?:\n\n|""") # stop on empty line or end of comment - ''', code, re.VERBOSE) + ''', + code, + re.VERBOSE, + ) if match: comment = " — " + match.group(1) else: comment = "" - overview.append( - f" * [{example.name}](#{slug}){comment}\n" - ) - listings.append(f""" + overview.append(f" * [{example.name}](#{slug}){comment}\n") + listings.append( + f"""

Example: {example.name}

```python {code.strip()} ``` -""") +""" + ) -print(f""" +print( + f""" # Addon Examples ### Dedicated Example Addons @@ -62,4 +67,5 @@ ------------------------- {"".join(listings)} -""") +""" +) diff --git a/docs/scripts/filters.py b/docs/scripts/filters.py index 05cc7a0fbc..32634196a8 100755 --- a/docs/scripts/filters.py +++ b/docs/scripts/filters.py @@ -3,7 +3,7 @@ from mitmproxy import flowfilter -print("") +print('
') for i in flowfilter.help: print("" % i) print("
%s%s
") diff --git a/docs/scripts/options.py b/docs/scripts/options.py index ff7d0f7f8e..3747d3fb77 100755 --- a/docs/scripts/options.py +++ b/docs/scripts/options.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import asyncio from mitmproxy import options, optmanager from mitmproxy.tools import dump, console, web @@ -6,22 +7,28 @@ masters = { "mitmproxy": console.master.ConsoleMaster, "mitmdump": dump.DumpMaster, - "mitmweb": web.master.WebMaster + "mitmweb": web.master.WebMaster, } unified_options = {} -for tool_name, master in masters.items(): - opts = options.Options() - inst = master(opts) - for key, option in optmanager.dump_dicts(opts).items(): - if key in unified_options: - unified_options[key]['tools'].append(tool_name) - else: - unified_options[key] = option - unified_options[key]['tools'] = [tool_name] - -print(""" + +async def dump(): + for tool_name, master in masters.items(): + opts = options.Options() + _ = master(opts) + for key, option in optmanager.dump_dicts(opts).items(): + if key in unified_options: + unified_options[key]["tools"].append(tool_name) + else: + unified_options[key] = option + unified_options[key]["tools"] = [tool_name] + + +asyncio.run(dump()) + +print( + """ @@ -31,23 +38,22 @@ - """.strip()) + """.strip() +) for key, option in sorted(unified_options.items(), key=lambda t: t[0]): - print(""" - - - - + + + - """.strip().format( - key, - ' '.join(["{}".format(t) for t in option['tools']]), - option['type'], - option['help'], - option['default'], - "
Choices: {}".format(', '.join(option['choices'])) if option['choices'] else "", - )) + """.strip() + ) print("
{}
{}
{}{}
- Default: {} - {} + print( + f""" +
+ #   + {key}
+ {' '.join(["{}".format(t) for t in option['tools']])}
{option['type']}{option['help']}
+ Default: {option['default']} + {"
Choices: {}".format(', '.join(option['choices'])) if option['choices'] else ""}
") diff --git a/docs/scripts/pdoc-template/frame.html.jinja2 b/docs/scripts/pdoc-template/frame.html.jinja2 index ce2bf96d5a..d7ef3ab4d7 100644 --- a/docs/scripts/pdoc-template/frame.html.jinja2 +++ b/docs/scripts/pdoc-template/frame.html.jinja2 @@ -1,3 +1,9 @@ -{% block style %}{% endblock %} -{% block body %}{% endblock %} +{% filter minify_css %} + {% block style %} + + + + {% endblock %} +{% endfilter %} +{% block content %}{% endblock %}
{% block attribution %}{% endblock %}
diff --git a/docs/scripts/pdoc-template/module.html.jinja2 b/docs/scripts/pdoc-template/module.html.jinja2 index 268c393fb8..8cb53dbabe 100644 --- a/docs/scripts/pdoc-template/module.html.jinja2 +++ b/docs/scripts/pdoc-template/module.html.jinja2 @@ -1,7 +1,4 @@ {% extends "default/module.html.jinja2" %} -{% block nav %}{% endblock %} -{% block style_layout %}{% endblock %} -{% block style_pygments %}{% endblock %} {# To document all event hooks, we do a bit of hackery: 1. scripts/api-events.py auto-generates generated/events.py. @@ -10,13 +7,13 @@ To document all event hooks, we do a bit of hackery: #} {% if module.name == "events" %} - {% macro module_name() %} - {% endmacro %} - {% macro view_source(doc) %} - {% if doc.type != "module" %} - {{ default_view_source(doc) }} - {% endif %} + {% macro module_name() %}{% endmacro %} + {% macro class(cls) %} + {{ cls.qualname.replace("Events", " Events").replace("AdvancedLifecycle", "Advanced Lifecycle") }} {% endmacro %} + {% macro view_source_state(doc) %}{% endmacro %} + {% macro view_source_button(doc) %}{% endmacro %} + {% macro view_source_code(doc) %}{% endmacro %} {% macro is_public(doc) %} {% if doc.name != "__init__" %} {{ default_is_public(doc) }} @@ -39,6 +36,10 @@ To document all event hooks, we do a bit of hackery: {% else %} {{ default_is_public(doc) }} {% endif %} + {% elif doc.modulename == "mitmproxy.dns" %} + {% if doc.qualname.startswith("DNSFlow") %} + {{ default_is_public(doc) }} + {% endif %} {% elif doc.modulename == "mitmproxy.flow" %} {% if doc.name is not in(["__init__", "reply", "metadata"]) %} {{ default_is_public(doc) }} @@ -55,6 +56,14 @@ To document all event hooks, we do a bit of hackery: {% if doc.qualname.startswith("ServerConnectionHookData") and doc.name != "__init__" %} {{ default_is_public(doc) }} {% endif %} + {% elif doc.modulename == "mitmproxy.proxy.context" %} + {% if doc.qualname is not in(["Context.__init__", "Context.fork", "Context.options"]) %} + {{ default_is_public(doc) }} + {% endif %} + {% elif doc.modulename == "mitmproxy.tls" %} + {% if doc.qualname is not in(["TlsData.__init__", "ClientHelloData.__init__"]) %} + {{ default_is_public(doc) }} + {% endif %} {% elif doc.modulename == "mitmproxy.websocket" %} {% if doc.qualname != "WebSocketMessage.type" %} {{ default_is_public(doc) }} diff --git a/docs/src/assets/style.scss b/docs/src/assets/style.scss index 86b19b86de..ec46d37450 100644 --- a/docs/src/assets/style.scss +++ b/docs/src/assets/style.scss @@ -82,7 +82,7 @@ code { } } -h1, h2, h3, h4, h5, h6 { +h1, h2, h3, h4, h5, h6, th { .anchor { display: inline-block; width: 0; diff --git a/docs/src/assets/syntax.css b/docs/src/assets/syntax.css index 23be044645..86c307fa80 100644 --- a/docs/src/assets/syntax.css +++ b/docs/src/assets/syntax.css @@ -1,3 +1,4 @@ +/* Line Numbers */ .chroma span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 20px; } /* Background */ .chroma { background-color: #ffffff } /* Other */ .chroma .x { } /* Error */ .chroma .err { color: #a61717; background-color: #e3d2d2 } diff --git a/docs/src/config.toml b/docs/src/config.toml index 0d26241bcc..4dbe047fdd 100644 --- a/docs/src/config.toml +++ b/docs/src/config.toml @@ -12,3 +12,6 @@ tag = "tags" [markup.goldmark.renderer] unsafe = true + +[security.funcs] +getenv = ["DOCS_ARCHIVE"] diff --git a/docs/src/content/addons-overview.md b/docs/src/content/addons-overview.md index fc29c461f3..a4f48efed0 100644 --- a/docs/src/content/addons-overview.md +++ b/docs/src/content/addons-overview.md @@ -30,7 +30,7 @@ mitmproxy's internal logging mechanism to announce its tally. The output can be found in the event log in the interactive tools, or on the console in mitmdump. Take it for a spin and make sure that it does what it's supposed to, by loading -it into your mitmproxy tool of choice. We'll use mitmpdump in these examples, +it into your mitmproxy tool of choice. We'll use mitmdump in these examples, but the flag is identical for all tools: ```bash diff --git a/docs/src/content/api/mitmproxy.dns.md b/docs/src/content/api/mitmproxy.dns.md new file mode 100644 index 0000000000..e317e1f018 --- /dev/null +++ b/docs/src/content/api/mitmproxy.dns.md @@ -0,0 +1,11 @@ + +--- +title: "mitmproxy.dns" +url: "api/mitmproxy/dns.html" + +menu: + addons: + parent: 'Event Hooks & API' +--- + +{{< readfile file="/generated/api/mitmproxy/dns.html" >}} diff --git a/docs/src/content/api/mitmproxy.proxy.context.md b/docs/src/content/api/mitmproxy.proxy.context.md new file mode 100644 index 0000000000..b4aa488654 --- /dev/null +++ b/docs/src/content/api/mitmproxy.proxy.context.md @@ -0,0 +1,11 @@ + +--- +title: "mitmproxy.proxy.context" +url: "api/mitmproxy/proxy/context.html" + +menu: + addons: + parent: 'Event Hooks & API' +--- + +{{< readfile file="/generated/api/mitmproxy/proxy/context.html" >}} diff --git a/docs/src/content/api/mitmproxy.tls.md b/docs/src/content/api/mitmproxy.tls.md new file mode 100644 index 0000000000..02dcbeb30e --- /dev/null +++ b/docs/src/content/api/mitmproxy.tls.md @@ -0,0 +1,11 @@ + +--- +title: "mitmproxy.tls" +url: "api/mitmproxy/tls.html" + +menu: + addons: + parent: 'Event Hooks & API' +--- + +{{< readfile file="/generated/api/mitmproxy/tls.html" >}} diff --git a/docs/src/content/concepts-commands.md b/docs/src/content/concepts-commands.md index 73e8adaed4..d826ec89e0 100644 --- a/docs/src/content/concepts-commands.md +++ b/docs/src/content/concepts-commands.md @@ -18,7 +18,7 @@ and many of the built-in argument types - give it a try. The canonical reference for commands is the `--commands` flag, which is exposed by each of the mitmproxy tools. Passing this flag will dump an annotated list of all registered commands, their arguments and their return values to screen. In -mimtproxy console you can also view a palette of all commands in the command +mitmproxy console you can also view a palette of all commands in the command browser (by default accessible with the `C` key binding). # Working with Flows diff --git a/docs/src/content/howto-transparent.md b/docs/src/content/howto-transparent.md index b143f1a475..205ddf68a7 100644 --- a/docs/src/content/howto-transparent.md +++ b/docs/src/content/howto-transparent.md @@ -278,6 +278,11 @@ sudo -u nobody mitmproxy --mode transparent --showhost ## "Full" transparent mode on Linux +{{% note %}} +This feature is currently unavailable in mitmproxy 7 and above +(#4914). +{{% /note %}} + By default mitmproxy will use its own local IP address for its server-side connections. In case this isn't desired, the --spoof-source-address argument can be used to use the client's IP address for server-side connections. The diff --git a/docs/src/content/overview-features.md b/docs/src/content/overview-features.md index 46ab86507d..f635436a2a 100644 --- a/docs/src/content/overview-features.md +++ b/docs/src/content/overview-features.md @@ -94,10 +94,10 @@ The _separator_ is arbitrary, and is defined by the first character (`|` in the Pattern | Description ------- | ----------- -`|example.com/main.js|~/main-local.js` | Replace `example.com/main.js` with `~/main-local.js`. -`|example.com/static|~/static` | Replace `example.com/static/foo/bar.css` with `~/static/foo/bar.css`. -`|example.com/static/foo|~/static` | Replace `example.com/static/foo/bar.css` with `~/static/bar.css`. -`|~m GET|example.com/static|~/static` | Replace `example.com/static/foo/bar.css` with `~/static/foo/bar.css` (but only for GET requests). +`\|example.com/main.js\|~/main-local.js` | Replace `example.com/main.js` with `~/main-local.js`. +`\|example.com/static\|~/static` | Replace `example.com/static/foo/bar.css` with `~/static/foo/bar.css`. +`\|example.com/static/foo\|~/static` | Replace `example.com/static/foo/bar.css` with `~/static/bar.css`. +`\|~m GET\|example.com/static\|~/static` | Replace `example.com/static/foo/bar.css` with `~/static/foo/bar.css` (but only for GET requests). ### Details @@ -275,7 +275,7 @@ Set the `User-Agent` header to the data read from `~/useragent.txt` for all requ (existing `User-Agent` headers are replaced): ``` -/~q/Host/@~/useragent.txt +/~q/User-Agent/@~/useragent.txt ``` Remove existing `Host` headers from all requests: @@ -358,7 +358,8 @@ By default, mitmproxy will read an entire request/response, perform any indicated manipulations on it, and then send the message on to the other party. This can be problematic when downloading or uploading large files. When streaming is enabled, message bodies are not buffered on the proxy but instead -sent directly to the server/client. HTTP headers are still fully buffered before +sent directly to the server/client. This currently means that the message body +will not be accessible within mitmproxy. HTTP headers are still fully buffered before being sent. Request/response streaming is enabled by specifying a size cutoff in the diff --git a/docs/src/content/overview-getting-started.md b/docs/src/content/overview-getting-started.md index 10a4aa7ddd..7bcfd63598 100644 --- a/docs/src/content/overview-getting-started.md +++ b/docs/src/content/overview-getting-started.md @@ -47,6 +47,6 @@ new flow and you can inspect it. ## Resources -* [**StackOverflow**](https://stackoverflow.com/questions/tagged/mitmproxy): If you want to ask usage questions, please do so on StackOverflow. -* [**GitHub**](https://github.com/mitmproxy/): If you want to contribute to mitmproxy or submit a bug report, please do so on GitHub. -* [**Slack**](https://mitmproxy.slack.com): If you want to get in touch with the developers or other users, please use our Slack channel. +* [**GitHub**](https://github.com/mitmproxy/mitmproxy): If you want to ask usage questions, contribute + to mitmproxy, or submit a bug report, please use GitHub. +* [**Slack**](https://mitmproxy.slack.com): For ephemeral development questions/coordination, please use our Slack channel. diff --git a/docs/src/content/overview-installation.md b/docs/src/content/overview-installation.md index 0f13dd2579..a65ae0fa49 100644 --- a/docs/src/content/overview-installation.md +++ b/docs/src/content/overview-installation.md @@ -34,8 +34,10 @@ the repository maintainers directly for issues with native packages. ## Windows -To install mitmproxy on Windows, download the installer from [mitmproxy.org](https://mitmproxy.org/). After -installation, mitmproxy, mitmdump and mitmweb are also added to your PATH and can be invoked from the command line. +To install mitmproxy on Windows, download the installer from [mitmproxy.org](https://mitmproxy.org/). +We also provide standalone binaries, they take significantly longer to start +as some files need to be extracted to temporary directories first. +After installation, mitmproxy, mitmdump and mitmweb are also added to your PATH and can be invoked from the command line. We highly recommend to [install Windows Terminal](https://aka.ms/terminal) to improve the rendering of the console interface. @@ -64,7 +66,7 @@ While there are plenty of options around[^1], we recommend the installation usin packages. Most of them (pip, virtualenv, pipenv, etc.) should just work, but we don't have the capacity to provide support for it. -1. Install a recent version of Python (we require at least 3.8). +1. Install a recent version of Python (we require at least 3.9). 2. Install [pipx](https://pipxproject.github.io/pipx/). 3. `pipx install mitmproxy` diff --git a/examples/addons/anatomy.py b/examples/addons/anatomy.py index ffe0720087..f86b3b916d 100644 --- a/examples/addons/anatomy.py +++ b/examples/addons/anatomy.py @@ -15,6 +15,4 @@ def request(self, flow): ctx.log.info("We've seen %d flows" % self.num) -addons = [ - Counter() -] +addons = [Counter()] diff --git a/examples/addons/commands-flows.py b/examples/addons/commands-flows.py index 866a461706..053d11d424 100644 --- a/examples/addons/commands-flows.py +++ b/examples/addons/commands-flows.py @@ -1,5 +1,5 @@ """Handle flows as command arguments.""" -import typing +from collections.abc import Sequence from mitmproxy import command from mitmproxy import ctx @@ -9,13 +9,11 @@ class MyAddon: @command.command("myaddon.addheader") - def addheader(self, flows: typing.Sequence[flow.Flow]) -> None: + def addheader(self, flows: Sequence[flow.Flow]) -> None: for f in flows: if isinstance(f, http.HTTPFlow): f.request.headers["myheader"] = "value" ctx.log.alert("done") -addons = [ - MyAddon() -] +addons = [MyAddon()] diff --git a/examples/addons/commands-paths.py b/examples/addons/commands-paths.py index ea3431c34e..80987e27b6 100644 --- a/examples/addons/commands-paths.py +++ b/examples/addons/commands-paths.py @@ -1,5 +1,5 @@ """Handle file paths as command arguments.""" -import typing +from collections.abc import Sequence from mitmproxy import command from mitmproxy import ctx @@ -12,21 +12,19 @@ class MyAddon: @command.command("myaddon.histogram") def histogram( self, - flows: typing.Sequence[flow.Flow], + flows: Sequence[flow.Flow], path: types.Path, ) -> None: - totals: typing.Dict[str, int] = {} + totals: dict[str, int] = {} for f in flows: if isinstance(f, http.HTTPFlow): totals[f.request.host] = totals.setdefault(f.request.host, 0) + 1 with open(path, "w+") as fp: - for cnt, dom in sorted([(v, k) for (k, v) in totals.items()]): + for cnt, dom in sorted((v, k) for (k, v) in totals.items()): fp.write(f"{cnt}: {dom}\n") ctx.log.alert("done") -addons = [ - MyAddon() -] +addons = [MyAddon()] diff --git a/examples/addons/commands-simple.py b/examples/addons/commands-simple.py index 3ff857a274..153a7db58b 100644 --- a/examples/addons/commands-simple.py +++ b/examples/addons/commands-simple.py @@ -13,6 +13,4 @@ def inc(self) -> None: ctx.log.info(f"num = {self.num}") -addons = [ - MyAddon() -] +addons = [MyAddon()] diff --git a/examples/addons/contentview-custom-grpc.py b/examples/addons/contentview-custom-grpc.py index b1bdb45964..c84da91c89 100644 --- a/examples/addons/contentview-custom-grpc.py +++ b/examples/addons/contentview-custom-grpc.py @@ -24,34 +24,59 @@ # # The actual 'filter' definition in the rule, would still match the whole flow. This means '~u' expressions could # be used, to match the URL from the request of a flow, while the ParserRuleResponse is only applied to the response. - ProtoParser.ParserRuleRequest( - name = "Geo coordinate lookup request", + name="Geo coordinate lookup request", # note on flowfilter: for tflow the port gets appended to the URL's host part - filter = "example\\.com.*/ReverseGeocode", + filter="example\\.com.*/ReverseGeocode", field_definitions=[ ProtoParser.ParserFieldDefinition(tag="1", name="position"), - ProtoParser.ParserFieldDefinition(tag="1.1", name="latitude", intended_decoding=ProtoParser.DecodedTypes.double), - ProtoParser.ParserFieldDefinition(tag="1.2", name="longitude", intended_decoding=ProtoParser.DecodedTypes.double), + ProtoParser.ParserFieldDefinition( + tag="1.1", + name="latitude", + intended_decoding=ProtoParser.DecodedTypes.double, + ), + ProtoParser.ParserFieldDefinition( + tag="1.2", + name="longitude", + intended_decoding=ProtoParser.DecodedTypes.double, + ), ProtoParser.ParserFieldDefinition(tag="3", name="country"), ProtoParser.ParserFieldDefinition(tag="7", name="app"), - ] + ], ), ProtoParser.ParserRuleResponse( - name = "Geo coordinate lookup response", + name="Geo coordinate lookup response", # note on flowfilter: for tflow the port gets appended to the URL's host part - filter = "example\\.com.*/ReverseGeocode", + filter="example\\.com.*/ReverseGeocode", field_definitions=[ ProtoParser.ParserFieldDefinition(tag="1.2", name="address"), ProtoParser.ParserFieldDefinition(tag="1.3", name="address array element"), - ProtoParser.ParserFieldDefinition(tag="1.3.1", name="unknown bytes", intended_decoding=ProtoParser.DecodedTypes.bytes), + ProtoParser.ParserFieldDefinition( + tag="1.3.1", + name="unknown bytes", + intended_decoding=ProtoParser.DecodedTypes.bytes, + ), ProtoParser.ParserFieldDefinition(tag="1.3.2", name="element value long"), ProtoParser.ParserFieldDefinition(tag="1.3.3", name="element value short"), - ProtoParser.ParserFieldDefinition(tag="", tag_prefixes=["1.5.1", "1.5.3", "1.5.4", "1.5.5", "1.5.6"], name="position"), - ProtoParser.ParserFieldDefinition(tag=".1", tag_prefixes=["1.5.1", "1.5.3", "1.5.4", "1.5.5", "1.5.6"], name="latitude", intended_decoding=ProtoParser.DecodedTypes.double), # noqa: E501 - ProtoParser.ParserFieldDefinition(tag=".2", tag_prefixes=["1.5.1", "1.5.3", "1.5.4", "1.5.5", "1.5.6"], name="longitude", intended_decoding=ProtoParser.DecodedTypes.double), # noqa: E501 + ProtoParser.ParserFieldDefinition( + tag="", + tag_prefixes=["1.5.1", "1.5.3", "1.5.4", "1.5.5", "1.5.6"], + name="position", + ), + ProtoParser.ParserFieldDefinition( + tag=".1", + tag_prefixes=["1.5.1", "1.5.3", "1.5.4", "1.5.5", "1.5.6"], + name="latitude", + intended_decoding=ProtoParser.DecodedTypes.double, + ), # noqa: E501 + ProtoParser.ParserFieldDefinition( + tag=".2", + tag_prefixes=["1.5.1", "1.5.3", "1.5.4", "1.5.5", "1.5.6"], + name="longitude", + intended_decoding=ProtoParser.DecodedTypes.double, + ), # noqa: E501 ProtoParser.ParserFieldDefinition(tag="7", name="app"), - ] + ], ), ] diff --git a/examples/addons/filter-flows.py b/examples/addons/filter-flows.py index c5e7e62759..13b54dbe34 100644 --- a/examples/addons/filter-flows.py +++ b/examples/addons/filter-flows.py @@ -14,9 +14,7 @@ def configure(self, updated): self.filter = flowfilter.parse(ctx.options.flowfilter) def load(self, l): - l.add_option( - "flowfilter", str, "", "Check that flow matches filter." - ) + l.add_option("flowfilter", str, "", "Check that flow matches filter.") def response(self, flow: http.HTTPFlow) -> None: if flowfilter.match(self.filter, flow): diff --git a/examples/addons/http-add-header.py b/examples/addons/http-add-header.py index 7badacf448..b4a623fbad 100644 --- a/examples/addons/http-add-header.py +++ b/examples/addons/http-add-header.py @@ -10,6 +10,4 @@ def response(self, flow): flow.response.headers["count"] = str(self.num) -addons = [ - AddHeader() -] +addons = [AddHeader()] diff --git a/examples/addons/http-modify-form.py b/examples/addons/http-modify-form.py index d296c4445d..b4ad178fdb 100644 --- a/examples/addons/http-modify-form.py +++ b/examples/addons/http-modify-form.py @@ -9,6 +9,4 @@ def request(flow: http.HTTPFlow) -> None: else: # One can also just pass new form data. # This sets the proper content type and overrides the body. - flow.request.urlencoded_form = [ - ("foo", "bar") - ] + flow.request.urlencoded_form = [("foo", "bar")] diff --git a/examples/addons/http-reply-from-proxy.py b/examples/addons/http-reply-from-proxy.py index 20a1e6f8cf..3ce35c5a4f 100644 --- a/examples/addons/http-reply-from-proxy.py +++ b/examples/addons/http-reply-from-proxy.py @@ -7,5 +7,5 @@ def request(flow: http.HTTPFlow) -> None: flow.response = http.Response.make( 200, # (optional) status code b"Hello World", # (optional) content - {"Content-Type": "text/html"} # (optional) headers + {"Content-Type": "text/html"}, # (optional) headers ) diff --git a/examples/addons/http-trailers.py b/examples/addons/http-trailers.py index 35dc30a868..26a51f23bd 100644 --- a/examples/addons/http-trailers.py +++ b/examples/addons/http-trailers.py @@ -1,7 +1,7 @@ """ This script simply prints all received HTTP Trailers. -HTTP requests and responses can container trailing headers which are sent after +HTTP requests and responses can contain trailing headers which are sent after the body is fully transmitted. Such trailers need to be announced in the initial headers by name, so the receiving endpoint can wait and read them after the body. @@ -29,9 +29,7 @@ def request(flow: http.HTTPFlow): # HTTP 2+ supports trailers on all requests/responses flow.request.headers["trailer"] = "x-my-injected-trailer-header" - flow.request.trailers = Headers([ - (b"x-my-injected-trailer-header", b"foobar") - ]) + flow.request.trailers = Headers([(b"x-my-injected-trailer-header", b"foobar")]) print("Injected a new request trailer...", flow.request.headers["trailer"]) @@ -48,7 +46,5 @@ def response(flow: http.HTTPFlow): flow.response.headers["transfer-encoding"] = "chunked" flow.response.headers["trailer"] = "x-my-injected-trailer-header" - flow.response.trailers = Headers([ - (b"x-my-injected-trailer-header", b"foobar") - ]) + flow.response.trailers = Headers([(b"x-my-injected-trailer-header", b"foobar")]) print("Injected a new response trailer...", flow.response.headers["trailer"]) diff --git a/examples/addons/internet-in-mirror.py b/examples/addons/internet-in-mirror.py index 0c274903e8..749eb1f322 100644 --- a/examples/addons/internet-in-mirror.py +++ b/examples/addons/internet-in-mirror.py @@ -9,6 +9,5 @@ def response(flow: http.HTTPFlow) -> None: if flow.response and flow.response.content: flow.response.content = flow.response.content.replace( - b"", - b"" + b"", b"" ) diff --git a/examples/addons/io-write-flow-file.py b/examples/addons/io-write-flow-file.py index e885051d4c..ecc0528e7f 100644 --- a/examples/addons/io-write-flow-file.py +++ b/examples/addons/io-write-flow-file.py @@ -9,13 +9,14 @@ """ import random import sys +from typing import BinaryIO + from mitmproxy import io, http -import typing # noqa class Writer: def __init__(self, path: str) -> None: - self.f: typing.IO[bytes] = open(path, "wb") + self.f: BinaryIO = open(path, "wb") self.w = io.FlowWriter(self.f) def response(self, flow: http.HTTPFlow) -> None: diff --git a/examples/addons/nonblocking.py b/examples/addons/nonblocking.py index e6b79f87d9..2ee0929808 100644 --- a/examples/addons/nonblocking.py +++ b/examples/addons/nonblocking.py @@ -1,16 +1,26 @@ """ -Make events hooks non-blocking. - -When event hooks are decorated with @concurrent, they will be run in their own thread, freeing the main event loop. -Please note that this generally opens the door to race conditions and decreases performance if not required. +Make events hooks non-blocking using async or @concurrent """ +import asyncio import time from mitmproxy.script import concurrent +from mitmproxy import ctx + + +# Hooks can be async, which allows the hook to call async functions and perform async I/O +# without blocking other requests. This is generally preferred for new addons. +async def request(flow): + ctx.log.info(f"handle request: {flow.request.host}{flow.request.path}") + await asyncio.sleep(5) + ctx.log.info(f"start request: {flow.request.host}{flow.request.path}") -@concurrent # Remove this and see what happens -def request(flow): +# Another option is to use @concurrent, which launches the hook in its own thread. +# Please note that this generally opens the door to race conditions and decreases performance if not required. +# Rename the function below to request(flow) to try it out. +@concurrent # Remove this to make it synchronous and see what happens +def request_concurrent(flow): # This is ugly in mitmproxy's UI, but you don't want to use mitmproxy.ctx.log from a different thread. print(f"handle request: {flow.request.host}{flow.request.path}") time.sleep(5) diff --git a/examples/addons/options-configure.py b/examples/addons/options-configure.py index 39e982ba9e..81551b3dee 100644 --- a/examples/addons/options-configure.py +++ b/examples/addons/options-configure.py @@ -1,5 +1,5 @@ """React to configuration changes.""" -import typing +from typing import Optional from mitmproxy import ctx from mitmproxy import exceptions @@ -8,10 +8,10 @@ class AddHeader: def load(self, loader): loader.add_option( - name = "addheader", - typespec = typing.Optional[int], - default = None, - help = "Add a header to responses", + name="addheader", + typespec=Optional[int], + default=None, + help="Add a header to responses", ) def configure(self, updates): @@ -24,6 +24,4 @@ def response(self, flow): flow.response.headers["addheader"] = str(ctx.options.addheader) -addons = [ - AddHeader() -] +addons = [AddHeader()] diff --git a/examples/addons/options-simple.py b/examples/addons/options-simple.py index 4eb8d54d94..6014bc8f94 100644 --- a/examples/addons/options-simple.py +++ b/examples/addons/options-simple.py @@ -14,10 +14,10 @@ def __init__(self): def load(self, loader): loader.add_option( - name = "addheader", - typespec = bool, - default = False, - help = "Add a count header to responses", + name="addheader", + typespec=bool, + default=False, + help="Add a count header to responses", ) def response(self, flow): @@ -26,6 +26,4 @@ def response(self, flow): flow.response.headers["count"] = str(self.num) -addons = [ - AddHeader() -] +addons = [AddHeader()] diff --git a/examples/addons/websocket-inject-message.py b/examples/addons/websocket-inject-message.py index 5542879ea6..a0b73d24c2 100644 --- a/examples/addons/websocket-inject-message.py +++ b/examples/addons/websocket-inject-message.py @@ -10,16 +10,20 @@ # Simple example: Inject a message as a response to an event + def websocket_message(flow: http.HTTPFlow): assert flow.websocket is not None # make type checker happy last_message = flow.websocket.messages[-1] if last_message.is_text and "secret" in last_message.text: last_message.drop() - ctx.master.commands.call("inject.websocket", flow, last_message.from_client, "ssssssh".encode()) + ctx.master.commands.call( + "inject.websocket", flow, last_message.from_client, b"ssssssh" + ) # Complex example: Schedule a periodic timer + async def inject_async(flow: http.HTTPFlow): msg = "hello from mitmproxy! " assert flow.websocket is not None # make type checker happy diff --git a/examples/addons/websocket-simple.py b/examples/addons/websocket-simple.py index 558327d5c3..03ad63d21d 100644 --- a/examples/addons/websocket-simple.py +++ b/examples/addons/websocket-simple.py @@ -15,8 +15,8 @@ def websocket_message(flow: http.HTTPFlow): ctx.log.info(f"Server sent a message: {message.content!r}") # manipulate the message content - message.content = re.sub(rb'^Hello', b'HAPPY', message.content) + message.content = re.sub(rb"^Hello", b"HAPPY", message.content) - if b'FOOBAR' in message.content: + if b"FOOBAR" in message.content: # kill the message and not send it to the other endpoint message.drop() diff --git a/examples/addons/wsgi-flask-app.py b/examples/addons/wsgi-flask-app.py index 43fd8cdcae..4f117f05ab 100644 --- a/examples/addons/wsgi-flask-app.py +++ b/examples/addons/wsgi-flask-app.py @@ -11,17 +11,18 @@ app = Flask("proxapp") -@app.route('/') +@app.route("/") def hello_world() -> str: - return 'Hello World!' + return "Hello World!" addons = [ # Host app at the magic domain "example.com" on port 80. Requests to this # domain and port combination will now be routed to the WSGI app instance. - asgiapp.WSGIApp(app, "example.com", 80) - # SSL works too, but the magic domain needs to be resolvable from the mitmproxy machine due to mitmproxy's design. - # mitmproxy will connect to said domain and use serve its certificate (unless --no-upstream-cert is set) - # but won't send any data. - # mitmproxy.ctx.master.apps.add(app, "example.com", 443) + asgiapp.WSGIApp(app, "example.com", 80), + # TLS works too, but the magic domain needs to be resolvable from the mitmproxy machine due to mitmproxy's design. + # mitmproxy will connect to said domain and use its certificate but won't send any data. + # By using `--set upstream_cert=false` and `--set connection_strategy_lazy` the local certificate is used instead. + # asgiapp.WSGIApp(app, "example.com", 443), + ] diff --git a/examples/contrib/README.md b/examples/contrib/README.md index 751277b7a4..d15034c75b 100644 --- a/examples/contrib/README.md +++ b/examples/contrib/README.md @@ -3,8 +3,11 @@ Examples in this directory are contributed by the mitmproxy community. If you developed something thats useful for a wider audience, please add it here! -# Maintenance +### Additional Examples Hosted Externally + + - [**wsreplay.py**](https://github.com/KOLANICH-tools/wsreplay.py): a simple tool to replay WebSocket streams +# Maintenance :warning: The examples in this directory are _not_ actively maintained by the core developers. diff --git a/examples/contrib/block_dns_over_https.py b/examples/contrib/block_dns_over_https.py index b53b3a4f22..4d527af39d 100644 --- a/examples/contrib/block_dns_over_https.py +++ b/examples/contrib/block_dns_over_https.py @@ -4,7 +4,6 @@ It loads a blocklist of IPs and hostnames that are known to serve DNS over HTTPS requests. It also uses headers, query params, and paths to detect DoH (and block it) """ -from typing import List from mitmproxy import ctx @@ -74,12 +73,12 @@ } # additional hostnames to block -additional_doh_names: List[str] = [ +additional_doh_names: list[str] = [ 'dns.google.com' ] # additional IPs to block -additional_doh_ips: List[str] = [ +additional_doh_ips: list[str] = [ ] diff --git a/examples/contrib/change_upstream_proxy.py b/examples/contrib/change_upstream_proxy.py index a0e7e57288..ddcbabf100 100644 --- a/examples/contrib/change_upstream_proxy.py +++ b/examples/contrib/change_upstream_proxy.py @@ -1,15 +1,22 @@ + from mitmproxy import http -import typing +from mitmproxy.connection import Server +from mitmproxy.net.server_spec import ServerSpec + # This scripts demonstrates how mitmproxy can switch to a second/different upstream proxy # in upstream proxy mode. # -# Usage: mitmdump -U http://default-upstream-proxy.local:8080/ -s change_upstream_proxy.py +# Usage: mitmdump +# -s change_upstream_proxy.py +# --mode upstream:http://default-upstream-proxy:8080/ +# --set connection_strategy=lazy +# --set upstream_cert=false # # If you want to change the target server, you should modify flow.request.host and flow.request.port -def proxy_address(flow: http.HTTPFlow) -> typing.Tuple[str, int]: +def proxy_address(flow: http.HTTPFlow) -> tuple[str, int]: # Poor man's loadbalancing: route every second domain through the alternative proxy. if hash(flow.request.host) % 2 == 1: return ("localhost", 8082) @@ -18,10 +25,12 @@ def proxy_address(flow: http.HTTPFlow) -> typing.Tuple[str, int]: def request(flow: http.HTTPFlow) -> None: - if flow.request.method == "CONNECT": - # If the decision is done by domain, one could also modify the server address here. - # We do it after CONNECT here to have the request data available as well. - return address = proxy_address(flow) - if flow.live: - flow.live.change_upstream_proxy_server(address) # type: ignore + + is_proxy_change = address != flow.server_conn.via.address + server_connection_already_open = flow.server_conn.timestamp_start is not None + if is_proxy_change and server_connection_already_open: + # server_conn already refers to an existing connection (which cannot be modified), + # so we need to replace it with a new server connection object. + flow.server_conn = Server(flow.server_conn.address) + flow.server_conn.via = ServerSpec("http", address) diff --git a/examples/contrib/domain_fronting.py b/examples/contrib/domain_fronting.py new file mode 100644 index 0000000000..fd73d29856 --- /dev/null +++ b/examples/contrib/domain_fronting.py @@ -0,0 +1,130 @@ +from typing import Optional, Union +import json +from dataclasses import dataclass +from mitmproxy import ctx +from mitmproxy.addonmanager import Loader +from mitmproxy.http import HTTPFlow + + +""" +This extension implements support for domain fronting. + +Usage: + + mitmproxy -s examples/contrib/domain_fronting.py --set domainfrontingfile=./domain_fronting.json + +In the following basic example, www.example.com will be used for DNS requests and SNI values +but the secret.example.com value will be used for the HTTP host header: + + { + "mappings": [ + { + "patterns": ["secret.example.com"], + "server": "www.example.com" + } + ] + } + +The following example demonstrates the usage of a wildcard (at the beginning of the domain name only): + + { + "mappings": [ + { + "patterns": ["*.foo.example.com"], + "server": "www.example.com" + } + ] + } + +In the following example, we override the HTTP host header: + + { + "mappings": [ + { + "patterns": ["foo.example"], + "server": "www.example.com", + "host": "foo.proxy.example.com" + } + ] + } + +""" + + +@dataclass +class Mapping: + server: Union[str, None] + host: Union[str, None] + + +class HttpsDomainFronting: + + # configurations for regular ("foo.example.com") mappings: + star_mappings: dict[str, Mapping] + + # Configurations for star ("*.example.com") mappings: + strict_mappings: dict[str, Mapping] + + def __init__(self) -> None: + self.strict_mappings = {} + self.star_mappings = {} + + def _resolve_addresses(self, host: str) -> Optional[Mapping]: + mapping = self.strict_mappings.get(host) + if mapping is not None: + return mapping + + index = 0 + while True: + index = host.find(".", index) + if index == -1: + break + super_domain = host[(index + 1):] + mapping = self.star_mappings.get(super_domain) + if mapping is not None: + return mapping + index += 1 + + return None + + def load(self, loader: Loader) -> None: + loader.add_option( + name="domainfrontingfile", + typespec=str, + default="./fronting.json", + help="Domain fronting configuration file", + ) + + def _load_configuration_file(self, filename: str) -> None: + config = json.load(open(filename)) + strict_mappings: dict[str, Mapping] = {} + star_mappings: dict[str, Mapping] = {} + for mapping in config["mappings"]: + item = Mapping(server=mapping.get("server"), host=mapping.get("host")) + for pattern in mapping["patterns"]: + if pattern.startswith("*."): + star_mappings[pattern[2:]] = item + else: + strict_mappings[pattern] = item + self.strict_mappings = strict_mappings + self.star_mappings = star_mappings + + def configure(self, updated: set[str]) -> None: + if "domainfrontingfile" in updated: + domain_fronting_file = ctx.options.domainfrontingfile + self._load_configuration_file(domain_fronting_file) + + def request(self, flow: HTTPFlow) -> None: + if not flow.request.scheme == "https": + return + # We use the host header to dispatch the request: + target = flow.request.host_header + if target is None: + return + mapping = self._resolve_addresses(target) + if mapping is not None: + flow.request.host = mapping.server or target + flow.request.headers["host"] = mapping.host or target + + +addons = [HttpsDomainFronting()] diff --git a/examples/contrib/har_dump.py b/examples/contrib/har_dump.py index 14ecad7015..8f70ede7f7 100644 --- a/examples/contrib/har_dump.py +++ b/examples/contrib/har_dump.py @@ -13,7 +13,6 @@ import base64 import zlib import os -import typing # noqa from datetime import datetime from datetime import timezone @@ -26,11 +25,11 @@ from mitmproxy.utils import strutils from mitmproxy.net.http import cookies -HAR: typing.Dict = {} +HAR: dict = {} # A list of server seen till now is maintained so we can avoid # using 'connect' time for entries that use an existing connection. -SERVERS_SEEN: typing.Set[connection.Server] = set() +SERVERS_SEEN: set[connection.Server] = set() def load(l): diff --git a/examples/contrib/http_manipulate_cookies.py b/examples/contrib/http_manipulate_cookies.py new file mode 100644 index 0000000000..f420dc411b --- /dev/null +++ b/examples/contrib/http_manipulate_cookies.py @@ -0,0 +1,90 @@ +""" +This script is an example of how to manipulate cookies both outgoing (requests) +and ingoing (responses). In particular, this script inserts a cookie (specified +in a json file) into every request (overwriting any existing cookie of the same +name), and removes cookies from every response that have a certain set of names +specified in the variable (set) FILTER_COOKIES. + +Usage: + + mitmproxy -s examples/contrib/http_manipulate_cookies.py + +Note: + this was created as a response to SO post: + https://stackoverflow.com/questions/55358072/cookie-manipulation-in-mitmproxy-requests-and-responses + +""" +import json +from mitmproxy import http + + +PATH_TO_COOKIES = "./cookies.json" # insert your path to the cookie file here +FILTER_COOKIES = { + "mycookie", + "_ga", +} # update this to the specific cookie names you want to remove +# NOTE: use a set for lookup efficiency + + +# -- Helper functions -- +def load_json_cookies() -> list[dict[str, str]]: + """ + Load a particular json file containing a list of cookies. + """ + with open(PATH_TO_COOKIES) as f: + return json.load(f) + + +# NOTE: or just hardcode the cookies as [{"name": "", "value": ""}] + + +def stringify_cookies(cookies: list[dict]) -> str: + """ + Creates a cookie string from a list of cookie dicts. + """ + return ";".join([f"{c['name']}={c['value']}" for c in cookies]) + + +def parse_cookies(cookie_string: str) -> list[dict[str, str]]: + """ + Parses a cookie string into a list of cookie dicts. + """ + cookies = [] + for c in cookie_string.split(";"): + c = c.strip() + if c: + k, v = c.split("=", 1) + cookies.append({"name": k, "value": v}) + return cookies + + +# -- Main interception functionality -- +def request(flow: http.HTTPFlow) -> None: + """Add a specific set of cookies to every request.""" + # obtain any cookies from the request + _req_cookies_str = flow.request.headers.get("cookie", "") + req_cookies = parse_cookies(_req_cookies_str) + + # add our cookies to the original cookies from the request + all_cookies = req_cookies + load_json_cookies() + # NOTE: by adding it to the end we should overwrite any existing cookies + # of the same name but if you want to be more careful you can iterate over + # the req_cookies and remove the ones you want to overwrite first. + + # modify the request with the combined cookies + flow.request.headers["cookie"] = stringify_cookies(all_cookies) + + +def response(flow: http.HTTPFlow) -> None: + """Remove a specific cookie from every response.""" + set_cookies_str = flow.response.headers.get("set-cookie", "") + # NOTE: use safe attribute access (.get), in some cases there might not be a set-cookie header + + if set_cookies_str: + resp_cookies = parse_cookies(set_cookies_str) + + # remove the cookie we want to remove + resp_cookies = [c for c in resp_cookies if c["name"] not in FILTER_COOKIES] + + # modify the request with the combined cookies + flow.response.headers["set-cookie"] = stringify_cookies(resp_cookies) diff --git a/examples/contrib/httpdump.py b/examples/contrib/httpdump.py new file mode 100644 index 0000000000..c9e9eede31 --- /dev/null +++ b/examples/contrib/httpdump.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# dump content to files based on a filter +# usage: mitmdump -s httpdump.py "~ts application/json" +# +# options: +# - dumper_folder: content dump destination folder (default: ./httpdump) +# - open_browser: open integrated browser with proxy configured at start (default: true) +# +# remember to add your own mitmproxy authorative certs in your browser/os! +# certs docs: https://docs.mitmproxy.org/stable/concepts-certificates/ +# filter expressions docs: https://docs.mitmproxy.org/stable/concepts-filters/ +import os +import mimetypes +from pathlib import Path + +from mitmproxy import flowfilter +from mitmproxy import ctx, http + + +class HTTPDump: + def load(self, loader): + self.filter = ctx.options.dumper_filter + + loader.add_option( + name = "dumper_folder", + typespec = str, + default = "httpdump", + help = "content dump destination folder", + ) + loader.add_option( + name = "open_browser", + typespec = bool, + default = True, + help = "open integrated browser at start" + ) + + def running(self): + if ctx.options.open_browser: + ctx.master.commands.call("browser.start") + + def configure(self, updated): + if "dumper_filter" in updated: + self.filter = ctx.options.dumper_filter + + def response(self, flow: http.HTTPFlow) -> None: + if flowfilter.match(self.filter, flow): + self.dump(flow) + + def dump(self, flow: http.HTTPFlow): + if not flow.response: + return + + # create dir + folder = Path(ctx.options.dumper_folder) / flow.request.host + if not folder.exists(): + os.makedirs(folder) + + # calculate path + path = "-".join(flow.request.path_components) + filename = "-".join([path, flow.id]) + content_type = flow.response.headers.get("content-type", "").split(";")[0] + ext = mimetypes.guess_extension(content_type) or "" + filepath = folder / f"{filename}{ext}" + + # dump to file + if flow.response.content: + with open(filepath, "wb") as f: + f.write(flow.response.content) + ctx.log.info(f"Saved! {filepath}") + + +addons = [HTTPDump()] diff --git a/examples/contrib/ntlm_upstream_proxy.py b/examples/contrib/ntlm_upstream_proxy.py new file mode 100644 index 0000000000..6d99269d3b --- /dev/null +++ b/examples/contrib/ntlm_upstream_proxy.py @@ -0,0 +1,186 @@ +import base64 +import binascii +import socket +from typing import Any, Optional + +from ntlm_auth import gss_channel_bindings, ntlm + +from mitmproxy import addonmanager, http +from mitmproxy import ctx +from mitmproxy.net.http import http1 +from mitmproxy.proxy import commands, layer +from mitmproxy.proxy.context import Context +from mitmproxy.proxy.layers.http import HttpConnectUpstreamHook, HttpLayer, HttpStream +from mitmproxy.proxy.layers.http._upstream_proxy import HttpUpstreamProxy + + +class NTLMUpstreamAuth: + """ + This addon handles authentication to systems upstream from us for the + upstream proxy and reverse proxy mode. There are 3 cases: + - Upstream proxy CONNECT requests should have authentication added, and + subsequent already connected requests should not. + - Upstream proxy regular requests + - Reverse proxy regular requests (CONNECT is invalid in this mode) + """ + + def load(self, loader: addonmanager.Loader) -> None: + ctx.log.info("NTLMUpstreamAuth loader") + loader.add_option( + name="upstream_ntlm_auth", + typespec=Optional[str], + default=None, + help=""" + Add HTTP NTLM authentication to upstream proxy requests. + Format: username:password. + """ + ) + loader.add_option( + name="upstream_ntlm_domain", + typespec=Optional[str], + default=None, + help=""" + Add HTTP NTLM domain for authentication to upstream proxy requests. + """ + ) + loader.add_option( + name="upstream_proxy_address", + typespec=Optional[str], + default=None, + help=""" + upstream poxy address. + """ + ) + loader.add_option( + name="upstream_ntlm_compatibility", + typespec=int, + default=3, + help=""" + Add HTTP NTLM compatibility for authentication to upstream proxy requests. + Valid values are 0-5 (Default: 3) + """ + ) + ctx.log.debug("AddOn: NTLM Upstream Authentication - Loaded") + + def running(self): + def extract_flow_from_context(context: Context) -> http.HTTPFlow: + if context and context.layers: + for l in context.layers: + if isinstance(l, HttpLayer): + for _, stream in l.streams.items(): + return stream.flow if isinstance(stream, HttpStream) else None + + def build_connect_flow(context: Context, connect_header: tuple) -> http.HTTPFlow: + flow = extract_flow_from_context(context) + if not flow: + ctx.log.error("failed to build connect flow") + raise + flow.request.content = b"" # we should send empty content for handshake + header_name, header_value = connect_header + flow.request.headers.add(header_name, header_value) + return flow + + def patched_start_handshake(self) -> layer.CommandGenerator[None]: + assert self.conn.address + self.ntlm_context = CustomNTLMContext(ctx) + proxy_authorization = self.ntlm_context.get_ntlm_start_negotiate_message() + self.flow = build_connect_flow(self.context, ("Proxy-Authorization", proxy_authorization)) + yield HttpConnectUpstreamHook(self.flow) + raw = http1.assemble_request(self.flow.request) + yield commands.SendData(self.tunnel_connection, raw) + + def extract_proxy_authenticate_msg(response_head: list) -> str: + for header in response_head: + if b'Proxy-Authenticate' in header: + challenge_message = str(bytes(header).decode('utf-8')) + try: + token = challenge_message.split(': ')[1] + except IndexError: + ctx.log.error("Failed to extract challenge_message") + raise + return token + + def patched_receive_handshake_data(self, data) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: + self.buf += data + response_head = self.buf.maybe_extract_lines() + if response_head: + response_head = [bytes(x) for x in response_head] + try: + response = http1.read_response_head(response_head) + except ValueError: + return True, None + challenge_message = extract_proxy_authenticate_msg(response_head) + if 200 <= response.status_code < 300: + if self.buf: + yield from self.receive_data(data) + del self.buf + return True, None + else: + if not challenge_message: + return True, None + proxy_authorization = self.ntlm_context.get_ntlm_challenge_response_message(challenge_message) + self.flow = build_connect_flow(self.context, ("Proxy-Authorization", proxy_authorization)) + raw = http1.assemble_request(self.flow.request) + yield commands.SendData(self.tunnel_connection, raw) + return False, None + else: + return False, None + + HttpUpstreamProxy.start_handshake = patched_start_handshake + HttpUpstreamProxy.receive_handshake_data = patched_receive_handshake_data + + def done(self): + ctx.log.info('close ntlm session') + + +addons = [ + NTLMUpstreamAuth() +] + + +class CustomNTLMContext: + def __init__(self, + ctx, + preferred_type: str = 'NTLM', + cbt_data: gss_channel_bindings.GssChannelBindingsStruct = None): + # TODO:// take care the cbt_data + auth: str = ctx.options.upstream_ntlm_auth + domain: str = str(ctx.options.upstream_ntlm_domain).upper() + ntlm_compatibility: int = ctx.options.upstream_ntlm_compatibility + username, password = tuple(auth.split(":")) + workstation = socket.gethostname().upper() + ctx.log.debug(f'\nntlm context with the details: "{domain}\\{username}", *****') + self.ctx_log = ctx.log + self.preferred_type = preferred_type + self.ntlm_context = ntlm.NtlmContext( + username=username, + password=password, + domain=domain, + workstation=workstation, + ntlm_compatibility=ntlm_compatibility, + cbt_data=cbt_data) + + def get_ntlm_start_negotiate_message(self) -> str: + negotiate_message = self.ntlm_context.step() + negotiate_message_base_64_in_bytes = base64.b64encode(negotiate_message) + negotiate_message_base_64_ascii = negotiate_message_base_64_in_bytes.decode("ascii") + negotiate_message_base_64_final = f'{self.preferred_type} {negotiate_message_base_64_ascii}' + self.ctx_log.debug( + f'{self.preferred_type} Authentication, negotiate message: {negotiate_message_base_64_final}' + ) + return negotiate_message_base_64_final + + def get_ntlm_challenge_response_message(self, challenge_message: str) -> Any: + challenge_message = challenge_message.replace(self.preferred_type + " ", "", 1) + try: + challenge_message_ascii_bytes = base64.b64decode(challenge_message, validate=True) + except binascii.Error as err: + self.ctx_log.debug(f'{self.preferred_type} Authentication fail with error {err.__str__()}') + return False + authenticate_message = self.ntlm_context.step(challenge_message_ascii_bytes) + negotiate_message_base_64 = '{} {}'.format(self.preferred_type, + base64.b64encode(authenticate_message).decode('ascii')) + self.ctx_log.debug( + f'{self.preferred_type} Authentication, response to challenge message: {negotiate_message_base_64}' + ) + return negotiate_message_base_64 diff --git a/examples/contrib/save_streamed_data.py b/examples/contrib/save_streamed_data.py new file mode 100644 index 0000000000..e258f963e1 --- /dev/null +++ b/examples/contrib/save_streamed_data.py @@ -0,0 +1,121 @@ +""" +Save streamed requests and responses + +If the option 'save_streamed_data' is set to a format string then +streamed requests and responses are written to individual files with a name +derived from the string. Apart from python strftime() formating (using the +request start time) the following codes can also be used: + - %+T: The time stamp of the request with microseconds + - %+D: 'req' or 'rsp' indicating the direction of the data + - %+I: The client connection ID + - %+C: The client IP address +A good starting point for a template could be '~/streamed_files/%+D:%+T:%+I', +a more complex example is '~/streamed_files/%+C/%Y-%m-%d%/%+D:%+T:%+I'. +The client connection ID combined with the request time stamp should be unique +for associating a file with its corresponding flow in the stream saved with +'--save-stream-file'. + +This addon is not compatible with addons that use the same mechanism to +capture streamed data, http-stream-modify.py for instance. +""" +from typing import Optional + +from mitmproxy import ctx +from datetime import datetime +from pathlib import Path +import os + + +class StreamSaver: + + TAG = "save_streamed_data: " + + def __init__(self, flow, direction): + self.flow = flow + self.direction = direction + self.fh = None + self.path = None + + def done(self): + if self.fh: + self.fh.close() + self.fh = None + # Make sure we have no circular references + self.flow = None + + def __call__(self, data): + # End of stream? + if len(data) == 0: + self.done() + return data + + # Just in case the option changes while a stream is in flight + if not ctx.options.save_streamed_data: + return data + + # This is a safeguard but should not be needed + if not self.flow or not self.flow.request: + return data + + if not self.fh: + self.path = datetime.fromtimestamp(self.flow.request.timestamp_start).strftime(ctx.options.save_streamed_data) + self.path = self.path.replace('%+T', str(self.flow.request.timestamp_start)) + self.path = self.path.replace('%+I', str(self.flow.client_conn.id)) + self.path = self.path.replace('%+D', self.direction) + self.path = self.path.replace('%+C', self.flow.client_conn.address[0]) + self.path = os.path.expanduser(self.path) + + parent = Path(self.path).parent + try: + if not parent.exists(): + parent.mkdir(parents=True, exist_ok=True) + except OSError: + ctx.log.error(f"{self.TAG}Failed to create directory: {parent}") + + try: + self.fh = open(self.path, "wb", buffering=0) + except OSError: + ctx.log.error(f"{self.TAG}Failed to open for writing: {self.path}") + + if self.fh: + try: + self.fh.write(data) + except OSError: + ctx.log.error(f"{self.TAG}Failed to write to: {self.path}") + + return data + + +def load(loader): + loader.add_option( + "save_streamed_data", Optional[str], None, + "Format string for saving streamed data to files. If set each streamed request or response is written " + "to a file with a name derived from the string. In addition to formating supported by python " + "strftime() (using the request start time) the code '%+T' is replaced with the time stamp of the request, " + "'%+D' by 'req' or 'rsp' depending on the direction of the data, '%+C' by the client IP addresses and " + "'%+I' by the client connection ID." + ) + + +def requestheaders(flow): + if ctx.options.save_streamed_data and flow.request.stream: + flow.request.stream = StreamSaver(flow, 'req') + + +def responseheaders(flow): + if isinstance(flow.request.stream, StreamSaver): + flow.request.stream.done() + if ctx.options.save_streamed_data and flow.response.stream: + flow.response.stream = StreamSaver(flow, 'rsp') + + +def response(flow): + if isinstance(flow.response.stream, StreamSaver): + flow.response.stream.done() + + +def error(flow): + if flow.request and isinstance(flow.request.stream, StreamSaver): + flow.request.stream.done() + if flow.response and isinstance(flow.response.stream, StreamSaver): + flow.response.stream.done() diff --git a/examples/contrib/search.py b/examples/contrib/search.py new file mode 100644 index 0000000000..c60361698b --- /dev/null +++ b/examples/contrib/search.py @@ -0,0 +1,94 @@ +import re +from collections.abc import Sequence + +from json import dumps + +from mitmproxy import command, ctx, flow + + +MARKER = ':mag:' +RESULTS_STR = 'Search Results: ' + + +class Search: + def __init__(self): + self.exp = None + + @command.command('search') + def _search(self, + flows: Sequence[flow.Flow], + regex: str) -> None: + """ + Defines a command named "search" that matches + the given regular expression against most parts + of each request/response included in the selected flows. + + Usage: from the flow list view, type ":search" followed by + a space, then a flow selection expression; e.g., "@shown", + then the desired regular expression to perform the search. + + Alternatively, define a custom shortcut in keys.yaml; e.g.: + - + key: "/" + ctx: ["flowlist"] + cmd: "console.command search @shown " + + Flows containing matches to the expression will be marked + with the magnifying glass emoji, and their comments will + contain JSON-formatted search results. + + To view flow comments, enter the flow view + and navigate to the detail tab. + """ + + try: + self.exp = re.compile(regex) + except re.error as e: + ctx.log.error(e) + return + + for _flow in flows: + # Erase previous results while preserving other comments: + comments = list() + for c in _flow.comment.split('\n'): + if c.startswith(RESULTS_STR): + break + comments.append(c) + _flow.comment = '\n'.join(comments) + + if _flow.marked == MARKER: + _flow.marked = False + + results = {k: v for k, v in self.flow_results(_flow).items() if v} + if results: + comments.append(RESULTS_STR) + comments.append(dumps(results, indent=2)) + _flow.comment = '\n'.join(comments) + _flow.marked = MARKER + + def header_results(self, message): + results = {k: self.exp.findall(v) for k, v in message.headers.items()} + return {k: v for k, v in results.items() if v} + + def flow_results(self, _flow): + results = dict() + results.update( + {'flow_comment': self.exp.findall(_flow.comment)}) + if _flow.request is not None: + results.update( + {'request_path': self.exp.findall(_flow.request.path)}) + results.update( + {'request_headers': self.header_results(_flow.request)}) + if _flow.request.text: + results.update( + {'request_body': self.exp.findall(_flow.request.text)}) + if _flow.response is not None: + results.update( + {'response_headers': self.header_results(_flow.response)}) + if _flow.response.text: + results.update( + {'response_body': self.exp.findall(_flow.response.text)}) + return results + + +addons = [Search()] diff --git a/examples/contrib/sslstrip.py b/examples/contrib/sslstrip.py index 16d9b59a47..05aa5f3e5f 100644 --- a/examples/contrib/sslstrip.py +++ b/examples/contrib/sslstrip.py @@ -4,12 +4,11 @@ """ import re import urllib.parse -import typing # noqa from mitmproxy import http # set of SSL/TLS capable hosts -secure_hosts: typing.Set[str] = set() +secure_hosts: set[str] = set() def request(flow: http.HTTPFlow) -> None: diff --git a/examples/contrib/tls_passthrough.py b/examples/contrib/tls_passthrough.py index 8f84f318f9..811d56abfe 100644 --- a/examples/contrib/tls_passthrough.py +++ b/examples/contrib/tls_passthrough.py @@ -1,8 +1,5 @@ -# FIXME: This addon is currently not compatible with mitmproxy 7 and above. - """ -This inline script allows conditional TLS Interception based -on a user-defined strategy. +This addon allows conditional TLS Interception based on a user-defined strategy. Example: @@ -11,138 +8,101 @@ 1. curl --proxy http://localhost:8080 https://example.com --insecure // works - we'll also see the contents in mitmproxy - 2. curl --proxy http://localhost:8080 https://example.com --insecure - // still works - we'll also see the contents in mitmproxy - - 3. curl --proxy http://localhost:8080 https://example.com + 2. curl --proxy http://localhost:8080 https://example.com // fails with a certificate error, which we will also see in mitmproxy - 4. curl --proxy http://localhost:8080 https://example.com + 3. curl --proxy http://localhost:8080 https://example.com // works again, but mitmproxy does not intercept and we do *not* see the contents - -Authors: Maximilian Hils, Matthew Tuusberg """ import collections import random - +from abc import ABC, abstractmethod from enum import Enum -import mitmproxy -from mitmproxy import ctx -from mitmproxy.exceptions import TlsProtocolException -from mitmproxy.proxy.protocol import TlsLayer, RawTCPLayer +from mitmproxy import connection, ctx, tls +from mitmproxy.utils import human class InterceptionResult(Enum): - success = True - failure = False - skipped = None + SUCCESS = 1 + FAILURE = 2 + SKIPPED = 3 -class _TlsStrategy: - """ - Abstract base class for interception strategies. - """ - +class TlsStrategy(ABC): def __init__(self): # A server_address -> interception results mapping self.history = collections.defaultdict(lambda: collections.deque(maxlen=200)) - def should_intercept(self, server_address): - """ - Returns: - True, if we should attempt to intercept the connection. - False, if we want to employ pass-through instead. - """ + @abstractmethod + def should_intercept(self, server_address: connection.Address) -> bool: raise NotImplementedError() def record_success(self, server_address): - self.history[server_address].append(InterceptionResult.success) + self.history[server_address].append(InterceptionResult.SUCCESS) def record_failure(self, server_address): - self.history[server_address].append(InterceptionResult.failure) + self.history[server_address].append(InterceptionResult.FAILURE) def record_skipped(self, server_address): - self.history[server_address].append(InterceptionResult.skipped) + self.history[server_address].append(InterceptionResult.SKIPPED) -class ConservativeStrategy(_TlsStrategy): +class ConservativeStrategy(TlsStrategy): """ Conservative Interception Strategy - only intercept if there haven't been any failed attempts in the history. """ + def should_intercept(self, server_address: connection.Address) -> bool: + return InterceptionResult.FAILURE not in self.history[server_address] - def should_intercept(self, server_address): - if InterceptionResult.failure in self.history[server_address]: - return False - return True - -class ProbabilisticStrategy(_TlsStrategy): +class ProbabilisticStrategy(TlsStrategy): """ Fixed probability that we intercept a given connection. """ - - def __init__(self, p): + def __init__(self, p: float): self.p = p super().__init__() - def should_intercept(self, server_address): + def should_intercept(self, server_address: connection.Address) -> bool: return random.uniform(0, 1) < self.p -class TlsFeedback(TlsLayer): - """ - Monkey-patch _establish_tls_with_client to get feedback if TLS could be established - successfully on the client connection (which may fail due to cert pinning). - """ +class MaybeTls: + strategy: TlsStrategy - def _establish_tls_with_client(self): - server_address = self.server_conn.address + def load(self, l): + l.add_option( + "tls_strategy", int, 0, + "TLS passthrough strategy. If set to 0, connections will be passed through after the first unsuccessful " + "handshake. If set to 0 < p <= 100, connections with be passed through with probability p.", + ) - try: - super()._establish_tls_with_client() - except TlsProtocolException as e: - tls_strategy.record_failure(server_address) - raise e + def configure(self, updated): + if "tls_strategy" not in updated: + return + if ctx.options.tls_strategy > 0: + self.strategy = ProbabilisticStrategy(ctx.options.tls_strategy / 100) else: - tls_strategy.record_success(server_address) - - -# inline script hooks below. - -tls_strategy = None + self.strategy = ConservativeStrategy() + def tls_clienthello(self, data: tls.ClientHelloData): + server_address = data.context.server.peername + if not self.strategy.should_intercept(server_address): + ctx.log(f"TLS passthrough: {human.format_address(server_address)}.") + data.ignore_connection = True + self.strategy.record_skipped(server_address) -def load(l): - l.add_option( - "tlsstrat", int, 0, "TLS passthrough strategy (0-100)", - ) + def tls_established_client(self, data: tls.TlsData): + server_address = data.context.server.peername + ctx.log(f"TLS handshake successful: {human.format_address(server_address)}") + self.strategy.record_success(server_address) + def tls_failed_client(self, data: tls.TlsData): + server_address = data.context.server.peername + ctx.log(f"TLS handshake failed: {human.format_address(server_address)}") + self.strategy.record_failure(server_address) -def configure(updated): - global tls_strategy - if ctx.options.tlsstrat > 0: - tls_strategy = ProbabilisticStrategy(float(ctx.options.tlsstrat) / 100.0) - else: - tls_strategy = ConservativeStrategy() - -def next_layer(next_layer): - """ - This hook does the actual magic - if the next layer is planned to be a TLS layer, - we check if we want to enter pass-through mode instead. - """ - if isinstance(next_layer, TlsLayer) and next_layer._client_tls: - server_address = next_layer.server_conn.address - - if tls_strategy.should_intercept(server_address): - # We try to intercept. - # Monkey-Patch the layer to get feedback from the TLSLayer if interception worked. - next_layer.__class__ = TlsFeedback - else: - # We don't intercept - reply with a pass-through layer and add a "skipped" entry. - mitmproxy.ctx.log("TLS passthrough for %s" % repr(next_layer.server_conn.address), "info") - next_layer_replacement = RawTCPLayer(next_layer.ctx, ignore=True) - next_layer.reply.send(next_layer_replacement) - tls_strategy.record_skipped(server_address) +addons = [MaybeTls()] diff --git a/examples/contrib/webscanner_helper/mapping.py b/examples/contrib/webscanner_helper/mapping.py index 4868f14a7a..333809f537 100644 --- a/examples/contrib/webscanner_helper/mapping.py +++ b/examples/contrib/webscanner_helper/mapping.py @@ -1,7 +1,5 @@ import copy import logging -import typing -from typing import Dict from bs4 import BeautifulSoup @@ -96,7 +94,7 @@ def replace(self, soup: BeautifulSoup, css_sel: str, replace: BeautifulSoup) -> self.logger.debug(f"replace \"{content}\" with \"{replace}\"") content.replace_with(copy.copy(replace)) - def apply_template(self, soup: BeautifulSoup, template: Dict[str, typing.Union[BeautifulSoup]]) -> None: + def apply_template(self, soup: BeautifulSoup, template: dict[str, BeautifulSoup]) -> None: """Applies the given mapping template to the given soup.""" for css_sel, replace in template.items(): mapped = soup.select(css_sel) diff --git a/examples/contrib/webscanner_helper/proxyauth_selenium.py b/examples/contrib/webscanner_helper/proxyauth_selenium.py index 4a00a8d067..6ac1d94de6 100644 --- a/examples/contrib/webscanner_helper/proxyauth_selenium.py +++ b/examples/contrib/webscanner_helper/proxyauth_selenium.py @@ -3,7 +3,7 @@ import random import string import time -from typing import Dict, List, cast, Any +from typing import Any, cast import mitmproxy.http from mitmproxy import flowfilter @@ -65,7 +65,7 @@ def __init__(self, fltr: str, domain: str, profile.set_preference('network.proxy.type', 0) self.browser = webdriver.Firefox(firefox_profile=profile, options=options) - self.cookies: List[Dict[str, str]] = [] + self.cookies: list[dict[str, str]] = [] def _login(self, flow): self.cookies = self.login(flow) @@ -128,5 +128,5 @@ def _set_request_cookies(self, flow: mitmproxy.http.HTTPFlow): flow.request.headers["cookie"] = cookies @abc.abstractmethod - def login(self, flow: mitmproxy.http.HTTPFlow) -> List[Dict[str, str]]: + def login(self, flow: mitmproxy.http.HTTPFlow) -> list[dict[str, str]]: pass diff --git a/examples/contrib/webscanner_helper/test_urlindex.py b/examples/contrib/webscanner_helper/test_urlindex.py index e144d4fa0c..058a36068f 100644 --- a/examples/contrib/webscanner_helper/test_urlindex.py +++ b/examples/contrib/webscanner_helper/test_urlindex.py @@ -2,7 +2,6 @@ from json import JSONDecodeError from pathlib import Path from unittest import mock -from typing import List from unittest.mock import patch from mitmproxy.test import tflow @@ -30,7 +29,7 @@ class TestSetEncoder: def test_set_encoder_set(self): test_set = {"foo", "bar", "42"} result = SetEncoder.default(SetEncoder(), test_set) - assert isinstance(result, List) + assert isinstance(result, list) assert 'foo' in result assert 'bar' in result assert '42' in result diff --git a/examples/contrib/webscanner_helper/urldict.py b/examples/contrib/webscanner_helper/urldict.py index daceb47fe6..7e990f1afc 100644 --- a/examples/contrib/webscanner_helper/urldict.py +++ b/examples/contrib/webscanner_helper/urldict.py @@ -1,8 +1,7 @@ import itertools import json -import typing from collections.abc import MutableMapping -from typing import Any, Dict, Generator, List, TextIO, Callable +from typing import Any, Callable, Generator, TextIO, Union, cast from mitmproxy import flowfilter from mitmproxy.http import HTTPFlow @@ -16,7 +15,7 @@ class URLDict(MutableMapping): """Data structure to store information using filters as keys.""" def __init__(self): - self.store: Dict[flowfilter.TFilter, Any] = {} + self.store: dict[flowfilter.TFilter, Any] = {} def __getitem__(self, key, *, count=0): if count: @@ -51,7 +50,7 @@ def get_generator(self, flow: HTTPFlow) -> Generator[Any, None, None]: if flowfilter.match(fltr, flow): yield value - def get(self, flow: HTTPFlow, default=None, *, count=0) -> List[Any]: + def get(self, flow: HTTPFlow, default=None, *, count=0) -> list[Any]: try: return self.__getitem__(flow, count=count) except KeyError: @@ -74,12 +73,12 @@ def loads(cls, json_str: str, value_loader: Callable = f_id): json_obj = json.loads(json_str) return cls._load(json_obj, value_loader) - def _dump(self, value_dumper: Callable = f_id) -> Dict: - dumped: Dict[typing.Union[flowfilter.TFilter, str], Any] = {} + def _dump(self, value_dumper: Callable = f_id) -> dict: + dumped: dict[Union[flowfilter.TFilter, str], Any] = {} for fltr, value in self.store.items(): if hasattr(fltr, 'pattern'): # cast necessary for mypy - dumped[typing.cast(Any, fltr).pattern] = value_dumper(value) + dumped[cast(Any, fltr).pattern] = value_dumper(value) else: dumped[str(fltr)] = value_dumper(value) return dumped diff --git a/examples/contrib/webscanner_helper/urlindex.py b/examples/contrib/webscanner_helper/urlindex.py index db8b1c5622..650e47c015 100644 --- a/examples/contrib/webscanner_helper/urlindex.py +++ b/examples/contrib/webscanner_helper/urlindex.py @@ -3,7 +3,7 @@ import json import logging from pathlib import Path -from typing import Type, Dict, Union, Optional +from typing import Optional, Union from mitmproxy import flowfilter from mitmproxy.http import HTTPFlow @@ -29,12 +29,10 @@ def __init__(self, filename: Path): @abc.abstractmethod def load(self): """Load existing URL index.""" - pass @abc.abstractmethod def add_url(self, flow: HTTPFlow): """Add new URL to URL index.""" - pass @abc.abstractmethod def save(self): @@ -97,7 +95,7 @@ def save(self): pass -WRITER: Dict[str, Type[UrlIndexWriter]] = { +WRITER: dict[str, type[UrlIndexWriter]] = { "json": JSONUrlIndexWriter, "text": TextUrlIndexWriter, } diff --git a/examples/contrib/webscanner_helper/urlinjection.py b/examples/contrib/webscanner_helper/urlinjection.py index aa86168203..6c4f982915 100644 --- a/examples/contrib/webscanner_helper/urlinjection.py +++ b/examples/contrib/webscanner_helper/urlinjection.py @@ -16,7 +16,6 @@ class InjectionGenerator: @abc.abstractmethod def inject(self, index, flow: HTTPFlow): """Injects the given URL index into the given flow.""" - pass class HTMLInjection(InjectionGenerator): diff --git a/examples/contrib/webscanner_helper/watchdog.py b/examples/contrib/webscanner_helper/watchdog.py index 867d219681..48f58d9c08 100644 --- a/examples/contrib/webscanner_helper/watchdog.py +++ b/examples/contrib/webscanner_helper/watchdog.py @@ -1,8 +1,8 @@ import pathlib import time -import typing import logging from datetime import datetime +from typing import Union import mitmproxy.connections import mitmproxy.http @@ -35,8 +35,8 @@ def __init__(self, event, outdir: pathlib.Path, timeout=None): raise RuntimeError("Watchtdog output path must be a directory.") elif not self.flow_dir.exists(): self.flow_dir.mkdir(parents=True) - self.last_trigger: typing.Union[None, float] = None - self.timeout: typing.Union[None, float] = timeout + self.last_trigger: Union[None, float] = None + self.timeout: Union[None, float] = timeout def serverconnect(self, conn: mitmproxy.connections.ServerConnection): if self.timeout is not None: diff --git a/examples/contrib/xss_scanner.py b/examples/contrib/xss_scanner.py index 683f23fdd7..368662cfbd 100644 --- a/examples/contrib/xss_scanner.py +++ b/examples/contrib/xss_scanner.py @@ -36,7 +36,7 @@ """ from html.parser import HTMLParser -from typing import Dict, Union, Tuple, Optional, List, NamedTuple +from typing import NamedTuple, Optional, Union from urllib.parse import urlparse import re import socket @@ -79,8 +79,8 @@ class SQLiData(NamedTuple): dbms: str -VulnData = Tuple[Optional[XSSData], Optional[SQLiData]] -Cookies = Dict[str, str] +VulnData = tuple[Optional[XSSData], Optional[SQLiData]] +Cookies = dict[str, str] def get_cookies(flow: http.HTTPFlow) -> Cookies: @@ -92,14 +92,14 @@ def get_cookies(flow: http.HTTPFlow) -> Cookies: def find_unclaimed_URLs(body, requestUrl): """ Look for unclaimed URLs in script tags and log them if found""" - def getValue(attrs: List[Tuple[str, str]], attrName: str) -> Optional[str]: + def getValue(attrs: list[tuple[str, str]], attrName: str) -> Optional[str]: for name, value in attrs: if attrName == name: return value return None class ScriptURLExtractor(HTMLParser): - script_URLs: List[str] = [] + script_URLs: list[str] = [] def handle_starttag(self, tag, attrs): if (tag == "script" or tag == "iframe") and "src" in [name for name, value in attrs]: @@ -245,7 +245,7 @@ def inside_quote(qc: str, substring_bytes: bytes, text_index: int, body_bytes: b return False -def paths_to_text(html: str, string: str) -> List[str]: +def paths_to_text(html: str, string: str) -> list[str]: """ Return list of Paths to a given str in the given HTML tree - Note that it does a BFS """ @@ -258,7 +258,7 @@ def remove_last_occurence_of_sub_string(string: str, substr: str) -> str: class PathHTMLParser(HTMLParser): currentPath = "" - paths: List[str] = [] + paths: list[str] = [] def handle_starttag(self, tag, attrs): self.currentPath += ("/" + tag) diff --git a/mitmproxy/addonmanager.py b/mitmproxy/addonmanager.py index a0def682ca..27c3a8a560 100644 --- a/mitmproxy/addonmanager.py +++ b/mitmproxy/addonmanager.py @@ -1,12 +1,13 @@ import contextlib +import inspect import pprint import sys import traceback import types -import typing +from collections.abc import Callable, Sequence from dataclasses import dataclass +from typing import Any, Optional -from mitmproxy import controller from mitmproxy import hooks from mitmproxy import exceptions from mitmproxy import flow @@ -45,59 +46,52 @@ def safecall(): raise except Exception: etype, value, tb = sys.exc_info() + tb = cut_traceback(tb, "invoke_addon_sync") tb = cut_traceback(tb, "invoke_addon") ctx.log.error( - "Addon error: %s" % "".join( - traceback.format_exception(etype, value, tb) - ) + "Addon error: %s" % "".join(traceback.format_exception(etype, value, tb)) ) class Loader: """ - A loader object is passed to the load() event when addons start up. + A loader object is passed to the load() event when addons start up. """ def __init__(self, master): self.master = master def add_option( - self, - name: str, - typespec: type, - default: typing.Any, - help: str, - choices: typing.Optional[typing.Sequence[str]] = None + self, + name: str, + typespec: type, + default: Any, + help: str, + choices: Optional[Sequence[str]] = None, ) -> None: """ - Add an option to mitmproxy. + Add an option to mitmproxy. - Help should be a single paragraph with no linebreaks - it will be - reflowed by tools. Information on the data type should be omitted - - it will be generated and added by tools as needed. + Help should be a single paragraph with no linebreaks - it will be + reflowed by tools. Information on the data type should be omitted - + it will be generated and added by tools as needed. """ if name in self.master.options: existing = self.master.options._options[name] same_signature = ( - existing.name == name and - existing.typespec == typespec and - existing.default == default and - existing.help == help and - existing.choices == choices + existing.name == name + and existing.typespec == typespec + and existing.default == default + and existing.help == help + and existing.choices == choices ) if same_signature: return else: ctx.log.warn("Over-riding existing option %s" % name) - self.master.options.add_option( - name, - typespec, - default, - help, - choices - ) + self.master.options.add_option(name, typespec, default, help, choices) - def add_command(self, path: str, func: typing.Callable) -> None: + def add_command(self, path: str, func: Callable) -> None: """Add a command to mitmproxy. Unless you are generating commands programatically, @@ -108,7 +102,7 @@ def add_command(self, path: str, func: typing.Callable) -> None: def traverse(chain): """ - Recursively traverse an addon chain. + Recursively traverse an addon chain. """ for a in chain: yield a @@ -123,6 +117,7 @@ class LoadHook(hooks.Hook): object, which contains methods for adding options and commands. This method is where the addon configures itself. """ + loader: Loader @@ -138,30 +133,30 @@ def _configure_all(self, options, updated): def clear(self): """ - Remove all addons. + Remove all addons. """ for a in self.chain: - self.invoke_addon(a, hooks.DoneHook()) + self.invoke_addon_sync(a, hooks.DoneHook()) self.lookup = {} self.chain = [] def get(self, name): """ - Retrieve an addon by name. Addon names are equal to the .name - attribute on the instance, or the lower case class name if that - does not exist. + Retrieve an addon by name. Addon names are equal to the .name + attribute on the instance, or the lower case class name if that + does not exist. """ return self.lookup.get(name, None) def register(self, addon): """ - Register an addon, call its load event, and then register all its - sub-addons. This should be used by addons that dynamically manage - addons. + Register an addon, call its load event, and then register all its + sub-addons. This should be used by addons that dynamically manage + addons. - If the calling addon is already running, it should follow with - running and configure events. Must be called within a current - context. + If the calling addon is already running, it should follow with + running and configure events. Must be called within a current + context. """ api_changes = { # mitmproxy 6 -> mitmproxy 7 @@ -173,15 +168,17 @@ def register(self, addon): for a in traverse([addon]): for old, new in api_changes.items(): if hasattr(a, old): - ctx.log.warn(f"The {old} event has been removed, use {new} instead. " - f"For more details, see https://docs.mitmproxy.org/stable/addons-events/.") + ctx.log.warn( + f"The {old} event has been removed, use {new} instead. " + f"For more details, see https://docs.mitmproxy.org/stable/addons-events/." + ) name = _get_name(a) if name in self.lookup: raise exceptions.AddonManagerError( "An addon called '%s' already exists." % name ) l = Loader(self.master) - self.invoke_addon(addon, LoadHook(l)) + self.invoke_addon_sync(addon, LoadHook(l)) for a in traverse([addon]): name = _get_name(a) self.lookup[name] = a @@ -192,19 +189,19 @@ def register(self, addon): def add(self, *addons): """ - Add addons to the end of the chain, and run their load event. - If any addon has sub-addons, they are registered. + Add addons to the end of the chain, and run their load event. + If any addon has sub-addons, they are registered. """ for i in addons: self.chain.append(self.register(i)) def remove(self, addon): """ - Remove an addon and all its sub-addons. + Remove an addon and all its sub-addons. - If the addon is not in the chain - that is, if it's managed by a - parent addon - it's the parent's responsibility to remove it from - its own addons attribute. + If the addon is not in the chain - that is, if it's managed by a + parent addon - it's the parent's responsibility to remove it from + its own addons attribute. """ for a in traverse([addon]): n = _get_name(a) @@ -212,7 +209,7 @@ def remove(self, addon): raise exceptions.AddonManagerError("No such addon: %s" % n) self.chain = [i for i in self.chain if i is not a] del self.lookup[_get_name(a)] - self.invoke_addon(addon, hooks.DoneHook()) + self.invoke_addon_sync(addon, hooks.DoneHook()) def __len__(self): return len(self.chain) @@ -226,42 +223,25 @@ def __contains__(self, item): async def handle_lifecycle(self, event: hooks.Hook): """ - Handle a lifecycle event. + Handle a lifecycle event. """ message = event.args()[0] - if not hasattr(message, "reply"): # pragma: no cover - raise exceptions.ControlException( - "Message %s has no reply attribute" % message - ) - - # We can use DummyReply objects multiple times. We only clear them up on - # the next handler so that we can access value and state in the - # meantime. - if isinstance(message.reply, controller.DummyReply): - message.reply.reset() - - self.trigger(event) - if message.reply.state == "start": - message.reply.take() - message.reply.commit() - - if isinstance(message.reply, controller.DummyReply): - message.reply.mark_reset() + await self.trigger_event(event) if isinstance(message, flow.Flow): - self.trigger(hooks.UpdateHook([message])) + await self.trigger_event(hooks.UpdateHook([message])) - def invoke_addon(self, addon, event: hooks.Hook): + def _iter_hooks(self, addon, event: hooks.Hook): """ - Invoke an event on an addon and all its children. + Enumerate all hook callables belonging to the given addon """ assert isinstance(event, hooks.Hook) for a in traverse([addon]): func = getattr(a, event.name, None) if func: if callable(func): - func(*event.args()) + yield a, func elif isinstance(func, types.ModuleType): # we gracefully exclude module imports with the same name as hooks. # For example, a user may have "from mitmproxy import log" in an addon, @@ -273,13 +253,48 @@ def invoke_addon(self, addon, event: hooks.Hook): f"Addon handler {event.name} ({a}) not callable" ) + async def invoke_addon(self, addon, event: hooks.Hook): + """ + Asynchronously invoke an event on an addon and all its children. + """ + for addon, func in self._iter_hooks(addon, event): + res = func(*event.args()) + # Support both async and sync hook functions + if res is not None and inspect.isawaitable(res): + await res + + def invoke_addon_sync(self, addon, event: hooks.Hook): + """ + Invoke an event on an addon and all its children. + """ + for addon, func in self._iter_hooks(addon, event): + if inspect.iscoroutinefunction(func): + raise exceptions.AddonManagerError( + f"Async handler {event.name} ({addon}) cannot be called from sync context" + ) + func(*event.args()) + + async def trigger_event(self, event: hooks.Hook): + """ + Asynchronously trigger an event across all addons. + """ + for i in self.chain: + try: + with safecall(): + await self.invoke_addon(i, event) + except exceptions.AddonHalt: + return + def trigger(self, event: hooks.Hook): """ - Trigger an event across all addons. + Trigger an event across all addons. + + This API is discouraged and may be deprecated in the future. + Use `trigger_event()` instead, which provides the same functionality but supports async hooks. """ for i in self.chain: try: with safecall(): - self.invoke_addon(i, event) + self.invoke_addon_sync(i, event) except exceptions.AddonHalt: return diff --git a/mitmproxy/addons/__init__.py b/mitmproxy/addons/__init__.py index 291199ae34..36ea9bfe55 100644 --- a/mitmproxy/addons/__init__.py +++ b/mitmproxy/addons/__init__.py @@ -9,6 +9,7 @@ from mitmproxy.addons import core from mitmproxy.addons import cut from mitmproxy.addons import disable_h2c +from mitmproxy.addons import dns_resolver from mitmproxy.addons import export from mitmproxy.addons import next_layer from mitmproxy.addons import onboarding @@ -44,6 +45,7 @@ def default_addons(): onboarding.Onboarding(), proxyauth.ProxyAuth(), proxyserver.Proxyserver(), + dns_resolver.DnsResolver(), script.ScriptLoader(), next_layer.NextLayer(), serverplayback.ServerPlayback(), diff --git a/mitmproxy/addons/anticache.py b/mitmproxy/addons/anticache.py index 9f5c2dc1db..38b636de2b 100644 --- a/mitmproxy/addons/anticache.py +++ b/mitmproxy/addons/anticache.py @@ -4,11 +4,13 @@ class AntiCache: def load(self, loader): loader.add_option( - "anticache", bool, False, + "anticache", + bool, + False, """ Strip out request headers that might cause the server to return 304-not-modified. - """ + """, ) def request(self, flow): diff --git a/mitmproxy/addons/anticomp.py b/mitmproxy/addons/anticomp.py index 3415302a64..eae8bc5b67 100644 --- a/mitmproxy/addons/anticomp.py +++ b/mitmproxy/addons/anticomp.py @@ -4,8 +4,10 @@ class AntiComp: def load(self, loader): loader.add_option( - "anticomp", bool, False, - "Try to convince servers to send us un-compressed data." + "anticomp", + bool, + False, + "Try to convince servers to send us un-compressed data.", ) def request(self, flow): diff --git a/mitmproxy/addons/asgiapp.py b/mitmproxy/addons/asgiapp.py index 22286b302c..c0bafa10ac 100644 --- a/mitmproxy/addons/asgiapp.py +++ b/mitmproxy/addons/asgiapp.py @@ -5,7 +5,6 @@ import asgiref.compatibility import asgiref.wsgi from mitmproxy import ctx, http -from mitmproxy.controller import DummyReply class ASGIApp: @@ -26,18 +25,16 @@ def name(self) -> str: return f"asgiapp:{self.host}:{self.port}" def should_serve(self, flow: http.HTTPFlow) -> bool: - assert flow.reply return bool( (flow.request.pretty_host, flow.request.port) == (self.host, self.port) - and flow.reply.state == "start" and not flow.error and not flow.response - and not isinstance(flow.reply, DummyReply) # ignore the HTTP flows of this app loaded from somewhere + and flow.live + and not flow.error + and not flow.response ) - def request(self, flow: http.HTTPFlow) -> None: - assert flow.reply + async def request(self, flow: http.HTTPFlow) -> None: if self.should_serve(flow): - flow.reply.take() # pause hook completion - asyncio.ensure_future(serve(self.asgi_app, flow)) + await serve(self.asgi_app, flow) class WSGIApp(ASGIApp): @@ -55,7 +52,9 @@ def __init__(self, wsgi_app, host: str, port: int): def make_scope(flow: http.HTTPFlow) -> dict: # %3F is a quoted question mark - quoted_path = urllib.parse.quote_from_bytes(flow.request.data.path).split("%3F", maxsplit=1) + quoted_path = urllib.parse.quote_from_bytes(flow.request.data.path).split( + "%3F", maxsplit=1 + ) # (Unicode string) – HTTP request target excluding any query string, with percent-encoded # sequences and UTF-8 byte sequences decoded into characters. @@ -80,11 +79,13 @@ def make_scope(flow: http.HTTPFlow) -> dict: "path": path, "raw_path": flow.request.path, "query_string": query_string, - "headers": [(name.lower(), value) for (name, value) in flow.request.headers.fields], + "headers": [ + (name.lower(), value) for (name, value) in flow.request.headers.fields + ], "client": flow.client_conn.peername, "extensions": { "mitmproxy.master": ctx.master, - } + }, } @@ -92,7 +93,6 @@ async def serve(app, flow: http.HTTPFlow): """ Serves app on flow. """ - assert flow.reply scope = make_scope(flow) done = asyncio.Event() @@ -111,13 +111,13 @@ async def receive(): # We really don't expect this to be called a second time, but what to do? # We just wait until the request is done before we continue here with sending a disconnect. await done.wait() - return { - "type": "http.disconnect" - } + return {"type": "http.disconnect"} async def send(event): if event["type"] == "http.response.start": - flow.response = http.Response.make(event["status"], b"", event.get("headers", [])) + flow.response = http.Response.make( + event["status"], b"", event.get("headers", []) + ) flow.response.decode() elif event["type"] == "http.response.body": flow.response.content += event.get("body", b"") @@ -135,5 +135,4 @@ async def send(event): ctx.log.error(f"Error in asgi app:\n{traceback.format_exc(limit=-5)}") flow.response = http.Response.make(500, b"ASGI Error.") finally: - flow.reply.commit() done.set() diff --git a/mitmproxy/addons/block.py b/mitmproxy/addons/block.py index 37ef39a88b..45301c6545 100644 --- a/mitmproxy/addons/block.py +++ b/mitmproxy/addons/block.py @@ -5,18 +5,22 @@ class Block: def load(self, loader): loader.add_option( - "block_global", bool, True, + "block_global", + bool, + True, """ Block connections from public IP addresses. - """ + """, ) loader.add_option( - "block_private", bool, False, + "block_private", + bool, + False, """ Block connections from local (private) IP addresses. This option does not affect loopback addresses (connections from the local machine), which are always permitted. - """ + """, ) def client_connected(self, client): @@ -29,9 +33,13 @@ def client_connected(self, client): return if ctx.options.block_private and address.is_private: - ctx.log.warn(f"Client connection from {client.peername[0]} killed by block_private option.") + ctx.log.warn( + f"Client connection from {client.peername[0]} killed by block_private option." + ) client.error = "Connection killed by block_private." if ctx.options.block_global and address.is_global: - ctx.log.warn(f"Client connection from {client.peername[0]} killed by block_global option.") + ctx.log.warn( + f"Client connection from {client.peername[0]} killed by block_global option." + ) client.error = "Connection killed by block_global." diff --git a/mitmproxy/addons/blocklist.py b/mitmproxy/addons/blocklist.py index 5a422cbbbe..4945fa4997 100644 --- a/mitmproxy/addons/blocklist.py +++ b/mitmproxy/addons/blocklist.py @@ -1,11 +1,12 @@ -import typing +from collections.abc import Sequence +from typing import NamedTuple from mitmproxy import ctx, exceptions, flowfilter, http, version from mitmproxy.net.http.status_codes import NO_RESPONSE from mitmproxy.net.http.status_codes import RESPONSES -class BlockSpec(typing.NamedTuple): +class BlockSpec(NamedTuple): matches: flowfilter.TFilter status_code: int @@ -36,18 +37,20 @@ def parse_spec(option: str) -> BlockSpec: class BlockList: def __init__(self): - self.items: typing.List[BlockSpec] = [] + self.items: list[BlockSpec] = [] def load(self, loader): loader.add_option( - "block_list", typing.Sequence[str], [], + "block_list", + Sequence[str], + [], """ Block matching requests and return an empty response with the specified HTTP status. Option syntax is "/flow-filter/status-code", where flow-filter describes which requests this rule should be applied to and status-code is the HTTP status code to return for blocked requests. The separator ("/" in the example) can be any character. Setting a non-standard status code of 444 will close the connection without sending a response. - """ + """, ) def configure(self, updated): @@ -57,20 +60,21 @@ def configure(self, updated): try: spec = parse_spec(option) except ValueError as e: - raise exceptions.OptionsError(f"Cannot parse block_list option {option}: {e}") from e + raise exceptions.OptionsError( + f"Cannot parse block_list option {option}: {e}" + ) from e self.items.append(spec) def request(self, flow: http.HTTPFlow) -> None: - if flow.response or flow.error or (flow.reply and flow.reply.state == "taken"): + if flow.response or flow.error or not flow.live: return for spec in self.items: if spec.matches(flow): - flow.metadata['blocklisted'] = True + flow.metadata["blocklisted"] = True if spec.status_code == NO_RESPONSE: flow.kill() else: flow.response = http.Response.make( - spec.status_code, - headers={"Server": version.MITMPROXY} + spec.status_code, headers={"Server": version.MITMPROXY} ) diff --git a/mitmproxy/addons/browser.py b/mitmproxy/addons/browser.py index e3aa24d7a4..68d974eb0d 100644 --- a/mitmproxy/addons/browser.py +++ b/mitmproxy/addons/browser.py @@ -1,69 +1,100 @@ import shutil import subprocess import tempfile -import typing +from typing import Optional from mitmproxy import command from mitmproxy import ctx -def get_chrome_executable() -> typing.Optional[str]: +def get_chrome_executable() -> Optional[str]: for browser in ( - "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - # https://stackoverflow.com/questions/40674914/google-chrome-path-in-windows-10 - r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", - r"C:\Program Files (x86)\Google\Application\chrome.exe", - # Linux binary names from Python's webbrowser module. - "google-chrome", - "google-chrome-stable", - "chrome", - "chromium", - "chromium-browser", - "google-chrome-unstable", + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + # https://stackoverflow.com/questions/40674914/google-chrome-path-in-windows-10 + r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", + r"C:\Program Files (x86)\Google\Application\chrome.exe", + # Linux binary names from Python's webbrowser module. + "google-chrome", + "google-chrome-stable", + "chrome", + "chromium", + "chromium-browser", + "google-chrome-unstable", ): if shutil.which(browser): return browser + + return None + + +def get_chrome_flatpak() -> Optional[str]: + if shutil.which("flatpak"): + for browser in ( + "com.google.Chrome", + "org.chromium.Chromium", + "com.github.Eloston.UngoogledChromium", + "com.google.ChromeDev", + ): + if ( + subprocess.run( + ["flatpak", "info", browser], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ).returncode + == 0 + ): + return browser + + return None + + +def get_browser_cmd() -> Optional[list[str]]: + if browser := get_chrome_executable(): + return [browser] + elif browser := get_chrome_flatpak(): + return ["flatpak", "run", "-p", browser] + return None class Browser: - browser: typing.List[subprocess.Popen] = [] - tdir: typing.List[tempfile.TemporaryDirectory] = [] + browser: list[subprocess.Popen] = [] + tdir: list[tempfile.TemporaryDirectory] = [] @command.command("browser.start") def start(self) -> None: """ - Start an isolated instance of Chrome that points to the currently - running proxy. + Start an isolated instance of Chrome that points to the currently + running proxy. """ if len(self.browser) > 0: ctx.log.alert("Starting additional browser") - cmd = get_chrome_executable() + cmd = get_browser_cmd() if not cmd: ctx.log.alert("Your platform is not supported yet - please submit a patch.") return tdir = tempfile.TemporaryDirectory() self.tdir.append(tdir) - self.browser.append(subprocess.Popen( - [ - cmd, - "--user-data-dir=%s" % str(tdir.name), - "--proxy-server={}:{}".format( - ctx.options.listen_host or "127.0.0.1", - ctx.options.listen_port - ), - "--disable-fre", - "--no-default-browser-check", - "--no-first-run", - "--disable-extensions", - - "about:blank", - ], - stdout = subprocess.DEVNULL, - stderr = subprocess.DEVNULL, - )) + self.browser.append( + subprocess.Popen( + [ + *cmd, + "--user-data-dir=%s" % str(tdir.name), + "--proxy-server={}:{}".format( + ctx.options.listen_host or "127.0.0.1", ctx.options.listen_port + ), + "--disable-fre", + "--no-default-browser-check", + "--no-first-run", + "--disable-extensions", + "about:blank", + ], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + ) def done(self): for browser in self.browser: @@ -71,4 +102,4 @@ def done(self): for tdir in self.tdir: tdir.cleanup() self.browser = [] - self.tdir = [] \ No newline at end of file + self.tdir = [] diff --git a/mitmproxy/addons/clientplayback.py b/mitmproxy/addons/clientplayback.py index 8dea6f794f..fb6dfad1d1 100644 --- a/mitmproxy/addons/clientplayback.py +++ b/mitmproxy/addons/clientplayback.py @@ -1,7 +1,8 @@ import asyncio import time import traceback -import typing +from collections.abc import Sequence +from typing import Optional, cast import mitmproxy.types from mitmproxy import command @@ -10,7 +11,6 @@ from mitmproxy import flow from mitmproxy import http from mitmproxy import io -from mitmproxy.addons.proxyserver import AsyncReply from mitmproxy.hooks import UpdateHook from mitmproxy.net import server_spec from mitmproxy.options import Options @@ -27,6 +27,7 @@ class MockServer(layers.http.HttpConnection): A mock HTTP "server" that just pretends it received a full HTTP request, which is then processed by the proxy core. """ + flow: http.HTTPFlow def __init__(self, flow: http.HTTPFlow, context: Context): @@ -36,26 +37,35 @@ def __init__(self, flow: http.HTTPFlow, context: Context): def _handle_event(self, event: events.Event) -> CommandGenerator[None]: if isinstance(event, events.Start): content = self.flow.request.raw_content - self.flow.request.timestamp_start = self.flow.request.timestamp_end = time.time() - yield layers.http.ReceiveHttp(layers.http.RequestHeaders( - 1, - self.flow.request, - end_stream=not (content or self.flow.request.trailers), - replay_flow=self.flow, - )) + self.flow.request.timestamp_start = ( + self.flow.request.timestamp_end + ) = time.time() + yield layers.http.ReceiveHttp( + layers.http.RequestHeaders( + 1, + self.flow.request, + end_stream=not (content or self.flow.request.trailers), + replay_flow=self.flow, + ) + ) if content: yield layers.http.ReceiveHttp(layers.http.RequestData(1, content)) if self.flow.request.trailers: # pragma: no cover # TODO: Cover this once we support HTTP/1 trailers. - yield layers.http.ReceiveHttp(layers.http.RequestTrailers(1, self.flow.request.trailers)) + yield layers.http.ReceiveHttp( + layers.http.RequestTrailers(1, self.flow.request.trailers) + ) yield layers.http.ReceiveHttp(layers.http.RequestEndOfMessage(1)) - elif isinstance(event, ( + elif isinstance( + event, + ( layers.http.ResponseHeaders, layers.http.ResponseData, layers.http.ResponseTrailers, layers.http.ResponseEndOfMessage, layers.http.ResponseProtocolError, - )): + ), + ): pass else: # pragma: no cover ctx.log(f"Unexpected event during replay: {event}") @@ -69,12 +79,12 @@ def __init__(self, flow: http.HTTPFlow, options: Options) -> None: client.state = ConnectionState.OPEN context = Context(client, options) - context.server = Server( - (flow.request.host, flow.request.port) - ) + context.server = Server((flow.request.host, flow.request.port)) context.server.tls = flow.request.scheme == "https" if options.mode.startswith("upstream:"): - context.server.via = server_spec.parse_with_mode(options.mode)[1] + context.server.via = flow.server_conn.via = server_spec.parse_with_mode( + options.mode + )[1] super().__init__(context) @@ -94,24 +104,26 @@ def log(self, message: str, level: str = "info") -> None: ctx.log(f"[replay] {message}", level) async def handle_hook(self, hook: commands.StartHook) -> None: - data, = hook.args() - data.reply = AsyncReply(data) + (data,) = hook.args() await ctx.master.addons.handle_lifecycle(hook) - await data.reply.done.wait() + if isinstance(data, flow.Flow): + await data.wait_for_resume() if isinstance(hook, (layers.http.HttpResponseHook, layers.http.HttpErrorHook)): if self.transports: # close server connections for x in self.transports.values(): if x.handler: x.handler.cancel() - await asyncio.wait([x.handler for x in self.transports.values() if x.handler]) + await asyncio.wait( + [x.handler for x in self.transports.values() if x.handler] + ) # signal completion self.done.set() class ClientPlayback: - playback_task: typing.Optional[asyncio.Task] = None - inflight: typing.Optional[http.HTTPFlow] + playback_task: Optional[asyncio.Task] = None + inflight: Optional[http.HTTPFlow] queue: asyncio.Queue options: Options @@ -122,8 +134,7 @@ def __init__(self): def running(self): self.playback_task = asyncio_utils.create_task( - self.playback(), - name="client playback" + self.playback(), name="client playback" ) self.options = ctx.options @@ -137,15 +148,19 @@ async def playback(self): try: h = ReplayHandler(self.inflight, self.options) if ctx.options.client_replay_concurrency == -1: - asyncio_utils.create_task(h.replay(), name="client playback awaiting response") + asyncio_utils.create_task( + h.replay(), name="client playback awaiting response" + ) else: await h.replay() except Exception: - ctx.log(f"Client replay has crashed!\n{traceback.format_exc()}", "error") + ctx.log( + f"Client replay has crashed!\n{traceback.format_exc()}", "error" + ) self.queue.task_done() self.inflight = None - def check(self, f: flow.Flow) -> typing.Optional[str]: + def check(self, f: flow.Flow) -> Optional[str]: if f.live or f == self.inflight: return "Can't replay live flow." if f.intercepted: @@ -163,12 +178,16 @@ def check(self, f: flow.Flow) -> typing.Optional[str]: def load(self, loader): loader.add_option( - "client_replay", typing.Sequence[str], [], - "Replay client requests from a saved file." + "client_replay", + Sequence[str], + [], + "Replay client requests from a saved file.", ) loader.add_option( - "client_replay_concurrency", int, 1, - "Concurrency limit on in-flight client replay requests. Currently the only valid values are 1 and -1 (no limit)." + "client_replay_concurrency", + int, + 1, + "Concurrency limit on in-flight client replay requests. Currently the only valid values are 1 and -1 (no limit).", ) def configure(self, updated): @@ -181,19 +200,21 @@ def configure(self, updated): if "client_replay_concurrency" in updated: if ctx.options.client_replay_concurrency not in [-1, 1]: - raise exceptions.OptionsError("Currently the only valid client_replay_concurrency values are -1 and 1.") + raise exceptions.OptionsError( + "Currently the only valid client_replay_concurrency values are -1 and 1." + ) @command.command("replay.client.count") def count(self) -> int: """ - Approximate number of flows queued for replay. + Approximate number of flows queued for replay. """ return self.queue.qsize() + int(bool(self.inflight)) @command.command("replay.client.stop") def stop_replay(self) -> None: """ - Clear the replay queue. + Clear the replay queue. """ updated = [] while True: @@ -210,18 +231,18 @@ def stop_replay(self) -> None: ctx.log.alert("Client replay queue cleared.") @command.command("replay.client") - def start_replay(self, flows: typing.Sequence[flow.Flow]) -> None: + def start_replay(self, flows: Sequence[flow.Flow]) -> None: """ - Add flows to the replay queue, skipping flows that can't be replayed. + Add flows to the replay queue, skipping flows that can't be replayed. """ - updated: typing.List[http.HTTPFlow] = [] + updated: list[http.HTTPFlow] = [] for f in flows: err = self.check(f) if err: ctx.log.warn(err) continue - http_flow = typing.cast(http.HTTPFlow, f) + http_flow = cast(http.HTTPFlow, f) # Prepare the flow for replay http_flow.backup() @@ -235,7 +256,7 @@ def start_replay(self, flows: typing.Sequence[flow.Flow]) -> None: @command.command("replay.client.file") def load_file(self, path: mitmproxy.types.Path) -> None: """ - Load flows from file, and add them to the replay queue. + Load flows from file, and add them to the replay queue. """ try: flows = io.read_flows_from_paths([path]) diff --git a/mitmproxy/addons/command_history.py b/mitmproxy/addons/command_history.py index a54a344620..96c71387a1 100644 --- a/mitmproxy/addons/command_history.py +++ b/mitmproxy/addons/command_history.py @@ -1,6 +1,6 @@ import os import pathlib -import typing +from collections.abc import Sequence from mitmproxy import command from mitmproxy import ctx @@ -10,14 +10,16 @@ class CommandHistory: VACUUM_SIZE = 1024 def __init__(self) -> None: - self.history: typing.List[str] = [] - self.filtered_history: typing.List[str] = [""] + self.history: list[str] = [] + self.filtered_history: list[str] = [""] self.current_index: int = 0 def load(self, loader): loader.add_option( - "command_history", bool, True, - """Persist command history between mitmproxy invocations.""" + "command_history", + bool, + True, + """Persist command history between mitmproxy invocations.""", ) @property @@ -33,12 +35,12 @@ def configure(self, updated): if "command_history" in updated or "confdir" in updated: if ctx.options.command_history and self.history_file.is_file(): self.history = self.history_file.read_text().splitlines() - self.set_filter('') + self.set_filter("") def done(self): if ctx.options.command_history and len(self.history) >= self.VACUUM_SIZE: # vacuum history so that it doesn't grow indefinitely. - history_str = "\n".join(self.history[-self.VACUUM_SIZE // 2:]) + "\n" + history_str = "\n".join(self.history[-self.VACUUM_SIZE // 2 :]) + "\n" try: self.history_file.write_text(history_str) except Exception as e: @@ -57,10 +59,10 @@ def add_command(self, command: str) -> None: except Exception as e: ctx.log.alert(f"Failed writing to {self.history_file}: {e}") - self.set_filter('') + self.set_filter("") @command.command("commands.history.get") - def get_history(self) -> typing.Sequence[str]: + def get_history(self) -> Sequence[str]: """Get the entire command history.""" return self.history.copy() @@ -72,17 +74,13 @@ def clear_history(self): except Exception as e: ctx.log.alert(f"Failed deleting {self.history_file}: {e}") self.history = [] - self.set_filter('') + self.set_filter("") # Functionality to provide a filtered list that can be iterated through. @command.command("commands.history.filter") def set_filter(self, prefix: str) -> None: - self.filtered_history = [ - cmd - for cmd in self.history - if cmd.startswith(prefix) - ] + self.filtered_history = [cmd for cmd in self.history if cmd.startswith(prefix)] self.filtered_history.append(prefix) self.current_index = len(self.filtered_history) - 1 diff --git a/mitmproxy/addons/comment.py b/mitmproxy/addons/comment.py index 2239cca7ba..3e9b549c72 100644 --- a/mitmproxy/addons/comment.py +++ b/mitmproxy/addons/comment.py @@ -1,11 +1,12 @@ -import typing +from collections.abc import Sequence + from mitmproxy import command, flow, ctx from mitmproxy.hooks import UpdateHook class Comment: @command.command("flow.comment") - def comment(self, flow: typing.Sequence[flow.Flow], comment: str) -> None: + def comment(self, flow: Sequence[flow.Flow], comment: str) -> None: "Add a comment to a flow" updated = [] diff --git a/mitmproxy/addons/core.py b/mitmproxy/addons/core.py index d3e553dbc7..230afac4a0 100644 --- a/mitmproxy/addons/core.py +++ b/mitmproxy/addons/core.py @@ -1,6 +1,6 @@ -import typing - import os +from collections.abc import Sequence +from typing import Union from mitmproxy.utils import emoji from mitmproxy import ctx, hooks @@ -38,9 +38,7 @@ def configure(self, updated): "Transparent mode not supported on this platform." ) elif mode not in ["regular", "socks5"]: - raise exceptions.OptionsError( - "Invalid mode specification: %s" % mode - ) + raise exceptions.OptionsError("Invalid mode specification: %s" % mode) if "client_certs" in updated: if opts.client_certs: client_certs = os.path.expanduser(opts.client_certs) @@ -50,25 +48,26 @@ def configure(self, updated): ) @command.command("set") - def set(self, option: str, value: str = "") -> None: + def set(self, option: str, *value: str) -> None: """ - Set an option. When the value is omitted, booleans are set to true, - strings and integers are set to None (if permitted), and sequences - are emptied. Boolean values can be true, false or toggle. - Multiple values are concatenated with a single space. - Sequences are set using multiple invocations to set for - the same option. + Set an option. When the value is omitted, booleans are set to true, + strings and integers are set to None (if permitted), and sequences + are emptied. Boolean values can be true, false or toggle. + Multiple values are concatenated with a single space. """ - strspec = f"{option}={value}" + if value: + specs = [f"{option}={v}" for v in value] + else: + specs = [option] try: - ctx.options.set(strspec) + ctx.options.set(*specs) except exceptions.OptionsError as e: raise exceptions.CommandError(e) from e @command.command("flow.resume") - def resume(self, flows: typing.Sequence[flow.Flow]) -> None: + def resume(self, flows: Sequence[flow.Flow]) -> None: """ - Resume flows if they are intercepted. + Resume flows if they are intercepted. """ intercepted = [i for i in flows if i.intercepted] for f in intercepted: @@ -77,9 +76,9 @@ def resume(self, flows: typing.Sequence[flow.Flow]) -> None: # FIXME: this will become view.mark later @command.command("flow.mark") - def mark(self, flows: typing.Sequence[flow.Flow], marker: mitmproxy.types.Marker) -> None: + def mark(self, flows: Sequence[flow.Flow], marker: mitmproxy.types.Marker) -> None: """ - Mark flows. + Mark flows. """ updated = [] if marker not in emoji.emoji: @@ -92,9 +91,9 @@ def mark(self, flows: typing.Sequence[flow.Flow], marker: mitmproxy.types.Marker # FIXME: this will become view.mark.toggle later @command.command("flow.mark.toggle") - def mark_toggle(self, flows: typing.Sequence[flow.Flow]) -> None: + def mark_toggle(self, flows: Sequence[flow.Flow]) -> None: """ - Toggle mark for flows. + Toggle mark for flows. """ for i in flows: if i.marked: @@ -104,9 +103,9 @@ def mark_toggle(self, flows: typing.Sequence[flow.Flow]) -> None: ctx.master.addons.trigger(hooks.UpdateHook(flows)) @command.command("flow.kill") - def kill(self, flows: typing.Sequence[flow.Flow]) -> None: + def kill(self, flows: Sequence[flow.Flow]) -> None: """ - Kill running flows. + Kill running flows. """ updated = [] for f in flows: @@ -118,9 +117,9 @@ def kill(self, flows: typing.Sequence[flow.Flow]) -> None: # FIXME: this will become view.revert later @command.command("flow.revert") - def revert(self, flows: typing.Sequence[flow.Flow]) -> None: + def revert(self, flows: Sequence[flow.Flow]) -> None: """ - Revert flow changes. + Revert flow changes. """ updated = [] for f in flows: @@ -131,7 +130,7 @@ def revert(self, flows: typing.Sequence[flow.Flow]) -> None: ctx.master.addons.trigger(hooks.UpdateHook(updated)) @command.command("flow.set.options") - def flow_set_options(self) -> typing.Sequence[str]: + def flow_set_options(self) -> Sequence[str]: return [ "host", "status_code", @@ -143,16 +142,11 @@ def flow_set_options(self) -> typing.Sequence[str]: @command.command("flow.set") @command.argument("attr", type=mitmproxy.types.Choice("flow.set.options")) - def flow_set( - self, - flows: typing.Sequence[flow.Flow], - attr: str, - value: str - ) -> None: + def flow_set(self, flows: Sequence[flow.Flow], attr: str, value: str) -> None: """ - Quickly set a number of common values on flows. + Quickly set a number of common values on flows. """ - val: typing.Union[int, str] = value + val: Union[int, str] = value if attr == "status_code": try: val = int(val) # type: ignore @@ -177,7 +171,7 @@ def flow_set( req.url = val except ValueError as e: raise exceptions.CommandError( - "URL {} is invalid: {}".format(repr(val), e) + f"URL {repr(val)} is invalid: {e}" ) from e else: self.rupdate = False @@ -198,12 +192,12 @@ def flow_set( updated.append(f) ctx.master.addons.trigger(hooks.UpdateHook(updated)) - ctx.log.alert("Set {} on {} flows.".format(attr, len(updated))) + ctx.log.alert(f"Set {attr} on {len(updated)} flows.") @command.command("flow.decode") - def decode(self, flows: typing.Sequence[flow.Flow], part: str) -> None: + def decode(self, flows: Sequence[flow.Flow], part: str) -> None: """ - Decode flows. + Decode flows. """ updated = [] for f in flows: @@ -216,9 +210,9 @@ def decode(self, flows: typing.Sequence[flow.Flow], part: str) -> None: ctx.log.alert("Decoded %s flows." % len(updated)) @command.command("flow.encode.toggle") - def encode_toggle(self, flows: typing.Sequence[flow.Flow], part: str) -> None: + def encode_toggle(self, flows: Sequence[flow.Flow], part: str) -> None: """ - Toggle flow encoding on and off, using deflate for encoding. + Toggle flow encoding on and off, using deflate for encoding. """ updated = [] for f in flows: @@ -238,12 +232,12 @@ def encode_toggle(self, flows: typing.Sequence[flow.Flow], part: str) -> None: @command.argument("encoding", type=mitmproxy.types.Choice("flow.encode.options")) def encode( self, - flows: typing.Sequence[flow.Flow], + flows: Sequence[flow.Flow], part: str, encoding: str, ) -> None: """ - Encode flows with a specified encoding. + Encode flows with a specified encoding. """ updated = [] for f in flows: @@ -258,47 +252,43 @@ def encode( ctx.log.alert("Encoded %s flows." % len(updated)) @command.command("flow.encode.options") - def encode_options(self) -> typing.Sequence[str]: + def encode_options(self) -> Sequence[str]: """ - The possible values for an encoding specification. + The possible values for an encoding specification. """ return ["gzip", "deflate", "br", "zstd"] @command.command("options.load") def options_load(self, path: mitmproxy.types.Path) -> None: """ - Load options from a file. + Load options from a file. """ try: optmanager.load_paths(ctx.options, path) except (OSError, exceptions.OptionsError) as e: - raise exceptions.CommandError( - "Could not load options - %s" % e - ) from e + raise exceptions.CommandError("Could not load options - %s" % e) from e @command.command("options.save") def options_save(self, path: mitmproxy.types.Path) -> None: """ - Save options to a file. + Save options to a file. """ try: optmanager.save(ctx.options, path) except OSError as e: - raise exceptions.CommandError( - "Could not save options - %s" % e - ) from e + raise exceptions.CommandError("Could not save options - %s" % e) from e @command.command("options.reset") def options_reset(self) -> None: """ - Reset all options to defaults. + Reset all options to defaults. """ ctx.options.reset() @command.command("options.reset.one") def options_reset_one(self, name: str) -> None: """ - Reset one option to its default value. + Reset one option to its default value. """ if name not in ctx.options: raise exceptions.CommandError("No such option: %s" % name) diff --git a/mitmproxy/addons/cut.py b/mitmproxy/addons/cut.py index c75c14ca7c..1c8a171ee6 100644 --- a/mitmproxy/addons/cut.py +++ b/mitmproxy/addons/cut.py @@ -1,14 +1,14 @@ import io import csv -import typing import os.path +from collections.abc import Sequence +from typing import Any, Union from mitmproxy import command from mitmproxy import exceptions from mitmproxy import flow from mitmproxy import ctx from mitmproxy import certs -from mitmproxy.utils import strutils import mitmproxy.types import pyperclip @@ -17,16 +17,16 @@ def headername(spec: str): if not (spec.startswith("header[") and spec.endswith("]")): raise exceptions.CommandError("Invalid header spec: %s" % spec) - return spec[len("header["):-1].strip() + return spec[len("header[") : -1].strip() def is_addr(v): return isinstance(v, tuple) and len(v) > 1 -def extract(cut: str, f: flow.Flow) -> typing.Union[str, bytes]: +def extract(cut: str, f: flow.Flow) -> Union[str, bytes]: path = cut.split(".") - current: typing.Any = f + current: Any = f for i, spec in enumerate(path): if spec.startswith("_"): raise exceptions.CommandError("Can't access internal attribute %s" % spec) @@ -47,30 +47,42 @@ def extract(cut: str, f: flow.Flow) -> typing.Union[str, bytes]: return "true" if part else "false" elif isinstance(part, certs.Cert): # pragma: no cover return part.to_pem().decode("ascii") - elif isinstance(part, list) and len(part) > 0 and isinstance(part[0], certs.Cert): + elif ( + isinstance(part, list) + and len(part) > 0 + and isinstance(part[0], certs.Cert) + ): # TODO: currently this extracts only the very first cert as PEM-encoded string. return part[0].to_pem().decode("ascii") current = part return str(current or "") +def extract_str(cut: str, f: flow.Flow) -> str: + ret = extract(cut, f) + if isinstance(ret, bytes): + return repr(ret) + else: + return ret + + class Cut: @command.command("cut") def cut( self, - flows: typing.Sequence[flow.Flow], + flows: Sequence[flow.Flow], cuts: mitmproxy.types.CutSpec, ) -> mitmproxy.types.Data: """ - Cut data from a set of flows. Cut specifications are attribute paths - from the base of the flow object, with a few conveniences - "port" - and "host" retrieve parts of an address tuple, ".header[key]" - retrieves a header value. Return values converted to strings or - bytes: SSL certificates are converted to PEM format, bools are "true" - or "false", "bytes" are preserved, and all other values are - converted to strings. + Cut data from a set of flows. Cut specifications are attribute paths + from the base of the flow object, with a few conveniences - "port" + and "host" retrieve parts of an address tuple, ".header[key]" + retrieves a header value. Return values converted to strings or + bytes: SSL certificates are converted to PEM format, bools are "true" + or "false", "bytes" are preserved, and all other values are + converted to strings. """ - ret: typing.List[typing.List[typing.Union[str, bytes]]] = [] + ret: list[list[Union[str, bytes]]] = [] for f in flows: ret.append([extract(c, f) for c in cuts]) return ret # type: ignore @@ -78,16 +90,16 @@ def cut( @command.command("cut.save") def save( self, - flows: typing.Sequence[flow.Flow], + flows: Sequence[flow.Flow], cuts: mitmproxy.types.CutSpec, - path: mitmproxy.types.Path + path: mitmproxy.types.Path, ) -> None: """ - Save cuts to file. If there are multiple flows or cuts, the format - is UTF-8 encoded CSV. If there is exactly one row and one column, - the data is written to file as-is, with raw bytes preserved. If the - path is prefixed with a "+", values are appended if there is an - existing file. + Save cuts to file. If there are multiple flows or cuts, the format + is UTF-8 encoded CSV. If there is exactly one row and one column, + the data is written to file as-is, with raw bytes preserved. If the + path is prefixed with a "+", values are appended if there is an + existing file. """ append = False if path.startswith("+"): @@ -107,41 +119,41 @@ def save( fp.write(v.encode("utf8")) ctx.log.alert("Saved single cut.") else: - with open(path, "a" if append else "w", newline='', encoding="utf8") as tfp: + with open( + path, "a" if append else "w", newline="", encoding="utf8" + ) as tfp: writer = csv.writer(tfp) for f in flows: - vals = [extract(c, f) for c in cuts] - writer.writerow( - [strutils.always_str(x) or "" for x in vals] # type: ignore - ) - ctx.log.alert("Saved %s cuts over %d flows as CSV." % (len(cuts), len(flows))) + vals = [extract_str(c, f) for c in cuts] + writer.writerow(vals) + ctx.log.alert( + "Saved %s cuts over %d flows as CSV." % (len(cuts), len(flows)) + ) except OSError as e: ctx.log.error(str(e)) @command.command("cut.clip") def clip( self, - flows: typing.Sequence[flow.Flow], + flows: Sequence[flow.Flow], cuts: mitmproxy.types.CutSpec, ) -> None: """ - Send cuts to the clipboard. If there are multiple flows or cuts, the - format is UTF-8 encoded CSV. If there is exactly one row and one - column, the data is written to file as-is, with raw bytes preserved. + Send cuts to the clipboard. If there are multiple flows or cuts, the + format is UTF-8 encoded CSV. If there is exactly one row and one + column, the data is written to file as-is, with raw bytes preserved. """ - v: typing.Union[str, bytes] + v: Union[str, bytes] fp = io.StringIO(newline="") if len(cuts) == 1 and len(flows) == 1: - v = extract(cuts[0], flows[0]) - fp.write(strutils.always_str(v)) # type: ignore + v = extract_str(cuts[0], flows[0]) + fp.write(v) ctx.log.alert("Clipped single cut.") else: writer = csv.writer(fp) for f in flows: - vals = [extract(c, f) for c in cuts] - writer.writerow( - [strutils.always_str(v) for v in vals] - ) + vals = [extract_str(c, f) for c in cuts] + writer.writerow(vals) ctx.log.alert("Clipped %s cuts as CSV." % len(cuts)) try: pyperclip.copy(fp.getvalue()) diff --git a/mitmproxy/addons/disable_h2c.py b/mitmproxy/addons/disable_h2c.py index 392a29a57a..b2652f4849 100644 --- a/mitmproxy/addons/disable_h2c.py +++ b/mitmproxy/addons/disable_h2c.py @@ -15,22 +15,26 @@ class DisableH2C: """ def process_flow(self, f): - if f.request.headers.get('upgrade', '') == 'h2c': - mitmproxy.ctx.log.warn("HTTP/2 cleartext connections (h2c upgrade requests) are currently not supported.") - del f.request.headers['upgrade'] - if 'connection' in f.request.headers: - del f.request.headers['connection'] - if 'http2-settings' in f.request.headers: - del f.request.headers['http2-settings'] + if f.request.headers.get("upgrade", "") == "h2c": + mitmproxy.ctx.log.warn( + "HTTP/2 cleartext connections (h2c upgrade requests) are currently not supported." + ) + del f.request.headers["upgrade"] + if "connection" in f.request.headers: + del f.request.headers["connection"] + if "http2-settings" in f.request.headers: + del f.request.headers["http2-settings"] is_connection_preface = ( - f.request.method == 'PRI' and - f.request.path == '*' and - f.request.http_version == 'HTTP/2.0' + f.request.method == "PRI" + and f.request.path == "*" + and f.request.http_version == "HTTP/2.0" ) if is_connection_preface: f.kill() - mitmproxy.ctx.log.warn("Initiating HTTP/2 connections with prior knowledge are currently not supported.") + mitmproxy.ctx.log.warn( + "Initiating HTTP/2 connections with prior knowledge are currently not supported." + ) # Handlers diff --git a/mitmproxy/addons/dns_resolver.py b/mitmproxy/addons/dns_resolver.py new file mode 100644 index 0000000000..e1af088b3e --- /dev/null +++ b/mitmproxy/addons/dns_resolver.py @@ -0,0 +1,149 @@ +import asyncio +import ipaddress +import socket +from typing import Callable, Iterable, Union +from mitmproxy import ctx, dns + +IP4_PTR_SUFFIX = ".in-addr.arpa" +IP6_PTR_SUFFIX = ".ip6.arpa" + + +class ResolveError(Exception): + """Exception thrown by different resolve methods.""" + + def __init__(self, response_code: int) -> None: + assert response_code != dns.response_codes.NOERROR + self.response_code = response_code + + +async def resolve_question_by_name( + question: dns.Question, + loop: asyncio.AbstractEventLoop, + family: socket.AddressFamily, + ip: Callable[[str], Union[ipaddress.IPv4Address, ipaddress.IPv6Address]], +) -> Iterable[dns.ResourceRecord]: + try: + addrinfos = await loop.getaddrinfo(host=question.name, port=0, family=family) + except socket.gaierror as e: + if e.errno == socket.EAI_NONAME: + raise ResolveError(dns.response_codes.NXDOMAIN) + else: + # NOTE might fail on Windows for IPv6 queries: + # https://stackoverflow.com/questions/66755681/getaddrinfo-c-on-windows-not-handling-ipv6-correctly-returning-error-code-1 + raise ResolveError(dns.response_codes.SERVFAIL) + return map( + lambda addrinfo: dns.ResourceRecord( + name=question.name, + type=question.type, + class_=question.class_, + ttl=dns.ResourceRecord.DEFAULT_TTL, + data=ip(addrinfo[4][0]).packed, + ), + addrinfos, + ) + + +async def resolve_question_by_addr( + question: dns.Question, + loop: asyncio.AbstractEventLoop, + suffix: str, + sockaddr: Callable[[list[str]], Union[tuple[str, int], tuple[str, int, int, int]]], +) -> Iterable[dns.ResourceRecord]: + try: + addr = sockaddr(question.name[: -len(suffix)].split(".")[::-1]) + except ValueError: + raise ResolveError(dns.response_codes.FORMERR) + try: + name, _ = await loop.getnameinfo(addr, flags=socket.NI_NAMEREQD) + except socket.gaierror as e: + raise ResolveError( + dns.response_codes.NXDOMAIN + if e.errno == socket.EAI_NONAME + else dns.response_codes.SERVFAIL + ) + return [ + dns.ResourceRecord( + name=question.name, + type=question.type, + class_=question.class_, + ttl=dns.ResourceRecord.DEFAULT_TTL, + data=dns.domain_names.pack(name), + ) + ] + + +async def resolve_question( + question: dns.Question, loop: asyncio.AbstractEventLoop +) -> Iterable[dns.ResourceRecord]: + """Resolve the question into resource record(s), throwing ResolveError if an error condition occurs.""" + + if question.class_ != dns.classes.IN: + raise ResolveError(dns.response_codes.NOTIMP) + if question.type == dns.types.A: + return await resolve_question_by_name( + question, loop, socket.AddressFamily.AF_INET, ipaddress.IPv4Address + ) + elif question.type == dns.types.AAAA: + return await resolve_question_by_name( + question, loop, socket.AddressFamily.AF_INET6, ipaddress.IPv6Address + ) + elif question.type == dns.types.PTR: + name_lower = question.name.lower() + if name_lower.endswith(IP4_PTR_SUFFIX): + return await resolve_question_by_addr( + question=question, + loop=loop, + suffix=IP4_PTR_SUFFIX, + sockaddr=lambda x: (str(ipaddress.IPv4Address(".".join(x))), 0), + ) + elif name_lower.endswith(IP6_PTR_SUFFIX): + return await resolve_question_by_addr( + question=question, + loop=loop, + suffix=IP6_PTR_SUFFIX, + sockaddr=lambda x: ( + str(ipaddress.IPv6Address(bytes.fromhex("".join(x)))), + 0, + 0, + 0, + ), + ) + else: + raise ResolveError(dns.response_codes.FORMERR) + else: + raise ResolveError(dns.response_codes.NOTIMP) + + +async def resolve_message( + message: dns.Message, loop: asyncio.AbstractEventLoop +) -> dns.Message: + try: + if not message.query: + raise ResolveError( + dns.response_codes.REFUSED + ) # we cannot resolve an answer + if message.op_code != dns.op_codes.QUERY: + raise ResolveError( + dns.response_codes.NOTIMP + ) # inverse queries and others are not supported + rrs: list[dns.ResourceRecord] = [] + for question in message.questions: + rrs.extend(await resolve_question(question, loop)) + except ResolveError as e: + return message.fail(e.response_code) + else: + return message.succeed(rrs) + + +class DnsResolver: + async def dns_request(self, flow: dns.DNSFlow) -> None: + should_resolve = ( + flow.live + and not flow.response + and not flow.error + and ctx.options.dns_mode == "regular" + ) + if should_resolve: + flow.response = await resolve_message( + flow.request, asyncio.get_running_loop() + ) diff --git a/mitmproxy/addons/dumper.py b/mitmproxy/addons/dumper.py index 233bf8596c..7da08ecf4c 100644 --- a/mitmproxy/addons/dumper.py +++ b/mitmproxy/addons/dumper.py @@ -1,20 +1,23 @@ import itertools import shutil +import sys from typing import IO, Optional, Union -import click +from wsproto.frame_protocol import CloseReason from mitmproxy import contentviews from mitmproxy import ctx +from mitmproxy import dns from mitmproxy import exceptions +from mitmproxy import flow from mitmproxy import flowfilter from mitmproxy import http -from mitmproxy import flow +from mitmproxy.contrib import click as miniclick from mitmproxy.tcp import TCPFlow, TCPMessage from mitmproxy.utils import human from mitmproxy.utils import strutils -from mitmproxy.websocket import WebSocketMessage, WebSocketData -from wsproto.frame_protocol import CloseReason +from mitmproxy.utils import vt_codes +from mitmproxy.websocket import WebSocketData, WebSocketMessage def indent(n: int, text: str) -> str: @@ -23,37 +26,43 @@ def indent(n: int, text: str) -> str: return "\n".join(pad + i for i in l) -def colorful(line, styles): - yield " " # we can already indent here - for (style, text) in line: - yield click.style(text, **styles.get(style, {})) +CONTENTVIEW_STYLES = { + "highlight": dict(bold=True), + "offset": dict(fg="blue"), + "header": dict(fg="green", bold=True), + "text": dict(fg="green"), +} class Dumper: - def __init__(self, outfile=None): + def __init__(self, outfile: Optional[IO[str]] = None): self.filter: Optional[flowfilter.TFilter] = None - self.outfp: Optional[IO] = outfile + self.outfp: IO[str] = outfile or sys.stdout + self.out_has_vt_codes = vt_codes.ensure_supported(self.outfp) def load(self, loader): loader.add_option( - "flow_detail", int, 1, + "flow_detail", + int, + 1, """ - The display detail level for flows in mitmdump: 0 (almost quiet) to 3 (very verbose). - 0: shortened request URL, response status code, WebSocket and TCP message notifications. - 1: full request URL with response status code - 2: 1 + HTTP headers + The display detail level for flows in mitmdump: 0 (quiet) to 4 (very verbose). + 0: no output + 1: shortened request URL with response status code + 2: full request URL with response status code and HTTP headers 3: 2 + truncated response content, content of WebSocket and TCP messages 4: 3 + nothing is truncated - """ + """, ) loader.add_option( - "dumper_default_contentview", str, "auto", + "dumper_default_contentview", + str, + "auto", "The default content view mode.", - choices=[i.name.lower() for i in contentviews.views] + choices=[i.name.lower() for i in contentviews.views], ) loader.add_option( - "dumper_filter", Optional[str], None, - "Limit which flows are dumped." + "dumper_filter", Optional[str], None, "Limit which flows are dumped." ) def configure(self, updated): @@ -66,38 +75,42 @@ def configure(self, updated): else: self.filter = None + def style(self, text: str, **style) -> str: + if style and self.out_has_vt_codes: + text = miniclick.style(text, **style) + return text + def echo(self, text: str, ident=None, **style): if ident: text = indent(ident, text) - click.secho(text, file=self.outfp, err=False, **style) - if self.outfp: - self.outfp.flush() + text = self.style(text, **style) + print(text, file=self.outfp) def _echo_headers(self, headers: http.Headers): for k, v in headers.fields: ks = strutils.bytes_to_escaped_str(k) + ks = self.style(ks, fg="blue") vs = strutils.bytes_to_escaped_str(v) - out = "{}: {}".format( - click.style(ks, fg="blue"), - click.style(vs) - ) - self.echo(out, ident=4) + self.echo(f"{ks}: {vs}", ident=4) def _echo_trailers(self, trailers: Optional[http.Headers]): if not trailers: return - self.echo(click.style("--- HTTP Trailers", fg="magenta"), ident=4) + self.echo("--- HTTP Trailers", fg="magenta", ident=4) self._echo_headers(trailers) + def _colorful(self, line): + yield " " # we can already indent here + for (style, text) in line: + yield self.style(text, **CONTENTVIEW_STYLES.get(style, {})) + def _echo_message( self, message: Union[http.Message, TCPMessage, WebSocketMessage], - flow: Union[http.HTTPFlow, TCPFlow] + flow: Union[http.HTTPFlow, TCPFlow], ): _, lines, error = contentviews.get_message_content_view( - ctx.options.dumper_default_contentview, - message, - flow + ctx.options.dumper_default_contentview, message, flow ) if error: ctx.log.debug(error) @@ -107,16 +120,7 @@ def _echo_message( else: lines_to_echo = lines - styles = dict( - highlight=dict(bold=True), - offset=dict(fg="blue"), - header=dict(fg="green", bold=True), - text=dict(fg="green") - ) - - content = "\r\n".join( - "".join(colorful(line, styles)) for line in lines_to_echo - ) + content = "\r\n".join("".join(self._colorful(line)) for line in lines_to_echo) if content: self.echo("") self.echo(content) @@ -127,46 +131,45 @@ def _echo_message( if ctx.options.flow_detail >= 2: self.echo("") - def _echo_request_line(self, flow: http.HTTPFlow) -> None: + def _fmt_client(self, flow: flow.Flow) -> str: if flow.is_replay == "request": - client = click.style("[replay]", fg="yellow", bold=True) + return self.style("[replay]", fg="yellow", bold=True) elif flow.client_conn.peername: - client = click.style( + return self.style( strutils.escape_control_characters( human.format_address(flow.client_conn.peername) ) ) else: # pragma: no cover # this should not happen, but we're defensive here. - client = "" + return "" + + def _echo_request_line(self, flow: http.HTTPFlow) -> None: + client = self._fmt_client(flow) - pushed = ' PUSH_PROMISE' if 'h2-pushed-stream' in flow.metadata else '' + pushed = " PUSH_PROMISE" if "h2-pushed-stream" in flow.metadata else "" method = flow.request.method + pushed - method_color = dict( - GET="green", - DELETE="red" - ).get(method.upper(), "magenta") - method = click.style( - strutils.escape_control_characters(method), - fg=method_color, - bold=True + method_color = dict(GET="green", DELETE="red").get(method.upper(), "magenta") + method = self.style( + strutils.escape_control_characters(method), fg=method_color, bold=True ) if ctx.options.showhost: url = flow.request.pretty_url else: url = flow.request.url - if ctx.options.flow_detail <= 1: + if ctx.options.flow_detail == 1: # We need to truncate before applying styles, so we just focus on the URL. terminal_width_limit = max(shutil.get_terminal_size()[0] - 25, 50) if len(url) > terminal_width_limit: url = url[:terminal_width_limit] + "…" - url = click.style(strutils.escape_control_characters(url), bold=True) + url = self.style(strutils.escape_control_characters(url), bold=True) http_version = "" - if ( - not (flow.request.is_http10 or flow.request.is_http11) - or flow.request.http_version != getattr(flow.response, "http_version", "HTTP/1.1") + if not ( + flow.request.is_http10 or flow.request.is_http11 + ) or flow.request.http_version != getattr( + flow.response, "http_version", "HTTP/1.1" ): # Hide version for h1 <-> h1 connections. http_version = " " + flow.request.http_version @@ -176,7 +179,7 @@ def _echo_request_line(self, flow: http.HTTPFlow) -> None: def _echo_response_line(self, flow: http.HTTPFlow) -> None: if flow.is_replay == "response": replay_str = "[replay]" - replay = click.style(replay_str, fg="yellow", bold=True) + replay = self.style(replay_str, fg="yellow", bold=True) else: replay_str = "" replay = "" @@ -190,7 +193,7 @@ def _echo_response_line(self, flow: http.HTTPFlow) -> None: code_color = "magenta" elif 400 <= code_int < 600: code_color = "red" - code = click.style( + code = self.style( str(code_int), fg=code_color, bold=True, @@ -201,17 +204,15 @@ def _echo_response_line(self, flow: http.HTTPFlow) -> None: reason = flow.response.reason else: reason = http.status_codes.RESPONSES.get(flow.response.status_code, "") - reason = click.style( - strutils.escape_control_characters(reason), - fg=code_color, - bold=True + reason = self.style( + strutils.escape_control_characters(reason), fg=code_color, bold=True ) if flow.response.raw_content is None: size = "(content missing)" else: size = human.pretty_size(len(flow.response.raw_content)) - size = click.style(size, bold=True) + size = self.style(size, bold=True) http_version = "" if ( @@ -221,13 +222,16 @@ def _echo_response_line(self, flow: http.HTTPFlow) -> None: # Hide version for h1 <-> h1 connections. http_version = f"{flow.response.http_version} " - arrows = click.style(" <<", bold=True) + arrows = self.style(" <<", bold=True) if ctx.options.flow_detail == 1: # This aligns the HTTP response code with the HTTP request method: # 127.0.0.1:59519: GET http://example.com/ # << 304 Not Modified 0b - pad = max(0, - len(human.format_address(flow.client_conn.peername)) - (2 + len(http_version) + len(replay_str))) + pad = max( + 0, + len(human.format_address(flow.client_conn.peername)) + - (2 + len(http_version) + len(replay_str)), + ) arrows = " " * pad + arrows self.echo(f"{replay}{arrows} {http_version}{code} {reason} {size}") @@ -255,6 +259,8 @@ def echo_flow(self, f: http.HTTPFlow) -> None: msg = strutils.escape_control_characters(f.error.msg) self.echo(f" << {msg}", bold=True, fg="red") + self.outfp.flush() + def match(self, f): if ctx.options.flow_detail == 0: return False @@ -290,13 +296,17 @@ def websocket_end(self, f: http.HTTPFlow): assert f.websocket is not None # satisfy type checker if self.match(f): if f.websocket.close_code in {1000, 1001, 1005}: - c = 'client' if f.websocket.closed_by_client else 'server' - self.echo(f"WebSocket connection closed by {c}: {f.websocket.close_code} {f.websocket.close_reason}") + c = "client" if f.websocket.closed_by_client else "server" + self.echo( + f"WebSocket connection closed by {c}: {f.websocket.close_code} {f.websocket.close_reason}" + ) else: - error = flow.Error(f"WebSocket Error: {self.format_websocket_error(f.websocket)}") + error = flow.Error( + f"WebSocket Error: {self.format_websocket_error(f.websocket)}" + ) self.echo( f"Error in WebSocket connection to {human.format_address(f.server_conn.address)}: {error}", - fg="red" + fg="red", ) def format_websocket_error(self, websocket: WebSocketData) -> str: @@ -312,17 +322,52 @@ def tcp_error(self, f): if self.match(f): self.echo( f"Error in TCP connection to {human.format_address(f.server_conn.address)}: {f.error}", - fg="red" + fg="red", ) def tcp_message(self, f): if self.match(f): message = f.messages[-1] direction = "->" if message.from_client else "<-" - self.echo("{client} {direction} tcp {direction} {server}".format( - client=human.format_address(f.client_conn.peername), - server=human.format_address(f.server_conn.address), - direction=direction, - )) + self.echo( + "{client} {direction} tcp {direction} {server}".format( + client=human.format_address(f.client_conn.peername), + server=human.format_address(f.server_conn.address), + direction=direction, + ) + ) if ctx.options.flow_detail >= 3: self._echo_message(message, f) + + def _echo_dns_query(self, f: dns.DNSFlow) -> None: + client = self._fmt_client(f) + opcode = dns.op_codes.to_str(f.request.op_code) + type = dns.types.to_str(f.request.questions[0].type) + + desc = f"DNS {opcode} ({type})" + desc_color = { + "DNS QUERY (A)": "green", + "DNS QUERY (AAAA)": "magenta", + }.get(type, "red") + desc = self.style(desc, fg=desc_color) + + name = self.style(f.request.questions[0].name, bold=True) + self.echo(f"{client}: {desc} {name}") + + def dns_response(self, f: dns.DNSFlow): + assert f.response + if self.match(f): + self._echo_dns_query(f) + + arrows = self.style(" <<", bold=True) + answers = ", ".join( + self.style(str(x), fg="bright_blue") for x in f.response.answers + ) + self.echo(f"{arrows} {answers}") + + def dns_error(self, f: dns.DNSFlow): + assert f.error + if self.match(f): + self._echo_dns_query(f) + msg = strutils.escape_control_characters(f.error.msg) + self.echo(f" << {msg}", bold=True, fg="red") diff --git a/mitmproxy/addons/errorcheck.py b/mitmproxy/addons/errorcheck.py new file mode 100644 index 0000000000..6669ee435e --- /dev/null +++ b/mitmproxy/addons/errorcheck.py @@ -0,0 +1,29 @@ +import asyncio +import sys + +from mitmproxy import log + + +class ErrorCheck: + """Monitor startup for error log entries, and terminate immediately if there are some.""" + + def __init__(self, log_to_stderr: bool = False): + self.has_errored: list[str] = [] + self.log_to_stderr = log_to_stderr + + def add_log(self, e: log.LogEntry): + if e.level == "error": + self.has_errored.append(e.msg) + + async def running(self): + # don't run immediately, wait for all logging tasks to finish. + asyncio.create_task(self._shutdown_if_errored()) + + async def _shutdown_if_errored(self): + if self.has_errored: + if self.log_to_stderr: + plural = "s" if len(self.has_errored) > 1 else "" + msg = "\n".join(self.has_errored) + print(f"Error{plural} on startup: {msg}", file=sys.stderr) + + sys.exit(1) diff --git a/mitmproxy/addons/eventstore.py b/mitmproxy/addons/eventstore.py index ca20dcd1f3..07dbdf06bb 100644 --- a/mitmproxy/addons/eventstore.py +++ b/mitmproxy/addons/eventstore.py @@ -1,5 +1,5 @@ import collections -import typing # noqa +from typing import Optional import blinker @@ -9,12 +9,12 @@ class EventStore: def __init__(self, size=10000): - self.data: typing.Deque[LogEntry] = collections.deque(maxlen=size) + self.data: collections.deque[LogEntry] = collections.deque(maxlen=size) self.sig_add = blinker.Signal() self.sig_refresh = blinker.Signal() @property - def size(self) -> typing.Optional[int]: + def size(self) -> Optional[int]: return self.data.maxlen def add_log(self, entry: LogEntry) -> None: diff --git a/mitmproxy/addons/export.py b/mitmproxy/addons/export.py index 10c530623d..731c4f3420 100644 --- a/mitmproxy/addons/export.py +++ b/mitmproxy/addons/export.py @@ -1,5 +1,6 @@ import shlex -import typing +from collections.abc import Callable, Sequence +from typing import Any, Union import pyperclip @@ -23,7 +24,7 @@ def cleanup_request(f: flow.Flow) -> http.Request: def pop_headers(request: http.Request) -> http.Request: # Remove some headers that are redundant for curl/httpie export - request.headers.pop('content-length') + request.headers.pop("content-length") if request.headers.get("host", "") == request.host: request.headers.pop("host") if request.headers.get(":authority", "") == request.host: @@ -49,10 +50,7 @@ def request_content_for_console(request: http.Request) -> str: # see https://github.com/python/cpython/pull/10871 raise exceptions.CommandError("Request content must be valid unicode") escape_control_chars = {chr(i): f"\\x{i:02x}" for i in range(32)} - return "".join( - escape_control_chars.get(x, x) - for x in text - ) + return "".join(escape_control_chars.get(x, x) for x in text) def curl_command(f: flow.Flow) -> str: @@ -62,8 +60,12 @@ def curl_command(f: flow.Flow) -> str: server_addr = f.server_conn.peername[0] if f.server_conn.peername else None - if ctx.options.export_preserve_original_ip and server_addr and request.pretty_host != server_addr: - resolve = "{}:{}:[{}]".format(request.pretty_host, request.port, server_addr) + if ( + ctx.options.export_preserve_original_ip + and server_addr + and request.pretty_host != server_addr + ): + resolve = f"{request.pretty_host}:{request.port}:[{server_addr}]" args.append("--resolve") args.append(resolve) @@ -80,7 +82,7 @@ def curl_command(f: flow.Flow) -> str: if request.content: args += ["-d", request_content_for_console(request)] - return ' '.join(shlex.quote(arg) for arg in args) + return " ".join(shlex.quote(arg) for arg in args) def httpie_command(f: flow.Flow) -> str: @@ -95,7 +97,7 @@ def httpie_command(f: flow.Flow) -> str: args = ["http", request.method, url] for k, v in request.headers.items(multi=True): args.append(f"{k}: {v}") - cmd = ' '.join(shlex.quote(arg) for arg in args) + cmd = " ".join(shlex.quote(arg) for arg in args) if request.content: cmd += " <<< " + shlex.quote(request_content_for_console(request)) return cmd @@ -117,8 +119,14 @@ def raw_response(f: flow.Flow) -> bytes: def raw(f: flow.Flow, separator=b"\r\n\r\n") -> bytes: """Return either the request or response if only one exists, otherwise return both""" - request_present = isinstance(f, http.HTTPFlow) and f.request and f.request.raw_content is not None - response_present = isinstance(f, http.HTTPFlow) and f.response and f.response.raw_content is not None + request_present = ( + isinstance(f, http.HTTPFlow) and f.request and f.request.raw_content is not None + ) + response_present = ( + isinstance(f, http.HTTPFlow) + and f.response + and f.response.raw_content is not None + ) if request_present and response_present: return b"".join([raw_request(f), separator, raw_response(f)]) @@ -130,7 +138,7 @@ def raw(f: flow.Flow, separator=b"\r\n\r\n") -> bytes: raise exceptions.CommandError("Can't export flow with no request or response.") -formats: typing.Dict[str, typing.Callable[[flow.Flow], typing.Union[str, bytes]]] = dict( +formats: dict[str, Callable[[flow.Flow], Union[str, bytes]]] = dict( curl=curl_command, httpie=httpie_command, raw=raw, @@ -142,31 +150,33 @@ def raw(f: flow.Flow, separator=b"\r\n\r\n") -> bytes: class Export: def load(self, loader): loader.add_option( - "export_preserve_original_ip", bool, False, + "export_preserve_original_ip", + bool, + False, """ When exporting a request as an external command, make an effort to connect to the same IP as in the original request. This helps with reproducibility in cases where the behaviour depends on the particular host we are connecting to. Currently this only affects curl exports. - """ + """, ) @command.command("export.formats") - def formats(self) -> typing.Sequence[str]: + def formats(self) -> Sequence[str]: """ - Return a list of the supported export formats. + Return a list of the supported export formats. """ return list(sorted(formats.keys())) @command.command("export.file") def file(self, format: str, flow: flow.Flow, path: mitmproxy.types.Path) -> None: """ - Export a flow to path. + Export a flow to path. """ if format not in formats: raise exceptions.CommandError("No such export format: %s" % format) - func: typing.Any = formats[format] + func: Any = formats[format] v = func(flow) try: with open(path, "wb") as fp: @@ -180,7 +190,7 @@ def file(self, format: str, flow: flow.Flow, path: mitmproxy.types.Path) -> None @command.command("export.clip") def clip(self, format: str, f: flow.Flow) -> None: """ - Export a flow to the system clipboard. + Export a flow to the system clipboard. """ try: pyperclip.copy(self.export_str(format, f)) @@ -190,7 +200,7 @@ def clip(self, format: str, f: flow.Flow) -> None: @command.command("export") def export_str(self, format: str, f: flow.Flow) -> str: """ - Export a flow and return the result. + Export a flow and return the result. """ if format not in formats: raise exceptions.CommandError("No such export format: %s" % format) diff --git a/mitmproxy/addons/intercept.py b/mitmproxy/addons/intercept.py index 36f269bd46..d021dc2f4c 100644 --- a/mitmproxy/addons/intercept.py +++ b/mitmproxy/addons/intercept.py @@ -1,4 +1,4 @@ -import typing +from typing import Optional from mitmproxy import flow, flowfilter from mitmproxy import exceptions @@ -6,16 +6,12 @@ class Intercept: - filt: typing.Optional[flowfilter.TFilter] = None + filt: Optional[flowfilter.TFilter] = None def load(self, loader): + loader.add_option("intercept_active", bool, False, "Intercept toggle") loader.add_option( - "intercept_active", bool, False, - "Intercept toggle" - ) - loader.add_option( - "intercept", typing.Optional[str], None, - "Intercept filter expression." + "intercept", Optional[str], None, "Intercept filter expression." ) def configure(self, updated): @@ -32,17 +28,14 @@ def configure(self, updated): def should_intercept(self, f: flow.Flow) -> bool: return bool( - ctx.options.intercept_active - and self.filt - and self.filt(f) - and not f.is_replay + ctx.options.intercept_active + and self.filt + and self.filt(f) + and not f.is_replay ) def process_flow(self, f: flow.Flow) -> None: if self.should_intercept(f): - assert f.reply - if f.reply.state != "start": - return ctx.log.debug("Cannot intercept request that is already taken by another addon.") f.intercept() # Handlers @@ -55,3 +48,9 @@ def response(self, f): def tcp_message(self, f): self.process_flow(f) + + def dns_request(self, f): + self.process_flow(f) + + def dns_response(self, f): + self.process_flow(f) diff --git a/mitmproxy/addons/keepserving.py b/mitmproxy/addons/keepserving.py index 161f33ff04..5a149fdec1 100644 --- a/mitmproxy/addons/keepserving.py +++ b/mitmproxy/addons/keepserving.py @@ -5,12 +5,14 @@ class KeepServing: def load(self, loader): loader.add_option( - "keepserving", bool, False, + "keepserving", + bool, + False, """ Continue serving after client playback, server playback or file read. This option is ignored by interactive tools, which always keep serving. - """ + """, ) def keepgoing(self) -> bool: @@ -37,4 +39,4 @@ def running(self): ctx.options.rfile, ] if any(opts) and not ctx.options.keepserving: - asyncio.get_event_loop().create_task(self.watch()) + asyncio.get_running_loop().create_task(self.watch()) diff --git a/mitmproxy/addons/maplocal.py b/mitmproxy/addons/maplocal.py index bcee93e03f..4ced741ce3 100644 --- a/mitmproxy/addons/maplocal.py +++ b/mitmproxy/addons/maplocal.py @@ -1,8 +1,9 @@ import mimetypes import re -import typing import urllib.parse +from collections.abc import Sequence from pathlib import Path +from typing import NamedTuple from werkzeug.security import safe_join @@ -10,7 +11,7 @@ from mitmproxy.utils.spec import parse_spec -class MapLocalSpec(typing.NamedTuple): +class MapLocalSpec(NamedTuple): matches: flowfilter.TFilter regex: str local_path: Path @@ -38,16 +39,13 @@ def _safe_path_join(root: Path, untrusted: str) -> Path: This is a convenience wrapper for werkzeug's safe_join, raising a ValueError if the path is malformed.""" untrusted_parts = Path(untrusted).parts - joined = safe_join( - root.as_posix(), - *untrusted_parts - ) + joined = safe_join(root.as_posix(), *untrusted_parts) if joined is None: raise ValueError("Untrusted paths.") return Path(joined) -def file_candidates(url: str, spec: MapLocalSpec) -> typing.List[Path]: +def file_candidates(url: str, spec: MapLocalSpec) -> list[Path]: """ Get all potential file candidates given a URL and a mapping spec ordered by preference. This function already assumes that the spec regex matches the URL. @@ -69,10 +67,7 @@ def file_candidates(url: str, spec: MapLocalSpec) -> typing.List[Path]: if decoded_suffix != escaped_suffix: suffix_candidates.extend([escaped_suffix, f"{escaped_suffix}/index.html"]) try: - return [ - _safe_path_join(spec.local_path, x) - for x in suffix_candidates - ] + return [_safe_path_join(spec.local_path, x) for x in suffix_candidates] except ValueError: return [] else: @@ -81,16 +76,18 @@ def file_candidates(url: str, spec: MapLocalSpec) -> typing.List[Path]: class MapLocal: def __init__(self): - self.replacements: typing.List[MapLocalSpec] = [] + self.replacements: list[MapLocalSpec] = [] def load(self, loader): loader.add_option( - "map_local", typing.Sequence[str], [], + "map_local", + Sequence[str], + [], """ Map remote resources to a local file using a pattern of the form "[/flow-filter]/url-regex/file-or-directory-path", where the separator can be any character. - """ + """, ) def configure(self, updated): @@ -100,12 +97,14 @@ def configure(self, updated): try: spec = parse_map_local_spec(option) except ValueError as e: - raise exceptions.OptionsError(f"Cannot parse map_local option {option}: {e}") from e + raise exceptions.OptionsError( + f"Cannot parse map_local option {option}: {e}" + ) from e self.replacements.append(spec) def request(self, flow: http.HTTPFlow) -> None: - if flow.response or flow.error or (flow.reply and flow.reply.state == "taken"): + if flow.response or flow.error or not flow.live: return url = flow.request.pretty_url @@ -126,9 +125,7 @@ def request(self, flow: http.HTTPFlow) -> None: break if local_file: - headers = { - "Server": version.MITMPROXY - } + headers = {"Server": version.MITMPROXY} mimetype = mimetypes.guess_type(str(local_file))[0] if mimetype: headers["Content-Type"] = mimetype @@ -139,13 +136,11 @@ def request(self, flow: http.HTTPFlow) -> None: ctx.log.warn(f"Could not read file: {e}") continue - flow.response = http.Response.make( - 200, - contents, - headers - ) + flow.response = http.Response.make(200, contents, headers) # only set flow.response once, for the first matching rule return if all_candidates: flow.response = http.Response.make(404) - ctx.log.info(f"None of the local file candidates exist: {', '.join(str(x) for x in all_candidates)}") + ctx.log.info( + f"None of the local file candidates exist: {', '.join(str(x) for x in all_candidates)}" + ) diff --git a/mitmproxy/addons/mapremote.py b/mitmproxy/addons/mapremote.py index 183a4c282d..2fe6c2d2ef 100644 --- a/mitmproxy/addons/mapremote.py +++ b/mitmproxy/addons/mapremote.py @@ -1,11 +1,12 @@ import re -import typing +from collections.abc import Sequence +from typing import NamedTuple from mitmproxy import ctx, exceptions, flowfilter, http from mitmproxy.utils.spec import parse_spec -class MapRemoteSpec(typing.NamedTuple): +class MapRemoteSpec(NamedTuple): matches: flowfilter.TFilter subject: str replacement: str @@ -24,16 +25,18 @@ def parse_map_remote_spec(option: str) -> MapRemoteSpec: class MapRemote: def __init__(self): - self.replacements: typing.List[MapRemoteSpec] = [] + self.replacements: list[MapRemoteSpec] = [] def load(self, loader): loader.add_option( - "map_remote", typing.Sequence[str], [], + "map_remote", + Sequence[str], + [], """ Map remote resources to another remote URL using a pattern of the form "[/flow-filter]/url-regex/replacement", where the separator can be any character. - """ + """, ) def configure(self, updated): @@ -43,12 +46,14 @@ def configure(self, updated): try: spec = parse_map_remote_spec(option) except ValueError as e: - raise exceptions.OptionsError(f"Cannot parse map_remote option {option}: {e}") from e + raise exceptions.OptionsError( + f"Cannot parse map_remote option {option}: {e}" + ) from e self.replacements.append(spec) def request(self, flow: http.HTTPFlow) -> None: - if flow.response or flow.error or (flow.reply and flow.reply.state == "taken"): + if flow.response or flow.error or not flow.live: return for spec in self.replacements: if spec.matches(flow): diff --git a/mitmproxy/addons/modifybody.py b/mitmproxy/addons/modifybody.py index 6aa42ee41e..c8e3760a17 100644 --- a/mitmproxy/addons/modifybody.py +++ b/mitmproxy/addons/modifybody.py @@ -1,5 +1,5 @@ import re -import typing +from collections.abc import Sequence from mitmproxy import ctx, exceptions from mitmproxy.addons.modifyheaders import parse_modify_spec, ModifySpec @@ -7,16 +7,18 @@ class ModifyBody: def __init__(self): - self.replacements: typing.List[ModifySpec] = [] + self.replacements: list[ModifySpec] = [] def load(self, loader): loader.add_option( - "modify_body", typing.Sequence[str], [], + "modify_body", + Sequence[str], + [], """ Replacement pattern of the form "[/flow-filter]/regex/[@]replacement", where the separator can be any character. The @ allows to provide a file path that is used to read the replacement string. - """ + """, ) def configure(self, updated): @@ -26,17 +28,19 @@ def configure(self, updated): try: spec = parse_modify_spec(option, True) except ValueError as e: - raise exceptions.OptionsError(f"Cannot parse modify_body option {option}: {e}") from e + raise exceptions.OptionsError( + f"Cannot parse modify_body option {option}: {e}" + ) from e self.replacements.append(spec) def request(self, flow): - if flow.response or flow.error or flow.reply.state == "taken": + if flow.response or flow.error or not flow.live: return self.run(flow) def response(self, flow): - if flow.error or flow.reply.state == "taken": + if flow.error or not flow.live: return self.run(flow) @@ -49,6 +53,13 @@ def run(self, flow): ctx.log.warn(f"Could not read replacement file: {e}") continue if flow.response: - flow.response.content = re.sub(spec.subject, replacement, flow.response.content, flags=re.DOTALL) + flow.response.content = re.sub( + spec.subject, + replacement, + flow.response.content, + flags=re.DOTALL, + ) else: - flow.request.content = re.sub(spec.subject, replacement, flow.request.content, flags=re.DOTALL) + flow.request.content = re.sub( + spec.subject, replacement, flow.request.content, flags=re.DOTALL + ) diff --git a/mitmproxy/addons/modifyheaders.py b/mitmproxy/addons/modifyheaders.py index 38545d30de..d571d4a10a 100644 --- a/mitmproxy/addons/modifyheaders.py +++ b/mitmproxy/addons/modifyheaders.py @@ -1,6 +1,7 @@ import re -import typing +from collections.abc import Sequence from pathlib import Path +from typing import NamedTuple from mitmproxy import ctx, exceptions, flowfilter, http from mitmproxy.http import Headers @@ -8,7 +9,7 @@ from mitmproxy.utils.spec import parse_spec -class ModifySpec(typing.NamedTuple): +class ModifySpec(NamedTuple): matches: flowfilter.TFilter subject: bytes replacement_str: str @@ -50,16 +51,18 @@ def parse_modify_spec(option: str, subject_is_regex: bool) -> ModifySpec: class ModifyHeaders: def __init__(self): - self.replacements: typing.List[ModifySpec] = [] + self.replacements: list[ModifySpec] = [] def load(self, loader): loader.add_option( - "modify_headers", typing.Sequence[str], [], + "modify_headers", + Sequence[str], + [], """ Header modify pattern of the form "[/flow-filter]/header-name/[@]header-value", where the separator can be any character. The @ allows to provide a file path that is used to read the header value string. An empty header-value removes existing header-name headers. - """ + """, ) def configure(self, updated): @@ -69,16 +72,18 @@ def configure(self, updated): try: spec = parse_modify_spec(option, False) except ValueError as e: - raise exceptions.OptionsError(f"Cannot parse modify_headers option {option}: {e}") from e + raise exceptions.OptionsError( + f"Cannot parse modify_headers option {option}: {e}" + ) from e self.replacements.append(spec) def request(self, flow): - if flow.response or flow.error or flow.reply.state == "taken": + if flow.response or flow.error or not flow.live: return self.run(flow, flow.request.headers) def response(self, flow): - if flow.error or flow.reply.state == "taken": + if flow.error or not flow.live: return self.run(flow, flow.response.headers) diff --git a/mitmproxy/addons/next_layer.py b/mitmproxy/addons/next_layer.py index 090abe3886..e2e57e96b9 100644 --- a/mitmproxy/addons/next_layer.py +++ b/mitmproxy/addons/next_layer.py @@ -15,7 +15,8 @@ that sets nextlayer.layer works just as well. """ import re -from typing import Type, Sequence, Union, Tuple, Any, Iterable, Optional, List +from collections.abc import Sequence +from typing import Any, Iterable, Optional, Union from mitmproxy import ctx, exceptions, connection from mitmproxy.net.tls import is_tls_record_magic @@ -24,12 +25,11 @@ from mitmproxy.proxy.layers import modes from mitmproxy.proxy.layers.tls import HTTP_ALPNS, parse_client_hello -LayerCls = Type[layer.Layer] +LayerCls = type[layer.Layer] def stack_match( - context: context.Context, - layers: Sequence[Union[LayerCls, Tuple[LayerCls, ...]]] + context: context.Context, layers: Sequence[Union[LayerCls, tuple[LayerCls, ...]]] ) -> bool: if len(context.layers) != len(layers): return False @@ -51,7 +51,9 @@ def configure(self, updated): ] if "allow_hosts" in updated or "ignore_hosts" in updated: if ctx.options.allow_hosts and ctx.options.ignore_hosts: - raise exceptions.OptionsError("The allow_hosts and ignore_hosts options are mutually exclusive.") + raise exceptions.OptionsError( + "The allow_hosts and ignore_hosts options are mutually exclusive." + ) self.ignore_hosts = [ re.compile(x, re.IGNORECASE) for x in ctx.options.ignore_hosts ] @@ -59,7 +61,9 @@ def configure(self, updated): re.compile(x, re.IGNORECASE) for x in ctx.options.allow_hosts ] - def ignore_connection(self, server_address: Optional[connection.Address], data_client: bytes) -> Optional[bool]: + def ignore_connection( + self, server_address: Optional[connection.Address], data_client: bytes + ) -> Optional[bool]: """ Returns: True, if the connection should be ignored. @@ -69,7 +73,7 @@ def ignore_connection(self, server_address: Optional[connection.Address], data_c if not ctx.options.ignore_hosts and not ctx.options.allow_hosts: return False - hostnames: List[str] = [] + hostnames: list[str] = [] if server_address is not None: hostnames.append(server_address[0]) if is_tls_record_magic(data_client): @@ -110,7 +114,9 @@ def next_layer(self, nextlayer: layer.NextLayer): nextlayer.data_server(), ) - def _next_layer(self, context: context.Context, data_client: bytes, data_server: bytes) -> Optional[layer.Layer]: + def _next_layer( + self, context: context.Context, data_client: bytes, data_server: bytes + ) -> Optional[layer.Layer]: if len(context.layers) == 0: return self.make_top_layer(context) @@ -135,9 +141,9 @@ def s(*layers): # - a secure web proxy doesn't have a server part. # - reverse proxy mode manages this itself. if ( - s(modes.HttpProxy) or - s(modes.ReverseProxy) or - s(modes.ReverseProxy, layers.ServerTLSLayer) + s(modes.HttpProxy) + or s(modes.ReverseProxy) + or s(modes.ReverseProxy, layers.ServerTLSLayer) ): return layers.ClientTLSLayer(context) else: @@ -149,7 +155,8 @@ def s(*layers): # 3. Setup the HTTP layer for a regular HTTP proxy or an upstream proxy. if ( - s(modes.HttpProxy) or + s(modes.HttpProxy) + or # or a "Secure Web Proxy", see https://www.chromium.org/developers/design-documents/secure-web-proxy s(modes.HttpProxy, layers.ClientTLSLayer) ): @@ -160,19 +167,19 @@ def s(*layers): # 4. Check for --tcp if any( - (context.server.address and rex.search(context.server.address[0])) or - (context.client.sni and rex.search(context.client.sni)) - for rex in self.tcp_hosts + (context.server.address and rex.search(context.server.address[0])) + or (context.client.sni and rex.search(context.client.sni)) + for rex in self.tcp_hosts ): return layers.TCPLayer(context) # 5. Check for raw tcp mode. - very_likely_http = ( - context.client.alpn and context.client.alpn in HTTP_ALPNS - ) + very_likely_http = context.client.alpn and context.client.alpn in HTTP_ALPNS probably_no_http = not very_likely_http and ( - not data_client[:3].isalpha() # the first three bytes should be the HTTP verb, so A-Za-z is expected. - or data_server # a server greeting would be uncharacteristic. + not data_client[ + :3 + ].isalpha() # the first three bytes should be the HTTP verb, so A-Za-z is expected. + or data_server # a server greeting would be uncharacteristic. ) if ctx.options.rawtcp and probably_no_http: return layers.TCPLayer(context) diff --git a/mitmproxy/addons/onboarding.py b/mitmproxy/addons/onboarding.py index b104140e6e..272068cf96 100644 --- a/mitmproxy/addons/onboarding.py +++ b/mitmproxy/addons/onboarding.py @@ -14,19 +14,19 @@ def __init__(self): def load(self, loader): loader.add_option( - "onboarding", bool, True, - "Toggle the mitmproxy onboarding app." + "onboarding", bool, True, "Toggle the mitmproxy onboarding app." ) loader.add_option( - "onboarding_host", str, APP_HOST, + "onboarding_host", + str, + APP_HOST, """ Onboarding app domain. For transparent mode, use an IP when a DNS entry for the app domain is not present. - """ + """, ) loader.add_option( - "onboarding_port", int, APP_PORT, - "Port to serve the onboarding app from." + "onboarding_port", int, APP_PORT, "Port to serve the onboarding app from." ) def configure(self, updated): @@ -34,6 +34,6 @@ def configure(self, updated): self.port = ctx.options.onboarding_port app.config["CONFDIR"] = ctx.options.confdir - def request(self, f): + async def request(self, f): if ctx.options.onboarding: - super().request(f) + await super().request(f) diff --git a/mitmproxy/addons/onboardingapp/__init__.py b/mitmproxy/addons/onboardingapp/__init__.py index dbc67dc3c4..380633c95f 100644 --- a/mitmproxy/addons/onboardingapp/__init__.py +++ b/mitmproxy/addons/onboardingapp/__init__.py @@ -9,22 +9,22 @@ app.config["CONFDIR"] = CONF_DIR -@app.route('/') +@app.route("/") def index(): return render_template("index.html") -@app.route('/cert/pem') +@app.route("/cert/pem") def pem(): return read_cert("pem", "application/x-x509-ca-cert") -@app.route('/cert/p12') +@app.route("/cert/p12") def p12(): return read_cert("p12", "application/x-pkcs12") -@app.route('/cert/cer') +@app.route("/cert/cer") def cer(): return read_cert("cer", "application/x-x509-ca-cert") diff --git a/mitmproxy/addons/onboardingapp/templates/index.html b/mitmproxy/addons/onboardingapp/templates/index.html index 632f682c95..0da178624a 100644 --- a/mitmproxy/addons/onboardingapp/templates/index.html +++ b/mitmproxy/addons/onboardingapp/templates/index.html @@ -68,11 +68,8 @@
Automated Installation
iOS 13+
  1. Use Safari to download the certificate. Other browsers may not open the proper installation prompt.
  2. -
  3. Install the new Profile and activate it (Settings -> General -> - Profile). -
  4. -
  5. Important: Goto - Settings -> General -> About -> Certificate Trust Settings. +
  6. Install the new Profile (Settings -> General -> VPN & Device Management).
  7. +
  8. Important: Go to Settings -> General -> About -> Certificate Trust Settings. Toggle mitmproxy to ON.
{% endcall %} @@ -86,14 +83,15 @@
Android 10+

Some Android distributions require you to install the certificate via Settings -> Security -> Advanced -> - Encryption and credentials -> Install from storage instead.

+ Encryption and credentials -> Install a certificate -> CA certificate (or similar) instead.

{% endcall %} diff --git a/mitmproxy/addons/proxyauth.py b/mitmproxy/addons/proxyauth.py index e80295036d..494ecbe99b 100644 --- a/mitmproxy/addons/proxyauth.py +++ b/mitmproxy/addons/proxyauth.py @@ -5,7 +5,6 @@ from abc import ABC, abstractmethod from typing import MutableMapping from typing import Optional -from typing import Tuple import ldap3 import passlib.apache @@ -20,22 +19,26 @@ class ProxyAuth: - validator: Optional[Validator] = None + validator: Validator | None = None def __init__(self): - self.authenticated: MutableMapping[connection.Client, Tuple[str, str]] = weakref.WeakKeyDictionary() + self.authenticated: MutableMapping[ + connection.Client, tuple[str, str] + ] = weakref.WeakKeyDictionary() """Contains all connections that are permanently authenticated after an HTTP CONNECT""" def load(self, loader): loader.add_option( - "proxyauth", Optional[str], None, + "proxyauth", + Optional[str], + None, """ Require proxy authentication. Format: "username:pass", "any" to accept any user/pass combination, "@path" to use an Apache htpasswd file, - or "ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" for LDAP authentication. - """ + or "ldap[s]:url_server_ldap[:port]:dn_auth:password:dn_subtree" for LDAP authentication. + """, ) def configure(self, updated): @@ -44,7 +47,9 @@ def configure(self, updated): auth = ctx.options.proxyauth if auth: if ctx.options.mode == "transparent": - raise exceptions.OptionsError("Proxy Authentication not supported in transparent mode.") + raise exceptions.OptionsError( + "Proxy Authentication not supported in transparent mode." + ) if auth == "any": self.validator = AcceptAll() @@ -120,7 +125,7 @@ def make_auth_required_response(self) -> http.Response: f"

{status_code} {reason}

" f"" ), - headers + headers, ) @property @@ -144,13 +149,11 @@ def mkauth(username: str, password: str, scheme: str = "basic") -> str: """ Craft a basic auth string """ - v = binascii.b2a_base64( - (username + ":" + password).encode("utf8") - ).decode("ascii") + v = binascii.b2a_base64((username + ":" + password).encode("utf8")).decode("ascii") return scheme + " " + v -def parse_http_basic_auth(s: str) -> Tuple[str, str, str]: +def parse_http_basic_auth(s: str) -> tuple[str, str, str]: """ Parse a basic auth header. Raises a ValueError if the input is invalid. @@ -159,7 +162,9 @@ def parse_http_basic_auth(s: str) -> Tuple[str, str, str]: if scheme.lower() != "basic": raise ValueError("Unknown scheme") try: - user, password = binascii.a2b_base64(authinfo.encode()).decode("utf8", "replace").split(":") + user, password = ( + binascii.a2b_base64(authinfo.encode()).decode("utf8", "replace").split(":") + ) except binascii.Error as e: raise ValueError(str(e)) return scheme, user, password @@ -181,7 +186,7 @@ def __call__(self, username: str, password: str) -> bool: class SingleUser(Validator): def __init__(self, proxyauth: str): try: - self.username, self.password = proxyauth.split(':') + self.username, self.password = proxyauth.split(":") except ValueError: raise exceptions.OptionsError("Invalid single-user auth specification.") @@ -207,35 +212,54 @@ class Ldap(Validator): dn_subtree: str def __init__(self, proxyauth: str): - try: - security, url, ldap_user, ldap_pass, self.dn_subtree = proxyauth.split(":") - except ValueError: - raise exceptions.OptionsError("Invalid ldap specification") - if security == "ldaps": - server = ldap3.Server(url, use_ssl=True) - elif security == "ldap": - server = ldap3.Server(url) - else: - raise exceptions.OptionsError("Invalid ldap specification on the first part") - conn = ldap3.Connection( - server, + ( + use_ssl, + url, + port, ldap_user, ldap_pass, - auto_bind=True - ) + self.dn_subtree, + ) = self.parse_spec(proxyauth) + server = ldap3.Server(url, port=port, use_ssl=use_ssl) + conn = ldap3.Connection(server, ldap_user, ldap_pass, auto_bind=True) self.conn = conn self.server = server + @staticmethod + def parse_spec(spec: str) -> tuple[bool, str, int | None, str, str, str]: + try: + if spec.count(":") > 4: + ( + security, + url, + port_str, + ldap_user, + ldap_pass, + dn_subtree, + ) = spec.split(":") + port = int(port_str) + else: + security, url, ldap_user, ldap_pass, dn_subtree = spec.split(":") + port = None + + if security == "ldaps": + use_ssl = True + elif security == "ldap": + use_ssl = False + else: + raise ValueError + + return use_ssl, url, port, ldap_user, ldap_pass, dn_subtree + except ValueError: + raise exceptions.OptionsError(f"Invalid LDAP specification: {spec}") + def __call__(self, username: str, password: str) -> bool: if not username or not password: return False self.conn.search(self.dn_subtree, f"(cn={username})") if self.conn.response: c = ldap3.Connection( - self.server, - self.conn.response[0]["dn"], - password, - auto_bind=True + self.server, self.conn.response[0]["dn"], password, auto_bind=True ) if c: return True diff --git a/mitmproxy/addons/proxyserver.py b/mitmproxy/addons/proxyserver.py index 8dac0d88c1..f512de7e81 100644 --- a/mitmproxy/addons/proxyserver.py +++ b/mitmproxy/addons/proxyserver.py @@ -1,10 +1,27 @@ import asyncio -import warnings -from typing import Dict, Optional, Tuple +from asyncio import base_events +import ipaddress +import re +import struct +from typing import Optional -from mitmproxy import command, controller, ctx, exceptions, flow, http, log, master, options, platform, tcp, websocket -from mitmproxy.flow import Error, Flow -from mitmproxy.proxy import commands, events, server_hooks +from mitmproxy import ( + command, + ctx, + exceptions, + flow, + http, + log, + master, + options, + platform, + tcp, + websocket, +) +from mitmproxy.connection import Address +from mitmproxy.flow import Flow +from mitmproxy.net import udp +from mitmproxy.proxy import commands, events, layers, server_hooks from mitmproxy.proxy import server from mitmproxy.proxy.layers.tcp import TcpMessageInjected from mitmproxy.proxy.layers.websocket import WebSocketMessageInjected @@ -12,52 +29,29 @@ from wsproto.frame_protocol import Opcode -class AsyncReply(controller.Reply): - """ - controller.Reply.q.get() is blocking, which we definitely want to avoid in a coroutine. - This stub adds a .done asyncio.Event() that can be used instead. - """ - - def __init__(self, *args): - self.done = asyncio.Event() - self.loop = asyncio.get_event_loop() - super().__init__(*args) - - def commit(self): - super().commit() - try: - self.loop.call_soon_threadsafe(lambda: self.done.set()) - except RuntimeError: # pragma: no cover - pass # event loop may already be closed. - - def kill(self, force=False): # pragma: no cover - warnings.warn("reply.kill() is deprecated, set the error attribute instead.", DeprecationWarning, stacklevel=2) - self.obj.error = flow.Error(Error.KILLED_MESSAGE) - - -class ProxyConnectionHandler(server.StreamConnectionHandler): +class ProxyConnectionHandler(server.LiveConnectionHandler): master: master.Master - def __init__(self, master, r, w, options): + def __init__(self, master, r, w, options, timeout=None): self.master = master super().__init__(r, w, options) self.log_prefix = f"{human.format_address(self.client.peername)}: " + if timeout is not None: + self.timeout_watchdog.CONNECTION_TIMEOUT = timeout async def handle_hook(self, hook: commands.StartHook) -> None: with self.timeout_watchdog.disarm(): # We currently only support single-argument hooks. - data, = hook.args() - data.reply = AsyncReply(data) + (data,) = hook.args() await self.master.addons.handle_lifecycle(hook) - await data.reply.done.wait() - data.reply = None + if isinstance(data, flow.Flow): + await data.wait_for_resume() def log(self, message: str, level: str = "info") -> None: x = log.LogEntry(self.log_prefix + message, level) - x.reply = controller.DummyReply() # type: ignore asyncio_utils.create_task( self.master.addons.handle_lifecycle(log.AddLogHook(x)), - name="ProxyConnectionHandler.log" + name="ProxyConnectionHandler.log", ) @@ -65,139 +59,329 @@ class Proxyserver: """ This addon runs the actual proxy server. """ - server: Optional[asyncio.AbstractServer] + + tcp_server: Optional[base_events.Server] + dns_server: Optional[udp.UdpServer] + connect_addr: Optional[Address] listen_port: int + dns_reverse_addr: Optional[tuple[str, int]] master: master.Master options: options.Options is_running: bool - _connections: Dict[Tuple, ProxyConnectionHandler] + _connections: dict[tuple, ProxyConnectionHandler] def __init__(self): self._lock = asyncio.Lock() - self.server = None + self.tcp_server = None + self.dns_server = None + self.connect_addr = None + self.dns_reverse_addr = None self.is_running = False self._connections = {} def __repr__(self): - return f"ProxyServer({'running' if self.server else 'stopped'}, {len(self._connections)} active conns)" + return f"ProxyServer({'running' if self.running_servers else 'stopped'}, {len(self._connections)} active conns)" + + @property + def _server_desc(self): + yield "Proxy", self.tcp_server, lambda x: setattr( + self, "tcp_server", x + ), ctx.options.server, lambda: asyncio.start_server( + self.handle_tcp_connection, + self.options.listen_host, + self.options.listen_port, + ) + yield "DNS", self.dns_server, lambda x: setattr( + self, "dns_server", x + ), ctx.options.dns_server, lambda: udp.start_server( + self.handle_dns_datagram, + self.options.dns_listen_host or "127.0.0.1", + self.options.dns_listen_port, + transparent=self.options.dns_mode == "transparent", + ) + + @property + def running_servers(self): + return tuple( + instance + for _, instance, _, _, _ in self._server_desc + if instance is not None + ) def load(self, loader): loader.add_option( - "connection_strategy", str, "eager", + "connection_strategy", + str, + "eager", "Determine when server connections should be established. When set to lazy, mitmproxy " "tries to defer establishing an upstream connection as long as possible. This makes it possible to " "use server replay while being offline. When set to eager, mitmproxy can detect protocols with " "server-side greetings, as well as accurately mirror TLS ALPN negotiation.", - choices=("eager", "lazy") + choices=("eager", "lazy"), ) loader.add_option( - "stream_large_bodies", Optional[str], None, + "stream_large_bodies", + Optional[str], + None, """ Stream data to the client if response body exceeds the given threshold. If streamed, the body will not be stored in any way. Understands k/m/g suffixes, i.e. 3m for 3 megabytes. - """ + """, ) loader.add_option( - "body_size_limit", Optional[str], None, + "body_size_limit", + Optional[str], + None, """ Byte size limit of HTTP request and response bodies. Understands k/m/g suffixes, i.e. 3m for 3 megabytes. - """ + """, ) loader.add_option( - "keep_host_header", bool, False, + "keep_host_header", + bool, + False, """ Reverse Proxy: Keep the original host header instead of rewriting it to the reverse proxy target. - """ + """, ) loader.add_option( - "proxy_debug", bool, False, + "proxy_debug", + bool, + False, "Enable debug logs in the proxy core.", ) + loader.add_option( + "normalize_outbound_headers", + bool, + True, + """ + Normalize outgoing HTTP/2 header names, but emit a warning when doing so. + HTTP/2 does not allow uppercase header names. This option makes sure that HTTP/2 headers set + in custom scripts are lowercased before they are sent. + """, + ) + loader.add_option( + "validate_inbound_headers", + bool, + True, + """ + Make sure that incoming HTTP requests are not malformed. + Disabling this option makes mitmproxy vulnerable to HTTP smuggling attacks. + """, + ) + loader.add_option( + "connect_addr", + Optional[str], + None, + """Set the local IP address that mitmproxy should use when connecting to upstream servers.""", + ) + loader.add_option( + "dns_server", bool, False, """Start a DNS server. Disabled by default.""" + ) + loader.add_option( + "dns_listen_host", str, "", """Address to bind DNS server to.""" + ) + loader.add_option("dns_listen_port", int, 53, """DNS server service port.""") + loader.add_option( + "dns_mode", + str, + "regular", + """ + One of "regular", "reverse:[:]" or "transparent". + regular....: requests will be resolved using the local resolver + reverse....: forward queries to another DNS server + transparent: transparent mode + """, + ) - def running(self): + async def running(self): self.master = ctx.master self.options = ctx.options self.is_running = True - self.configure(["listen_port"]) + await self.refresh_server() def configure(self, updated): if "stream_large_bodies" in updated: try: human.parse_size(ctx.options.stream_large_bodies) except ValueError: - raise exceptions.OptionsError(f"Invalid stream_large_bodies specification: " - f"{ctx.options.stream_large_bodies}") + raise exceptions.OptionsError( + f"Invalid stream_large_bodies specification: " + f"{ctx.options.stream_large_bodies}" + ) if "body_size_limit" in updated: try: human.parse_size(ctx.options.body_size_limit) except ValueError: - raise exceptions.OptionsError(f"Invalid body_size_limit specification: " - f"{ctx.options.body_size_limit}") - if not self.is_running: - return + raise exceptions.OptionsError( + f"Invalid body_size_limit specification: " + f"{ctx.options.body_size_limit}" + ) + if "connect_addr" in updated: + try: + self.connect_addr = (str(ipaddress.ip_address(ctx.options.connect_addr)), 0) if ctx.options.connect_addr else None + except ValueError: + raise exceptions.OptionsError( + f"Invalid connection address {ctx.options.connect_addr!r}, specify a valid IP address." + ) + + if "dns_mode" in updated: + m = re.match( + r"^(regular|reverse:(?P[^:]+)(:(?P\d+))?|transparent)$", + ctx.options.dns_mode, + ) + if not m: + raise exceptions.OptionsError( + f"Invalid DNS mode {ctx.options.dns_mode!r}." + ) + if m["host"]: + try: + self.dns_reverse_addr = ( + str(ipaddress.ip_address(m["host"])), + int(m["port"]) if m["port"] is not None else 53, + ) + except ValueError: + raise exceptions.OptionsError( + f"Invalid DNS reverse mode, expected 'reverse:ip[:port]' got {ctx.options.dns_mode!r}." + ) + else: + self.dns_reverse_addr = None if "mode" in updated and ctx.options.mode == "transparent": # pragma: no cover platform.init_transparent_mode() - if any(x in updated for x in ["server", "listen_host", "listen_port"]): + if self.is_running and any( + x in updated + for x in [ + "server", + "listen_host", + "listen_port", + "dns_server", + "dns_mode", + "dns_listen_host", + "dns_listen_port", + ] + ): asyncio.create_task(self.refresh_server()) async def refresh_server(self): async with self._lock: - if self.server: - await self.shutdown_server() - self.server = None - if ctx.options.server: - if not ctx.master.addons.get("nextlayer"): - ctx.log.warn("Warning: Running proxyserver without nextlayer addon!") - self.server = await asyncio.start_server( - self.handle_connection, - self.options.listen_host, - self.options.listen_port, - ) - addrs = {f"http://{human.format_address(s.getsockname())}" for s in self.server.sockets} - ctx.log.info(f"Proxy server listening at {' and '.join(addrs)}") + await self.shutdown_server() + if ctx.options.server and not ctx.master.addons.get("nextlayer"): + ctx.log.warn("Warning: Running proxyserver without nextlayer addon!") + for name, instance, set_instance, enabled, start in self._server_desc: + if instance is None and enabled: + try: + instance = await start() + except OSError as e: + ctx.log.error(str(e)) + else: + set_instance(instance) + # TODO: This is a bit confusing currently for `-p 0`. + addrs = { + f"{human.format_address(s.getsockname())}" + for s in instance.sockets + } + ctx.log.info( + f"{name} server listening at {' and '.join(addrs)}" + ) async def shutdown_server(self): - ctx.log.info("Stopping server...") - self.server.close() - await self.server.wait_closed() - self.server = None + for name, instance, set_instance, _, _ in self._server_desc: + if instance is not None: + ctx.log.info(f"Stopping {name} server...") + try: + instance.close() + await instance.wait_closed() + except OSError as e: + ctx.log.error(str(e)) + else: + set_instance(None) - async def handle_connection(self, r, w): - peername = w.get_extra_info('peername') + async def handle_connection(self, connection_id: tuple): + handler = self._connections[connection_id] + task = asyncio.current_task() + assert task asyncio_utils.set_task_debug_info( - asyncio.current_task(), + task, name=f"Proxyserver.handle_connection", - client=peername, - ) - handler = ProxyConnectionHandler( - self.master, - r, - w, - self.options + client=handler.client.peername, ) - self._connections[peername] = handler try: await handler.handle_client() finally: - del self._connections[peername] + del self._connections[connection_id] + + async def handle_tcp_connection( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: + connection_id = ( + "tcp", + writer.get_extra_info("peername"), + writer.get_extra_info("sockname"), + ) + self._connections[connection_id] = ProxyConnectionHandler( + self.master, reader, writer, self.options + ) + await self.handle_connection(connection_id) + + def handle_dns_datagram( + self, + transport: asyncio.DatagramTransport, + data: bytes, + remote_addr: Address, + local_addr: Address, + ) -> None: + try: + dns_id = struct.unpack_from("!H", data, 0) + except struct.error: + ctx.log.info( + f"Invalid DNS datagram received from {human.format_address(remote_addr)}." + ) + return + connection_id = ("udp", dns_id, remote_addr, local_addr) + if connection_id not in self._connections: + reader = udp.DatagramReader() + writer = udp.DatagramWriter(transport, remote_addr, reader) + handler = ProxyConnectionHandler( + self.master, reader, writer, self.options, 20 + ) + handler.layer = layers.DNSLayer(handler.layer.context) + handler.layer.context.server.address = ( + local_addr + if self.options.dns_mode == "transparent" + else self.dns_reverse_addr + ) + handler.layer.context.server.transport_protocol = "udp" + self._connections[connection_id] = handler + asyncio.create_task(self.handle_connection(connection_id)) + else: + handler = self._connections[connection_id] + client_reader = handler.transports[handler.client].reader + assert isinstance(client_reader, udp.DatagramReader) + reader = client_reader + reader.feed_data(data, remote_addr) def inject_event(self, event: events.MessageInjected): - if event.flow.client_conn.peername not in self._connections: + connection_id = ( + "tcp", + event.flow.client_conn.peername, + event.flow.client_conn.sockname, + ) + if connection_id not in self._connections: raise ValueError("Flow is not from a live connection.") - self._connections[event.flow.client_conn.peername].server_event(event) + self._connections[connection_id].server_event(event) @command.command("inject.websocket") - def inject_websocket(self, flow: Flow, to_client: bool, message: bytes, is_text: bool = True): + def inject_websocket( + self, flow: Flow, to_client: bool, message: bytes, is_text: bool = True + ): if not isinstance(flow, http.HTTPFlow) or not flow.websocket: ctx.log.warn("Cannot inject WebSocket messages into non-WebSocket flows.") msg = websocket.WebSocketMessage( - Opcode.TEXT if is_text else Opcode.BINARY, - not to_client, - message + Opcode.TEXT if is_text else Opcode.BINARY, not to_client, message ) event = WebSocketMessageInjected(flow, msg) try: @@ -219,10 +403,21 @@ def inject_tcp(self, flow: Flow, to_client: bool, message: bytes): def server_connect(self, ctx: server_hooks.ServerConnectionHookData): return # :) assert ctx.server.address - self_connect = ( - ctx.server.address[1] == self.options.listen_port - and - ctx.server.address[0] in ("localhost", "127.0.0.1", "::1", self.options.listen_host) + # FIXME: Move this to individual proxy modes. + self_connect = ctx.server.address[1] in ( + self.options.dns_listen_port, + self.options.listen_port, + ) and ctx.server.address[0] in ( + "localhost", + "127.0.0.1", + "::1", + self.options.listen_host, + self.options.dns_listen_host, ) if self_connect: - ctx.server.error = "Stopped mitmproxy from recursively connecting to itself." + ctx.server.error = ( + "Request destination unknown. " + "Unable to figure out where this request should be forwarded to." + ) + if ctx.server.sockname is None: + ctx.server.sockname = self.connect_addr diff --git a/mitmproxy/addons/readfile.py b/mitmproxy/addons/readfile.py index c75bc8f7d3..4ff0048801 100644 --- a/mitmproxy/addons/readfile.py +++ b/mitmproxy/addons/readfile.py @@ -1,7 +1,7 @@ import asyncio import os.path import sys -import typing +from typing import BinaryIO, Optional from mitmproxy import ctx from mitmproxy import exceptions @@ -12,20 +12,17 @@ class ReadFile: """ - An addon that handles reading from file on startup. + An addon that handles reading from file on startup. """ + def __init__(self): self.filter = None self.is_reading = False def load(self, loader): + loader.add_option("rfile", Optional[str], None, "Read flows from file.") loader.add_option( - "rfile", typing.Optional[str], None, - "Read flows from file." - ) - loader.add_option( - "readfile_filter", typing.Optional[str], None, - "Read only matching flows." + "readfile_filter", Optional[str], None, "Read only matching flows." ) def configure(self, updated): @@ -38,7 +35,7 @@ def configure(self, updated): else: self.filter = None - async def load_flows(self, fo: typing.IO[bytes]) -> int: + async def load_flows(self, fo: BinaryIO) -> int: cnt = 0 freader = io.FlowReader(fo) try: @@ -76,7 +73,7 @@ async def doread(self, rfile): def running(self): if ctx.options.rfile: - asyncio.get_event_loop().create_task(self.doread(ctx.options.rfile)) + asyncio.get_running_loop().create_task(self.doread(ctx.options.rfile)) @command.command("readfile.reading") def reading(self) -> bool: @@ -85,6 +82,7 @@ def reading(self) -> bool: class ReadFileStdin(ReadFile): """Support the special case of "-" for reading from stdin""" + async def load_flows_from_path(self, path: str) -> int: if path == "-": # pragma: no cover # Need to think about how to test this. This function is scheduled diff --git a/mitmproxy/addons/save.py b/mitmproxy/addons/save.py index 3b3766dc64..32d3b4bd41 100644 --- a/mitmproxy/addons/save.py +++ b/mitmproxy/addons/save.py @@ -1,51 +1,66 @@ import os.path -import typing +import sys +from collections.abc import Sequence +from datetime import datetime +from functools import lru_cache +from pathlib import Path +from typing import Literal, Optional -from mitmproxy import command -from mitmproxy import exceptions -from mitmproxy import flowfilter -from mitmproxy import io +import mitmproxy.types +from mitmproxy import command, tcp from mitmproxy import ctx +from mitmproxy import dns +from mitmproxy import exceptions from mitmproxy import flow +from mitmproxy import flowfilter from mitmproxy import http -import mitmproxy.types +from mitmproxy import io + + +@lru_cache +def _path(path: str) -> str: + """Extract the path from a path spec (which may have an extra "+" at the front)""" + if path.startswith("+"): + path = path[1:] + return os.path.expanduser(path) + + +@lru_cache +def _mode(path: str) -> Literal["ab", "wb"]: + """Extract the writing mode (overwrite or append) from a path spec""" + if path.startswith("+"): + return "ab" + else: + return "wb" class Save: - def __init__(self): - self.stream = None - self.filt = None - self.active_flows: typing.Set[flow.Flow] = set() + def __init__(self) -> None: + self.stream: Optional[io.FilteredFlowWriter] = None + self.filt: Optional[flowfilter.TFilter] = None + self.active_flows: set[flow.Flow] = set() + self.current_path: Optional[str] = None def load(self, loader): loader.add_option( - "save_stream_file", typing.Optional[str], None, - "Stream flows to file as they arrive. Prefix path with + to append." + "save_stream_file", + Optional[str], + None, + """ + Stream flows to file as they arrive. Prefix path with + to append. + The full path can use python strftime() formating, missing + directories are created as needed. A new file is opened every time + the formatted string changes. + """, ) loader.add_option( - "save_stream_filter", typing.Optional[str], None, - "Filter which flows are written to file." + "save_stream_filter", + Optional[str], + None, + "Filter which flows are written to file.", ) - def open_file(self, path): - if path.startswith("+"): - path = path[1:] - mode = "ab" - else: - mode = "wb" - path = os.path.expanduser(path) - return open(path, mode) - - def start_stream_to_path(self, path, flt): - try: - f = self.open_file(path) - except OSError as v: - raise exceptions.OptionsError(str(v)) - self.stream = io.FilteredFlowWriter(f, flt) - self.active_flows = set() - def configure(self, updated): - # We're already streaming - stop the previous stream and restart if "save_stream_filter" in updated: if ctx.options.save_stream_filter: try: @@ -55,43 +70,86 @@ def configure(self, updated): else: self.filt = None if "save_stream_file" in updated or "save_stream_filter" in updated: - if self.stream: - self.done() if ctx.options.save_stream_file: - self.start_stream_to_path(ctx.options.save_stream_file, self.filt) + try: + self.maybe_rotate_to_new_file() + except OSError as e: + raise exceptions.OptionsError(str(e)) from e + self.stream.flt = self.filt + else: + self.done() + + def maybe_rotate_to_new_file(self) -> None: + path = datetime.today().strftime(_path(ctx.options.save_stream_file)) + if self.current_path == path: + return + + if self.stream: + self.stream.fo.close() + self.stream = None + + new_log_file = Path(path) + new_log_file.parent.mkdir(parents=True, exist_ok=True) + + f = new_log_file.open(_mode(ctx.options.save_stream_file)) + self.stream = io.FilteredFlowWriter(f, self.filt) + self.current_path = path + + def save_flow(self, flow: flow.Flow) -> None: + """ + Write the flow to the stream, but first check if we need to rotate to a new file. + """ + if not self.stream: + return + try: + self.maybe_rotate_to_new_file() + self.stream.add(flow) + except OSError as e: + # If we somehow fail to write flows to a logfile, we really want to crash visibly + # instead of letting traffic through unrecorded. + # No normal logging here, that would not be triggered anymore. + sys.stderr.write(f"Error while writing to {self.current_path}: {e}") + sys.exit(1) + else: + self.active_flows.discard(flow) + + def done(self) -> None: + if self.stream: + for f in self.active_flows: + self.stream.add(f) + self.active_flows.clear() + + self.current_path = None + self.stream.fo.close() + self.stream = None @command.command("save.file") - def save(self, flows: typing.Sequence[flow.Flow], path: mitmproxy.types.Path) -> None: + def save(self, flows: Sequence[flow.Flow], path: mitmproxy.types.Path) -> None: """ - Save flows to a file. If the path starts with a +, flows are - appended to the file, otherwise it is over-written. + Save flows to a file. If the path starts with a +, flows are + appended to the file, otherwise it is over-written. """ try: - f = self.open_file(path) - except OSError as v: - raise exceptions.CommandError(v) from v - stream = io.FlowWriter(f) - for i in flows: - stream.add(i) - f.close() - ctx.log.alert("Saved %s flows." % len(flows)) - - def tcp_start(self, flow): + with open(_path(path), _mode(path)) as f: + stream = io.FlowWriter(f) + for i in flows: + stream.add(i) + except OSError as e: + raise exceptions.CommandError(e) from e + ctx.log.alert(f"Saved {len(flows)} flows.") + + def tcp_start(self, flow: tcp.TCPFlow): if self.stream: self.active_flows.add(flow) - def tcp_end(self, flow): - if self.stream: - self.stream.add(flow) - self.active_flows.discard(flow) + def tcp_end(self, flow: tcp.TCPFlow): + self.save_flow(flow) - def tcp_error(self, flow): + def tcp_error(self, flow: tcp.TCPFlow): self.tcp_end(flow) def websocket_end(self, flow: http.HTTPFlow): - if self.stream: - self.stream.add(flow) - self.active_flows.discard(flow) + self.save_flow(flow) def request(self, flow: http.HTTPFlow): if self.stream: @@ -100,17 +158,18 @@ def request(self, flow: http.HTTPFlow): def response(self, flow: http.HTTPFlow): # websocket flows will receive a websocket_end, # we don't want to persist them here already - if self.stream and flow.websocket is None: - self.stream.add(flow) - self.active_flows.discard(flow) + if flow.websocket is None: + self.save_flow(flow) def error(self, flow: http.HTTPFlow): self.response(flow) - def done(self): + def dns_request(self, flow: dns.DNSFlow): if self.stream: - for f in self.active_flows: - self.stream.add(f) - self.active_flows = set() - self.stream.fo.close() - self.stream = None + self.active_flows.add(flow) + + def dns_response(self, flow: dns.DNSFlow): + self.save_flow(flow) + + def dns_error(self, flow: dns.DNSFlow): + self.save_flow(flow) diff --git a/mitmproxy/addons/script.py b/mitmproxy/addons/script.py index 4e146d67d9..50b939c7e5 100644 --- a/mitmproxy/addons/script.py +++ b/mitmproxy/addons/script.py @@ -4,8 +4,9 @@ import importlib.machinery import sys import types -import typing import traceback +from collections.abc import Sequence +from typing import Optional from mitmproxy import addonmanager, hooks from mitmproxy import exceptions @@ -14,9 +15,10 @@ from mitmproxy import eventsequence from mitmproxy import ctx import mitmproxy.types as mtypes +from mitmproxy.utils import asyncio_utils -def load_script(path: str) -> typing.Optional[types.ModuleType]: +def load_script(path: str) -> Optional[types.ModuleType]: fullname = "__mitmproxy_script__.{}".format( os.path.splitext(os.path.basename(path))[0] ) @@ -43,8 +45,8 @@ def load_script(path: str) -> typing.Optional[types.ModuleType]: def script_error_handler(path, exc, msg="", tb=False): """ - Handles all the user's script errors with - an optional traceback + Handles all the user's script errors with + an optional traceback """ exception = type(exc).__name__ if msg: @@ -55,8 +57,10 @@ def script_error_handler(path, exc, msg="", tb=False): log_msg = f"in script {path}:{lineno} {exception}" if tb: etype, value, tback = sys.exc_info() - tback = addonmanager.cut_traceback(tback, "invoke_addon") - log_msg = log_msg + "\n" + "".join(traceback.format_exception(etype, value, tback)) + tback = addonmanager.cut_traceback(tback, "invoke_addon_sync") + log_msg = ( + log_msg + "\n" + "".join(traceback.format_exception(etype, value, tback)) + ) ctx.log.error(log_msg) @@ -65,26 +69,31 @@ def script_error_handler(path, exc, msg="", tb=False): class Script: """ - An addon that manages a single script. + An addon that manages a single script. """ def __init__(self, path: str, reload: bool) -> None: self.name = "scriptmanager:" + path self.path = path - self.fullpath = os.path.expanduser( - path.strip("'\" ") - ) + self.fullpath = os.path.expanduser(path.strip("'\" ")) self.ns = None + self.is_running = False if not os.path.isfile(self.fullpath): - raise exceptions.OptionsError('No such script') + raise exceptions.OptionsError("No such script") self.reloadtask = None if reload: - self.reloadtask = asyncio.ensure_future(self.watcher()) + self.reloadtask = asyncio_utils.create_task( + self.watcher(), + name=f"script watcher for {path}", + ) else: self.loadscript() + def running(self): + self.is_running = True + def done(self): if self.reloadtask: self.reloadtask.cancel() @@ -103,16 +112,15 @@ def loadscript(self): ctx.master.addons.register(ns) self.ns = ns if self.ns: - # We're already running, so we have to explicitly register and - # configure the addon - ctx.master.addons.invoke_addon(self.ns, hooks.RunningHook()) try: - ctx.master.addons.invoke_addon( - self.ns, - hooks.ConfigureHook(ctx.options.keys()) + ctx.master.addons.invoke_addon_sync( + self.ns, hooks.ConfigureHook(ctx.options.keys()) ) except exceptions.OptionsError as e: script_error_handler(self.fullpath, e, msg=str(e)) + if self.is_running: + # We're already running, so we call that on the addon now. + ctx.master.addons.invoke_addon_sync(self.ns, hooks.RunningHook()) async def watcher(self): last_mtime = 0 @@ -133,42 +141,40 @@ async def watcher(self): class ScriptLoader: """ - An addon that manages loading scripts from options. + An addon that manages loading scripts from options. """ + def __init__(self): self.is_running = False self.addons = [] def load(self, loader): - loader.add_option( - "scripts", typing.Sequence[str], [], - "Execute a script." - ) + loader.add_option("scripts", Sequence[str], [], "Execute a script.") def running(self): self.is_running = True @command.command("script.run") - def script_run(self, flows: typing.Sequence[flow.Flow], path: mtypes.Path) -> None: + def script_run(self, flows: Sequence[flow.Flow], path: mtypes.Path) -> None: """ - Run a script on the specified flows. The script is configured with - the current options and all lifecycle events for each flow are - simulated. Note that the load event is not invoked. + Run a script on the specified flows. The script is configured with + the current options and all lifecycle events for each flow are + simulated. Note that the load event is not invoked. """ if not os.path.isfile(path): - ctx.log.error('No such script: %s' % path) + ctx.log.error("No such script: %s" % path) return mod = load_script(path) if mod: with addonmanager.safecall(): - ctx.master.addons.invoke_addon(mod, hooks.RunningHook()) - ctx.master.addons.invoke_addon( + ctx.master.addons.invoke_addon_sync( mod, hooks.ConfigureHook(ctx.options.keys()), ) + ctx.master.addons.invoke_addon_sync(mod, hooks.RunningHook()) for f in flows: for evt in eventsequence.iterate(f): - ctx.master.addons.invoke_addon(mod, evt) + ctx.master.addons.invoke_addon_sync(mod, evt) def configure(self, updated): if "scripts" in updated: @@ -209,4 +215,4 @@ def configure(self, updated): if self.is_running: # If we're already running, we configure and tell the addon # we're up and running. - ctx.master.addons.invoke_addon(s, hooks.RunningHook()) + ctx.master.addons.invoke_addon_sync(s, hooks.RunningHook()) diff --git a/mitmproxy/addons/server_side_events.py b/mitmproxy/addons/server_side_events.py new file mode 100644 index 0000000000..111244d3a9 --- /dev/null +++ b/mitmproxy/addons/server_side_events.py @@ -0,0 +1,21 @@ +from mitmproxy import ctx, http + + +class ServerSideEvents: + """ + Server-Side Events are currently swallowed if there's no streaming, + see https://github.com/mitmproxy/mitmproxy/issues/4469. + + Until this bug is fixed, this addon warns the user about this. + """ + + def response(self, flow: http.HTTPFlow): + assert flow.response + is_sse = flow.response.headers.get("content-type", "").startswith( + "text/event-stream" + ) + if is_sse and not flow.response.stream: + ctx.log.warn( + "mitmproxy currently does not support server side events. As a workaround, you can enable response " + "streaming for such flows: https://github.com/mitmproxy/mitmproxy/issues/4469" + ) diff --git a/mitmproxy/addons/serverplayback.py b/mitmproxy/addons/serverplayback.py index 149a7c0a26..3924db67df 100644 --- a/mitmproxy/addons/serverplayback.py +++ b/mitmproxy/addons/serverplayback.py @@ -1,6 +1,7 @@ import hashlib -import typing import urllib +from collections.abc import Hashable, Sequence +from typing import Any, Optional import mitmproxy.types from mitmproxy import command, hooks @@ -11,7 +12,7 @@ class ServerPlayback: - flowmap: typing.Dict[typing.Hashable, typing.List[http.HTTPFlow]] + flowmap: dict[Hashable, list[http.HTTPFlow]] configured: bool def __init__(self): @@ -20,69 +21,92 @@ def __init__(self): def load(self, loader): loader.add_option( - "server_replay_kill_extra", bool, False, - "Kill extra requests during replay." + "server_replay_kill_extra", + bool, + False, + "Kill extra requests during replay (for which no replayable response was found).", ) loader.add_option( - "server_replay_nopop", bool, False, + "server_replay_nopop", + bool, + False, """ Don't remove flows from server replay state after use. This makes it possible to replay same response multiple times. - """ + """, ) loader.add_option( - "server_replay_refresh", bool, True, + "server_replay_refresh", + bool, + True, """ Refresh server replay responses by adjusting date, expires and last-modified headers, as well as adjusting cookie expiration. - """ + """, ) loader.add_option( - "server_replay_use_headers", typing.Sequence[str], [], - "Request headers to be considered during replay." + "server_replay_use_headers", + Sequence[str], + [], + """ + Request headers that need to match while searching for a saved flow + to replay. + """, ) loader.add_option( - "server_replay", typing.Sequence[str], [], - "Replay server responses from a saved file." + "server_replay", + Sequence[str], + [], + "Replay server responses from a saved file.", ) loader.add_option( - "server_replay_ignore_content", bool, False, - "Ignore request's content while searching for a saved flow to replay." + "server_replay_ignore_content", + bool, + False, + "Ignore request content while searching for a saved flow to replay.", ) loader.add_option( - "server_replay_ignore_params", typing.Sequence[str], [], + "server_replay_ignore_params", + Sequence[str], + [], """ - Request's parameters to be ignored while searching for a saved flow + Request parameters to be ignored while searching for a saved flow to replay. - """ + """, ) loader.add_option( - "server_replay_ignore_payload_params", typing.Sequence[str], [], + "server_replay_ignore_payload_params", + Sequence[str], + [], """ - Request's payload parameters (application/x-www-form-urlencoded or + Request payload parameters (application/x-www-form-urlencoded or multipart/form-data) to be ignored while searching for a saved flow to replay. - """ + """, ) loader.add_option( - "server_replay_ignore_host", bool, False, + "server_replay_ignore_host", + bool, + False, """ - Ignore request's destination host while searching for a saved flow + Ignore request destination host while searching for a saved flow to replay. - """ + """, ) loader.add_option( - "server_replay_ignore_port", bool, False, + "server_replay_ignore_port", + bool, + False, """ - Ignore request's destination port while searching for a saved flow + Ignore request destination port while searching for a saved flow to replay. - """ + """, ) @command.command("replay.server") - def load_flows(self, flows: typing.Sequence[flow.Flow]) -> None: + def load_flows(self, flows: Sequence[flow.Flow]) -> None: """ - Replay server responses from flows. + Replay server responses from flows. """ self.flowmap = {} for f in flows: @@ -102,30 +126,31 @@ def load_file(self, path: mitmproxy.types.Path) -> None: @command.command("replay.server.stop") def clear(self) -> None: """ - Stop server replay. + Stop server replay. """ self.flowmap = {} ctx.master.addons.trigger(hooks.UpdateHook([])) @command.command("replay.server.count") def count(self) -> int: - return sum([len(i) for i in self.flowmap.values()]) + return sum(len(i) for i in self.flowmap.values()) - def _hash(self, flow: http.HTTPFlow) -> typing.Hashable: + def _hash(self, flow: http.HTTPFlow) -> Hashable: """ - Calculates a loose hash of the flow request. + Calculates a loose hash of the flow request. """ r = flow.request _, _, path, _, query, _ = urllib.parse.urlparse(r.url) queriesArray = urllib.parse.parse_qsl(query, keep_blank_values=True) - key: typing.List[typing.Any] = [str(r.scheme), str(r.method), str(path)] + key: list[Any] = [str(r.scheme), str(r.method), str(path)] if not ctx.options.server_replay_ignore_content: if ctx.options.server_replay_ignore_payload_params and r.multipart_form: key.extend( (k, v) for k, v in r.multipart_form.items(multi=True) - if k.decode(errors="replace") not in ctx.options.server_replay_ignore_payload_params + if k.decode(errors="replace") + not in ctx.options.server_replay_ignore_payload_params ) elif ctx.options.server_replay_ignore_payload_params and r.urlencoded_form: key.extend( @@ -156,23 +181,19 @@ def _hash(self, flow: http.HTTPFlow) -> typing.Hashable: v = r.headers.get(i) headers.append((i, v)) key.append(headers) - return hashlib.sha256( - repr(key).encode("utf8", "surrogateescape") - ).digest() + return hashlib.sha256(repr(key).encode("utf8", "surrogateescape")).digest() - def next_flow(self, flow: http.HTTPFlow) -> typing.Optional[http.HTTPFlow]: + def next_flow(self, flow: http.HTTPFlow) -> Optional[http.HTTPFlow]: """ - Returns the next flow object, or None if no matching flow was - found. + Returns the next flow object, or None if no matching flow was + found. """ hash = self._hash(flow) if hash in self.flowmap: if ctx.options.server_replay_nopop: - return next(( - flow - for flow in self.flowmap[hash] - if flow.response - ), None) + return next( + (flow for flow in self.flowmap[hash] if flow.response), None + ) else: ret = self.flowmap[hash].pop(0) while not ret.response: diff --git a/mitmproxy/addons/stickyauth.py b/mitmproxy/addons/stickyauth.py index 43098b2d7e..15d98c33d2 100644 --- a/mitmproxy/addons/stickyauth.py +++ b/mitmproxy/addons/stickyauth.py @@ -1,4 +1,4 @@ -import typing +from typing import Optional from mitmproxy import exceptions from mitmproxy import flowfilter @@ -12,8 +12,10 @@ def __init__(self): def load(self, loader): loader.add_option( - "stickyauth", typing.Optional[str], None, - "Set sticky auth filter. Matched against requests." + "stickyauth", + Optional[str], + None, + "Set sticky auth filter. Matched against requests.", ) def configure(self, updated): diff --git a/mitmproxy/addons/stickycookie.py b/mitmproxy/addons/stickycookie.py index 3b67bb82ca..df0abfbd98 100644 --- a/mitmproxy/addons/stickycookie.py +++ b/mitmproxy/addons/stickycookie.py @@ -1,16 +1,16 @@ import collections from http import cookiejar -from typing import List, Tuple, Dict, Optional # noqa +from typing import Optional from mitmproxy import http, flowfilter, ctx, exceptions from mitmproxy.net.http import cookies -TOrigin = Tuple[str, int, str] +TOrigin = tuple[str, int, str] -def ckey(attrs: Dict[str, str], f: http.HTTPFlow) -> TOrigin: +def ckey(attrs: dict[str, str], f: http.HTTPFlow) -> TOrigin: """ - Returns a (domain, port, path) tuple. + Returns a (domain, port, path) tuple. """ domain = f.request.host path = "/" @@ -31,13 +31,15 @@ def domain_match(a: str, b: str) -> bool: class StickyCookie: def __init__(self): - self.jar: Dict[TOrigin, Dict[str, str]] = collections.defaultdict(dict) + self.jar: dict[TOrigin, dict[str, str]] = collections.defaultdict(dict) self.flt: Optional[flowfilter.TFilter] = None def load(self, loader): loader.add_option( - "stickycookie", Optional[str], None, - "Set sticky cookie filter. Matched against requests." + "stickycookie", + Optional[str], + None, + "Set sticky cookie filter. Matched against requests.", ) def configure(self, updated): @@ -72,17 +74,19 @@ def response(self, flow: http.HTTPFlow): def request(self, flow: http.HTTPFlow): if self.flt: - cookie_list: List[Tuple[str, str]] = [] + cookie_list: list[tuple[str, str]] = [] if flowfilter.match(self.flt, flow): for (domain, port, path), c in self.jar.items(): match = [ domain_match(flow.request.host, domain), flow.request.port == port, - flow.request.path.startswith(path) + flow.request.path.startswith(path), ] if all(match): cookie_list.extend(c.items()) if cookie_list: # FIXME: we need to formalise this... flow.metadata["stickycookie"] = True - flow.request.headers["cookie"] = cookies.format_cookie_header(cookie_list) + flow.request.headers["cookie"] = cookies.format_cookie_header( + cookie_list + ) diff --git a/mitmproxy/addons/termlog.py b/mitmproxy/addons/termlog.py index b4ac300d83..faaed6b808 100644 --- a/mitmproxy/addons/termlog.py +++ b/mitmproxy/addons/termlog.py @@ -1,29 +1,44 @@ +import sys from typing import IO, Optional -import click - -from mitmproxy import log from mitmproxy import ctx +from mitmproxy import log +from mitmproxy.contrib import click as miniclick +from mitmproxy.utils import vt_codes + +LOG_COLORS = {"error": "red", "warn": "yellow", "alert": "magenta"} class TermLog: - def __init__(self, outfile=None): - self.outfile: Optional[IO] = outfile + def __init__( + self, + out: Optional[IO[str]] = None, + err: Optional[IO[str]] = None, + ): + self.out_file: IO[str] = out or sys.stdout + self.out_has_vt_codes = vt_codes.ensure_supported(self.out_file) + self.err_file: IO[str] = err or sys.stderr + self.err_has_vt_codes = vt_codes.ensure_supported(self.err_file) def load(self, loader): loader.add_option( - "termlog_verbosity", str, 'info', - "Log verbosity.", - choices=log.LogTierOrder + "termlog_verbosity", str, "info", "Log verbosity.", choices=log.LogTierOrder ) - def add_log(self, e): + def add_log(self, e: log.LogEntry): if log.log_tier(ctx.options.termlog_verbosity) >= log.log_tier(e.level): - click.secho( - e.msg, - file=self.outfile, - fg=dict(error="red", warn="yellow", - alert="magenta").get(e.level), - dim=(e.level == "debug"), - err=(e.level == "error") - ) + if e.level == "error": + f = self.err_file + has_vt_codes = self.err_has_vt_codes + else: + f = self.out_file + has_vt_codes = self.out_has_vt_codes + + msg = e.msg + if has_vt_codes: + msg = miniclick.style( + e.msg, + fg=LOG_COLORS.get(e.level), + dim=(e.level == "debug"), + ) + print(msg, file=f) diff --git a/mitmproxy/addons/tlsconfig.py b/mitmproxy/addons/tlsconfig.py index 0c8ebf92a1..0cb7492e28 100644 --- a/mitmproxy/addons/tlsconfig.py +++ b/mitmproxy/addons/tlsconfig.py @@ -1,26 +1,52 @@ import ipaddress import os from pathlib import Path -from typing import List, Optional, TypedDict, Any +from typing import Any, Optional, TypedDict from OpenSSL import SSL -from mitmproxy import certs, ctx, exceptions, connection +from mitmproxy import certs, ctx, exceptions, connection, tls from mitmproxy.net import tls as net_tls from mitmproxy.options import CONF_BASENAME from mitmproxy.proxy import context -from mitmproxy.proxy.layers import tls, modes +from mitmproxy.proxy.layers import modes +from mitmproxy.proxy.layers import tls as proxy_tls # We manually need to specify this, otherwise OpenSSL may select a non-HTTP2 cipher by default. # https://ssl-config.mozilla.org/#config=old DEFAULT_CIPHERS = ( - 'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES256-GCM-SHA384', - 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-CHACHA20-POLY1305', 'ECDHE-RSA-CHACHA20-POLY1305', - 'DHE-RSA-AES128-GCM-SHA256', 'DHE-RSA-AES256-GCM-SHA384', 'DHE-RSA-CHACHA20-POLY1305', 'ECDHE-ECDSA-AES128-SHA256', - 'ECDHE-RSA-AES128-SHA256', 'ECDHE-ECDSA-AES128-SHA', 'ECDHE-RSA-AES128-SHA', 'ECDHE-ECDSA-AES256-SHA384', - 'ECDHE-RSA-AES256-SHA384', 'ECDHE-ECDSA-AES256-SHA', 'ECDHE-RSA-AES256-SHA', 'DHE-RSA-AES128-SHA256', - 'DHE-RSA-AES256-SHA256', 'AES128-GCM-SHA256', 'AES256-GCM-SHA384', 'AES128-SHA256', 'AES256-SHA256', 'AES128-SHA', - 'AES256-SHA', 'DES-CBC3-SHA' + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305", + "DHE-RSA-AES128-GCM-SHA256", + "DHE-RSA-AES256-GCM-SHA384", + "DHE-RSA-CHACHA20-POLY1305", + "ECDHE-ECDSA-AES128-SHA256", + "ECDHE-RSA-AES128-SHA256", + "ECDHE-ECDSA-AES128-SHA", + "ECDHE-RSA-AES128-SHA", + "ECDHE-ECDSA-AES256-SHA384", + "ECDHE-RSA-AES256-SHA384", + "ECDHE-ECDSA-AES256-SHA", + "ECDHE-RSA-AES256-SHA", + "DHE-RSA-AES128-SHA256", + "DHE-RSA-AES256-SHA256", + "AES128-GCM-SHA256", + "AES256-GCM-SHA384", + "AES128-SHA256", + "AES256-SHA256", + "AES128-SHA", + "AES256-SHA", + "DES-CBC3-SHA", +) + +# 2022/05: X509_CHECK_FLAG_NEVER_CHECK_SUBJECT is not available in LibreSSL, ignore gracefully as it's not critical. +DEFAULT_HOSTFLAGS = ( + SSL._lib.X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS # type: ignore + | getattr(SSL._lib, "X509_CHECK_FLAG_NEVER_CHECK_SUBJECT", 0) # type: ignore ) @@ -30,7 +56,7 @@ class AppData(TypedDict): http2: bool -def alpn_select_callback(conn: SSL.Connection, options: List[bytes]) -> Any: +def alpn_select_callback(conn: SSL.Connection, options: list[bytes]) -> Any: app_data: AppData = conn.get_app_data() client_alpn = app_data["client_alpn"] server_alpn = app_data["server_alpn"] @@ -46,8 +72,9 @@ def alpn_select_callback(conn: SSL.Connection, options: List[bytes]) -> Any: # We do have a server connection, but the remote server refused to negotiate a protocol: # We need to mirror this on the client connection. return SSL.NO_OVERLAPPING_PROTOCOLS - http_alpns = tls.HTTP_ALPNS if http2 else tls.HTTP1_ALPNS - for alpn in options: # client sends in order of preference, so we are nice and respect that. + http_alpns = proxy_tls.HTTP_ALPNS if http2 else proxy_tls.HTTP1_ALPNS + # client sends in order of preference, so we are nice and respect that. + for alpn in options: if alpn in http_alpns: return alpn else: @@ -58,6 +85,7 @@ class TlsConfig: """ This addon supplies the proxy core with the desired OpenSSL connection objects to negotiate TLS. """ + certstore: certs.CertStore = None # type: ignore # TODO: We should support configuring TLS 1.3 cipher suites (https://github.com/mitmproxy/mitmproxy/issues/4260) @@ -107,14 +135,19 @@ def load(self, loader): def tls_clienthello(self, tls_clienthello: tls.ClientHelloData): conn_context = tls_clienthello.context tls_clienthello.establish_server_tls_first = conn_context.server.tls and ( - ctx.options.connection_strategy == "eager" or - ctx.options.add_upstream_certs_to_client_chain or - ctx.options.upstream_cert + ctx.options.connection_strategy == "eager" + or ctx.options.add_upstream_certs_to_client_chain + or ctx.options.upstream_cert ) - def tls_start_client(self, tls_start: tls.TlsStartData) -> None: + def tls_start_client(self, tls_start: tls.TlsData) -> None: """Establish TLS between client and proxy.""" - client: connection.Client = tls_start.context.client + if tls_start.ssl_conn is not None: + return # a user addon has already provided the pyOpenSSL context. + + assert isinstance(tls_start.conn, connection.Client) + + client: connection.Client = tls_start.conn server: connection.Server = tls_start.context.server entry = self.get_cert(tls_start.context) @@ -147,22 +180,32 @@ def tls_start_client(self, tls_start: tls.TlsStartData) -> None: # Force HTTP/1 for secure web proxies, we currently don't support CONNECT over HTTP/2. # There is a proof-of-concept branch at https://github.com/mhils/mitmproxy/tree/http2-proxy, # but the complexity outweighs the benefits for now. - if len(tls_start.context.layers) == 2 and isinstance(tls_start.context.layers[0], modes.HttpProxy): + if len(tls_start.context.layers) == 2 and isinstance( + tls_start.context.layers[0], modes.HttpProxy + ): client_alpn: Optional[bytes] = b"http/1.1" else: client_alpn = client.alpn - tls_start.ssl_conn.set_app_data(AppData( - client_alpn=client_alpn, - server_alpn=server.alpn, - http2=ctx.options.http2, - )) + tls_start.ssl_conn.set_app_data( + AppData( + client_alpn=client_alpn, + server_alpn=server.alpn, + http2=ctx.options.http2, + ) + ) tls_start.ssl_conn.set_accept_state() - def tls_start_server(self, tls_start: tls.TlsStartData) -> None: + def tls_start_server(self, tls_start: tls.TlsData) -> None: """Establish TLS between proxy and server.""" + if tls_start.ssl_conn is not None: + return # a user addon has already provided the pyOpenSSL context. + + assert isinstance(tls_start.conn, connection.Server) + client: connection.Client = tls_start.context.client - server: connection.Server = tls_start.context.server + # tls_start.conn may be different from tls_start.context.server, e.g. an upstream HTTPS proxy. + server: connection.Server = tls_start.conn assert server.address if ctx.options.ssl_insecure: @@ -181,7 +224,9 @@ def tls_start_server(self, tls_start: tls.TlsStartData) -> None: # accurately, for example header capitalization. server.alpn_offers = tuple(client.alpn_offers) else: - server.alpn_offers = tuple(x for x in client.alpn_offers if x != b"h2") + server.alpn_offers = tuple( + x for x in client.alpn_offers if x != b"h2" + ) else: # We either have no client TLS or a client without ALPN. # - If the client does use TLS but did not send an ALPN extension, we want to mirror that upstream. @@ -211,24 +256,41 @@ def tls_start_server(self, tls_start: tls.TlsStartData) -> None: max_version=net_tls.Version[ctx.options.tls_version_client_max], cipher_list=tuple(cipher_list), verify=verify, - hostname=server.sni, ca_path=ctx.options.ssl_verify_upstream_trusted_confdir, ca_pemfile=ctx.options.ssl_verify_upstream_trusted_ca, client_cert=client_cert, - alpn_protos=tuple(server.alpn_offers), ) tls_start.ssl_conn = SSL.Connection(ssl_ctx) if server.sni: + # We need to set SNI + enable hostname verification. + assert isinstance(server.sni, str) + # Manually enable hostname verification on the context object. + # https://wiki.openssl.org/index.php/Hostname_validation + param = SSL._lib.SSL_get0_param(tls_start.ssl_conn._ssl) # type: ignore + # Matching on the CN is disabled in both Chrome and Firefox, so we disable it, too. + # https://www.chromestatus.com/feature/4981025180483584 + + SSL._lib.X509_VERIFY_PARAM_set_hostflags(param, DEFAULT_HOSTFLAGS) # type: ignore + try: - ipaddress.ip_address(server.sni) + ip: bytes = ipaddress.ip_address(server.sni).packed except ValueError: - tls_start.ssl_conn.set_tlsext_host_name(server.sni.encode()) + host_name = server.sni.encode("idna") + tls_start.ssl_conn.set_tlsext_host_name(host_name) + ok = SSL._lib.X509_VERIFY_PARAM_set1_host(param, host_name, len(host_name)) # type: ignore + SSL._openssl_assert(ok == 1) # type: ignore else: - # RFC 6066: Literal IPv4 and IPv6 addresses are not permitted in "HostName". - # It's not really ideal that we only enforce that here, but otherwise we need to add checks everywhere - # where we assign .sni, which is much less robust. - pass + # RFC 6066: Literal IPv4 and IPv6 addresses are not permitted in "HostName", + # so we don't call set_tlsext_host_name. + ok = SSL._lib.X509_VERIFY_PARAM_set1_ip(param, ip, len(ip)) # type: ignore + SSL._openssl_assert(ok == 1) # type: ignore + elif verify is not net_tls.Verify.VERIFY_NONE: + raise ValueError("Cannot validate certificate hostname without SNI") + + if server.alpn_offers: + tls_start.ssl_conn.set_alpn_protos(server.alpn_offers) + tls_start.ssl_conn.set_connect_state() def running(self): @@ -245,7 +307,9 @@ def configure(self, updated): path=certstore_path, basename=CONF_BASENAME, key_size=ctx.options.key_size, - passphrase=ctx.options.cert_passphrase.encode("utf8") if ctx.options.cert_passphrase else None, + passphrase=ctx.options.cert_passphrase.encode("utf8") + if ctx.options.cert_passphrase + else None, ) if self.certstore.default_ca.has_expired(): ctx.log.warn( @@ -262,39 +326,38 @@ def configure(self, updated): cert = Path(parts[1]).expanduser() if not cert.exists(): - raise exceptions.OptionsError(f"Certificate file does not exist: {cert}") + raise exceptions.OptionsError( + f"Certificate file does not exist: {cert}" + ) try: self.certstore.add_cert_file( parts[0], cert, - passphrase=ctx.options.cert_passphrase.encode("utf8") if ctx.options.cert_passphrase else None, + passphrase=ctx.options.cert_passphrase.encode("utf8") + if ctx.options.cert_passphrase + else None, ) except ValueError as e: - raise exceptions.OptionsError(f"Invalid certificate format for {cert}: {e}") from e + raise exceptions.OptionsError( + f"Invalid certificate format for {cert}: {e}" + ) from e def get_cert(self, conn_context: context.Context) -> certs.CertStoreEntry: """ This function determines the Common Name (CN), Subject Alternative Names (SANs) and Organization Name our certificate should have and then fetches a matching cert from the certstore. """ - altnames: List[str] = [] + altnames: list[str] = [] organization: Optional[str] = None # Use upstream certificate if available. if ctx.options.upstream_cert and conn_context.server.certificate_list: upstream_cert = conn_context.server.certificate_list[0] - try: - # a bit clunky: access to .cn can fail, see https://github.com/mitmproxy/mitmproxy/issues/4713 - if upstream_cert.cn: - altnames.append(upstream_cert.cn) - except ValueError: - pass + if upstream_cert.cn: + altnames.append(upstream_cert.cn) altnames.extend(upstream_cert.altnames) - try: - if upstream_cert.organization: - organization = upstream_cert.organization - except ValueError: - pass + if upstream_cert.organization: + organization = upstream_cert.organization # Add SNI. If not available, try the server address as well. if conn_context.client.sni: diff --git a/mitmproxy/addons/upstream_auth.py b/mitmproxy/addons/upstream_auth.py index c3053bed1b..0c1cb1d625 100644 --- a/mitmproxy/addons/upstream_auth.py +++ b/mitmproxy/addons/upstream_auth.py @@ -1,6 +1,6 @@ import re -import typing import base64 +from typing import Optional from mitmproxy import exceptions from mitmproxy import ctx @@ -11,31 +11,32 @@ def parse_upstream_auth(auth: str) -> bytes: pattern = re.compile(".+:") if pattern.search(auth) is None: - raise exceptions.OptionsError( - "Invalid upstream auth specification: %s" % auth - ) + raise exceptions.OptionsError("Invalid upstream auth specification: %s" % auth) return b"Basic" + b" " + base64.b64encode(strutils.always_bytes(auth)) class UpstreamAuth: """ - This addon handles authentication to systems upstream from us for the - upstream proxy and reverse proxy mode. There are 3 cases: + This addon handles authentication to systems upstream from us for the + upstream proxy and reverse proxy mode. There are 3 cases: - - Upstream proxy CONNECT requests should have authentication added, and - subsequent already connected requests should not. - - Upstream proxy regular requests - - Reverse proxy regular requests (CONNECT is invalid in this mode) + - Upstream proxy CONNECT requests should have authentication added, and + subsequent already connected requests should not. + - Upstream proxy regular requests + - Reverse proxy regular requests (CONNECT is invalid in this mode) """ - auth: typing.Optional[bytes] = None + + auth: Optional[bytes] = None def load(self, loader): loader.add_option( - "upstream_auth", typing.Optional[str], None, + "upstream_auth", + Optional[str], + None, """ Add HTTP Basic authentication to upstream proxy and reverse proxy requests. Format: username:password. - """ + """, ) def configure(self, updated): diff --git a/mitmproxy/addons/view.py b/mitmproxy/addons/view.py index b8d738d334..51f9862805 100644 --- a/mitmproxy/addons/view.py +++ b/mitmproxy/addons/view.py @@ -10,7 +10,8 @@ """ import collections import re -import typing +from collections.abc import Iterator, MutableMapping, Sequence +from typing import Any, Optional import blinker import sortedcontainers @@ -18,6 +19,7 @@ import mitmproxy.flow from mitmproxy import command from mitmproxy import ctx +from mitmproxy import dns from mitmproxy import exceptions from mitmproxy import hooks from mitmproxy import connection @@ -43,7 +45,7 @@ class _OrderKey: def __init__(self, view): self.view = view - def generate(self, f: mitmproxy.flow.Flow) -> typing.Any: # pragma: no cover + def generate(self, f: mitmproxy.flow.Flow) -> Any: # pragma: no cover pass def refresh(self, f): @@ -74,7 +76,7 @@ def __call__(self, f): class OrderRequestStart(_OrderKey): def generate(self, f: mitmproxy.flow.Flow) -> float: - return f.timestamp_start + return f.timestamp_created class OrderRequestMethod(_OrderKey): @@ -83,6 +85,8 @@ def generate(self, f: mitmproxy.flow.Flow) -> str: return f.request.method elif isinstance(f, tcp.TCPFlow): return "TCP" + elif isinstance(f, dns.DNSFlow): + return dns.op_codes.to_str(f.request.op_code) else: raise NotImplementedError() @@ -93,6 +97,8 @@ def generate(self, f: mitmproxy.flow.Flow) -> str: return f.request.url elif isinstance(f, tcp.TCPFlow): return human.format_address(f.server_conn.address) + elif isinstance(f, dns.DNSFlow): + return f.request.questions[0].name if f.request.questions else "" else: raise NotImplementedError() @@ -111,6 +117,8 @@ def generate(self, f: mitmproxy.flow.Flow) -> int: for message in f.messages: size += len(message.content) return size + elif isinstance(f, dns.DNSFlow): + return f.response.size if f.response else 0 else: raise NotImplementedError() @@ -133,16 +141,16 @@ def __init__(self): self.default_order = OrderRequestStart(self) self.orders = dict( - time=OrderRequestStart(self), method=OrderRequestMethod(self), - url=OrderRequestURL(self), size=OrderKeySize(self), + time=OrderRequestStart(self), + method=OrderRequestMethod(self), + url=OrderRequestURL(self), + size=OrderKeySize(self), ) self.order_key = self.default_order self.order_reversed = False self.focus_follow = False - self._view = sortedcontainers.SortedListWithKey( - key=self.order_key - ) + self._view = sortedcontainers.SortedListWithKey(key=self.order_key) # The sig_view* signals broadcast events that affect the view. That is, # an update to a flow in the store but not in the view does not trigger @@ -166,21 +174,20 @@ def __init__(self): def load(self, loader): loader.add_option( - "view_filter", typing.Optional[str], None, - "Limit the view to matching flows." + "view_filter", Optional[str], None, "Limit the view to matching flows." ) loader.add_option( - "view_order", str, "time", + "view_order", + str, + "time", "Flow sort order.", choices=list(map(lambda c: c[1], orders)), ) loader.add_option( - "view_order_reversed", bool, False, - "Reverse the sorting order." + "view_order_reversed", bool, False, "Reverse the sorting order." ) loader.add_option( - "console_focus_follow", bool, False, - "Focus follows new flows." + "console_focus_follow", bool, False, "Focus follows new flows." ) def store_count(self): @@ -188,7 +195,7 @@ def store_count(self): def _rev(self, idx: int) -> int: """ - Reverses an index, if needed + Reverses an index, if needed """ if self.order_reversed: if idx < 0: @@ -202,7 +209,7 @@ def _rev(self, idx: int) -> int: def __len__(self): return len(self._view) - def __getitem__(self, offset) -> typing.Any: + def __getitem__(self, offset) -> Any: return self._view[self._rev(offset)] # Reflect some methods to the efficient underlying implementation @@ -211,10 +218,12 @@ def _bisect(self, f: mitmproxy.flow.Flow) -> int: v = self._view.bisect_right(f) return self._rev(v - 1) + 1 - def index(self, f: mitmproxy.flow.Flow, start: int = 0, stop: typing.Optional[int] = None) -> int: + def index( + self, f: mitmproxy.flow.Flow, start: int = 0, stop: Optional[int] = None + ) -> int: return self._rev(self._view.index(f, start, stop)) - def __contains__(self, f: typing.Any) -> bool: + def __contains__(self, f: Any) -> bool: return self._view.__contains__(f) def _order_key_name(self): @@ -239,9 +248,9 @@ def _refilter(self): @command.command("view.focus.go") def go(self, offset: int) -> None: """ - Go to a specified offset. Positive offests are from the beginning of - the view, negative from the end of the view, so that 0 is the first - flow, -1 is the last flow. + Go to a specified offset. Positive offests are from the beginning of + the view, negative from the end of the view, so that 0 is the first + flow, -1 is the last flow. """ if len(self) == 0: return @@ -256,7 +265,7 @@ def go(self, offset: int) -> None: @command.command("view.focus.next") def focus_next(self) -> None: """ - Set focus to the next flow. + Set focus to the next flow. """ if self.focus.index is not None: idx = self.focus.index + 1 @@ -268,7 +277,7 @@ def focus_next(self) -> None: @command.command("view.focus.prev") def focus_prev(self) -> None: """ - Set focus to the previous flow. + Set focus to the previous flow. """ if self.focus.index is not None: idx = self.focus.index - 1 @@ -279,9 +288,9 @@ def focus_prev(self) -> None: # Order @command.command("view.order.options") - def order_options(self) -> typing.Sequence[str]: + def order_options(self) -> Sequence[str]: """ - Choices supported by the view_order option. + Choices supported by the view_order option. """ return list(sorted(self.orders.keys())) @@ -293,12 +302,10 @@ def set_reversed(self, boolean: bool) -> None: @command.command("view.order.set") def set_order(self, order_key: str) -> None: """ - Sets the current view order. + Sets the current view order. """ if order_key not in self.orders: - raise exceptions.CommandError( - "Unknown flow order: %s" % order_key - ) + raise exceptions.CommandError("Unknown flow order: %s" % order_key) order_key = self.orders[order_key] self.order_key = order_key newview = sortedcontainers.SortedListWithKey(key=order_key) @@ -320,7 +327,7 @@ def get_order(self) -> str: @command.command("view.filter.set") def set_filter_cmd(self, filter_expr: str) -> None: """ - Sets the current view filter. + Sets the current view filter. """ filt = None if filter_expr: @@ -330,7 +337,7 @@ def set_filter_cmd(self, filter_expr: str) -> None: raise exceptions.CommandError(str(e)) from e self.set_filter(filt) - def set_filter(self, flt: typing.Optional[flowfilter.TFilter]): + def set_filter(self, flt: Optional[flowfilter.TFilter]): self.filter = flt or flowfilter.match_all self._refilter() @@ -338,7 +345,7 @@ def set_filter(self, flt: typing.Optional[flowfilter.TFilter]): @command.command("view.clear") def clear(self) -> None: """ - Clears both the store and view. + Clears both the store and view. """ self._store.clear() self._view.clear() @@ -348,7 +355,7 @@ def clear(self) -> None: @command.command("view.clear_unmarked") def clear_not_marked(self) -> None: """ - Clears only the unmarked flows. + Clears only the unmarked flows. """ for flow in self._store.copy().values(): if not flow.marked: @@ -361,19 +368,15 @@ def clear_not_marked(self) -> None: @command.command("view.settings.getval") def getvalue(self, flow: mitmproxy.flow.Flow, key: str, default: str) -> str: """ - Get a value from the settings store for the specified flow. + Get a value from the settings store for the specified flow. """ return self.settings[flow].get(key, default) @command.command("view.settings.setval.toggle") - def setvalue_toggle( - self, - flows: typing.Sequence[mitmproxy.flow.Flow], - key: str - ) -> None: + def setvalue_toggle(self, flows: Sequence[mitmproxy.flow.Flow], key: str) -> None: """ - Toggle a boolean value in the settings store, setting the value to - the string "true" or "false". + Toggle a boolean value in the settings store, setting the value to + the string "true" or "false". """ updated = [] for f in flows: @@ -384,12 +387,10 @@ def setvalue_toggle( @command.command("view.settings.setval") def setvalue( - self, - flows: typing.Sequence[mitmproxy.flow.Flow], - key: str, value: str + self, flows: Sequence[mitmproxy.flow.Flow], key: str, value: str ) -> None: """ - Set a value in the settings store for the specified flows. + Set a value in the settings store for the specified flows. """ updated = [] for f in flows: @@ -399,10 +400,10 @@ def setvalue( # Flows @command.command("view.flows.duplicate") - def duplicate(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None: + def duplicate(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: """ - Duplicates the specified flows, and sets the focus to the first - duplicate. + Duplicates the specified flows, and sets the focus to the first + duplicate. """ dups = [f.copy() for f in flows] if dups: @@ -411,9 +412,9 @@ def duplicate(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None: ctx.log.alert("Duplicated %s flows" % len(dups)) @command.command("view.flows.remove") - def remove(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None: + def remove(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: """ - Removes the flow from the underlying store and the view. + Removes the flow from the underlying store and the view. """ for f in flows: if f.id in self._store: @@ -431,9 +432,9 @@ def remove(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None: ctx.log.alert("Removed %s flows" % len(flows)) @command.command("view.flows.resolve") - def resolve(self, flow_spec: str) -> typing.Sequence[mitmproxy.flow.Flow]: + def resolve(self, flow_spec: str) -> Sequence[mitmproxy.flow.Flow]: """ - Resolve a flow list specification to an actual list of flows. + Resolve a flow list specification to an actual list of flows. """ if flow_spec == "@all": return [i for i in self._store.values()] @@ -475,7 +476,7 @@ def create(self, method: str, url: str) -> None: @command.command("view.flows.load") def load_file(self, path: mitmproxy.types.Path) -> None: """ - Load flows into the view, without processing them with addons. + Load flows into the view, without processing them with addons. """ try: with open(path, "rb") as f: @@ -489,10 +490,10 @@ def load_file(self, path: mitmproxy.types.Path) -> None: except exceptions.FlowReadException as e: ctx.log.error(str(e)) - def add(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None: + def add(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: """ - Adds a flow to the state. If the flow already exists, it is - ignored. + Adds a flow to the state. If the flow already exists, it is + ignored. """ for f in flows: if f.id not in self._store: @@ -503,10 +504,10 @@ def add(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None: self.focus.flow = f self.sig_view_add.send(self, flow=f) - def get_by_id(self, flow_id: str) -> typing.Optional[mitmproxy.flow.Flow]: + def get_by_id(self, flow_id: str) -> Optional[mitmproxy.flow.Flow]: """ - Get flow with the given id from the store. - Returns None if the flow is not found. + Get flow with the given id from the store. + Returns None if the flow is not found. """ return self._store.get(flow_id) @@ -514,21 +515,21 @@ def get_by_id(self, flow_id: str) -> typing.Optional[mitmproxy.flow.Flow]: @command.command("view.properties.length") def get_length(self) -> int: """ - Returns view length. + Returns view length. """ return len(self) @command.command("view.properties.marked") def get_marked(self) -> bool: """ - Returns true if view is in marked mode. + Returns true if view is in marked mode. """ return self.show_marked @command.command("view.properties.marked.toggle") def toggle_marked(self) -> None: """ - Toggle whether to show marked views only. + Toggle whether to show marked views only. """ self.show_marked = not self.show_marked self._refilter() @@ -536,7 +537,7 @@ def toggle_marked(self) -> None: @command.command("view.properties.inbounds") def inbounds(self, index: int) -> bool: """ - Is this 0 <= index < len(self)? + Is this 0 <= index < len(self)? """ return 0 <= index < len(self) @@ -591,9 +592,18 @@ def tcp_error(self, f): def tcp_end(self, f): self.update([f]) - def update(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None: + def dns_request(self, f): + self.add([f]) + + def dns_response(self, f): + self.update([f]) + + def dns_error(self, f): + self.update([f]) + + def update(self, flows: Sequence[mitmproxy.flow.Flow]) -> None: """ - Updates a list of flows. If flow is not in the state, it's ignored. + Updates a list of flows. If flow is not in the state, it's ignored. """ for f in flows: if f.id in self._store: @@ -622,12 +632,12 @@ def update(self, flows: typing.Sequence[mitmproxy.flow.Flow]) -> None: class Focus: """ - Tracks a focus element within a View. + Tracks a focus element within a View. """ def __init__(self, v: View) -> None: self.view = v - self._flow: typing.Optional[mitmproxy.flow.Flow] = None + self._flow: Optional[mitmproxy.flow.Flow] = None self.sig_change = blinker.Signal() if len(self.view): self.flow = self.view[0] @@ -636,18 +646,18 @@ def __init__(self, v: View) -> None: v.sig_view_refresh.connect(self._sig_view_refresh) @property - def flow(self) -> typing.Optional[mitmproxy.flow.Flow]: + def flow(self) -> Optional[mitmproxy.flow.Flow]: return self._flow @flow.setter - def flow(self, f: typing.Optional[mitmproxy.flow.Flow]): + def flow(self, f: Optional[mitmproxy.flow.Flow]): if f is not None and f not in self.view: raise ValueError("Attempt to set focus to flow not in view") self._flow = f self.sig_change.send(self) @property - def index(self) -> typing.Optional[int]: + def index(self) -> Optional[int]: if self.flow: return self.view.index(self.flow) return None @@ -684,11 +694,11 @@ def _sig_view_add(self, view, flow): class Settings(collections.abc.Mapping): def __init__(self, view: View) -> None: self.view = view - self._values: typing.MutableMapping[str, typing.Dict] = {} + self._values: MutableMapping[str, dict] = {} view.sig_store_remove.connect(self._sig_store_remove) view.sig_store_refresh.connect(self._sig_store_refresh) - def __iter__(self) -> typing.Iterator: + def __iter__(self) -> Iterator: return iter(self._values) def __len__(self) -> int: diff --git a/mitmproxy/certs.py b/mitmproxy/certs.py index ed85e51a58..dc3787a8b7 100644 --- a/mitmproxy/certs.py +++ b/mitmproxy/certs.py @@ -6,7 +6,7 @@ import sys from dataclasses import dataclass from pathlib import Path -from typing import Tuple, Optional, Union, Dict, List, NewType +from typing import NewType, Optional, Union from cryptography import x509 from cryptography.hazmat.primitives import hashes, serialization @@ -41,6 +41,7 @@ class Cert(serializable.Serializable): """Representation of a (TLS) certificate.""" + _cert: x509.Certificate def __init__(self, cert: x509.Certificate): @@ -85,7 +86,7 @@ def fingerprint(self) -> bytes: return self._cert.fingerprint(hashes.SHA256()) @property - def issuer(self) -> List[Tuple[str, str]]: + def issuer(self) -> list[tuple[str, str]]: return _name_to_keyval(self._cert.issuer) @property @@ -102,7 +103,7 @@ def has_expired(self) -> bool: return datetime.datetime.utcnow() > self._cert.not_valid_after @property - def subject(self) -> List[Tuple[str, str]]: + def subject(self) -> list[tuple[str, str]]: return _name_to_keyval(self._cert.subject) @property @@ -110,7 +111,7 @@ def serial(self) -> int: return self._cert.serial_number @property - def keyinfo(self) -> Tuple[str, int]: + def keyinfo(self) -> tuple[str, int]: public_key = self._cert.public_key() if isinstance(public_key, rsa.RSAPublicKey): return "RSA", public_key.key_size @@ -118,8 +119,10 @@ def keyinfo(self) -> Tuple[str, int]: return "DSA", public_key.key_size if isinstance(public_key, ec.EllipticCurvePublicKey): return f"EC ({public_key.curve.name})", public_key.key_size - return (public_key.__class__.__name__.replace("PublicKey", "").replace("_", ""), - getattr(public_key, "key_size", -1)) # pragma: no cover + return ( + public_key.__class__.__name__.replace("PublicKey", "").replace("_", ""), + getattr(public_key, "key_size", -1), + ) # pragma: no cover @property def cn(self) -> Optional[str]: @@ -130,29 +133,31 @@ def cn(self) -> Optional[str]: @property def organization(self) -> Optional[str]: - attrs = self._cert.subject.get_attributes_for_oid(x509.NameOID.ORGANIZATION_NAME) + attrs = self._cert.subject.get_attributes_for_oid( + x509.NameOID.ORGANIZATION_NAME + ) if attrs: return attrs[0].value return None @property - def altnames(self) -> List[str]: + def altnames(self) -> list[str]: """ Get all SubjectAlternativeName DNS altnames. """ try: - ext = self._cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value + ext = self._cert.extensions.get_extension_for_class( + x509.SubjectAlternativeName + ).value except x509.ExtensionNotFound: return [] else: - return ( - ext.get_values_for_type(x509.DNSName) - + - [str(x) for x in ext.get_values_for_type(x509.IPAddress)] - ) + return ext.get_values_for_type(x509.DNSName) + [ + str(x) for x in ext.get_values_for_type(x509.IPAddress) + ] -def _name_to_keyval(name: x509.Name) -> List[Tuple[str, str]]: +def _name_to_keyval(name: x509.Name) -> list[tuple[str, str]]: parts = [] for attr in name: # pyca cryptography <35.0.0 backwards compatiblity @@ -169,17 +174,19 @@ def create_ca( organization: str, cn: str, key_size: int, -) -> Tuple[rsa.RSAPrivateKeyWithSerialization, x509.Certificate]: +) -> tuple[rsa.RSAPrivateKeyWithSerialization, x509.Certificate]: now = datetime.datetime.now() private_key = rsa.generate_private_key( public_exponent=65537, key_size=key_size, ) # type: ignore - name = x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, cn), - x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization) - ]) + name = x509.Name( + [ + x509.NameAttribute(NameOID.COMMON_NAME, cn), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, organization), + ] + ) builder = x509.CertificateBuilder() builder = builder.serial_number(x509.random_serial_number()) builder = builder.subject_name(name) @@ -187,8 +194,12 @@ def create_ca( builder = builder.not_valid_after(now + CA_EXPIRY) builder = builder.issuer_name(name) builder = builder.public_key(private_key.public_key()) - builder = builder.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True) - builder = builder.add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False) + builder = builder.add_extension( + x509.BasicConstraints(ca=True, path_length=None), critical=True + ) + builder = builder.add_extension( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False + ) builder = builder.add_extension( x509.KeyUsage( digital_signature=False, @@ -200,8 +211,13 @@ def create_ca( crl_sign=True, encipher_only=False, decipher_only=False, - ), critical=True) - builder = builder.add_extension(x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()), critical=False) + ), + critical=True, + ) + builder = builder.add_extension( + x509.SubjectKeyIdentifier.from_public_key(private_key.public_key()), + critical=False, + ) cert = builder.sign(private_key=private_key, algorithm=hashes.SHA256()) # type: ignore return private_key, cert @@ -210,23 +226,25 @@ def dummy_cert( privkey: rsa.RSAPrivateKey, cacert: x509.Certificate, commonname: Optional[str], - sans: List[str], + sans: list[str], organization: Optional[str] = None, ) -> Cert: """ - Generates a dummy certificate. + Generates a dummy certificate. - privkey: CA private key - cacert: CA certificate - commonname: Common name for the generated certificate. - sans: A list of Subject Alternate Names. - organization: Organization name for the generated certificate. + privkey: CA private key + cacert: CA certificate + commonname: Common name for the generated certificate. + sans: A list of Subject Alternate Names. + organization: Organization name for the generated certificate. - Returns cert if operation succeeded, None if not. + Returns cert if operation succeeded, None if not. """ builder = x509.CertificateBuilder() builder = builder.issuer_name(cacert.subject) - builder = builder.add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False) + builder = builder.add_extension( + x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False + ) builder = builder.public_key(cacert.public_key()) now = datetime.datetime.now() @@ -234,9 +252,7 @@ def dummy_cert( builder = builder.not_valid_after(now + CERT_EXPIRY) subject = [] - is_valid_commonname = ( - commonname is not None and len(commonname) < 64 - ) + is_valid_commonname = commonname is not None and len(commonname) < 64 if is_valid_commonname: assert commonname is not None subject.append(x509.NameAttribute(NameOID.COMMON_NAME, commonname)) @@ -246,7 +262,7 @@ def dummy_cert( builder = builder.subject_name(x509.Name(subject)) builder = builder.serial_number(x509.random_serial_number()) - ss: List[x509.GeneralName] = [] + ss: list[x509.GeneralName] = [] for x in sans: try: ip = ipaddress.ip_address(x) @@ -255,7 +271,9 @@ def dummy_cert( else: ss.append(x509.IPAddress(ip)) # RFC 5280 §4.2.1.6: subjectAltName is critical if subject is empty. - builder = builder.add_extension(x509.SubjectAlternativeName(ss), critical=not is_valid_commonname) + builder = builder.add_extension( + x509.SubjectAlternativeName(ss), critical=not is_valid_commonname + ) cert = builder.sign(private_key=privkey, algorithm=hashes.SHA256()) # type: ignore return Cert(cert) @@ -268,7 +286,7 @@ class CertStoreEntry: TCustomCertId = str # manually provided certs (e.g. mitmproxy's --certs) -TGeneratedCertId = Tuple[Optional[str], Tuple[str, ...]] # (common_name, sans) +TGeneratedCertId = tuple[Optional[str], tuple[str, ...]] # (common_name, sans) TCertId = Union[TCustomCertId, TGeneratedCertId] DHParams = NewType("DHParams", bytes) @@ -276,18 +294,19 @@ class CertStoreEntry: class CertStore: """ - Implements an in-memory certificate store. + Implements an in-memory certificate store. """ + STORE_CAP = 100 - certs: Dict[TCertId, CertStoreEntry] - expire_queue: List[CertStoreEntry] + certs: dict[TCertId, CertStoreEntry] + expire_queue: list[CertStoreEntry] def __init__( self, default_privatekey: rsa.RSAPrivateKey, default_ca: Cert, default_chain_file: Optional[Path], - dhparams: DHParams + dhparams: DHParams, ): self.default_privatekey = default_privatekey self.default_ca = default_ca @@ -318,7 +337,7 @@ def load_dhparam(path: Path) -> DHParams: bio, OpenSSL.SSL._ffi.NULL, # type: ignore OpenSSL.SSL._ffi.NULL, # type: ignore - OpenSSL.SSL._ffi.NULL # type: ignore + OpenSSL.SSL._ffi.NULL, # type: ignore ) dh = OpenSSL.SSL._ffi.gc(dh, OpenSSL.SSL._lib.DH_free) # type: ignore return dh @@ -330,7 +349,7 @@ def from_store( path: Union[Path, str], basename: str, key_size: int, - passphrase: Optional[bytes] = None + passphrase: Optional[bytes] = None, ) -> "CertStore": path = Path(path) ca_file = path / f"{basename}-ca.pem" @@ -340,7 +359,9 @@ def from_store( return cls.from_files(ca_file, dhparam_file, passphrase) @classmethod - def from_files(cls, ca_file: Path, dhparam_file: Path, passphrase: Optional[bytes] = None) -> "CertStore": + def from_files( + cls, ca_file: Path, dhparam_file: Path, passphrase: Optional[bytes] = None + ) -> "CertStore": raw = ca_file.read_bytes() key = load_pem_private_key(raw, passphrase) dh = cls.load_dhparam(dhparam_file) @@ -356,9 +377,9 @@ def from_files(cls, ca_file: Path, dhparam_file: Path, passphrase: Optional[byte @contextlib.contextmanager def umask_secret(): """ - Context to temporarily set umask to its original value bitor 0o77. - Useful when writing private keys to disk so that only the owner - will be able to read them. + Context to temporarily set umask to its original value bitor 0o77. + Useful when writing private keys to disk so that only the owner + will be able to read them. """ original_umask = os.umask(0) os.umask(original_umask | 0o77) @@ -368,7 +389,9 @@ def umask_secret(): os.umask(original_umask) @staticmethod - def create_store(path: Path, basename: str, key_size: int, organization=None, cn=None) -> None: + def create_store( + path: Path, basename: str, key_size: int, organization=None, cn=None + ) -> None: path.mkdir(parents=True, exist_ok=True) organization = organization or basename @@ -386,8 +409,8 @@ def create_store(path: Path, basename: str, key_size: int, organization=None, cn encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption(), - ) + - ca.public_bytes(serialization.Encoding.PEM) + ) + + ca.public_bytes(serialization.Encoding.PEM) ) # PKCS12 format for Windows devices @@ -420,7 +443,9 @@ def create_store(path: Path, basename: str, key_size: int, organization=None, cn (path / f"{basename}-dhparam.pem").write_bytes(DEFAULT_DHPARAM) - def add_cert_file(self, spec: str, path: Path, passphrase: Optional[bytes] = None) -> None: + def add_cert_file( + self, spec: str, path: Path, passphrase: Optional[bytes] = None + ) -> None: raw = path.read_bytes() cert = Cert.from_pem(raw) try: @@ -428,15 +453,12 @@ def add_cert_file(self, spec: str, path: Path, passphrase: Optional[bytes] = Non except ValueError: key = self.default_privatekey - self.add_cert( - CertStoreEntry(cert, key, path), - spec - ) + self.add_cert(CertStoreEntry(cert, key, path), spec) def add_cert(self, entry: CertStoreEntry, *names: str) -> None: """ - Adds a cert to the certstore. We register the CN in the cert plus - any SANs, and also the list of names provided as an argument. + Adds a cert to the certstore. We register the CN in the cert plus + any SANs, and also the list of names provided as an argument. """ if entry.cert.cn: self.certs[entry.cert.cn] = entry @@ -446,7 +468,7 @@ def add_cert(self, entry: CertStoreEntry, *names: str) -> None: self.certs[i] = entry @staticmethod - def asterisk_forms(dn: str) -> List[str]: + def asterisk_forms(dn: str) -> list[str]: """ Return all asterisk forms for a domain. For example, for www.example.com this will return [b"www.example.com", b"*.example.com", b"*.com"]. The single wildcard "*" is omitted. @@ -460,19 +482,19 @@ def asterisk_forms(dn: str) -> List[str]: def get_cert( self, commonname: Optional[str], - sans: List[str], - organization: Optional[str] = None + sans: list[str], + organization: Optional[str] = None, ) -> CertStoreEntry: """ - commonname: Common name for the generated certificate. Must be a - valid, plain-ASCII, IDNA-encoded domain name. + commonname: Common name for the generated certificate. Must be a + valid, plain-ASCII, IDNA-encoded domain name. - sans: A list of Subject Alternate Names. + sans: A list of Subject Alternate Names. - organization: Organization name for the generated certificate. + organization: Organization name for the generated certificate. """ - potential_keys: List[TCertId] = [] + potential_keys: list[TCertId] = [] if commonname: potential_keys.extend(self.asterisk_forms(commonname)) for s in sans: @@ -480,10 +502,7 @@ def get_cert( potential_keys.append("*") potential_keys.append((commonname, tuple(sans))) - name = next( - filter(lambda key: key in self.certs, potential_keys), - None - ) + name = next(filter(lambda key: key in self.certs, potential_keys), None) if name: entry = self.certs[name] else: @@ -493,9 +512,10 @@ def get_cert( self.default_ca._cert, commonname, sans, - organization), + organization, + ), privatekey=self.default_privatekey, - chain_file=self.default_chain_file + chain_file=self.default_chain_file, ) self.certs[(commonname, tuple(sans))] = entry self.expire(entry) diff --git a/mitmproxy/command.py b/mitmproxy/command.py index d67e9a424c..db36702880 100644 --- a/mitmproxy/command.py +++ b/mitmproxy/command.py @@ -6,14 +6,15 @@ import sys import textwrap import types -import typing +from collections.abc import Sequence, Callable, Iterable +from typing import Any, NamedTuple, Optional import mitmproxy.types from mitmproxy import exceptions, command_lexer from mitmproxy.command_lexer import unquote -def verify_arg_signature(f: typing.Callable, args: typing.Iterable[typing.Any], kwargs: dict) -> None: +def verify_arg_signature(f: Callable, args: Iterable[Any], kwargs: dict) -> None: sig = inspect.signature(f) try: sig.bind(*args, **kwargs) @@ -23,25 +24,27 @@ def verify_arg_signature(f: typing.Callable, args: typing.Iterable[typing.Any], def typename(t: type) -> str: """ - Translates a type to an explanatory string. + Translates a type to an explanatory string. """ if t == inspect._empty: # type: ignore raise exceptions.CommandError("missing type annotation") to = mitmproxy.types.CommandTypes.get(t, None) if not to: - raise exceptions.CommandError("unsupported type: %s" % getattr(t, "__name__", t)) + raise exceptions.CommandError( + "unsupported type: %s" % getattr(t, "__name__", t) + ) return to.display -def _empty_as_none(x: typing.Any) -> typing.Any: +def _empty_as_none(x: Any) -> Any: if x == inspect.Signature.empty: return None return x -class CommandParameter(typing.NamedTuple): +class CommandParameter(NamedTuple): name: str - type: typing.Type + type: type kind: inspect._ParameterKind = inspect.Parameter.POSITIONAL_OR_KEYWORD def __str__(self): @@ -55,9 +58,9 @@ class Command: name: str manager: "CommandManager" signature: inspect.Signature - help: typing.Optional[str] + help: Optional[str] - def __init__(self, manager: "CommandManager", name: str, func: typing.Callable) -> None: + def __init__(self, manager: "CommandManager", name: str, func: Callable) -> None: self.name = name self.manager = manager self.func = func @@ -73,16 +76,22 @@ def __init__(self, manager: "CommandManager", name: str, func: typing.Callable) for name, parameter in self.signature.parameters.items(): t = parameter.annotation if not mitmproxy.types.CommandTypes.get(parameter.annotation, None): - raise exceptions.CommandError(f"Argument {name} has an unknown type {t} in {func}.") - if self.return_type and not mitmproxy.types.CommandTypes.get(self.return_type, None): - raise exceptions.CommandError(f"Return type has an unknown type ({self.return_type}) in {func}.") + raise exceptions.CommandError( + f"Argument {name} has an unknown type {t} in {func}." + ) + if self.return_type and not mitmproxy.types.CommandTypes.get( + self.return_type, None + ): + raise exceptions.CommandError( + f"Return type has an unknown type ({self.return_type}) in {func}." + ) @property - def return_type(self) -> typing.Optional[typing.Type]: + def return_type(self) -> Optional[type]: return _empty_as_none(self.signature.return_annotation) @property - def parameters(self) -> typing.List[CommandParameter]: + def parameters(self) -> list[CommandParameter]: """Returns a list of CommandParameters.""" ret = [] for name, param in self.signature.parameters.items(): @@ -97,30 +106,33 @@ def signature_help(self) -> str: ret = "" return f"{self.name} {params}{ret}" - def prepare_args(self, args: typing.Sequence[str]) -> inspect.BoundArguments: + def prepare_args(self, args: Sequence[str]) -> inspect.BoundArguments: try: bound_arguments = self.signature.bind(*args) except TypeError: - expected = f'Expected: {str(self.signature.parameters)}' - received = f'Received: {str(args)}' - raise exceptions.CommandError(f"Command argument mismatch: \n {expected}\n {received}") + expected = f"Expected: {str(self.signature.parameters)}" + received = f"Received: {str(args)}" + raise exceptions.CommandError( + f"Command argument mismatch: \n {expected}\n {received}" + ) for name, value in bound_arguments.arguments.items(): param = self.signature.parameters[name] convert_to = param.annotation if param.kind == param.VAR_POSITIONAL: bound_arguments.arguments[name] = tuple( - parsearg(self.manager, x, convert_to) - for x in value + parsearg(self.manager, x, convert_to) for x in value ) else: - bound_arguments.arguments[name] = parsearg(self.manager, value, convert_to) + bound_arguments.arguments[name] = parsearg( + self.manager, value, convert_to + ) bound_arguments.apply_defaults() return bound_arguments - def call(self, args: typing.Sequence[str]) -> typing.Any: + def call(self, args: Sequence[str]) -> Any: """ Call the command with a list of arguments. At this point, all arguments are strings. @@ -138,14 +150,14 @@ def call(self, args: typing.Sequence[str]) -> typing.Any: return ret -class ParseResult(typing.NamedTuple): +class ParseResult(NamedTuple): value: str - type: typing.Type + type: type valid: bool class CommandManager: - commands: typing.Dict[str, Command] + commands: dict[str, Command] def __init__(self, master): self.master = master @@ -169,26 +181,25 @@ def collect_commands(self, addon): f"Could not load command {o.command_name}: {e}" ) - def add(self, path: str, func: typing.Callable): + def add(self, path: str, func: Callable): self.commands[path] = Command(self, path, func) @functools.lru_cache(maxsize=128) def parse_partial( - self, - cmdstr: str - ) -> typing.Tuple[typing.Sequence[ParseResult], typing.Sequence[CommandParameter]]: + self, cmdstr: str + ) -> tuple[Sequence[ParseResult], Sequence[CommandParameter]]: """ Parse a possibly partial command. Return a sequence of ParseResults and a sequence of remainder type help items. """ - parts: typing.List[str] = command_lexer.expr.parseString(cmdstr, parseAll=True) + parts: list[str] = command_lexer.expr.parseString(cmdstr, parseAll=True) - parsed: typing.List[ParseResult] = [] - next_params: typing.List[CommandParameter] = [ + parsed: list[ParseResult] = [] + next_params: list[CommandParameter] = [ CommandParameter("", mitmproxy.types.Cmd), CommandParameter("", mitmproxy.types.CmdArgs), ] - expected: typing.Optional[CommandParameter] = None + expected: Optional[CommandParameter] = None for part in parts: if part.isspace(): parsed.append( @@ -208,13 +219,13 @@ def parse_partial( expected = CommandParameter("", mitmproxy.types.Unknown) arg_is_known_command = ( - expected.type == mitmproxy.types.Cmd and part in self.commands + expected.type == mitmproxy.types.Cmd and part in self.commands ) arg_is_unknown_command = ( - expected.type == mitmproxy.types.Cmd and part not in self.commands + expected.type == mitmproxy.types.Cmd and part not in self.commands ) command_args_following = ( - next_params and next_params[0].type == mitmproxy.types.CmdArgs + next_params and next_params[0].type == mitmproxy.types.CmdArgs ) if arg_is_known_command and command_args_following: next_params = self.commands[part].parameters + next_params[1:] @@ -241,7 +252,7 @@ def parse_partial( return parsed, next_params - def call(self, command_name: str, *args: typing.Sequence[typing.Any]) -> typing.Any: + def call(self, command_name: str, *args: Any) -> Any: """ Call a command with native arguments. May raise CommandError. """ @@ -249,7 +260,7 @@ def call(self, command_name: str, *args: typing.Sequence[typing.Any]) -> typing. raise exceptions.CommandError("Unknown command: %s" % command_name) return self.commands[command_name].func(*args) - def call_strings(self, command_name: str, args: typing.Sequence[str]) -> typing.Any: + def call_strings(self, command_name: str, args: Sequence[str]) -> Any: """ Call a command using a list of string arguments. May raise CommandError. """ @@ -258,18 +269,16 @@ def call_strings(self, command_name: str, args: typing.Sequence[str]) -> typing. return self.commands[command_name].call(args) - def execute(self, cmdstr: str) -> typing.Any: + def execute(self, cmdstr: str) -> Any: """ Execute a command string. May raise CommandError. """ parts, _ = self.parse_partial(cmdstr) if not parts: raise exceptions.CommandError(f"Invalid command: {cmdstr!r}") - command_name, *args = [ - unquote(part.value) - for part in parts - if part.type != mitmproxy.types.Space - ] + command_name, *args = ( + unquote(part.value) for part in parts if part.type != mitmproxy.types.Space + ) return self.call_strings(command_name, args) def dump(self, out=sys.stdout) -> None: @@ -282,9 +291,9 @@ def dump(self, out=sys.stdout) -> None: print(file=out) -def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any: +def parsearg(manager: CommandManager, spec: str, argtype: type) -> Any: """ - Convert a string to a argument to the appropriate type. + Convert a string to a argument to the appropriate type. """ t = mitmproxy.types.CommandTypes.get(argtype, None) if not t: @@ -295,7 +304,7 @@ def parsearg(manager: CommandManager, spec: str, argtype: type) -> typing.Any: raise exceptions.CommandError(str(e)) from e -def command(name: typing.Optional[str] = None): +def command(name: Optional[str] = None): def decorator(function): @functools.wraps(function) def wrapper(*args, **kwargs): @@ -310,9 +319,9 @@ def wrapper(*args, **kwargs): def argument(name, type): """ - Set the type of a command argument at runtime. This is useful for more - specific types such as mitmproxy.types.Choice, which we cannot annotate - directly as mypy does not like that. + Set the type of a command argument at runtime. This is useful for more + specific types such as mitmproxy.types.Choice, which we cannot annotate + directly as mypy does not like that. """ def decorator(f: types.FunctionType) -> types.FunctionType: diff --git a/mitmproxy/command_lexer.py b/mitmproxy/command_lexer.py index 2ba691dff0..a391b9134a 100644 --- a/mitmproxy/command_lexer.py +++ b/mitmproxy/command_lexer.py @@ -8,12 +8,12 @@ PartialQuotedString = pyparsing.Regex( re.compile( - r''' + r""" "[^"]*(?:"|$) # double-quoted string that ends with double quote or EOF | '[^']*(?:'|$) # single-quoted string that ends with double quote or EOF - ''', - re.VERBOSE + """, + re.VERBOSE, ) ) diff --git a/mitmproxy/connection.py b/mitmproxy/connection.py index f2d4c9cb51..6ed08602f6 100644 --- a/mitmproxy/connection.py +++ b/mitmproxy/connection.py @@ -1,8 +1,9 @@ import uuid import warnings from abc import ABCMeta +from collections.abc import Sequence from enum import Flag -from typing import Optional, Sequence, Tuple +from typing import Literal, Optional from mitmproxy import certs from mitmproxy.coretypes import serializable @@ -12,16 +13,20 @@ class ConnectionState(Flag): """The current state of the underlying socket.""" + CLOSED = 0 CAN_READ = 1 CAN_WRITE = 2 OPEN = CAN_READ | CAN_WRITE +TransportProtocol = Literal["tcp", "udp"] + + # practically speaking we may have IPv6 addresses with flowinfo and scope_id, # but type checking isn't good enough to properly handle tuple unions. # this version at least provides useful type checking messages. -Address = Tuple[str, int] +Address = tuple[str, int] class Connection(serializable.Serializable, metaclass=ABCMeta): @@ -31,6 +36,7 @@ class Connection(serializable.Serializable, metaclass=ABCMeta): The connection object only exposes metadata about the connection, but not the underlying socket object. This is intentional, all I/O should be handled by `mitmproxy.proxy.server` exclusively. """ + # all connections have a unique id. While # f.client_conn == f2.client_conn already holds true for live flows (where we have object identity), # we also want these semantics for recorded flows. @@ -38,6 +44,8 @@ class Connection(serializable.Serializable, metaclass=ABCMeta): """A unique UUID to identify the connection.""" state: ConnectionState """The current connection state.""" + transport_protocol: TransportProtocol + """The connection protocol in use.""" peername: Optional[Address] """The remote's `(ip, port)` tuple for this connection.""" sockname: Optional[Address] @@ -115,25 +123,31 @@ def __hash__(self): return hash(self.id) def __repr__(self): - attrs = repr({ - k: { - "cipher_list": lambda: f"<{len(v)} ciphers>", - "id": lambda: f"…{v[-6:]}" - }.get(k, lambda: v)() - for k, v in self.__dict__.items() - }) + attrs = repr( + { + k: { + "cipher_list": lambda: f"<{len(v)} ciphers>", + "id": lambda: f"…{v[-6:]}", + }.get(k, lambda: v)() + for k, v in self.__dict__.items() + } + ) return f"{type(self).__name__}({attrs})" @property def alpn_proto_negotiated(self) -> Optional[bytes]: # pragma: no cover """*Deprecated:* An outdated alias for Connection.alpn.""" - warnings.warn("Connection.alpn_proto_negotiated is deprecated, use Connection.alpn instead.", - DeprecationWarning) + warnings.warn( + "Connection.alpn_proto_negotiated is deprecated, use Connection.alpn instead.", + DeprecationWarning, + stacklevel=2, + ) return self.alpn class Client(Connection): """A connection between a client and mitmproxy.""" + peername: Address """The client's address.""" sockname: Address @@ -147,12 +161,20 @@ class Client(Connection): timestamp_start: float """*Timestamp:* TCP SYN received""" - def __init__(self, peername: Address, sockname: Address, timestamp_start: float): + def __init__( + self, + peername: Address, + sockname: Address, + timestamp_start: float, + *, + transport_protocol: TransportProtocol = "tcp", + ): self.id = str(uuid.uuid4()) self.peername = peername self.sockname = sockname self.timestamp_start = timestamp_start self.state = ConnectionState.OPEN + self.transport_protocol = transport_protocol def __str__(self): if self.alpn: @@ -167,35 +189,33 @@ def get_state(self): # Important: Retain full compatibility with old proxy core for now! # This means we need to add all new fields to the old implementation. return { - 'address': self.peername, - 'alpn': self.alpn, - 'cipher_name': self.cipher, - 'id': self.id, - 'mitmcert': self.mitmcert.get_state() if self.mitmcert is not None else None, - 'sni': self.sni, - 'timestamp_end': self.timestamp_end, - 'timestamp_start': self.timestamp_start, - 'timestamp_tls_setup': self.timestamp_tls_setup, - 'tls_established': self.tls_established, - 'tls_extensions': [], - 'tls_version': self.tls_version, + "address": self.peername, + "alpn": self.alpn, + "cipher_name": self.cipher, + "id": self.id, + "mitmcert": self.mitmcert.get_state() + if self.mitmcert is not None + else None, + "sni": self.sni, + "timestamp_end": self.timestamp_end, + "timestamp_start": self.timestamp_start, + "timestamp_tls_setup": self.timestamp_tls_setup, + "tls_established": self.tls_established, + "tls_extensions": [], + "tls_version": self.tls_version, # only used in sans-io - 'state': self.state.value, - 'sockname': self.sockname, - 'error': self.error, - 'tls': self.tls, - 'certificate_list': [x.get_state() for x in self.certificate_list], - 'alpn_offers': self.alpn_offers, - 'cipher_list': self.cipher_list, + "state": self.state.value, + "sockname": self.sockname, + "error": self.error, + "tls": self.tls, + "certificate_list": [x.get_state() for x in self.certificate_list], + "alpn_offers": self.alpn_offers, + "cipher_list": self.cipher_list, } @classmethod def from_state(cls, state) -> "Client": - client = Client( - state["address"], - ("mitmproxy", 8080), - state["timestamp_start"] - ) + client = Client(state["address"], ("mitmproxy", 8080), state["timestamp_start"]) client.set_state(state) return client @@ -214,33 +234,54 @@ def set_state(self, state): self.sockname = tuple(state["sockname"]) if state["sockname"] else None self.error = state["error"] self.tls = state["tls"] - self.certificate_list = [certs.Cert.from_state(x) for x in state["certificate_list"]] - self.mitmcert = certs.Cert.from_state(state["mitmcert"]) if state["mitmcert"] is not None else None + self.certificate_list = [ + certs.Cert.from_state(x) for x in state["certificate_list"] + ] + self.mitmcert = ( + certs.Cert.from_state(state["mitmcert"]) + if state["mitmcert"] is not None + else None + ) self.alpn_offers = state["alpn_offers"] self.cipher_list = state["cipher_list"] @property def address(self): # pragma: no cover """*Deprecated:* An outdated alias for Client.peername.""" - warnings.warn("Client.address is deprecated, use Client.peername instead.", DeprecationWarning, stacklevel=2) + warnings.warn( + "Client.address is deprecated, use Client.peername instead.", + DeprecationWarning, + stacklevel=2, + ) return self.peername @address.setter def address(self, x): # pragma: no cover - warnings.warn("Client.address is deprecated, use Client.peername instead.", DeprecationWarning, stacklevel=2) + warnings.warn( + "Client.address is deprecated, use Client.peername instead.", + DeprecationWarning, + stacklevel=2, + ) self.peername = x @property def cipher_name(self) -> Optional[str]: # pragma: no cover """*Deprecated:* An outdated alias for Connection.cipher.""" - warnings.warn("Client.cipher_name is deprecated, use Client.cipher instead.", DeprecationWarning, stacklevel=2) + warnings.warn( + "Client.cipher_name is deprecated, use Client.cipher instead.", + DeprecationWarning, + stacklevel=2, + ) return self.cipher @property def clientcert(self) -> Optional[certs.Cert]: # pragma: no cover """*Deprecated:* An outdated alias for Connection.certificate_list[0].""" - warnings.warn("Client.clientcert is deprecated, use Client.certificate_list instead.", DeprecationWarning, - stacklevel=2) + warnings.warn( + "Client.clientcert is deprecated, use Client.certificate_list instead.", + DeprecationWarning, + stacklevel=2, + ) if self.certificate_list: return self.certificate_list[0] else: @@ -248,7 +289,11 @@ def clientcert(self) -> Optional[certs.Cert]: # pragma: no cover @clientcert.setter def clientcert(self, val): # pragma: no cover - warnings.warn("Client.clientcert is deprecated, use Client.certificate_list instead.", DeprecationWarning) + warnings.warn( + "Client.clientcert is deprecated, use Client.certificate_list instead.", + DeprecationWarning, + stacklevel=2, + ) if val: self.certificate_list = [val] else: @@ -272,10 +317,16 @@ class Server(Connection): via: Optional[server_spec.ServerSpec] = None """An optional proxy server specification via which the connection should be established.""" - def __init__(self, address: Optional[Address]): + def __init__( + self, + address: Optional[Address], + *, + transport_protocol: TransportProtocol = "tcp", + ): self.id = str(uuid.uuid4()) self.address = address self.state = ConnectionState.CLOSED + self.transport_protocol = transport_protocol def __str__(self): if self.alpn: @@ -292,7 +343,10 @@ def __str__(self): def __setattr__(self, name, value): if name in ("address", "via"): - connection_open = self.__dict__.get("state", ConnectionState.CLOSED) is ConnectionState.OPEN + connection_open = ( + self.__dict__.get("state", ConnectionState.CLOSED) + is ConnectionState.OPEN + ) # assigning the current value is okay, that may be an artifact of calling .set_state(). attr_changed = self.__dict__.get(name) != value if connection_open and attr_changed: @@ -301,28 +355,28 @@ def __setattr__(self, name, value): def get_state(self): return { - 'address': self.address, - 'alpn': self.alpn, - 'id': self.id, - 'ip_address': self.peername, - 'sni': self.sni, - 'source_address': self.sockname, - 'timestamp_end': self.timestamp_end, - 'timestamp_start': self.timestamp_start, - 'timestamp_tcp_setup': self.timestamp_tcp_setup, - 'timestamp_tls_setup': self.timestamp_tls_setup, - 'tls_established': self.tls_established, - 'tls_version': self.tls_version, - 'via': None, + "address": self.address, + "alpn": self.alpn, + "id": self.id, + "ip_address": self.peername, + "sni": self.sni, + "source_address": self.sockname, + "timestamp_end": self.timestamp_end, + "timestamp_start": self.timestamp_start, + "timestamp_tcp_setup": self.timestamp_tcp_setup, + "timestamp_tls_setup": self.timestamp_tls_setup, + "tls_established": self.tls_established, + "tls_version": self.tls_version, + "via": None, # only used in sans-io - 'state': self.state.value, - 'error': self.error, - 'tls': self.tls, - 'certificate_list': [x.get_state() for x in self.certificate_list], - 'alpn_offers': self.alpn_offers, - 'cipher_name': self.cipher, - 'cipher_list': self.cipher_list, - 'via2': self.via, + "state": self.state.value, + "error": self.error, + "tls": self.tls, + "certificate_list": [x.get_state() for x in self.certificate_list], + "alpn_offers": self.alpn_offers, + "cipher_name": self.cipher, + "cipher_list": self.cipher_list, + "via2": self.via, } @classmethod @@ -337,7 +391,9 @@ def set_state(self, state): self.id = state["id"] self.peername = tuple(state["ip_address"]) if state["ip_address"] else None self.sni = state["sni"] - self.sockname = tuple(state["source_address"]) if state["source_address"] else None + self.sockname = ( + tuple(state["source_address"]) if state["source_address"] else None + ) self.timestamp_end = state["timestamp_end"] self.timestamp_start = state["timestamp_start"] self.timestamp_tcp_setup = state["timestamp_tcp_setup"] @@ -346,7 +402,9 @@ def set_state(self, state): self.state = ConnectionState(state["state"]) self.error = state["error"] self.tls = state["tls"] - self.certificate_list = [certs.Cert.from_state(x) for x in state["certificate_list"]] + self.certificate_list = [ + certs.Cert.from_state(x) for x in state["certificate_list"] + ] self.alpn_offers = state["alpn_offers"] self.cipher = state["cipher_name"] self.cipher_list = state["cipher_list"] @@ -355,14 +413,21 @@ def set_state(self, state): @property def ip_address(self) -> Optional[Address]: # pragma: no cover """*Deprecated:* An outdated alias for `Server.peername`.""" - warnings.warn("Server.ip_address is deprecated, use Server.peername instead.", DeprecationWarning, stacklevel=2) + warnings.warn( + "Server.ip_address is deprecated, use Server.peername instead.", + DeprecationWarning, + stacklevel=2, + ) return self.peername @property def cert(self) -> Optional[certs.Cert]: # pragma: no cover """*Deprecated:* An outdated alias for `Connection.certificate_list[0]`.""" - warnings.warn("Server.cert is deprecated, use Server.certificate_list instead.", DeprecationWarning, - stacklevel=2) + warnings.warn( + "Server.cert is deprecated, use Server.certificate_list instead.", + DeprecationWarning, + stacklevel=2, + ) if self.certificate_list: return self.certificate_list[0] else: @@ -370,17 +435,15 @@ def cert(self) -> Optional[certs.Cert]: # pragma: no cover @cert.setter def cert(self, val): # pragma: no cover - warnings.warn("Server.cert is deprecated, use Server.certificate_list instead.", DeprecationWarning, - stacklevel=2) + warnings.warn( + "Server.cert is deprecated, use Server.certificate_list instead.", + DeprecationWarning, + stacklevel=2, + ) if val: self.certificate_list = [val] else: self.certificate_list = [] -__all__ = [ - "Connection", - "Client", - "Server", - "ConnectionState" -] +__all__ = ["Connection", "Client", "Server", "ConnectionState"] diff --git a/mitmproxy/contentviews/__init__.py b/mitmproxy/contentviews/__init__.py index 95f0318af7..b6cc2d9412 100644 --- a/mitmproxy/contentviews/__init__.py +++ b/mitmproxy/contentviews/__init__.py @@ -12,7 +12,7 @@ `base.View`. """ import traceback -from typing import List, Union +from typing import Union from typing import Optional import blinker @@ -21,18 +21,34 @@ from mitmproxy import http from mitmproxy.utils import strutils from . import ( - auto, raw, hex, json, xml_html, wbxml, javascript, css, - urlencoded, multipart, image, query, protobuf, msgpack, graphql, grpc + auto, + raw, + hex, + json, + xml_html, + wbxml, + javascript, + css, + urlencoded, + multipart, + image, + query, + protobuf, + msgpack, + graphql, + grpc, ) from .base import View, KEY_MAX, format_text, format_dict, TViewResult from ..http import HTTPFlow from ..tcp import TCPMessage, TCPFlow from ..websocket import WebSocketMessage -views: List[View] = [] +views: list[View] = [] on_add = blinker.Signal() """A new contentview has been added.""" +on_remove = blinker.Signal() +"""A contentview has been removed.""" def get(name: str) -> Optional[View]: @@ -54,6 +70,7 @@ def add(view: View) -> None: def remove(view: View) -> None: views.remove(view) + on_remove.send(view) def safe_to_print(lines, encoding="utf8"): @@ -92,9 +109,7 @@ def get_message_content_view( enc = "[cannot decode]" else: if isinstance(message, http.Message) and content != message.raw_content: - enc = "[decoded {}]".format( - message.headers.get("content-encoding") - ) + enc = "[decoded {}]".format(message.headers.get("content-encoding")) else: enc = "" @@ -110,7 +125,8 @@ def get_message_content_view( content_type = f"{ct[0]}/{ct[1]}" description, lines, error = get_content_view( - viewmode, content, + viewmode, + content, content_type=content_type, flow=flow, http_message=http_message, @@ -148,22 +164,30 @@ def get_content_view( http_message: Optional[http.Message] = None, ): """ - Args: - viewmode: the view to use. - data, **metadata: arguments passed to View instance. - - Returns: - A (description, content generator, error) tuple. - If the content view raised an exception generating the view, - the exception is returned in error and the flow is formatted in raw mode. - In contrast to calling the views directly, text is always safe-to-print unicode. + Args: + viewmode: the view to use. + data, **metadata: arguments passed to View instance. + + Returns: + A (description, content generator, error) tuple. + If the content view raised an exception generating the view, + the exception is returned in error and the flow is formatted in raw mode. + In contrast to calling the views directly, text is always safe-to-print unicode. """ try: - ret = viewmode(data, content_type=content_type, flow=flow, http_message=http_message) + ret = viewmode( + data, content_type=content_type, flow=flow, http_message=http_message + ) if ret is None: - ret = "Couldn't parse: falling back to Raw", get("Raw")( - data, content_type=content_type, flow=flow, http_message=http_message - )[1] + ret = ( + "Couldn't parse: falling back to Raw", + get("Raw")( + data, + content_type=content_type, + flow=flow, + http_message=http_message, + )[1], + ) desc, content = ret error = None # Third-party viewers can fail in unexpected ways... @@ -171,7 +195,9 @@ def get_content_view( desc = "Couldn't parse: falling back to Raw" raw = get("Raw") assert raw - content = raw(data, content_type=content_type, flow=flow, http_message=http_message)[1] + content = raw( + data, content_type=content_type, flow=flow, http_message=http_message + )[1] error = f"{getattr(viewmode, 'name')} content viewer failed: \n{traceback.format_exc()}" return desc, safe_to_print(content), error @@ -196,6 +222,14 @@ def get_content_view( add(grpc.ViewGrpcProtobuf()) __all__ = [ - "View", "KEY_MAX", "format_text", "format_dict", "TViewResult", - "get", "add", "remove", "get_content_view", "get_message_content_view", + "View", + "KEY_MAX", + "format_text", + "format_dict", + "TViewResult", + "get", + "add", + "remove", + "get_content_view", + "get_message_content_view", ] diff --git a/mitmproxy/contentviews/auto.py b/mitmproxy/contentviews/auto.py index 0dc75b65a9..d86dcf8108 100644 --- a/mitmproxy/contentviews/auto.py +++ b/mitmproxy/contentviews/auto.py @@ -9,8 +9,7 @@ def __call__(self, data, **metadata): # TODO: The auto view has little justification now that views implement render_priority, # but we keep it around for now to not touch more parts. priority, view = max( - (v.render_priority(data, **metadata), v) - for v in contentviews.views + (v.render_priority(data, **metadata), v) for v in contentviews.views ) if priority == 0 and not data: return "No content", [] diff --git a/mitmproxy/contentviews/base.py b/mitmproxy/contentviews/base.py index e355d7d562..d8baa9f25a 100644 --- a/mitmproxy/contentviews/base.py +++ b/mitmproxy/contentviews/base.py @@ -1,28 +1,29 @@ # Default view cutoff *in lines* -import typing from abc import ABC, abstractmethod +from collections.abc import Iterable, Iterator, Mapping +from typing import ClassVar, Optional, Union from mitmproxy import flow from mitmproxy import http KEY_MAX = 30 -TTextType = typing.Union[str, bytes] # FIXME: This should be either bytes or str ultimately. -TViewLine = typing.List[typing.Tuple[str, TTextType]] -TViewResult = typing.Tuple[str, typing.Iterator[TViewLine]] +TTextType = Union[str, bytes] # FIXME: This should be either bytes or str ultimately. +TViewLine = list[tuple[str, TTextType]] +TViewResult = tuple[str, Iterator[TViewLine]] class View(ABC): - name: typing.ClassVar[str] + name: ClassVar[str] @abstractmethod def __call__( self, data: bytes, *, - content_type: typing.Optional[str] = None, - flow: typing.Optional[flow.Flow] = None, - http_message: typing.Optional[http.Message] = None, + content_type: Optional[str] = None, + flow: Optional[flow.Flow] = None, + http_message: Optional[http.Message] = None, **unknown_metadata, ) -> TViewResult: """ @@ -46,9 +47,9 @@ def render_priority( self, data: bytes, *, - content_type: typing.Optional[str] = None, - flow: typing.Optional[flow.Flow] = None, - http_message: typing.Optional[http.Message] = None, + content_type: Optional[str] = None, + flow: Optional[flow.Flow] = None, + http_message: Optional[http.Message] = None, **unknown_metadata, ) -> float: """ @@ -65,9 +66,7 @@ def __lt__(self, other): return self.name.__lt__(other.name) -def format_pairs( - items: typing.Iterable[typing.Tuple[TTextType, TTextType]] -) -> typing.Iterator[TViewLine]: +def format_pairs(items: Iterable[tuple[TTextType, TTextType]]) -> Iterator[TViewLine]: """ Helper function that accepts a list of (k,v) pairs into a list of [ @@ -89,15 +88,10 @@ def format_pairs( key = key.ljust(max_key_len + 2) - yield [ - ("header", key), - ("text", value) - ] + yield [("header", key), ("text", value)] -def format_dict( - d: typing.Mapping[TTextType, TTextType] -) -> typing.Iterator[TViewLine]: +def format_dict(d: Mapping[TTextType, TTextType]) -> Iterator[TViewLine]: """ Helper function that transforms the given dictionary into a list of [ @@ -110,7 +104,7 @@ def format_dict( return format_pairs(d.items()) -def format_text(text: TTextType) -> typing.Iterator[TViewLine]: +def format_text(text: TTextType) -> Iterator[TViewLine]: """ Helper function that transforms bytes into the view output format. """ diff --git a/mitmproxy/contentviews/css.py b/mitmproxy/contentviews/css.py index 92a855a8dd..fd878c0afd 100644 --- a/mitmproxy/contentviews/css.py +++ b/mitmproxy/contentviews/css.py @@ -18,7 +18,7 @@ "'" + strutils.SINGLELINE_CONTENT + strutils.NO_ESCAPE + "'", '"' + strutils.SINGLELINE_CONTENT + strutils.NO_ESCAPE + '"', r"/\*" + strutils.MULTILINE_CONTENT + r"\*/", - "//" + strutils.SINGLELINE_CONTENT + "$" + "//" + strutils.SINGLELINE_CONTENT + "$", ) CSS_SPECIAL_CHARS = "{};:" @@ -57,7 +57,9 @@ def __call__(self, data, **metadata): beautified = beautify(data) return "CSS", base.format_text(beautified) - def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: return float(bool(data) and content_type == "text/css") @@ -67,4 +69,4 @@ def render_priority(self, data: bytes, *, content_type: Optional[str] = None, ** t = time.time() x = beautify(data) - print("Beautifying vendor.css took {:.2}s".format(time.time() - t)) + print(f"Beautifying vendor.css took {time.time() - t:.2}s") diff --git a/mitmproxy/contentviews/graphql.py b/mitmproxy/contentviews/graphql.py index cbf56e3d8a..c179828e87 100644 --- a/mitmproxy/contentviews/graphql.py +++ b/mitmproxy/contentviews/graphql.py @@ -1,6 +1,5 @@ import json - -import typing +from typing import Any, Optional from mitmproxy.contentviews import base from mitmproxy.contentviews.json import parse_json, PARSE_ERROR @@ -13,14 +12,16 @@ def format_graphql(data): return """{header} --- {query} -""".format(header=json.dumps(header_data, indent=2), query = query) +""".format( + header=json.dumps(header_data, indent=2), query=query + ) -def format_query_list(data: typing.List[typing.Any]): +def format_query_list(data: list[Any]): num_queries = len(data) - 1 result = "" for i, op in enumerate(data): - result += "--- {i}/{num_queries}\n".format(i=i, num_queries=num_queries) + result += f"--- {i}/{num_queries}\n" result += format_graphql(op) return result @@ -44,7 +45,9 @@ def __call__(self, data, **metadata): elif is_graphql_batch_query(data): return "GraphQL", base.format_text(format_query_list(data)) - def render_priority(self, data: bytes, *, content_type: typing.Optional[str] = None, **metadata) -> float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: if content_type != "application/json" or not data: return 0 diff --git a/mitmproxy/contentviews/grpc.py b/mitmproxy/contentviews/grpc.py index a28a6821b7..a5ef99708f 100644 --- a/mitmproxy/contentviews/grpc.py +++ b/mitmproxy/contentviews/grpc.py @@ -3,7 +3,7 @@ import struct from dataclasses import dataclass, field from enum import Enum -from typing import Dict, Generator, Iterable, Iterator, List, Optional, Tuple, Union +from typing import Generator, Iterable, Iterator from mitmproxy import contentviews, ctx, flow, flowfilter, http from mitmproxy.contentviews import base @@ -21,7 +21,7 @@ class ParserRule: To restrict a rule to a responses only use 'ParserRuleResponse', instead. """ - field_definitions: List[ProtoParser.ParserFieldDefinition] + field_definitions: list[ProtoParser.ParserFieldDefinition] """List of field definitions for this rule """ name: str = "" @@ -41,7 +41,6 @@ class ParserRuleResponse(ParserRule): The rule only applies if the processed message is a server response. """ - pass @dataclass class ParserRuleRequest(ParserRule): @@ -50,7 +49,6 @@ class ParserRuleRequest(ParserRule): The rule only applies if the processed message is a client request. """ - pass @dataclass class ParserFieldDefinition: @@ -97,16 +95,16 @@ class ParserFieldDefinition: tag: str """Field tag for which this description applies (including flattened tag path, f.e. '1.2.2.4')""" - tag_prefixes: List[str] = field(default_factory=list) + tag_prefixes: list[str] = field(default_factory=list) """List of prefixes for tag matching (f.e. tag_prefixes=['1.2.', '2.2.'] with tag='1' matches '1.2.1' and '2.2.1')""" - intended_decoding: Optional[ProtoParser.DecodedTypes] = None + intended_decoding: ProtoParser.DecodedTypes | None = None """optional: intended decoding for visualization (parser fails over to alternate decoding if not possible)""" - name: Optional[str] = None + name: str | None = None """optional: intended field for visualization (parser fails over to alternate decoding if not possible)""" - as_packed: Optional[bool] = False + as_packed: bool | None = False """optional: if set to true, the field is considered to be repeated and packed""" @dataclass @@ -149,12 +147,12 @@ class DecodedTypes(Enum): unknown = 17 @staticmethod - def _read_base128le(data: bytes) -> Tuple[int, int]: + def _read_base128le(data: bytes) -> tuple[int, int]: res = 0 offset = 0 while offset < len(data): o = data[offset] - res += ((o & 0x7f) << (7 * offset)) + res += (o & 0x7F) << (7 * offset) offset += 1 if o < 0x80: # the Kaitai parser for protobuf support base128 le values up @@ -172,11 +170,11 @@ def _read_base128le(data: bytes) -> Tuple[int, int]: raise ValueError("varint exceeds bounds of provided data") @staticmethod - def _read_u32(data: bytes) -> Tuple[int, int]: + def _read_u32(data: bytes) -> tuple[int, int]: return 4, struct.unpack(" Tuple[int, int]: + def _read_u64(data: bytes) -> tuple[int, int]: return 8, struct.unpack(" List[ProtoParser.Field]: - res: List[ProtoParser.Field] = [] + rules: list[ProtoParser.ParserRule], + ) -> list[ProtoParser.Field]: + res: list[ProtoParser.Field] = [] pos = 0 while pos < len(wire_data): # read field key (tag and wire_type) offset, key = ProtoParser._read_base128le(wire_data[pos:]) # casting raises exception for invalid WireTypes - wt = ProtoParser.WireTypes((key & 7)) - tag = (key >> 3) + wt = ProtoParser.WireTypes(key & 7) + tag = key >> 3 pos += offset - val: Union[bytes, int] + val: bytes | int preferred_decoding: ProtoParser.DecodedTypes if wt == ProtoParser.WireTypes.varint: offset, val = ProtoParser._read_base128le(wire_data[pos:]) @@ -225,14 +223,14 @@ def read_fields( pos += offset if length > len(wire_data[pos:]): raise ValueError("length delimited field exceeds data size") - val = wire_data[pos:pos + length] + val = wire_data[pos : pos + length] pos += length preferred_decoding = ProtoParser.DecodedTypes.message elif ( - wt == ProtoParser.WireTypes.group_start or - wt == ProtoParser.WireTypes.group_end + wt == ProtoParser.WireTypes.group_start + or wt == ProtoParser.WireTypes.group_end ): - raise ValueError("deprecated field: {}".format(wt)) + raise ValueError(f"deprecated field: {wt}") elif wt == ProtoParser.WireTypes.bit_32: offset, val = ProtoParser._read_u32(wire_data[pos:]) pos += offset @@ -249,7 +247,7 @@ def read_fields( rules=rules, tag=tag, wire_value=val, - parent_field=parent_field + parent_field=parent_field, ) res.append(field) @@ -258,113 +256,123 @@ def read_fields( @staticmethod def read_packed_fields( packed_field: ProtoParser.Field, - ) -> List[ProtoParser.Field]: + ) -> list[ProtoParser.Field]: if not isinstance(packed_field.wire_value, bytes): ctx.log(type(packed_field.wire_value)) raise ValueError("can not unpack field with data other than bytes") wire_data: bytes = packed_field.wire_value tag: int = packed_field.tag options: ProtoParser.ParserOptions = packed_field.options - rules: List[ProtoParser.ParserRule] = packed_field.rules + rules: list[ProtoParser.ParserRule] = packed_field.rules intended_decoding: ProtoParser.DecodedTypes = packed_field.preferred_decoding # the packed field has to have WireType length delimited, whereas the contained # individual types have to have a different WireType, which is derived from # the intended decoding if ( - packed_field.wire_type != ProtoParser.WireTypes.len_delimited or - not isinstance(packed_field.wire_value, bytes) + packed_field.wire_type != ProtoParser.WireTypes.len_delimited + or not isinstance(packed_field.wire_value, bytes) ): - raise ValueError("packed fields have to be embedded in a length delimited message") + raise ValueError( + "packed fields have to be embedded in a length delimited message" + ) # wiretype to read has to be determined from intended decoding packed_wire_type: ProtoParser.WireTypes if ( - intended_decoding == ProtoParser.DecodedTypes.int32 or - intended_decoding == ProtoParser.DecodedTypes.int64 or - intended_decoding == ProtoParser.DecodedTypes.uint32 or - intended_decoding == ProtoParser.DecodedTypes.uint64 or - intended_decoding == ProtoParser.DecodedTypes.sint32 or - intended_decoding == ProtoParser.DecodedTypes.sint64 or - intended_decoding == ProtoParser.DecodedTypes.bool or - intended_decoding == ProtoParser.DecodedTypes.enum + intended_decoding == ProtoParser.DecodedTypes.int32 + or intended_decoding == ProtoParser.DecodedTypes.int64 + or intended_decoding == ProtoParser.DecodedTypes.uint32 + or intended_decoding == ProtoParser.DecodedTypes.uint64 + or intended_decoding == ProtoParser.DecodedTypes.sint32 + or intended_decoding == ProtoParser.DecodedTypes.sint64 + or intended_decoding == ProtoParser.DecodedTypes.bool + or intended_decoding == ProtoParser.DecodedTypes.enum ): packed_wire_type = ProtoParser.WireTypes.varint elif ( - intended_decoding == ProtoParser.DecodedTypes.fixed32 or - intended_decoding == ProtoParser.DecodedTypes.sfixed32 or - intended_decoding == ProtoParser.DecodedTypes.float + intended_decoding == ProtoParser.DecodedTypes.fixed32 + or intended_decoding == ProtoParser.DecodedTypes.sfixed32 + or intended_decoding == ProtoParser.DecodedTypes.float ): packed_wire_type = ProtoParser.WireTypes.bit_32 elif ( - intended_decoding == ProtoParser.DecodedTypes.fixed64 or - intended_decoding == ProtoParser.DecodedTypes.sfixed64 or - intended_decoding == ProtoParser.DecodedTypes.double + intended_decoding == ProtoParser.DecodedTypes.fixed64 + or intended_decoding == ProtoParser.DecodedTypes.sfixed64 + or intended_decoding == ProtoParser.DecodedTypes.double ): packed_wire_type = ProtoParser.WireTypes.bit_64 elif ( - intended_decoding == ProtoParser.DecodedTypes.string or - intended_decoding == ProtoParser.DecodedTypes.bytes or - intended_decoding == ProtoParser.DecodedTypes.message + intended_decoding == ProtoParser.DecodedTypes.string + or intended_decoding == ProtoParser.DecodedTypes.bytes + or intended_decoding == ProtoParser.DecodedTypes.message ): packed_wire_type = ProtoParser.WireTypes.len_delimited else: # should never happen, no test - raise TypeError("Wire type could not be determined from packed decoding type") + raise TypeError( + "Wire type could not be determined from packed decoding type" + ) - res: List[ProtoParser.Field] = [] + res: list[ProtoParser.Field] = [] pos = 0 - val: Union[bytes, int] + val: bytes | int if packed_wire_type == ProtoParser.WireTypes.varint: while pos < len(wire_data): offset, val = ProtoParser._read_base128le(wire_data[pos:]) pos += offset - res.append(ProtoParser.Field( - options=options, - preferred_decoding=intended_decoding, - rules=rules, - tag=tag, - wire_type=packed_wire_type, - wire_value=val, - parent_field=packed_field.parent_field, - is_unpacked_children=True - )) + res.append( + ProtoParser.Field( + options=options, + preferred_decoding=intended_decoding, + rules=rules, + tag=tag, + wire_type=packed_wire_type, + wire_value=val, + parent_field=packed_field.parent_field, + is_unpacked_children=True, + ) + ) elif packed_wire_type == ProtoParser.WireTypes.bit_64: if len(wire_data) % 8 != 0: raise ValueError("can not parse as packed bit64") while pos < len(wire_data): offset, val = ProtoParser._read_u64(wire_data[pos:]) pos += offset - res.append(ProtoParser.Field( - options=options, - preferred_decoding=intended_decoding, - rules=rules, - tag=tag, - wire_type=packed_wire_type, - wire_value=val, - parent_field=packed_field.parent_field, - is_unpacked_children=True - )) + res.append( + ProtoParser.Field( + options=options, + preferred_decoding=intended_decoding, + rules=rules, + tag=tag, + wire_type=packed_wire_type, + wire_value=val, + parent_field=packed_field.parent_field, + is_unpacked_children=True, + ) + ) elif packed_wire_type == ProtoParser.WireTypes.len_delimited: while pos < len(wire_data): offset, length = ProtoParser._read_base128le(wire_data[pos:]) pos += offset - val = wire_data[pos: pos + length] + val = wire_data[pos : pos + length] if length > len(wire_data[pos:]): raise ValueError("packed length delimited field exceeds data size") - res.append(ProtoParser.Field( - options=options, - preferred_decoding=intended_decoding, - rules=rules, - tag=tag, - wire_type=packed_wire_type, - wire_value=val, - parent_field=packed_field.parent_field, - is_unpacked_children=True - )) + res.append( + ProtoParser.Field( + options=options, + preferred_decoding=intended_decoding, + rules=rules, + tag=tag, + wire_type=packed_wire_type, + wire_value=val, + parent_field=packed_field.parent_field, + is_unpacked_children=True, + ) + ) pos += length elif ( - packed_wire_type == ProtoParser.WireTypes.group_start or - packed_wire_type == ProtoParser.WireTypes.group_end + packed_wire_type == ProtoParser.WireTypes.group_start + or packed_wire_type == ProtoParser.WireTypes.group_end ): raise ValueError("group tags can not be encoded packed") elif packed_wire_type == ProtoParser.WireTypes.bit_32: @@ -373,16 +381,18 @@ def read_packed_fields( while pos < len(wire_data): offset, val = ProtoParser._read_u32(wire_data[pos:]) pos += offset - res.append(ProtoParser.Field( - options=options, - preferred_decoding=intended_decoding, - rules=rules, - tag=tag, - wire_type=packed_wire_type, - wire_value=val, - parent_field=packed_field.parent_field, - is_unpacked_children=True - )) + res.append( + ProtoParser.Field( + options=options, + preferred_decoding=intended_decoding, + rules=rules, + tag=tag, + wire_type=packed_wire_type, + wire_value=val, + parent_field=packed_field.parent_field, + is_unpacked_children=True, + ) + ) else: # should never happen raise ValueError("invalid WireType for protobuf messsage field") @@ -442,23 +452,27 @@ def __init__( wire_type: ProtoParser.WireTypes, preferred_decoding: ProtoParser.DecodedTypes, tag: int, - parent_field: Optional[ProtoParser.Field], - wire_value: Union[int, bytes], + parent_field: ProtoParser.Field | None, + wire_value: int | bytes, options: ProtoParser.ParserOptions, - rules: List[ProtoParser.ParserRule], - is_unpacked_children: bool = False + rules: list[ProtoParser.ParserRule], + is_unpacked_children: bool = False, ) -> None: self.wire_type: ProtoParser.WireTypes = wire_type self.preferred_decoding: ProtoParser.DecodedTypes = preferred_decoding - self.wire_value: Union[int, bytes] = wire_value + self.wire_value: int | bytes = wire_value self.tag: int = tag self.options: ProtoParser.ParserOptions = options self.name: str = "" - self.rules: List[ProtoParser.ParserRule] = rules - self.parent_field: Optional[ProtoParser.Field] = parent_field - self.is_unpacked_children: bool = is_unpacked_children # marks field as being a result of unpacking - self.is_packed_parent: bool = False # marks field as being parent of successfully unpacked children - self.parent_tags: List[int] = [] + self.rules: list[ProtoParser.ParserRule] = rules + self.parent_field: ProtoParser.Field | None = parent_field + self.is_unpacked_children: bool = ( + is_unpacked_children # marks field as being a result of unpacking + ) + self.is_packed_parent: bool = ( + False # marks field as being parent of successfully unpacked children + ) + self.parent_tags: list[int] = [] if self.parent_field is not None: self.parent_tags = self.parent_field.parent_tags[:] self.parent_tags.append(self.parent_field.tag) @@ -467,10 +481,7 @@ def __init__( # rules can overwrite self.try_unpack self.apply_rules() # do not unpack fields which are the result of unpacking - if ( - parent_field is not None and - self.is_unpacked_children - ): + if parent_field is not None and self.is_unpacked_children: self.try_unpack = False # no tests for only_first_hit=False, as not user-changable @@ -501,7 +512,11 @@ def apply_rules(self, only_first_hit=True): # overwrite matches till last rule was inspected # (f.e. allows to define name in one rule and intended_decoding in another one) name = fd.name if fd.name else name - decoding = fd.intended_decoding if fd.intended_decoding else decoding + decoding = ( + fd.intended_decoding + if fd.intended_decoding + else decoding + ) if fd.as_packed: as_packed = True @@ -512,7 +527,6 @@ def apply_rules(self, only_first_hit=True): self.try_unpack = as_packed except Exception as e: ctx.log.warn(e) - pass def _gen_tag_str(self): tags = self.parent_tags[:] @@ -522,8 +536,11 @@ def _gen_tag_str(self): def safe_decode_as( self, intended_decoding: ProtoParser.DecodedTypes, - try_as_packed: bool = False - ) -> Tuple[ProtoParser.DecodedTypes, Union[bool, float, int, bytes, str, List[ProtoParser.Field]]]: + try_as_packed: bool = False, + ) -> tuple[ + ProtoParser.DecodedTypes, + bool | float | int | bytes | str | list[ProtoParser.Field], + ]: """ Tries to decode as intended, applies failover, if not possible @@ -531,7 +548,9 @@ def safe_decode_as( """ if self.wire_type == ProtoParser.WireTypes.varint: try: - return intended_decoding, self.decode_as(intended_decoding, try_as_packed) + return intended_decoding, self.decode_as( + intended_decoding, try_as_packed + ) except: if int(self.wire_value).bit_length() > 32: # ignore the fact that varint could exceed 64bit (would violate the specs) @@ -540,30 +559,38 @@ def safe_decode_as( return ProtoParser.DecodedTypes.uint32, self.wire_value elif self.wire_type == ProtoParser.WireTypes.bit_64: try: - return intended_decoding, self.decode_as(intended_decoding, try_as_packed) + return intended_decoding, self.decode_as( + intended_decoding, try_as_packed + ) except: return ProtoParser.DecodedTypes.fixed64, self.wire_value elif self.wire_type == ProtoParser.WireTypes.bit_32: try: - return intended_decoding, self.decode_as(intended_decoding, try_as_packed) + return intended_decoding, self.decode_as( + intended_decoding, try_as_packed + ) except: return ProtoParser.DecodedTypes.fixed32, self.wire_value elif self.wire_type == ProtoParser.WireTypes.len_delimited: try: - return intended_decoding, self.decode_as(intended_decoding, try_as_packed) + return intended_decoding, self.decode_as( + intended_decoding, try_as_packed + ) except: # failover strategy: message --> string (valid UTF-8) --> bytes - len_delimited_strategy: List[ProtoParser.DecodedTypes] = [ + len_delimited_strategy: list[ProtoParser.DecodedTypes] = [ ProtoParser.DecodedTypes.message, ProtoParser.DecodedTypes.string, - ProtoParser.DecodedTypes.bytes # should always work + ProtoParser.DecodedTypes.bytes, # should always work ] for failover_decoding in len_delimited_strategy: if failover_decoding == intended_decoding and not try_as_packed: # don't try same decoding twice, unless first attempt was packed continue try: - return failover_decoding, self.decode_as(failover_decoding, False) + return failover_decoding, self.decode_as( + failover_decoding, False + ) except: pass @@ -571,10 +598,8 @@ def safe_decode_as( return ProtoParser.DecodedTypes.unknown, self.wire_value def decode_as( - self, - intended_decoding: ProtoParser.DecodedTypes, - as_packed: bool = False - ) -> Union[bool, int, float, bytes, str, List[ProtoParser.Field]]: + self, intended_decoding: ProtoParser.DecodedTypes, as_packed: bool = False + ) -> bool | int | float | bytes | str | list[ProtoParser.Field]: if as_packed is True: return ProtoParser.read_packed_fields(packed_field=self) @@ -582,7 +607,7 @@ def decode_as( assert isinstance(self.wire_value, int) if intended_decoding == ProtoParser.DecodedTypes.bool: # clamp result to 64bit - return self.wire_value & 0xffffffffffffffff != 0 + return self.wire_value & 0xFFFFFFFFFFFFFFFF != 0 elif intended_decoding == ProtoParser.DecodedTypes.int32: if self.wire_value.bit_length() > 32: raise TypeError("wire value too large for int32") @@ -596,8 +621,8 @@ def decode_as( raise TypeError("wire value too large for uint32") return self.wire_value # already 'int' which was parsed as unsigned elif ( - intended_decoding == ProtoParser.DecodedTypes.uint64 or - intended_decoding == ProtoParser.DecodedTypes.enum + intended_decoding == ProtoParser.DecodedTypes.uint64 + or intended_decoding == ProtoParser.DecodedTypes.enum ): if self.wire_value.bit_length() > 64: raise TypeError("wire value too large") @@ -605,7 +630,9 @@ def decode_as( elif intended_decoding == ProtoParser.DecodedTypes.sint32: if self.wire_value.bit_length() > 32: raise TypeError("wire value too large for sint32") - return (self.wire_value >> 1) ^ -(self.wire_value & 1) # zigzag_decode + return (self.wire_value >> 1) ^ -( + self.wire_value & 1 + ) # zigzag_decode elif intended_decoding == ProtoParser.DecodedTypes.sint64: if self.wire_value.bit_length() > 64: raise TypeError("wire value too large for sint64") @@ -613,8 +640,8 @@ def decode_as( # Ref: https://gist.github.com/mfuerstenau/ba870a29e16536fdbaba return (self.wire_value >> 1) ^ -(self.wire_value & 1) elif ( - intended_decoding == ProtoParser.DecodedTypes.float or - intended_decoding == ProtoParser.DecodedTypes.double + intended_decoding == ProtoParser.DecodedTypes.float + or intended_decoding == ProtoParser.DecodedTypes.double ): # special case, not complying to protobuf specs return self._wire_value_as_float() @@ -709,7 +736,7 @@ def wire_value_as_utf8(self, escape_newline=True) -> str: return res.replace("\n", "\\n") if escape_newline else res return str(self.wire_value) - def gen_flat_decoded_field_dicts(self) -> Generator[Dict, None, None]: + def gen_flat_decoded_field_dicts(self) -> Generator[dict, None, None]: """ Returns a generator which passes the field as a dict. @@ -718,7 +745,9 @@ def gen_flat_decoded_field_dicts(self) -> Generator[Dict, None, None]: If the field holds a nested message, the fields contained in the message are appended. Ultimately this flattens all fields recursively. """ - selected_decoding, decoded_val = self.safe_decode_as(self.preferred_decoding, self.try_unpack) + selected_decoding, decoded_val = self.safe_decode_as( + self.preferred_decoding, self.try_unpack + ) field_desc_dict = { "tag": self._gen_tag_str(), "wireType": self._wire_type_str(), @@ -727,7 +756,8 @@ def gen_flat_decoded_field_dicts(self) -> Generator[Dict, None, None]: } if isinstance(decoded_val, list): if ( - selected_decoding == ProtoParser.DecodedTypes.message # field is a message with subfields + selected_decoding + == ProtoParser.DecodedTypes.message # field is a message with subfields and not self.is_packed_parent # field is a message, but replaced by packed fields ): # Field is a message, not packed, thus include it as message header @@ -735,8 +765,7 @@ def gen_flat_decoded_field_dicts(self) -> Generator[Dict, None, None]: yield field_desc_dict # add sub-fields of messages or packed fields for f in decoded_val: - for field_dict in f.gen_flat_decoded_field_dicts(): - yield field_dict + yield from f.gen_flat_decoded_field_dicts() else: field_desc_dict["val"] = decoded_val yield field_desc_dict @@ -744,8 +773,8 @@ def gen_flat_decoded_field_dicts(self) -> Generator[Dict, None, None]: def __init__( self, data: bytes, - rules: List[ProtoParser.ParserRule] = None, - parser_options: ParserOptions = None + rules: list[ProtoParser.ParserRule] = None, + parser_options: ParserOptions = None, ) -> None: self.data: bytes = data if parser_options is None: @@ -756,23 +785,25 @@ def __init__( self.rules = rules try: - self.root_fields: List[ProtoParser.Field] = ProtoParser.read_fields( + self.root_fields: list[ProtoParser.Field] = ProtoParser.read_fields( wire_data=self.data, options=self.options, parent_field=None, - rules=self.rules + rules=self.rules, ) except Exception as e: raise ValueError("not a valid protobuf message") from e - def gen_flat_decoded_field_dicts(self) -> Generator[Dict, None, None]: + def gen_flat_decoded_field_dicts(self) -> Generator[dict, None, None]: for f in self.root_fields: - for field_dict in f.gen_flat_decoded_field_dicts(): - yield field_dict + yield from f.gen_flat_decoded_field_dicts() - def gen_str_rows(self) -> Generator[Tuple[str, ...], None, None]: + def gen_str_rows(self) -> Generator[tuple[str, ...], None, None]: for field_dict in self.gen_flat_decoded_field_dicts(): - if self.options.exclude_message_headers and field_dict["decoding"] == "message": + if ( + self.options.exclude_message_headers + and field_dict["decoding"] == "message" + ): continue if self.options.include_wiretype: @@ -789,7 +820,7 @@ def gen_str_rows(self) -> Generator[Tuple[str, ...], None, None]: # allow it to be use independently. # This function is generic enough, to consider moving it to mitmproxy.contentviews.base def format_table( - table_rows: Iterable[Tuple[str, ...]], + table_rows: Iterable[tuple[str, ...]], max_col_width=100, ) -> Iterator[base.TViewLine]: """ @@ -798,9 +829,9 @@ def format_table( Note: The function has to convert generators to a list, as all rows have to be processed twice (to determine the column widths first). """ - rows: List[Tuple[str, ...]] = [] + rows: list[tuple[str, ...]] = [] col_count = 0 - cols_width: List[int] = [] + cols_width: list[int] = [] for row in table_rows: col_count = max(col_count, len(row)) while len(cols_width) < col_count: @@ -822,25 +853,29 @@ def format_table( yield line -def parse_grpc_messages(data, compression_scheme) -> Generator[Tuple[bool, bytes], None, None]: +def parse_grpc_messages( + data, compression_scheme +) -> Generator[tuple[bool, bytes], None, None]: """Generator iterates over body data and returns a boolean indicating if the messages was compressed, along with the raw message data (decompressed) for each gRPC message contained in the body data""" while data: try: - msg_is_compressed, length = struct.unpack('!?i', data[:5]) - decoded_message = struct.unpack('!%is' % length, data[5:5 + length])[0] + msg_is_compressed, length = struct.unpack("!?i", data[:5]) + decoded_message = struct.unpack("!%is" % length, data[5 : 5 + length])[0] except Exception as e: raise ValueError("invalid gRPC message") from e if msg_is_compressed: try: - decoded_message = decode(encoded=decoded_message, encoding=compression_scheme) + decoded_message = decode( + encoded=decoded_message, encoding=compression_scheme + ) except Exception as e: raise ValueError("Failed to decompress gRPC message with gzip") from e yield msg_is_compressed, decoded_message - data = data[5 + length:] + data = data[5 + length :] # hacky fix for mitmproxy issue: @@ -878,35 +913,46 @@ def hack_generator_to_list(generator_func): return list(generator_func) -def format_pbuf(message: bytes, parser_options: ProtoParser.ParserOptions, rules: List[ProtoParser.ParserRule]): - for l in format_table(ProtoParser(data=message, parser_options=parser_options, rules=rules).gen_str_rows()): - yield l +def format_pbuf( + message: bytes, + parser_options: ProtoParser.ParserOptions, + rules: list[ProtoParser.ParserRule], +): + yield from format_table( + ProtoParser( + data=message, parser_options=parser_options, rules=rules + ).gen_str_rows() + ) def format_grpc( data: bytes, parser_options: ProtoParser.ParserOptions, - rules: List[ProtoParser.ParserRule], - compression_scheme="gzip" + rules: list[ProtoParser.ParserRule], + compression_scheme="gzip", ): message_count = 0 - for compressed, pb_message in parse_grpc_messages(data=data, compression_scheme=compression_scheme): - headline = 'gRPC message ' + str(message_count) + ' (compressed ' + str( - compression_scheme if compressed else compressed) + ')' + for compressed, pb_message in parse_grpc_messages( + data=data, compression_scheme=compression_scheme + ): + headline = ( + "gRPC message " + + str(message_count) + + " (compressed " + + str(compression_scheme if compressed else compressed) + + ")" + ) yield [("text", headline)] - for l in format_pbuf( - message=pb_message, - parser_options=parser_options, - rules=rules - ): - yield l + yield from format_pbuf( + message=pb_message, parser_options=parser_options, rules=rules + ) @dataclass class ViewConfig: parser_options: ProtoParser.ParserOptions = ProtoParser.ParserOptions() - parser_rules: List[ProtoParser.ParserRule] = field(default_factory=list) + parser_rules: list[ProtoParser.ParserRule] = field(default_factory=list) class ViewGrpcProtobuf(base.View): @@ -938,10 +984,10 @@ def __init__(self, config: ViewConfig = None) -> None: def _matching_rules( self, - rules: List[ProtoParser.ParserRule], - message: Optional[http.Message], - flow: Optional[flow.Flow] - ) -> List[ProtoParser.ParserRule]: + rules: list[ProtoParser.ParserRule], + message: http.Message | None, + flow: flow.Flow | None, + ) -> list[ProtoParser.ParserRule]: """ Checks which of the give rules applies and returns a List only containing those rules @@ -966,7 +1012,7 @@ def _matching_rules( - ParserRuleRequest: applies to requests only - ParserRuleResponse: applies to responses only """ - res: List[ProtoParser.ParserRule] = [] + res: list[ProtoParser.ParserRule] = [] if not flow: return res is_request = isinstance(message, http.Request) @@ -985,12 +1031,14 @@ def __call__( self, data: bytes, *, - content_type: Optional[str] = None, - flow: Optional[flow.Flow] = None, - http_message: Optional[http.Message] = None, + content_type: str | None = None, + flow: flow.Flow | None = None, + http_message: http.Message | None = None, **unknown_metadata, ) -> contentviews.TViewResult: - applicabble_rules = self._matching_rules(rules=self.config.parser_rules, flow=flow, message=http_message) + applicabble_rules = self._matching_rules( + rules=self.config.parser_rules, flow=flow, message=http_message + ) if content_type in self.__content_types_grpc: # If gRPC messages are flagged to be compressed, the compression algorithm is expressed in the # 'grpc-encoding' header. @@ -1006,7 +1054,11 @@ def __call__( try: assert http_message is not None h = http_message.headers["grpc-encoding"] - grpc_encoding = h if h in self.__valid_grpc_encodings else self.__valid_grpc_encodings[0] + grpc_encoding = ( + h + if h in self.__valid_grpc_encodings + else self.__valid_grpc_encodings[0] + ) except: grpc_encoding = self.__valid_grpc_encodings[0] @@ -1014,14 +1066,14 @@ def __call__( data=data, parser_options=self.config.parser_options, compression_scheme=grpc_encoding, - rules=applicabble_rules + rules=applicabble_rules, ) title = "gRPC" else: text_iter = format_pbuf( message=data, parser_options=self.config.parser_options, - rules=applicabble_rules + rules=applicabble_rules, ) title = "Protobuf (flattened)" @@ -1041,9 +1093,9 @@ def render_priority( self, data: bytes, *, - content_type: Optional[str] = None, - flow: Optional[flow.Flow] = None, - http_message: Optional[http.Message] = None, + content_type: str | None = None, + flow: flow.Flow | None = None, + http_message: http.Message | None = None, **unknown_metadata, ) -> float: diff --git a/mitmproxy/contentviews/hex.py b/mitmproxy/contentviews/hex.py index cca30f0ffb..5b53202c6b 100644 --- a/mitmproxy/contentviews/hex.py +++ b/mitmproxy/contentviews/hex.py @@ -8,11 +8,7 @@ class ViewHex(base.View): @staticmethod def _format(data): for offset, hexa, s in strutils.hexdump(data): - yield [ - ("offset", offset + " "), - ("text", hexa + " "), - ("text", s) - ] + yield [("offset", offset + " "), ("text", hexa + " "), ("text", s)] def __call__(self, data, **metadata): return "Hex", self._format(data) diff --git a/mitmproxy/contentviews/image/image_parser.py b/mitmproxy/contentviews/image/image_parser.py index 146022c63a..709851fedb 100644 --- a/mitmproxy/contentviews/image/image_parser.py +++ b/mitmproxy/contentviews/image/image_parser.py @@ -1,35 +1,36 @@ import io -import typing from kaitaistruct import KaitaiStream -from mitmproxy.contrib.kaitaistruct import png from mitmproxy.contrib.kaitaistruct import gif -from mitmproxy.contrib.kaitaistruct import jpeg from mitmproxy.contrib.kaitaistruct import ico +from mitmproxy.contrib.kaitaistruct import jpeg +from mitmproxy.contrib.kaitaistruct import png -Metadata = typing.List[typing.Tuple[str, str]] +Metadata = list[tuple[str, str]] def parse_png(data: bytes) -> Metadata: img = png.Png(KaitaiStream(io.BytesIO(data))) parts = [ - ('Format', 'Portable network graphics'), - ('Size', f"{img.ihdr.width} x {img.ihdr.height} px") + ("Format", "Portable network graphics"), + ("Size", f"{img.ihdr.width} x {img.ihdr.height} px"), ] for chunk in img.chunks: - if chunk.type == 'gAMA': - parts.append(('gamma', str(chunk.body.gamma_int / 100000))) - elif chunk.type == 'pHYs': + if chunk.type == "gAMA": + parts.append(("gamma", str(chunk.body.gamma_int / 100000))) + elif chunk.type == "pHYs": aspectx = chunk.body.pixels_per_unit_x aspecty = chunk.body.pixels_per_unit_y - parts.append(('aspect', f"{aspectx} x {aspecty}")) - elif chunk.type == 'tEXt': + parts.append(("aspect", f"{aspectx} x {aspecty}")) + elif chunk.type == "tEXt": parts.append((chunk.body.keyword, chunk.body.text)) - elif chunk.type == 'iTXt': + elif chunk.type == "iTXt": parts.append((chunk.body.keyword, chunk.body.text)) - elif chunk.type == 'zTXt': - parts.append((chunk.body.keyword, chunk.body.text_datastream.decode('iso8859-1'))) + elif chunk.type == "zTXt": + parts.append( + (chunk.body.keyword, chunk.body.text_datastream.decode("iso8859-1")) + ) return parts @@ -37,66 +38,83 @@ def parse_gif(data: bytes) -> Metadata: img = gif.Gif(KaitaiStream(io.BytesIO(data))) descriptor = img.logical_screen_descriptor parts = [ - ('Format', 'Compuserve GIF'), - ('Version', f"GIF{img.hdr.version}"), - ('Size', f"{descriptor.screen_width} x {descriptor.screen_height} px"), - ('background', str(descriptor.bg_color_index)) + ("Format", "Compuserve GIF"), + ("Version", f"GIF{img.hdr.version}"), + ("Size", f"{descriptor.screen_width} x {descriptor.screen_height} px"), + ("background", str(descriptor.bg_color_index)), ] ext_blocks = [] for block in img.blocks: - if block.block_type.name == 'extension': + if block.block_type.name == "extension": ext_blocks.append(block) comment_blocks = [] for block in ext_blocks: - if block.body.label._name_ == 'comment': + if block.body.label._name_ == "comment": comment_blocks.append(block) for block in comment_blocks: entries = block.body.body.entries for entry in entries: comment = entry.bytes - if comment != b'': - parts.append(('comment', str(comment))) + if comment != b"": + parts.append(("comment", str(comment))) return parts def parse_jpeg(data: bytes) -> Metadata: img = jpeg.Jpeg(KaitaiStream(io.BytesIO(data))) - parts = [ - ('Format', 'JPEG (ISO 10918)') - ] + parts = [("Format", "JPEG (ISO 10918)")] for segment in img.segments: - if segment.marker._name_ == 'sof0': - parts.append(('Size', f"{segment.data.image_width} x {segment.data.image_height} px")) - if segment.marker._name_ == 'app0': - parts.append(('jfif_version', f"({segment.data.version_major}, {segment.data.version_minor})")) - parts.append(('jfif_density', f"({segment.data.density_x}, {segment.data.density_y})")) - parts.append(('jfif_unit', str(segment.data.density_units._value_))) - if segment.marker._name_ == 'com': - parts.append(('comment', str(segment.data))) - if segment.marker._name_ == 'app1': - if hasattr(segment.data, 'body'): + if segment.marker._name_ == "sof0": + parts.append( + ("Size", f"{segment.data.image_width} x {segment.data.image_height} px") + ) + if segment.marker._name_ == "app0": + parts.append( + ( + "jfif_version", + f"({segment.data.version_major}, {segment.data.version_minor})", + ) + ) + parts.append( + ( + "jfif_density", + f"({segment.data.density_x}, {segment.data.density_y})", + ) + ) + parts.append(("jfif_unit", str(segment.data.density_units._value_))) + if segment.marker._name_ == "com": + parts.append(("comment", str(segment.data))) + if segment.marker._name_ == "app1": + if hasattr(segment.data, "body"): for field in segment.data.body.data.body.ifd0.fields: if field.data is not None: - parts.append((field.tag._name_, field.data.decode('UTF-8').strip('\x00'))) + parts.append( + (field.tag._name_, field.data.decode("UTF-8").strip("\x00")) + ) return parts def parse_ico(data: bytes) -> Metadata: img = ico.Ico(KaitaiStream(io.BytesIO(data))) parts = [ - ('Format', 'ICO'), - ('Number of images', str(img.num_images)), + ("Format", "ICO"), + ("Number of images", str(img.num_images)), ] for i, image in enumerate(img.images): parts.append( ( - 'Image {}'.format(i + 1), "Size: {} x {}\n" - "{: >18}Bits per pixel: {}\n" - "{: >18}PNG: {}".format(256 if not image.width else image.width, - 256 if not image.height else image.height, - '', image.bpp, - '', image.is_png) + f"Image {i + 1}", + "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format( + 256 if not image.width else image.width, + 256 if not image.height else image.height, + "", + image.bpp, + "", + image.is_png, + ), ) ) diff --git a/mitmproxy/contentviews/image/view.py b/mitmproxy/contentviews/image/view.py index 9ab515c8f6..a414a1a7f0 100644 --- a/mitmproxy/contentviews/image/view.py +++ b/mitmproxy/contentviews/image/view.py @@ -18,28 +18,30 @@ class ViewImage(base.View): name = "Image" def __call__(self, data, **metadata): - image_type = imghdr.what('', h=data) - if image_type == 'png': + image_type = imghdr.what("", h=data) + if image_type == "png": image_metadata = image_parser.parse_png(data) - elif image_type == 'gif': + elif image_type == "gif": image_metadata = image_parser.parse_gif(data) - elif image_type == 'jpeg': + elif image_type == "jpeg": image_metadata = image_parser.parse_jpeg(data) - elif image_type == 'ico': + elif image_type == "ico": image_metadata = image_parser.parse_ico(data) else: - image_metadata = [ - ("Image Format", image_type or "unknown") - ] + image_metadata = [("Image Format", image_type or "unknown")] if image_type: view_name = f"{image_type.upper()} Image" else: view_name = "Unknown Image" return view_name, base.format_dict(multidict.MultiDict(image_metadata)) - def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float: - return float(bool( - content_type - and content_type.startswith("image/") - and content_type != "image/svg+xml" - )) + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: + return float( + bool( + content_type + and content_type.startswith("image/") + and content_type != "image/svg+xml" + ) + ) diff --git a/mitmproxy/contentviews/javascript.py b/mitmproxy/contentviews/javascript.py index 875e2faa87..de04668386 100644 --- a/mitmproxy/contentviews/javascript.py +++ b/mitmproxy/contentviews/javascript.py @@ -5,12 +5,12 @@ from mitmproxy.utils import strutils from mitmproxy.contentviews import base -DELIMITERS = '{};\n' +DELIMITERS = "{};\n" SPECIAL_AREAS = ( r"(?<=[^\w\s)])\s*/(?:[^\n/]|(? float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: return float(bool(data) and content_type in self.__content_types) diff --git a/mitmproxy/contentviews/json.py b/mitmproxy/contentviews/json.py index 9425629aa0..a75ba83bb4 100644 --- a/mitmproxy/contentviews/json.py +++ b/mitmproxy/contentviews/json.py @@ -1,8 +1,8 @@ import re import json +from collections.abc import Iterator from functools import lru_cache - -import typing +from typing import Any, Optional from mitmproxy.contentviews import base @@ -10,31 +10,31 @@ @lru_cache(1) -def parse_json(s: bytes) -> typing.Any: +def parse_json(s: bytes) -> Any: try: - return json.loads(s.decode('utf-8')) + return json.loads(s.decode("utf-8")) except ValueError: return PARSE_ERROR -def format_json(data: typing.Any) -> typing.Iterator[base.TViewLine]: +def format_json(data: Any) -> Iterator[base.TViewLine]: encoder = json.JSONEncoder(indent=4, sort_keys=True, ensure_ascii=False) current_line: base.TViewLine = [] for chunk in encoder.iterencode(data): if "\n" in chunk: rest_of_last_line, chunk = chunk.split("\n", maxsplit=1) # rest_of_last_line is a delimiter such as , or [ - current_line.append(('text', rest_of_last_line)) + current_line.append(("text", rest_of_last_line)) yield current_line current_line = [] if re.match(r'\s*"', chunk): - current_line.append(('json_string', chunk)) - elif re.match(r'\s*\d', chunk): - current_line.append(('json_number', chunk)) - elif re.match(r'\s*(true|null|false)', chunk): - current_line.append(('json_boolean', chunk)) + current_line.append(("json_string", chunk)) + elif re.match(r"\s*\d", chunk): + current_line.append(("json_number", chunk)) + elif re.match(r"\s*(true|null|false)", chunk): + current_line.append(("json_boolean", chunk)) else: - current_line.append(('text', chunk)) + current_line.append(("text", chunk)) yield current_line @@ -46,7 +46,9 @@ def __call__(self, data, **metadata): if data is not PARSE_ERROR: return "JSON", format_json(data) - def render_priority(self, data: bytes, *, content_type: typing.Optional[str] = None, **metadata) -> float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: if not data: return 0 if content_type in ( @@ -54,6 +56,10 @@ def render_priority(self, data: bytes, *, content_type: typing.Optional[str] = N "application/json-rpc", ): return 1 - if content_type and content_type.startswith("application/") and content_type.endswith("+json"): + if ( + content_type + and content_type.startswith("application/") + and content_type.endswith("+json") + ): return 1 return 0 diff --git a/mitmproxy/contentviews/msgpack.py b/mitmproxy/contentviews/msgpack.py index ab8ac3ab67..01f74e49ad 100644 --- a/mitmproxy/contentviews/msgpack.py +++ b/mitmproxy/contentviews/msgpack.py @@ -1,4 +1,4 @@ -import typing +from typing import Any, Optional import msgpack @@ -8,7 +8,7 @@ PARSE_ERROR = object() -def parse_msgpack(s: bytes) -> typing.Any: +def parse_msgpack(s: bytes) -> Any: try: return msgpack.unpackb(s, raw=False) except (ValueError, msgpack.ExtraData, msgpack.FormatError, msgpack.StackError): @@ -24,10 +24,7 @@ def pretty(value, htchar=" ", lfchar="\n", indent=0): ] return "{%s}" % (",".join(items) + lfchar + htchar * indent) elif type(value) is list: - items = [ - nlch + pretty(item, htchar, lfchar, indent + 1) - for item in value - ] + items = [nlch + pretty(item, htchar, lfchar, indent + 1) for item in value] return "[%s]" % (",".join(items) + lfchar + htchar * indent) else: return repr(value) @@ -49,5 +46,7 @@ def __call__(self, data, **metadata): if data is not PARSE_ERROR: return "MsgPack", format_msgpack(data) - def render_priority(self, data: bytes, *, content_type: typing.Optional[str] = None, **metadata) -> float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: return float(bool(data) and content_type in self.__content_types) diff --git a/mitmproxy/contentviews/multipart.py b/mitmproxy/contentviews/multipart.py index 4b103f31fb..9485824ca0 100644 --- a/mitmproxy/contentviews/multipart.py +++ b/mitmproxy/contentviews/multipart.py @@ -20,5 +20,7 @@ def __call__(self, data: bytes, content_type: Optional[str] = None, **metadata): if v: return "Multipart form", self._format(v) - def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: return float(bool(data) and content_type == "multipart/form-data") diff --git a/mitmproxy/contentviews/protobuf.py b/mitmproxy/contentviews/protobuf.py index a47c7a90ad..758e25d437 100644 --- a/mitmproxy/contentviews/protobuf.py +++ b/mitmproxy/contentviews/protobuf.py @@ -8,12 +8,18 @@ def write_buf(out, field_tag, body, indent_level): if body is not None: - out.write("{: <{level}}{}: {}\n".format('', field_tag, body if isinstance(body, int) else str(body, 'utf-8'), - level=indent_level)) + out.write( + "{: <{level}}{}: {}\n".format( + "", + field_tag, + body if isinstance(body, int) else str(body, "utf-8"), + level=indent_level, + ) + ) elif field_tag is not None: - out.write(' ' * indent_level + str(field_tag) + " {\n") + out.write(" " * indent_level + str(field_tag) + " {\n") else: - out.write(' ' * indent_level + "}\n") + out.write(" " * indent_level + "}\n") def format_pbuf(raw): @@ -79,5 +85,7 @@ def __call__(self, data, **metadata): return "Protobuf", base.format_text(decoded) - def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: return float(bool(data) and content_type in self.__content_types) diff --git a/mitmproxy/contentviews/query.py b/mitmproxy/contentviews/query.py index 647805d0ff..49c9107028 100644 --- a/mitmproxy/contentviews/query.py +++ b/mitmproxy/contentviews/query.py @@ -7,15 +7,16 @@ class ViewQuery(base.View): name = "Query" - def __call__(self, data: bytes, http_message: Optional[http.Message] = None, **metadata): + def __call__( + self, data: bytes, http_message: Optional[http.Message] = None, **metadata + ): query = getattr(http_message, "query", None) if query: return "Query", base.format_pairs(query.items(multi=True)) else: return "Query", base.format_text("") - def render_priority(self, data: bytes, *, http_message: Optional[http.Message] = None, **metadata) -> float: - return 0.3 * float(bool( - getattr(http_message, "query", False) - and not data - )) + def render_priority( + self, data: bytes, *, http_message: Optional[http.Message] = None, **metadata + ) -> float: + return 0.3 * float(bool(getattr(http_message, "query", False) and not data)) diff --git a/mitmproxy/contentviews/raw.py b/mitmproxy/contentviews/raw.py index 248021403f..a0b0884ecb 100644 --- a/mitmproxy/contentviews/raw.py +++ b/mitmproxy/contentviews/raw.py @@ -1,5 +1,3 @@ -from typing import List # noqa - from mitmproxy.utils import strutils from . import base diff --git a/mitmproxy/contentviews/urlencoded.py b/mitmproxy/contentviews/urlencoded.py index b1d3679241..2988d85271 100644 --- a/mitmproxy/contentviews/urlencoded.py +++ b/mitmproxy/contentviews/urlencoded.py @@ -15,5 +15,7 @@ def __call__(self, data, **metadata): d = url.decode(data) return "URLEncoded form", base.format_pairs(d) - def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: return float(bool(data) and content_type == "application/x-www-form-urlencoded") diff --git a/mitmproxy/contentviews/wbxml.py b/mitmproxy/contentviews/wbxml.py index 836fec787f..4cd7fda89b 100644 --- a/mitmproxy/contentviews/wbxml.py +++ b/mitmproxy/contentviews/wbxml.py @@ -6,10 +6,7 @@ class ViewWBXML(base.View): name = "WBXML" - __content_types = ( - "application/vnd.wap.wbxml", - "application/vnd.ms-sync.wbxml" - ) + __content_types = ("application/vnd.wap.wbxml", "application/vnd.ms-sync.wbxml") def __call__(self, data, **metadata): try: @@ -20,5 +17,7 @@ def __call__(self, data, **metadata): except: return None - def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: return float(bool(data) and content_type in self.__content_types) diff --git a/mitmproxy/contentviews/xml_html.py b/mitmproxy/contentviews/xml_html.py index 90eb566a3a..b8e8f05f6f 100644 --- a/mitmproxy/contentviews/xml_html.py +++ b/mitmproxy/contentviews/xml_html.py @@ -21,8 +21,21 @@ REGEX_TAG = re.compile(r"[a-zA-Z0-9._:\-]+(?!=)") # https://www.w3.org/TR/html5/syntax.html#void-elements HTML_VOID_ELEMENTS = { - "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", - "source", "track", "wbr" + "area", + "base", + "br", + "col", + "embed", + "hr", + "img", + "input", + "keygen", + "link", + "meta", + "param", + "source", + "track", + "wbr", } NO_INDENT_TAGS = {"xml", "doctype", "html"} INDENT = 2 @@ -33,10 +46,7 @@ def __init__(self, data): self.data = data def __repr__(self): - return "{}({})".format( - type(self).__name__, - self.data - ) + return "{}({})".format(type(self).__name__, self.data) class Text(Token): @@ -67,8 +77,12 @@ def is_closing(self): @property def is_self_closing(self): - return self.is_comment or self.is_cdata or self.data.endswith( - "/>") or self.tag in HTML_VOID_ELEMENTS + return ( + self.is_comment + or self.is_cdata + or self.data.endswith("/>") + or self.tag in HTML_VOID_ELEMENTS + ) @property def is_opening(self): @@ -95,7 +109,7 @@ def readuntil(char, start, include=1): end = data.find(char, start) if end == -1: end = len(data) - ret = data[i:end + include] + ret = data[i : end + include] i = end + include return ret @@ -131,15 +145,31 @@ def is_inline_text(a: Optional[Token], b: Optional[Token], c: Optional[Token]) - return False -def is_inline(prev2: Optional[Token], prev1: Optional[Token], t: Optional[Token], next1: Optional[Token], next2: Optional[Token]) -> bool: +def is_inline( + prev2: Optional[Token], + prev1: Optional[Token], + t: Optional[Token], + next1: Optional[Token], + next2: Optional[Token], +) -> bool: if isinstance(t, Text): return is_inline_text(prev1, t, next1) elif isinstance(t, Tag): if is_inline_text(prev2, prev1, t) or is_inline_text(t, next1, next2): return True - if isinstance(next1, Tag) and t.is_opening and next1.is_closing and t.tag == next1.tag: + if ( + isinstance(next1, Tag) + and t.is_opening + and next1.is_closing + and t.tag == next1.tag + ): return True #
(start tag) - if isinstance(prev1, Tag) and prev1.is_opening and t.is_closing and prev1.tag == t.tag: + if ( + isinstance(prev1, Tag) + and prev1.is_opening + and t.is_closing + and prev1.tag == t.tag + ): return True #
(end tag) return False @@ -234,7 +264,9 @@ def __call__(self, data, **metadata): t = "XML" return t, pretty - def render_priority(self, data: bytes, *, content_type: Optional[str] = None, **metadata) -> float: + def render_priority( + self, data: bytes, *, content_type: Optional[str] = None, **metadata + ) -> float: if not data: return 0 if content_type in self.__content_types: diff --git a/mitmproxy/contrib/click/__init__.py b/mitmproxy/contrib/click/__init__.py new file mode 100644 index 0000000000..f32d0d964c --- /dev/null +++ b/mitmproxy/contrib/click/__init__.py @@ -0,0 +1,159 @@ +""" +SPDX-License-Identifier: BSD-3-Clause + +A vendored copy of click.style() @ 4f7b255 +""" +import typing as t + +_ansi_colors = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "reset": 39, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, +} +_ansi_reset_all = "\033[0m" + + +def _interpret_color( + color: t.Union[int, t.Tuple[int, int, int], str], offset: int = 0 +) -> str: + if isinstance(color, int): + return f"{38 + offset};5;{color:d}" + + if isinstance(color, (tuple, list)): + r, g, b = color + return f"{38 + offset};2;{r:d};{g:d};{b:d}" + + return str(_ansi_colors[color] + offset) + + +def style( + text: t.Any, + fg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, + bg: t.Optional[t.Union[int, t.Tuple[int, int, int], str]] = None, + bold: t.Optional[bool] = None, + dim: t.Optional[bool] = None, + underline: t.Optional[bool] = None, + overline: t.Optional[bool] = None, + italic: t.Optional[bool] = None, + blink: t.Optional[bool] = None, + reverse: t.Optional[bool] = None, + strikethrough: t.Optional[bool] = None, + reset: bool = True, +) -> str: + """Styles a text with ANSI styles and returns the new string. By + default the styling is self contained which means that at the end + of the string a reset code is issued. This can be prevented by + passing ``reset=False``. + Examples:: + click.echo(click.style('Hello World!', fg='green')) + click.echo(click.style('ATTENTION!', blink=True)) + click.echo(click.style('Some things', reverse=True, fg='cyan')) + click.echo(click.style('More colors', fg=(255, 12, 128), bg=117)) + Supported color names: + * ``black`` (might be a gray) + * ``red`` + * ``green`` + * ``yellow`` (might be an orange) + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` + * ``reset`` (reset the color code only) + If the terminal supports it, color may also be specified as: + - An integer in the interval [0, 255]. The terminal must support + 8-bit/256-color mode. + - An RGB tuple of three integers in [0, 255]. The terminal must + support 24-bit/true-color mode. + See https://en.wikipedia.org/wiki/ANSI_color and + https://gist.github.com/XVilka/8346728 for more information. + :param text: the string to style with ansi codes. + :param fg: if provided this will become the foreground color. + :param bg: if provided this will become the background color. + :param bold: if provided this will enable or disable bold mode. + :param dim: if provided this will enable or disable dim mode. This is + badly supported. + :param underline: if provided this will enable or disable underline. + :param overline: if provided this will enable or disable overline. + :param italic: if provided this will enable or disable italic. + :param blink: if provided this will enable or disable blinking. + :param reverse: if provided this will enable or disable inverse + rendering (foreground becomes background and the + other way round). + :param strikethrough: if provided this will enable or disable + striking through text. + :param reset: by default a reset-all code is added at the end of the + string which means that styles do not carry over. This + can be disabled to compose styles. + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. + .. versionchanged:: 8.0 + Added support for 256 and RGB color codes. + .. versionchanged:: 8.0 + Added the ``strikethrough``, ``italic``, and ``overline`` + parameters. + .. versionchanged:: 7.0 + Added support for bright colors. + .. versionadded:: 2.0 + """ + if not isinstance(text, str): + text = str(text) + + bits = [] + + if fg: + try: + bits.append(f"\033[{_interpret_color(fg)}m") + except KeyError: + raise TypeError(f"Unknown color {fg!r}") from None + + if bg: + try: + bits.append(f"\033[{_interpret_color(bg, 10)}m") + except KeyError: + raise TypeError(f"Unknown color {bg!r}") from None + + if bold is not None: + bits.append(f"\033[{1 if bold else 22}m") + if dim is not None: + bits.append(f"\033[{2 if dim else 22}m") + if underline is not None: + bits.append(f"\033[{4 if underline else 24}m") + if overline is not None: + bits.append(f"\033[{53 if overline else 55}m") + if italic is not None: + bits.append(f"\033[{3 if italic else 23}m") + if blink is not None: + bits.append(f"\033[{5 if blink else 25}m") + if reverse is not None: + bits.append(f"\033[{7 if reverse else 27}m") + if strikethrough is not None: + bits.append(f"\033[{9 if strikethrough else 29}m") + bits.append(text) + if reset: + bits.append(_ansi_reset_all) + return "".join(bits) + + +__all__ = ["style"] diff --git a/mitmproxy/contrib/kaitaistruct/exif.py b/mitmproxy/contrib/kaitaistruct/exif.py index d99cceef1b..d72034f430 100644 --- a/mitmproxy/contrib/kaitaistruct/exif.py +++ b/mitmproxy/contrib/kaitaistruct/exif.py @@ -4,13 +4,11 @@ import struct import zlib from enum import Enum -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 from .exif_le import ExifLe from .exif_be import ExifBe diff --git a/mitmproxy/contrib/kaitaistruct/exif_be.py b/mitmproxy/contrib/kaitaistruct/exif_be.py index 88ce4e54ba..145a28056a 100644 --- a/mitmproxy/contrib/kaitaistruct/exif_be.py +++ b/mitmproxy/contrib/kaitaistruct/exif_be.py @@ -1,12 +1,10 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO from enum import Enum -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 class ExifBe(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): diff --git a/mitmproxy/contrib/kaitaistruct/exif_le.py b/mitmproxy/contrib/kaitaistruct/exif_le.py index e25a2fc915..3c7e43ae29 100644 --- a/mitmproxy/contrib/kaitaistruct/exif_le.py +++ b/mitmproxy/contrib/kaitaistruct/exif_le.py @@ -1,12 +1,10 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO from enum import Enum -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 class ExifLe(KaitaiStruct): def __init__(self, _io, _parent=None, _root=None): diff --git a/mitmproxy/contrib/kaitaistruct/gif.py b/mitmproxy/contrib/kaitaistruct/gif.py index 76d7fc167a..a85ff0a9c0 100644 --- a/mitmproxy/contrib/kaitaistruct/gif.py +++ b/mitmproxy/contrib/kaitaistruct/gif.py @@ -4,13 +4,11 @@ import struct import zlib from enum import Enum -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 class Gif(KaitaiStruct): diff --git a/mitmproxy/contrib/kaitaistruct/google_protobuf.py b/mitmproxy/contrib/kaitaistruct/google_protobuf.py index fe2336cc9a..09a51949e2 100644 --- a/mitmproxy/contrib/kaitaistruct/google_protobuf.py +++ b/mitmproxy/contrib/kaitaistruct/google_protobuf.py @@ -1,12 +1,11 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO + +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO from enum import Enum -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 from .vlq_base128_le import VlqBase128Le class GoogleProtobuf(KaitaiStruct): diff --git a/mitmproxy/contrib/kaitaistruct/ico.py b/mitmproxy/contrib/kaitaistruct/ico.py index 94b1b8d96a..c8395dc4f4 100644 --- a/mitmproxy/contrib/kaitaistruct/ico.py +++ b/mitmproxy/contrib/kaitaistruct/ico.py @@ -1,12 +1,10 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO import struct -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 class Ico(KaitaiStruct): """Microsoft Windows uses specific file format to store applications diff --git a/mitmproxy/contrib/kaitaistruct/jpeg.py b/mitmproxy/contrib/kaitaistruct/jpeg.py index 33fc012fa6..7799152912 100644 --- a/mitmproxy/contrib/kaitaistruct/jpeg.py +++ b/mitmproxy/contrib/kaitaistruct/jpeg.py @@ -4,13 +4,11 @@ import struct import zlib from enum import Enum -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 from .exif import Exif diff --git a/mitmproxy/contrib/kaitaistruct/png.py b/mitmproxy/contrib/kaitaistruct/png.py index 45074d7031..799749da39 100644 --- a/mitmproxy/contrib/kaitaistruct/png.py +++ b/mitmproxy/contrib/kaitaistruct/png.py @@ -4,13 +4,11 @@ import struct import zlib from enum import Enum -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 class Png(KaitaiStruct): diff --git a/mitmproxy/contrib/kaitaistruct/tls_client_hello.py b/mitmproxy/contrib/kaitaistruct/tls_client_hello.py index 10e5367fa8..097b6641e9 100644 --- a/mitmproxy/contrib/kaitaistruct/tls_client_hello.py +++ b/mitmproxy/contrib/kaitaistruct/tls_client_hello.py @@ -4,12 +4,10 @@ import struct import zlib from enum import Enum -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 class TlsClientHello(KaitaiStruct): diff --git a/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py b/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py index 235759b7aa..5fb63bfed9 100644 --- a/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py +++ b/mitmproxy/contrib/kaitaistruct/vlq_base128_le.py @@ -1,11 +1,9 @@ # This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild -from pkg_resources import parse_version -from kaitaistruct import __version__ as ks_version, KaitaiStruct, KaitaiStream, BytesIO +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO -if parse_version(ks_version) < parse_version('0.7'): - raise Exception("Incompatible Kaitai Struct Python API: 0.7 or later is required, but you have %s" % (ks_version)) +# manually removed version check, see https://github.com/mitmproxy/mitmproxy/issues/5401 class VlqBase128Le(KaitaiStruct): """A variable-length unsigned integer using base128 encoding. 1-byte groups diff --git a/mitmproxy/contrib/tornado/__init__.py b/mitmproxy/contrib/tornado/__init__.py new file mode 100644 index 0000000000..c7000a7524 --- /dev/null +++ b/mitmproxy/contrib/tornado/__init__.py @@ -0,0 +1,82 @@ +""" +SPDX-License-Identifier: Apache-2.0 + +Vendored partial copy of https://github.com/tornadoweb/tornado/blob/master/tornado/platform/asyncio.py @ e18ea03 +to fix https://github.com/tornadoweb/tornado/issues/3092. Can be removed once tornado >6.1 is out. +""" +import errno + +import select +import tornado +import tornado.platform.asyncio + + +def patch_tornado(): + if tornado.version != "6.1": + return + + def _run_select(self) -> None: + while True: + with self._select_cond: + while self._select_args is None and not self._closing_selector: + self._select_cond.wait() + if self._closing_selector: + return + assert self._select_args is not None + to_read, to_write = self._select_args + self._select_args = None + + # We use the simpler interface of the select module instead of + # the more stateful interface in the selectors module because + # this class is only intended for use on windows, where + # select.select is the only option. The selector interface + # does not have well-documented thread-safety semantics that + # we can rely on so ensuring proper synchronization would be + # tricky. + try: + # On windows, selecting on a socket for write will not + # return the socket when there is an error (but selecting + # for reads works). Also select for errors when selecting + # for writes, and merge the results. + # + # This pattern is also used in + # https://github.com/python/cpython/blob/v3.8.0/Lib/selectors.py#L312-L317 + rs, ws, xs = select.select(to_read, to_write, to_write) + ws = ws + xs + except OSError as e: + # After remove_reader or remove_writer is called, the file + # descriptor may subsequently be closed on the event loop + # thread. It's possible that this select thread hasn't + # gotten into the select system call by the time that + # happens in which case (at least on macOS), select may + # raise a "bad file descriptor" error. If we get that + # error, check and see if we're also being woken up by + # polling the waker alone. If we are, just return to the + # event loop and we'll get the updated set of file + # descriptors on the next iteration. Otherwise, raise the + # original error. + if e.errno == getattr(errno, "WSAENOTSOCK", errno.EBADF): + rs, _, _ = select.select([self._waker_r.fileno()], [], [], 0) + if rs: + ws = [] + else: + raise + else: + raise + + try: + self._real_loop.call_soon_threadsafe(self._handle_select, rs, ws) + except RuntimeError: + # "Event loop is closed". Swallow the exception for + # consistency with PollIOLoop (and logical consistency + # with the fact that we can't guarantee that an + # add_callback that completes without error will + # eventually execute). + pass + except AttributeError: + # ProactorEventLoop may raise this instead of RuntimeError + # if call_soon_threadsafe races with a call to close(). + # Swallow it too for consistency. + pass + + tornado.platform.asyncio.AddThreadSelectorEventLoop._run_select = _run_select diff --git a/mitmproxy/contrib/urwid/__init__.py b/mitmproxy/contrib/urwid/__init__.py index e69de29bb2..c83ecbdda8 100644 --- a/mitmproxy/contrib/urwid/__init__.py +++ b/mitmproxy/contrib/urwid/__init__.py @@ -0,0 +1 @@ +from . import escape_patches diff --git a/mitmproxy/contrib/urwid/escape_patches.py b/mitmproxy/contrib/urwid/escape_patches.py new file mode 100644 index 0000000000..ac17c3a422 --- /dev/null +++ b/mitmproxy/contrib/urwid/escape_patches.py @@ -0,0 +1,254 @@ +# monkeypatch https://github.com/urwid/urwid/commit/e2423b5069f51d318ea1ac0f355a0efe5448f7eb into the urwid sources. +import urwid.escape + +if urwid.__version__ in ("2.1.1", "2.1.2"): + # fmt: off + urwid.escape.input_sequences = [ + ('[A','up'),('[B','down'),('[C','right'),('[D','left'), + ('[E','5'),('[F','end'),('[G','5'),('[H','home'), + + ('[1~','home'),('[2~','insert'),('[3~','delete'),('[4~','end'), + ('[5~','page up'),('[6~','page down'), + ('[7~','home'),('[8~','end'), + + ('[[A','f1'),('[[B','f2'),('[[C','f3'),('[[D','f4'),('[[E','f5'), + + ('[11~','f1'),('[12~','f2'),('[13~','f3'),('[14~','f4'), + ('[15~','f5'),('[17~','f6'),('[18~','f7'),('[19~','f8'), + ('[20~','f9'),('[21~','f10'),('[23~','f11'),('[24~','f12'), + ('[25~','f13'),('[26~','f14'),('[28~','f15'),('[29~','f16'), + ('[31~','f17'),('[32~','f18'),('[33~','f19'),('[34~','f20'), + + ('OA','up'),('OB','down'),('OC','right'),('OD','left'), + ('OH','home'),('OF','end'), + ('OP','f1'),('OQ','f2'),('OR','f3'),('OS','f4'), + ('Oo','/'),('Oj','*'),('Om','-'),('Ok','+'), + + ('[Z','shift tab'), + ('On', '.'), + + ('[200~', 'begin paste'), ('[201~', 'end paste'), + ] + [ + (prefix + letter, modifier + key) + for prefix, modifier in zip('O[', ('meta ', 'shift ')) + for letter, key in zip('abcd', ('up', 'down', 'right', 'left')) + ] + [ + ("[" + digit + symbol, modifier + key) + for modifier, symbol in zip(('shift ', 'meta '), '$^') + for digit, key in zip('235678', + ('insert', 'delete', 'page up', 'page down', 'home', 'end')) + ] + [ + ('O' + chr(ord('p')+n), str(n)) for n in range(10) + ] + [ + # modified cursor keys + home, end, 5 -- [#X and [1;#X forms + (prefix+digit+letter, urwid.escape.escape_modifier(digit) + key) + for prefix in ("[", "[1;") + for digit in "12345678" + for letter,key in zip("ABCDEFGH", + ('up','down','right','left','5','end','5','home')) + ] + [ + # modified F1-F4 keys -- O#X form + ("O"+digit+letter, urwid.escape.escape_modifier(digit) + key) + for digit in "12345678" + for letter,key in zip("PQRS",('f1','f2','f3','f4')) + ] + [ + # modified F1-F13 keys -- [XX;#~ form + ("["+str(num)+";"+digit+"~", urwid.escape.escape_modifier(digit) + key) + for digit in "12345678" + for num,key in zip( + (3,5,6,11,12,13,14,15,17,18,19,20,21,23,24,25,26,28,29,31,32,33,34), + ('delete', 'page up', 'page down', + 'f1','f2','f3','f4','f5','f6','f7','f8','f9','f10','f11', + 'f12','f13','f14','f15','f16','f17','f18','f19','f20')) + ] + [ + # mouse reporting (special handling done in KeyqueueTrie) + ('[M', 'mouse'), + + # mouse reporting for SGR 1006 + ('[<', 'sgrmouse'), + + # report status response + ('[0n', 'status ok') + ] + + + class KeyqueueTrie(object): + def __init__( self, sequences ): + self.data = {} + for s, result in sequences: + assert type(result) != dict + self.add(self.data, s, result) + + def add(self, root, s, result): + assert type(root) == dict, "trie conflict detected" + assert len(s) > 0, "trie conflict detected" + + if ord(s[0]) in root: + return self.add(root[ord(s[0])], s[1:], result) + if len(s)>1: + d = {} + root[ord(s[0])] = d + return self.add(d, s[1:], result) + root[ord(s)] = result + + def get(self, keys, more_available): + result = self.get_recurse(self.data, keys, more_available) + if not result: + result = self.read_cursor_position(keys, more_available) + return result + + def get_recurse(self, root, keys, more_available): + if type(root) != dict: + if root == "mouse": + return self.read_mouse_info(keys, + more_available) + elif root == "sgrmouse": + return self.read_sgrmouse_info (keys, more_available) + return (root, keys) + if not keys: + # get more keys + if more_available: + raise urwid.escape.MoreInputRequired() + return None + if keys[0] not in root: + return None + return self.get_recurse(root[keys[0]], keys[1:], more_available) + + def read_mouse_info(self, keys, more_available): + if len(keys) < 3: + if more_available: + raise urwid.escape.MoreInputRequired() + return None + + b = keys[0] - 32 + x, y = (keys[1] - 33)%256, (keys[2] - 33)%256 # supports 0-255 + + prefix = "" + if b & 4: prefix = prefix + "shift " + if b & 8: prefix = prefix + "meta " + if b & 16: prefix = prefix + "ctrl " + if (b & urwid.escape.MOUSE_MULTIPLE_CLICK_MASK)>>9 == 1: prefix = prefix + "double " + if (b & urwid.escape.MOUSE_MULTIPLE_CLICK_MASK)>>9 == 2: prefix = prefix + "triple " + + # 0->1, 1->2, 2->3, 64->4, 65->5 + button = ((b&64)//64*3) + (b & 3) + 1 + + if b & 3 == 3: + action = "release" + button = 0 + elif b & urwid.escape.MOUSE_RELEASE_FLAG: + action = "release" + elif b & urwid.escape.MOUSE_DRAG_FLAG: + action = "drag" + elif b & urwid.escape.MOUSE_MULTIPLE_CLICK_MASK: + action = "click" + else: + action = "press" + + return ( (prefix + "mouse " + action, button, x, y), keys[3:] ) + + def read_sgrmouse_info(self, keys, more_available): + # Helpful links: + # https://stackoverflow.com/questions/5966903/how-to-get-mousemove-and-mouseclick-in-bash + # http://invisible-island.net/xterm/ctlseqs/ctlseqs.pdf + + if not keys: + if more_available: + raise urwid.escape.MoreInputRequired() + return None + + value = '' + pos_m = 0 + found_m = False + for k in keys: + value = value + chr(k); + if ((k is ord('M')) or (k is ord('m'))): + found_m = True + break; + pos_m += 1 + if not found_m: + if more_available: + raise urwid.escape.MoreInputRequired() + return None + + (b, x, y) = value[:-1].split(';') + + # shift, meta, ctrl etc. is not communicated on my machine, so I + # can't and won't be able to add support for it. + # Double and triple clicks are not supported as well. They can be + # implemented by using a timer. This timer can check if the last + # registered click is below a certain threshold. This threshold + # is normally set in the operating system itself, so setting one + # here will cause an inconsistent behaviour. I do not plan to use + # that feature, so I won't implement it. + + button = ((int(b) & 64) // 64 * 3) + (int(b) & 3) + 1 + x = int(x) - 1 + y = int(y) - 1 + + if (value[-1] == 'M'): + if int(b) & urwid.escape.MOUSE_DRAG_FLAG: + action = "drag" + else: + action = "press" + else: + action = "release" + + return ( ("mouse " + action, button, x, y), keys[pos_m + 1:] ) + + + def read_cursor_position(self, keys, more_available): + """ + Interpret cursor position information being sent by the + user's terminal. Returned as ('cursor position', x, y) + where (x, y) == (0, 0) is the top left of the screen. + """ + if not keys: + if more_available: + raise urwid.escape.MoreInputRequired() + return None + if keys[0] != ord('['): + return None + # read y value + y = 0 + i = 1 + for k in keys[i:]: + i += 1 + if k == ord(';'): + if not y: + return None + break + if k < ord('0') or k > ord('9'): + return None + if not y and k == ord('0'): + return None + y = y * 10 + k - ord('0') + if not keys[i:]: + if more_available: + raise urwid.escape.MoreInputRequired() + return None + # read x value + x = 0 + for k in keys[i:]: + i += 1 + if k == ord('R'): + if not x: + return None + return (("cursor position", x-1, y-1), keys[i:]) + if k < ord('0') or k > ord('9'): + return None + if not x and k == ord('0'): + return None + x = x * 10 + k - ord('0') + if not keys[i:]: + if more_available: + raise urwid.escape.MoreInputRequired() + return None + + urwid.escape.KeyqueueTrie = KeyqueueTrie + urwid.escape.input_trie = KeyqueueTrie(urwid.escape.input_sequences) + + + ESC = urwid.escape.ESC + urwid.escape.MOUSE_TRACKING_ON = ESC+"[?1000h"+ESC+"[?1002h"+ESC+"[?1006h" + urwid.escape.MOUSE_TRACKING_OFF = ESC+"[?1006l"+ESC+"[?1002l"+ESC+"[?1000l" diff --git a/mitmproxy/contrib/urwid/raw_display.py b/mitmproxy/contrib/urwid/raw_display.py index ee6d30bdf3..8a0aaa451d 100644 --- a/mitmproxy/contrib/urwid/raw_display.py +++ b/mitmproxy/contrib/urwid/raw_display.py @@ -260,7 +260,9 @@ def _start(self, alternate_buffer=True): ) ok = win32.SetConsoleMode(hOut, dwOutMode) - assert ok + if not ok: + raise RuntimeError("Error enabling virtual terminal processing, " + "mitmproxy's console interface requires Windows 10 Build 10586 or above.") ok = win32.SetConsoleMode(hIn, dwInMode) assert ok else: @@ -1166,7 +1168,7 @@ def run(self) -> None: if inp.EventType == win32.EventType.KEY_EVENT: if not inp.Event.KeyEvent.bKeyDown: continue - self._input.send(inp.Event.KeyEvent.uChar.AsciiChar) + self._input.send(inp.Event.KeyEvent.uChar.UnicodeChar.encode("utf8")) elif inp.EventType == win32.EventType.WINDOW_BUFFER_SIZE_EVENT: self._resize() else: diff --git a/mitmproxy/contrib/urwid/win32.py b/mitmproxy/contrib/urwid/win32.py index 8724f5fda8..581fcdfd2d 100644 --- a/mitmproxy/contrib/urwid/win32.py +++ b/mitmproxy/contrib/urwid/win32.py @@ -4,6 +4,7 @@ # https://docs.microsoft.com/de-de/windows/console/getstdhandle STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 +STD_ERROR_HANDLE = -12 # https://docs.microsoft.com/de-de/windows/console/setconsolemode ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 diff --git a/mitmproxy/controller.py b/mitmproxy/controller.py deleted file mode 100644 index 56365362fd..0000000000 --- a/mitmproxy/controller.py +++ /dev/null @@ -1,91 +0,0 @@ -import asyncio -import warnings -from typing import Any - -from mitmproxy import exceptions, flow - - -class Reply: - """ - Messages sent through a channel are decorated with a "reply" attribute. This - object is used to respond to the message through the return channel. - """ - - def __init__(self, obj): - self.obj: Any = obj - self.done: asyncio.Event = asyncio.Event() - self._loop: asyncio.AbstractEventLoop = asyncio.get_event_loop() - self._state: str = "start" # "start" -> "taken" -> "committed" - - @property - def state(self): - """ - The state the reply is currently in. A normal reply object goes - sequentially through the following lifecycle: - - 1. start: Initial State. - 2. taken: The reply object has been taken to be committed. - 3. committed: The reply has been sent back to the requesting party. - - This attribute is read-only and can only be modified by calling one of - state transition functions. - """ - return self._state - - def take(self): - """ - Scripts or other parties make "take" a reply out of a normal flow. - For example, intercepted flows are taken out so that the connection thread does not proceed. - """ - if self.state != "start": - raise exceptions.ControlException(f"Reply is {self.state}, but expected it to be start.") - self._state = "taken" - - def commit(self): - """ - Ultimately, messages are committed. This is done either automatically by - the handler if the message is not taken or manually by the entity which - called .take(). - """ - if self.state != "taken": - raise exceptions.ControlException(f"Reply is {self.state}, but expected it to be taken.") - self._state = "committed" - try: - self._loop.call_soon_threadsafe(lambda: self.done.set()) - except RuntimeError: # pragma: no cover - pass # event loop may already be closed. - - def kill(self, force=False): # pragma: no cover - warnings.warn("reply.kill() is deprecated, use flow.kill() or set the error attribute instead.", - DeprecationWarning, stacklevel=2) - self.obj.error = flow.Error(flow.Error.KILLED_MESSAGE) - - def __del__(self): - if self.state != "committed": - # This will be ignored by the interpreter, but emit a warning - raise exceptions.ControlException(f"Uncommitted reply: {self.obj}") - - -class DummyReply(Reply): - """ - A reply object that is not connected to anything. In contrast to regular - Reply objects, DummyReply objects are reset to "start" at the end of an - handler so that they can be used multiple times. Useful when we need an - object to seem like it has a channel, and during testing. - """ - - def __init__(self): - super().__init__(None) - self._should_reset = False - - def mark_reset(self): - if self.state != "committed": - raise exceptions.ControlException(f"Uncommitted reply: {self.obj}") - self._should_reset = True - - def reset(self): - if self._should_reset: - self._state = "start" - - def __del__(self): - pass diff --git a/mitmproxy/coretypes/basethread.py b/mitmproxy/coretypes/basethread.py index a3c81d1968..9a3c64bdc4 100644 --- a/mitmproxy/coretypes/basethread.py +++ b/mitmproxy/coretypes/basethread.py @@ -8,7 +8,4 @@ def __init__(self, name, *args, **kwargs): self._thread_started = time.time() def _threadinfo(self): - return "%s - age: %is" % ( - self.name, - int(time.time() - self._thread_started) - ) + return "%s - age: %is" % (self.name, int(time.time() - self._thread_started)) diff --git a/mitmproxy/coretypes/bidi.py b/mitmproxy/coretypes/bidi.py index 49340429f0..943062898c 100644 --- a/mitmproxy/coretypes/bidi.py +++ b/mitmproxy/coretypes/bidi.py @@ -1,13 +1,13 @@ class BiDi: """ - A wee utility class for keeping bi-directional mappings, like field - constants in protocols. Names are attributes on the object, dict-like - access maps values to names: + A wee utility class for keeping bi-directional mappings, like field + constants in protocols. Names are attributes on the object, dict-like + access maps values to names: - CONST = BiDi(a=1, b=2) - assert CONST.a == 1 - assert CONST.get_name(1) == "a" + CONST = BiDi(a=1, b=2) + assert CONST.a == 1 + assert CONST.get_name(1) == "a" """ def __init__(self, **kwargs): diff --git a/mitmproxy/coretypes/multidict.py b/mitmproxy/coretypes/multidict.py index c1ac9f9a4e..15f24568a9 100644 --- a/mitmproxy/coretypes/multidict.py +++ b/mitmproxy/coretypes/multidict.py @@ -1,16 +1,12 @@ from abc import ABCMeta from abc import abstractmethod -from typing import Iterator -from typing import List -from typing import MutableMapping -from typing import Sequence -from typing import Tuple +from collections.abc import Iterator, MutableMapping, Sequence from typing import TypeVar from mitmproxy.coretypes import serializable -KT = TypeVar('KT') -VT = TypeVar('VT') +KT = TypeVar("KT") +VT = TypeVar("VT") class _MultiDict(MutableMapping[KT, VT], metaclass=ABCMeta): @@ -18,17 +14,13 @@ class _MultiDict(MutableMapping[KT, VT], metaclass=ABCMeta): A MultiDict is a dictionary-like data structure that supports multiple values per key. """ - fields: Tuple[Tuple[KT, VT], ...] + fields: tuple[tuple[KT, VT], ...] """The underlying raw datastructure.""" def __repr__(self): - fields = ( - repr(field) - for field in self.fields - ) + fields = (repr(field) for field in self.fields) return "{cls}[{fields}]".format( - cls=type(self).__name__, - fields=", ".join(fields) + cls=type(self).__name__, fields=", ".join(fields) ) @staticmethod @@ -63,8 +55,7 @@ def __delitem__(self, key: KT) -> None: raise KeyError(key) key = self._kconv(key) self.fields = tuple( - field for field in self.fields - if key != self._kconv(field[0]) + field for field in self.fields if key != self._kconv(field[0]) ) def __iter__(self) -> Iterator[KT]: @@ -83,37 +74,29 @@ def __eq__(self, other) -> bool: return self.fields == other.fields return False - def get_all(self, key: KT) -> List[VT]: + def get_all(self, key: KT) -> list[VT]: """ Return the list of all values for a given key. If that key is not in the MultiDict, the return value will be an empty list. """ key = self._kconv(key) - return [ - value - for k, value in self.fields - if self._kconv(k) == key - ] + return [value for k, value in self.fields if self._kconv(k) == key] - def set_all(self, key: KT, values: List[VT]) -> None: + def set_all(self, key: KT, values: list[VT]) -> None: """ Remove the old values for a key and add new ones. """ key_kconv = self._kconv(key) - new_fields: List[Tuple[KT, VT]] = [] + new_fields: list[tuple[KT, VT]] = [] for field in self.fields: if self._kconv(field[0]) == key_kconv: if values: - new_fields.append( - (field[0], values.pop(0)) - ) + new_fields.append((field[0], values.pop(0))) else: new_fields.append(field) while values: - new_fields.append( - (key, values.pop(0)) - ) + new_fields.append((key, values.pop(0))) self.fields = tuple(new_fields) def add(self, key: KT, value: VT) -> None: @@ -136,10 +119,7 @@ def keys(self, multi: bool = False): If `multi` is True, one key per value will be returned. If `multi` is False, duplicate keys will only be returned once. """ - return ( - k - for k, _ in self.items(multi) - ) + return (k for k, _ in self.items(multi)) def values(self, multi: bool = False): """ @@ -148,10 +128,7 @@ def values(self, multi: bool = False): If `multi` is True, all values will be returned. If `multi` is False, only the first value per key will be returned. """ - return ( - v - for _, v in self.items(multi) - ) + return (v for _, v in self.items(multi)) def items(self, multi: bool = False): """ @@ -171,9 +148,7 @@ class MultiDict(_MultiDict[KT, VT], serializable.Serializable): def __init__(self, fields=()): super().__init__() - self.fields = tuple( - tuple(i) for i in fields - ) + self.fields = tuple(tuple(i) for i in fields) @staticmethod def _reduce_values(values): diff --git a/mitmproxy/coretypes/serializable.py b/mitmproxy/coretypes/serializable.py index f582293fe1..df09598b6e 100644 --- a/mitmproxy/coretypes/serializable.py +++ b/mitmproxy/coretypes/serializable.py @@ -1,8 +1,8 @@ import abc import uuid -from typing import Type, TypeVar +from typing import TypeVar -T = TypeVar('T', bound='Serializable') +T = TypeVar("T", bound="Serializable") class Serializable(metaclass=abc.ABCMeta): @@ -12,7 +12,7 @@ class Serializable(metaclass=abc.ABCMeta): @classmethod @abc.abstractmethod - def from_state(cls: Type[T], state) -> T: + def from_state(cls: type[T], state) -> T: """ Create a new object from the given state. """ diff --git a/mitmproxy/dns.py b/mitmproxy/dns.py new file mode 100644 index 0000000000..4bfe25f7b2 --- /dev/null +++ b/mitmproxy/dns.py @@ -0,0 +1,473 @@ +from __future__ import annotations +from dataclasses import dataclass +import itertools +import random +import struct +from ipaddress import IPv4Address, IPv6Address +import time +from typing import ClassVar + +from mitmproxy import flow, stateobject +from mitmproxy.net.dns import classes, domain_names, op_codes, response_codes, types + +# DNS parameters taken from https://www.iana.org/assignments/dns-parameters/dns-parameters.xml + + +@dataclass +class Question(stateobject.StateObject): + HEADER: ClassVar[struct.Struct] = struct.Struct("!HH") + + name: str + type: int + class_: int + + _stateobject_attributes = dict(name=str, type=int, class_=int) + + @classmethod + def from_state(cls, state): + return cls(**state) + + def __str__(self) -> str: + return self.name + + def to_json(self) -> dict: + """ + Converts the question into json for mitmweb. + Sync with web/src/flow.ts. + """ + return { + "name": self.name, + "type": types.to_str(self.type), + "class": classes.to_str(self.class_), + } + + +@dataclass +class ResourceRecord(stateobject.StateObject): + DEFAULT_TTL: ClassVar[int] = 60 + HEADER: ClassVar[struct.Struct] = struct.Struct("!HHIH") + + name: str + type: int + class_: int + ttl: int + data: bytes + + _stateobject_attributes = dict(name=str, type=int, class_=int, ttl=int, data=bytes) + + @classmethod + def from_state(cls, state): + return cls(**state) + + def __str__(self) -> str: + try: + if self.type == types.A: + return str(self.ipv4_address) + if self.type == types.AAAA: + return str(self.ipv6_address) + if self.type in (types.NS, types.CNAME, types.PTR): + return self.domain_name + if self.type == types.TXT: + return self.text + except: + return f"0x{self.data.hex()} (invalid {types.to_str(self.type)} data)" + return f"0x{self.data.hex()}" + + @property + def text(self) -> str: + return self.data.decode("utf-8") + + @text.setter + def text(self, value: str) -> None: + self.data = value.encode("utf-8") + + @property + def ipv4_address(self) -> IPv4Address: + return IPv4Address(self.data) + + @ipv4_address.setter + def ipv4_address(self, ip: IPv4Address) -> None: + self.data = ip.packed + + @property + def ipv6_address(self) -> IPv6Address: + return IPv6Address(self.data) + + @ipv6_address.setter + def ipv6_address(self, ip: IPv6Address) -> None: + self.data = ip.packed + + @property + def domain_name(self) -> str: + return domain_names.unpack(self.data) + + @domain_name.setter + def domain_name(self, name: str) -> None: + self.data = domain_names.pack(name) + + def to_json(self) -> dict: + """ + Converts the resource record into json for mitmweb. + Sync with web/src/flow.ts. + """ + return { + "name": self.name, + "type": types.to_str(self.type), + "class": classes.to_str(self.class_), + "ttl": self.ttl, + "data": str(self), + } + + @classmethod + def A(cls, name: str, ip: IPv4Address, *, ttl: int = DEFAULT_TTL) -> ResourceRecord: + """Create an IPv4 resource record.""" + return cls(name, types.A, classes.IN, ttl, ip.packed) + + @classmethod + def AAAA( + cls, name: str, ip: IPv6Address, *, ttl: int = DEFAULT_TTL + ) -> ResourceRecord: + """Create an IPv6 resource record.""" + return cls(name, types.AAAA, classes.IN, ttl, ip.packed) + + @classmethod + def CNAME( + cls, alias: str, canonical: str, *, ttl: int = DEFAULT_TTL + ) -> ResourceRecord: + """Create a canonical internet name resource record.""" + return cls(alias, types.CNAME, classes.IN, ttl, domain_names.pack(canonical)) + + @classmethod + def PTR(cls, inaddr: str, ptr: str, *, ttl: int = DEFAULT_TTL) -> ResourceRecord: + """Create a canonical internet name resource record.""" + return cls(inaddr, types.PTR, classes.IN, ttl, domain_names.pack(ptr)) + + @classmethod + def TXT(cls, name: str, text: str, *, ttl: int = DEFAULT_TTL) -> ResourceRecord: + """Create a textual resource record.""" + return cls(name, types.TXT, classes.IN, ttl, text.encode("utf-8")) + + +# comments are taken from rfc1035 +@dataclass +class Message(stateobject.StateObject): + HEADER: ClassVar[struct.Struct] = struct.Struct("!HHHHHH") + + timestamp: float + """The time at which the message was sent or received.""" + id: int + """An identifier assigned by the program that generates any kind of query.""" + query: bool + """A field that specifies whether this message is a query.""" + op_code: int + """ + A field that specifies kind of query in this message. + This value is set by the originator of a request and copied into the response. + """ + authoritative_answer: bool + """ + This field is valid in responses, and specifies that the responding name server + is an authority for the domain name in question section. + """ + truncation: bool + """Specifies that this message was truncated due to length greater than that permitted on the transmission channel.""" + recursion_desired: bool + """ + This field may be set in a query and is copied into the response. + If set, it directs the name server to pursue the query recursively. + """ + recursion_available: bool + """This field is set or cleared in a response, and denotes whether recursive query support is available in the name server.""" + reserved: int + """Reserved for future use. Must be zero in all queries and responses.""" + response_code: int + """This field is set as part of responses.""" + questions: list[Question] + """ + The question section is used to carry the "question" in most queries, i.e. + the parameters that define what is being asked. + """ + answers: list[ResourceRecord] + """First resource record section.""" + authorities: list[ResourceRecord] + """Second resource record section.""" + additionals: list[ResourceRecord] + """Third resource record section.""" + + _stateobject_attributes = dict( + timestamp=float, + id=int, + query=bool, + op_code=int, + authoritative_answer=bool, + truncation=bool, + recursion_desired=bool, + recursion_available=bool, + reserved=int, + response_code=int, + questions=list[Question], + answers=list[ResourceRecord], + authorities=list[ResourceRecord], + additionals=list[ResourceRecord], + ) + + @classmethod + def from_state(cls, state): + obj = cls.__new__(cls) # `cls(**state)` won't work recursively + obj.set_state(state) + return obj + + def __str__(self) -> str: + return "\r\n".join( + map( + str, + itertools.chain( + self.questions, self.answers, self.authorities, self.additionals + ), + ) + ) + + @property + def content(self) -> bytes: + """Returns the user-friendly content of all parts as encoded bytes.""" + return str(self).encode() + + @property + def size(self) -> int: + """Returns the cumulative data size of all resource record sections.""" + return sum( + len(x.data) + for x in itertools.chain.from_iterable( + [self.answers, self.authorities, self.additionals] + ) + ) + + def fail(self, response_code: int) -> Message: + if response_code == response_codes.NOERROR: + raise ValueError("response_code must be an error code.") + return Message( + timestamp=time.time(), + id=self.id, + query=False, + op_code=self.op_code, + authoritative_answer=False, + truncation=False, + recursion_desired=self.recursion_desired, + recursion_available=False, + reserved=0, + response_code=response_code, + questions=self.questions, + answers=[], + authorities=[], + additionals=[], + ) + + def succeed(self, answers: list[ResourceRecord]) -> Message: + return Message( + timestamp=time.time(), + id=self.id, + query=False, + op_code=self.op_code, + authoritative_answer=False, + truncation=False, + recursion_desired=self.recursion_desired, + recursion_available=True, + reserved=0, + response_code=response_codes.NOERROR, + questions=self.questions, + answers=answers, + authorities=[], + additionals=[], + ) + + @classmethod + def unpack(cls, buffer: bytes) -> Message: + """Converts the entire given buffer into a DNS message.""" + length, msg = cls.unpack_from(buffer, 0) + if length != len(buffer): + raise struct.error(f"unpack requires a buffer of {length} bytes") + return msg + + @classmethod + def unpack_from(cls, buffer: bytes | bytearray, offset: int) -> tuple[int, Message]: + """Converts the buffer from a given offset into a DNS message and also returns its length.""" + ( + id, + flags, + len_questions, + len_answers, + len_authorities, + len_additionals, + ) = Message.HEADER.unpack_from(buffer, offset) + msg = Message( + timestamp=time.time(), + id=id, + query=(flags & (1 << 15)) == 0, + op_code=(flags >> 11) & 0b1111, + authoritative_answer=(flags & (1 << 10)) != 0, + truncation=(flags & (1 << 9)) != 0, + recursion_desired=(flags & (1 << 8)) != 0, + recursion_available=(flags & (1 << 7)) != 0, + reserved=(flags >> 4) & 0b111, + response_code=flags & 0b1111, + questions=[], + answers=[], + authorities=[], + additionals=[], + ) + offset += Message.HEADER.size + cached_names = domain_names.cache() + + def unpack_domain_name() -> str: + nonlocal buffer, offset, cached_names + name, length = domain_names.unpack_from_with_compression( + buffer, offset, cached_names + ) + offset += length + return name + + for i in range(0, len_questions): + try: + name = unpack_domain_name() + type, class_ = Question.HEADER.unpack_from(buffer, offset) + offset += Question.HEADER.size + msg.questions.append(Question(name=name, type=type, class_=class_)) + except struct.error as e: + raise struct.error(f"question #{i}: {str(e)}") + + def unpack_rrs( + section: list[ResourceRecord], section_name: str, count: int + ) -> None: + nonlocal buffer, offset + for i in range(0, count): + try: + name = unpack_domain_name() + type, class_, ttl, len_data = ResourceRecord.HEADER.unpack_from( + buffer, offset + ) + offset += ResourceRecord.HEADER.size + end_data = offset + len_data + if len(buffer) < end_data: + raise struct.error( + f"unpack requires a data buffer of {len_data} bytes" + ) + data = buffer[offset:end_data] + if 0b11000000 in data: + # the resource record might contains a compressed domain name, if so, uncompressed in advance + try: + ( + rr_name, + rr_name_len, + ) = domain_names.unpack_from_with_compression( + buffer, offset, cached_names + ) + if rr_name_len == len_data: + data = domain_names.pack(rr_name) + except struct.error: + pass + section.append(ResourceRecord(name, type, class_, ttl, data)) + offset += len_data + except struct.error as e: + raise struct.error(f"{section_name} #{i}: {str(e)}") + + unpack_rrs(msg.answers, "answer", len_answers) + unpack_rrs(msg.authorities, "authority", len_authorities) + unpack_rrs(msg.additionals, "additional", len_additionals) + return (offset, msg) + + @property + def packed(self) -> bytes: + """Converts the message into network bytes.""" + if self.id < 0 or self.id > 65535: + raise ValueError(f"DNS message's id {self.id} is out of bounds.") + flags = 0 + if not self.query: + flags |= 1 << 15 + if self.op_code < 0 or self.op_code > 0b1111: + raise ValueError(f"DNS message's op_code {self.op_code} is out of bounds.") + flags |= self.op_code << 11 + if self.authoritative_answer: + flags |= 1 << 10 + if self.truncation: + flags |= 1 << 9 + if self.recursion_desired: + flags |= 1 << 8 + if self.recursion_available: + flags |= 1 << 7 + if self.reserved < 0 or self.reserved > 0b111: + raise ValueError( + f"DNS message's reserved value of {self.reserved} is out of bounds." + ) + flags |= self.reserved << 4 + if self.response_code < 0 or self.response_code > 0b1111: + raise ValueError( + f"DNS message's response_code {self.response_code} is out of bounds." + ) + flags |= self.response_code + data = bytearray() + data.extend( + Message.HEADER.pack( + self.id, + flags, + len(self.questions), + len(self.answers), + len(self.authorities), + len(self.additionals), + ) + ) + # TODO implement compression + for question in self.questions: + data.extend(domain_names.pack(question.name)) + data.extend(Question.HEADER.pack(question.type, question.class_)) + for rr in (*self.answers, *self.authorities, *self.additionals): + data.extend(domain_names.pack(rr.name)) + data.extend( + ResourceRecord.HEADER.pack(rr.type, rr.class_, rr.ttl, len(rr.data)) + ) + data.extend(rr.data) + return bytes(data) + + def to_json(self) -> dict: + """ + Converts the message into json for mitmweb. + Sync with web/src/flow.ts. + """ + return { + "id": self.id, + "query": self.query, + "op_code": op_codes.to_str(self.op_code), + "authoritative_answer": self.authoritative_answer, + "truncation": self.truncation, + "recursion_desired": self.recursion_desired, + "recursion_available": self.recursion_available, + "response_code": response_codes.to_str(self.response_code), + "status_code": response_codes.http_equiv_status_code(self.response_code), + "questions": [question.to_json() for question in self.questions], + "answers": [rr.to_json() for rr in self.answers], + "authorities": [rr.to_json() for rr in self.authorities], + "additionals": [rr.to_json() for rr in self.additionals], + "size": self.size, + "timestamp": self.timestamp, + } + + def copy(self) -> Message: + # we keep the copy semantics but change the ID generation + state = self.get_state() + state["id"] = random.randint(0, 65535) + return Message.from_state(state) + + +class DNSFlow(flow.Flow): + """A DNSFlow is a collection of DNS messages representing a single DNS query.""" + + request: Message + """The DNS request.""" + response: Message | None = None + """The DNS response.""" + + _stateobject_attributes = flow.Flow._stateobject_attributes.copy() + _stateobject_attributes["request"] = Message + _stateobject_attributes["response"] = Message + + def __repr__(self) -> str: + return f"" diff --git a/mitmproxy/eventsequence.py b/mitmproxy/eventsequence.py index 8cd7ce69a4..bf15b6e291 100644 --- a/mitmproxy/eventsequence.py +++ b/mitmproxy/eventsequence.py @@ -1,6 +1,6 @@ -from typing import Any, Callable, Dict, Iterator, Type +from typing import Any, Callable, Iterator -from mitmproxy import controller +from mitmproxy import dns from mitmproxy import flow from mitmproxy import hooks from mitmproxy import http @@ -32,7 +32,6 @@ def _iterate_http(f: http.HTTPFlow) -> TEventGenerator: def _iterate_tcp(f: tcp.TCPFlow) -> TEventGenerator: messages = f.messages f.messages = [] - f.reply = controller.DummyReply() yield layers.tcp.TcpStartHook(f) while messages: f.messages.append(messages.pop(0)) @@ -43,9 +42,19 @@ def _iterate_tcp(f: tcp.TCPFlow) -> TEventGenerator: yield layers.tcp.TcpEndHook(f) -_iterate_map: Dict[Type[flow.Flow], Callable[[Any], TEventGenerator]] = { +def _iterate_dns(f: dns.DNSFlow) -> TEventGenerator: + if f.request: + yield layers.dns.DnsRequestHook(f) + if f.response: + yield layers.dns.DnsResponseHook(f) + if f.error: + yield layers.dns.DnsErrorHook(f) + + +_iterate_map: dict[type[flow.Flow], Callable[[Any], TEventGenerator]] = { http.HTTPFlow: _iterate_http, tcp.TCPFlow: _iterate_tcp, + dns.DNSFlow: _iterate_dns, } diff --git a/mitmproxy/exceptions.py b/mitmproxy/exceptions.py index 2e22f1a310..30005970af 100644 --- a/mitmproxy/exceptions.py +++ b/mitmproxy/exceptions.py @@ -47,9 +47,8 @@ class AddonManagerError(MitmproxyException): class AddonHalt(MitmproxyException): """ - Raised by addons to signal that no further handlers should handle this event. + Raised by addons to signal that no further handlers should handle this event. """ - pass class TypeError(MitmproxyException): diff --git a/mitmproxy/flow.py b/mitmproxy/flow.py index 43e21d8a57..e8284aebf3 100644 --- a/mitmproxy/flow.py +++ b/mitmproxy/flow.py @@ -1,8 +1,9 @@ +import asyncio import time -import typing # noqa import uuid +from typing import Any, ClassVar, Optional -from mitmproxy import controller, connection +from mitmproxy import connection from mitmproxy import exceptions from mitmproxy import stateobject from mitmproxy import version @@ -24,17 +25,14 @@ class Error(stateobject.StateObject): timestamp: float """Unix timestamp of when this error happened.""" - KILLED_MESSAGE: typing.ClassVar[str] = "Connection killed." + KILLED_MESSAGE: ClassVar[str] = "Connection killed." - def __init__(self, msg: str, timestamp: typing.Optional[float] = None) -> None: + def __init__(self, msg: str, timestamp: Optional[float] = None) -> None: """Create an error. If no timestamp is passed, the current time is used.""" self.msg = msg self.timestamp = timestamp or time.time() - _stateobject_attributes = dict( - msg=str, - timestamp=float - ) + _stateobject_attributes = dict(msg=str, timestamp=float) def __str__(self): return self.msg @@ -60,6 +58,7 @@ class Flow(stateobject.StateObject): - mitmproxy.http.HTTPFlow - mitmproxy.tcp.TCPFlow """ + client_conn: connection.Client """The client that connected to mitmproxy.""" @@ -73,7 +72,7 @@ class Flow(stateobject.StateObject): with a `timestamp_start` set to `None`. """ - error: typing.Optional[Error] = None + error: Optional[Error] = None """A connection or protocol error affecting this flow.""" intercepted: bool @@ -96,7 +95,7 @@ class Flow(stateobject.StateObject): The default marker for the view will be used if the Unicode emoji name can not be interpreted. """ - is_replay: typing.Optional[str] + is_replay: Optional[str] """ This attribute indicates if this flow has been replayed in either direction. @@ -104,25 +103,37 @@ class Flow(stateobject.StateObject): - a value of `response` indicates that the response to the client's request has been set by server replay. """ + live: bool + """ + If `True`, the flow belongs to a currently active connection. + If `False`, the flow may have been already completed or loaded from disk. + """ + + timestamp_created: float + """ + The Unix timestamp of when this flow was created. + + In contrast to `timestamp_start`, this value will not change when a flow is replayed. + """ + def __init__( self, - type: str, client_conn: connection.Client, server_conn: connection.Server, - live: bool = None + live: bool = False, ) -> None: - self.type = type self.id = str(uuid.uuid4()) self.client_conn = client_conn self.server_conn = server_conn self.live = live + self.timestamp_created = time.time() self.intercepted: bool = False - self._backup: typing.Optional[Flow] = None - self.reply: typing.Optional[controller.Reply] = None + self._resume_event: Optional[asyncio.Event] = None + self._backup: Optional[Flow] = None self.marked: str = "" - self.is_replay: typing.Optional[str] = None - self.metadata: typing.Dict[str, typing.Any] = dict() + self.is_replay: Optional[str] = None + self.metadata: dict[str, Any] = dict() self.comment: str = "" _stateobject_attributes = dict( @@ -130,17 +141,28 @@ def __init__( error=Error, client_conn=connection.Client, server_conn=connection.Server, - type=str, intercepted=bool, is_replay=str, marked=str, - metadata=typing.Dict[str, typing.Any], + metadata=dict[str, Any], comment=str, + timestamp_created=float, ) + __types: dict[str, type["Flow"]] = {} + + @classmethod + @property + def type(cls) -> str: + """The flow type, for example `http`, `tcp`, or `dns`.""" + return cls.__name__.removesuffix("Flow").lower() + + def __init_subclass__(cls, **kwargs): + Flow.__types[cls.type] = cls + def get_state(self): d = super().get_state() - d.update(version=version.FLOW_FORMAT_VERSION) + d.update(version=version.FLOW_FORMAT_VERSION, type=self.type) if self._backup and self._backup != d: d.update(backup=self._backup) return d @@ -148,13 +170,18 @@ def get_state(self): def set_state(self, state): state = state.copy() state.pop("version") + state.pop("type") if "backup" in state: self._backup = state.pop("backup") super().set_state(state) @classmethod def from_state(cls, state): - f = cls(None, None) + try: + flow_cls = Flow.__types[state["type"]] + except KeyError: + raise ValueError(f"Unknown flow type: {state['type']}") + f = flow_cls(None, None) # noqa f.set_state(state) return f @@ -162,8 +189,6 @@ def copy(self): """Make a copy of this flow.""" f = super().copy() f.live = False - if self.reply is not None: - f.reply = controller.DummyReply() return f def modified(self): @@ -184,7 +209,7 @@ def backup(self, force=False): def revert(self): """ - Revert to the last backed up state. + Revert to the last backed up state. """ if self._backup: self.set_state(self._backup) @@ -193,11 +218,7 @@ def revert(self): @property def killable(self): """*Read-only:* `True` if this flow can be killed, `False` otherwise.""" - return ( - self.reply and - self.reply.state in {"start", "taken"} and - not (self.error and self.error.msg == Error.KILLED_MESSAGE) - ) + return self.live and not (self.error and self.error.msg == Error.KILLED_MESSAGE) def kill(self): """ @@ -205,6 +226,10 @@ def kill(self): """ if not self.killable: raise exceptions.ControlException("Flow is not killable.") + # TODO: The way we currently signal killing is not ideal. One major problem is that we cannot kill + # flows in transit (https://github.com/mitmproxy/mitmproxy/issues/4711), even though they are advertised + # as killable. An alternative approach would be to introduce a `KillInjected` event similar to + # `MessageInjected`, which should fix this issue. self.error = Error(Error.KILLED_MESSAGE) self.intercepted = False self.live = False @@ -217,7 +242,18 @@ def intercept(self): if self.intercepted: return self.intercepted = True - self.reply.take() + if self._resume_event is not None: + self._resume_event.clear() + + async def wait_for_resume(self): + """ + Wait until this Flow is resumed. + """ + if not self.intercepted: + return + if self._resume_event is None: + self._resume_event = asyncio.Event() + await self._resume_event.wait() def resume(self): """ @@ -226,9 +262,8 @@ def resume(self): if not self.intercepted: return self.intercepted = False - # If a flow is intercepted and then duplicated, the duplicated one is not taken. - if self.reply.state == "taken": - self.reply.commit() + if self._resume_event is not None: + self._resume_event.set() @property def timestamp_start(self) -> float: diff --git a/mitmproxy/flowfilter.py b/mitmproxy/flowfilter.py index 29ba0af841..7e3dfac92a 100644 --- a/mitmproxy/flowfilter.py +++ b/mitmproxy/flowfilter.py @@ -15,7 +15,8 @@ application/javascript text/css image/* - application/x-shockwave-flash + font/* + application/font-* ~h rex Header line in either request or response ~hq rex Header in request ~hs rex Header in response @@ -35,10 +36,11 @@ import functools import re import sys -from typing import ClassVar, Sequence, Type, Protocol, Union +from collections.abc import Sequence +from typing import ClassVar, Protocol, Union import pyparsing as pp -from mitmproxy import flow, http, tcp +from mitmproxy import dns, flow, http, tcp def only(*types): @@ -55,13 +57,15 @@ def filter_types(self, flow): class _Token: - def dump(self, indent=0, fp=sys.stdout): - print("{spacing}{name}{expr}".format( - spacing="\t" * indent, - name=self.__class__.__name__, - expr=getattr(self, "expr", "") - ), file=fp) + print( + "{spacing}{name}{expr}".format( + spacing="\t" * indent, + name=self.__class__.__name__, + expr=getattr(self, "expr", ""), + ), + file=fp, + ) class _Action(_Token): @@ -116,11 +120,20 @@ def __call__(self, f): return True +class FDNS(_Action): + code = "dns" + help = "Match DNS flows" + + @only(dns.DNSFlow) + def __call__(self, f): + return True + + class FReq(_Action): code = "q" help = "Match request with no response" - @only(http.HTTPFlow) + @only(http.HTTPFlow, dns.DNSFlow) def __call__(self, f): if not f.response: return True @@ -130,7 +143,7 @@ class FResp(_Action): code = "s" help = "Match response" - @only(http.HTTPFlow) + @only(http.HTTPFlow, dns.DNSFlow) def __call__(self, f): return bool(f.response) @@ -159,22 +172,26 @@ def __init__(self, expr): def _check_content_type(rex, message): return any( - name.lower() == b"content-type" and - rex.search(value) + name.lower() == b"content-type" and rex.search(value) for name, value in message.headers.fields ) class FAsset(_Action): code = "a" - help = "Match asset in response: CSS, JavaScript, images." - ASSET_TYPES = [re.compile(x) for x in [ - b"text/javascript", - b"application/x-javascript", - b"application/javascript", - b"text/css", - b"image/.*" - ]] + help = "Match asset in response: CSS, JavaScript, images, fonts." + ASSET_TYPES = [ + re.compile(x) + for x in [ + b"text/javascript", + b"application/x-javascript", + b"application/javascript", + b"text/css", + b"image/.*", + b"font/.*", + b"application/font.*", + ] + ] @only(http.HTTPFlow) def __call__(self, f): @@ -259,7 +276,7 @@ class FBod(_Rex): help = "Body" flags = re.DOTALL - @only(http.HTTPFlow, tcp.TCPFlow) + @only(http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow) def __call__(self, f): if isinstance(f, http.HTTPFlow): if f.request and f.request.raw_content: @@ -276,6 +293,11 @@ def __call__(self, f): for msg in f.messages: if self.re.search(msg.content): return True + elif isinstance(f, dns.DNSFlow): + if f.request and self.re.search(f.request.content): + return True + if f.response and self.re.search(f.response.content): + return True return False @@ -284,7 +306,7 @@ class FBodRequest(_Rex): help = "Request body" flags = re.DOTALL - @only(http.HTTPFlow, tcp.TCPFlow) + @only(http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow) def __call__(self, f): if isinstance(f, http.HTTPFlow): if f.request and f.request.raw_content: @@ -298,6 +320,9 @@ def __call__(self, f): for msg in f.messages: if msg.from_client and self.re.search(msg.content): return True + elif isinstance(f, dns.DNSFlow): + if f.request and self.re.search(f.request.content): + return True class FBodResponse(_Rex): @@ -305,7 +330,7 @@ class FBodResponse(_Rex): help = "Response body" flags = re.DOTALL - @only(http.HTTPFlow, tcp.TCPFlow) + @only(http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow) def __call__(self, f): if isinstance(f, http.HTTPFlow): if f.response and f.response.raw_content: @@ -319,6 +344,9 @@ def __call__(self, f): for msg in f.messages: if not msg.from_client and self.re.search(msg.content): return True + elif isinstance(f, dns.DNSFlow): + if f.response and self.re.search(f.response.content): + return True class FMethod(_Rex): @@ -340,8 +368,7 @@ class FDomain(_Rex): @only(http.HTTPFlow) def __call__(self, f): return bool( - self.re.search(f.request.host) or - self.re.search(f.request.pretty_host) + self.re.search(f.request.host) or self.re.search(f.request.pretty_host) ) @@ -358,11 +385,14 @@ def make(klass, s, loc, toks): toks = toks[1:] return klass(*toks) - @only(http.HTTPFlow) + @only(http.HTTPFlow, dns.DNSFlow) def __call__(self, f): if not f or not f.request: return False - return self.re.search(f.request.pretty_url) + if isinstance(f, http.HTTPFlow): + return self.re.search(f.request.pretty_url) + elif isinstance(f, dns.DNSFlow): + return f.request.questions and self.re.search(f.request.questions[0].name) class FSrc(_Rex): @@ -373,7 +403,7 @@ class FSrc(_Rex): def __call__(self, f): if not f.client_conn or not f.client_conn.peername: return False - r = "{}:{}".format(f.client_conn.peername[0], f.client_conn.peername[1]) + r = f"{f.client_conn.peername[0]}:{f.client_conn.peername[1]}" return f.client_conn.peername and self.re.search(r) @@ -385,7 +415,7 @@ class FDst(_Rex): def __call__(self, f): if not f.server_conn or not f.server_conn.address: return False - r = "{}:{}".format(f.server_conn.address[0], f.server_conn.address[1]) + r = f"{f.server_conn.address[0]}:{f.server_conn.address[1]}" return f.server_conn.address and self.re.search(r) @@ -402,7 +432,7 @@ class FReplayClient(_Action): help = "Match replayed client request" def __call__(self, f): - return f.is_replay == 'request' + return f.is_replay == "request" class FReplayServer(_Action): @@ -410,7 +440,7 @@ class FReplayServer(_Action): help = "Match replayed server response" def __call__(self, f): - return f.is_replay == 'response' + return f.is_replay == "response" class FMeta(_Rex): @@ -444,7 +474,6 @@ def __call__(self, f): class _Int(_Action): - def __init__(self, num): self.num = int(num) @@ -460,7 +489,6 @@ def __call__(self, f): class FAnd(_Token): - def __init__(self, lst): self.lst = lst @@ -474,7 +502,6 @@ def __call__(self, f): class FOr(_Token): - def __init__(self, lst): self.lst = lst @@ -488,7 +515,6 @@ def __call__(self, f): class FNot(_Token): - def __init__(self, itm): self.itm = itm[0] @@ -500,7 +526,7 @@ def __call__(self, f): return not self.itm(f) -filter_unary: Sequence[Type[_Action]] = [ +filter_unary: Sequence[type[_Action]] = [ FAsset, FErr, FHTTP, @@ -511,10 +537,11 @@ def __call__(self, f): FReq, FResp, FTCP, + FDNS, FWebSocket, FAll, ] -filter_rex: Sequence[Type[_Rex]] = [ +filter_rex: Sequence[type[_Rex]] = [ FBod, FBodRequest, FBodResponse, @@ -533,9 +560,7 @@ def __call__(self, f): FMarker, FComment, ] -filter_int = [ - FCode -] +filter_int = [FCode] def _make(): @@ -553,8 +578,8 @@ def _make(): unicode_words.skipWhitespace = True regex = ( unicode_words - | pp.QuotedString('"', escChar='\\') - | pp.QuotedString("'", escChar='\\') + | pp.QuotedString('"', escChar="\\") + | pp.QuotedString("'", escChar="\\") ) for cls in filter_rex: f = pp.Literal(f"~{cls.code}") + pp.WordEnd() + regex.copy() @@ -574,19 +599,12 @@ def _make(): atom = pp.MatchFirst(parts) expr = pp.infixNotation( atom, - [(pp.Literal("!").suppress(), - 1, - pp.opAssoc.RIGHT, - lambda x: FNot(*x)), - (pp.Literal("&").suppress(), - 2, - pp.opAssoc.LEFT, - lambda x: FAnd(*x)), - (pp.Literal("|").suppress(), - 2, - pp.opAssoc.LEFT, - lambda x: FOr(*x)), - ]) + [ + (pp.Literal("!").suppress(), 1, pp.opAssoc.RIGHT, lambda x: FNot(*x)), + (pp.Literal("&").suppress(), 2, pp.opAssoc.LEFT, lambda x: FAnd(*x)), + (pp.Literal("|").suppress(), 2, pp.opAssoc.LEFT, lambda x: FOr(*x)), + ], + ) expr = pp.OneOrMore(expr) return expr.setParseAction(lambda x: FAnd(x) if len(x) != 1 else x) @@ -618,11 +636,11 @@ def parse(s: str) -> TFilter: def match(flt: Union[str, TFilter], flow: flow.Flow) -> bool: """ - Matches a flow against a compiled filter expression. - Returns True if matched, False if not. + Matches a flow against a compiled filter expression. + Returns True if matched, False if not. - If flt is a string, it will be compiled as a filter expression. - If the expression is invalid, ValueError is raised. + If flt is a string, it will be compiled as a filter expression. + If the expression is invalid, ValueError is raised. """ if isinstance(flt, str): flt = parse(flt) @@ -637,17 +655,11 @@ def match(flt: Union[str, TFilter], flow: flow.Flow) -> bool: help = [] for a in filter_unary: - help.append( - (f"~{a.code}", a.help) - ) + help.append((f"~{a.code}", a.help)) for b in filter_rex: - help.append( - (f"~{b.code} regex", b.help) - ) + help.append((f"~{b.code} regex", b.help)) for c in filter_int: - help.append( - (f"~{c.code} int", c.help) - ) + help.append((f"~{c.code} int", c.help)) help.sort() help.extend( [ diff --git a/mitmproxy/hooks.py b/mitmproxy/hooks.py index 77b6d207fd..d0c2934fa1 100644 --- a/mitmproxy/hooks.py +++ b/mitmproxy/hooks.py @@ -1,7 +1,8 @@ import re import warnings +from collections.abc import Sequence from dataclasses import dataclass, is_dataclass, fields -from typing import ClassVar, Any, Dict, Type, Set, List, TYPE_CHECKING, Sequence +from typing import Any, ClassVar, TYPE_CHECKING import mitmproxy.flow @@ -13,7 +14,7 @@ class Hook: name: ClassVar[str] - def args(self) -> List[Any]: + def args(self) -> list[Any]: args = [] for field in fields(self): args.append(getattr(self, field.name)) @@ -30,10 +31,13 @@ def __init_subclass__(cls, **kwargs): # initialize .name attribute. HttpRequestHook -> http_request if cls.__dict__.get("name", None) is None: name = cls.__name__.replace("Hook", "") - cls.name = re.sub('(?!^)([A-Z]+)', r'_\1', name).lower() + cls.name = re.sub("(?!^)([A-Z]+)", r"_\1", name).lower() if cls.name in all_hooks: other = all_hooks[cls.name] - warnings.warn(f"Two conflicting event classes for {cls.name}: {cls} and {other}", RuntimeWarning) + warnings.warn( + f"Two conflicting event classes for {cls.name}: {cls} and {other}", + RuntimeWarning, + ) if cls.name == "": return # don't register Hook class. all_hooks[cls.name] = cls @@ -43,7 +47,7 @@ def __init_subclass__(cls, **kwargs): cls.__eq__ = object.__eq__ -all_hooks: Dict[str, Type[Hook]] = {} +all_hooks: dict[str, type[Hook]] = {} @dataclass @@ -53,7 +57,8 @@ class ConfigureHook(Hook): set-like object containing the keys of all changed options. This event is called during startup with all options in the updated set. """ - updated: Set[str] + + updated: set[str] @dataclass @@ -72,8 +77,7 @@ class DoneHook(Hook): class RunningHook(Hook): """ Called when the proxy is completely up and running. At this point, - you can expect the proxy to be bound to a port, and all addons to be - loaded. + you can expect all addons to be loaded and all options to be set. """ @@ -83,4 +87,5 @@ class UpdateHook(Hook): Update is called when one or more flow objects have been modified, usually from a different addon. """ + flows: Sequence[mitmproxy.flow.Flow] diff --git a/mitmproxy/http.py b/mitmproxy/http.py index a1303ab2a4..e88242530c 100644 --- a/mitmproxy/http.py +++ b/mitmproxy/http.py @@ -4,19 +4,17 @@ import time import urllib.parse import json +import warnings from dataclasses import dataclass from dataclasses import fields from email.utils import formatdate from email.utils import mktime_tz from email.utils import parsedate_tz from typing import Callable -from typing import Dict from typing import Iterable from typing import Iterator -from typing import List from typing import Mapping from typing import Optional -from typing import Tuple from typing import Union from typing import cast from typing import Any @@ -93,7 +91,7 @@ class Headers(multidict.MultiDict): # type: ignore - For use with the "Set-Cookie" and "Cookie" headers, either use `Response.cookies` or see `Headers.get_all`. """ - def __init__(self, fields: Iterable[Tuple[bytes, bytes]] = (), **headers): + def __init__(self, fields: Iterable[tuple[bytes, bytes]] = (), **headers): """ *Args:* - *fields:* (optional) list of ``(name, value)`` header byte tuples, @@ -112,12 +110,14 @@ def __init__(self, fields: Iterable[Tuple[bytes, bytes]] = (), **headers): raise TypeError("Header fields must be bytes.") # content_type -> content-type - self.update({ - _always_bytes(name).replace(b"_", b"-"): _always_bytes(value) - for name, value in headers.items() - }) + self.update( + { + _always_bytes(name).replace(b"_", b"-"): _always_bytes(value) + for name, value in headers.items() + } + ) - fields: Tuple[Tuple[bytes, bytes], ...] + fields: tuple[tuple[bytes, bytes], ...] @staticmethod def _reduce_values(values) -> str: @@ -143,7 +143,7 @@ def __iter__(self) -> Iterator[str]: for x in super().__iter__(): yield _native(x) - def get_all(self, name: Union[str, bytes]) -> List[str]: + def get_all(self, name: Union[str, bytes]) -> list[str]: """ Like `Headers.get`, but does not fold multiple headers into a single one. This is useful for Set-Cookie and Cookie headers, which do not support folding. @@ -154,12 +154,9 @@ def get_all(self, name: Union[str, bytes]) -> List[str]: - """ name = _always_bytes(name) - return [ - _native(x) for x in - super().get_all(name) - ] + return [_native(x) for x in super().get_all(name)] - def set_all(self, name: Union[str, bytes], values: List[Union[str, bytes]]): + def set_all(self, name: Union[str, bytes], values: list[Union[str, bytes]]): """ Explicitly set multiple headers for the given key. See `Headers.get_all`. @@ -175,10 +172,7 @@ def insert(self, index: int, key: Union[str, bytes], value: Union[str, bytes]): def items(self, multi=False): if multi: - return ( - (_native(k), _native(v)) - for k, v in self.fields - ) + return ((_native(k), _native(v)) for k, v in self.fields) else: return super().items() @@ -194,6 +188,7 @@ class MessageData(serializable.Serializable): # noinspection PyUnreachableCode if __debug__: + def __post_init__(self): for field in fields(self): val = getattr(self, field.name) @@ -274,7 +269,9 @@ def http_version(self) -> str: @http_version.setter def http_version(self, http_version: Union[str, bytes]) -> None: - self.data.http_version = strutils.always_bytes(http_version, "utf-8", "surrogateescape") + self.data.http_version = strutils.always_bytes( + http_version, "utf-8", "surrogateescape" + ) @property def is_http10(self) -> bool: @@ -414,13 +411,18 @@ def _guess_encoding(self, content: bytes = b"") -> str: if "json" in self.headers.get("content-type", ""): enc = "utf8" if not enc: - meta_charset = re.search(rb"""]+charset=['"]?([^'">]+)""", content, re.IGNORECASE) - if meta_charset: - enc = meta_charset.group(1).decode("ascii", "ignore") + if "html" in self.headers.get("content-type", ""): + meta_charset = re.search( + rb"""]+charset=['"]?([^'">]+)""", content, re.IGNORECASE + ) + if meta_charset: + enc = meta_charset.group(1).decode("ascii", "ignore") if not enc: if "text/css" in self.headers.get("content-type", ""): # @charset rule must be the very first thing. - css_charset = re.match(rb"""@charset "([^"]+)";""", content, re.IGNORECASE) + css_charset = re.match( + rb"""@charset "([^"]+)";""", content, re.IGNORECASE + ) if css_charset: enc = css_charset.group(1).decode("ascii", "ignore") if not enc: @@ -441,7 +443,11 @@ def set_text(self, text: Optional[str]) -> None: self.content = cast(bytes, encoding.encode(text, enc)) except ValueError: # Fall back to UTF-8 and update the content-type header. - ct = parse_content_type(self.headers.get("content-type", "")) or ("text", "plain", {}) + ct = parse_content_type(self.headers.get("content-type", "")) or ( + "text", + "plain", + {}, + ) ct[2]["charset"] = "utf-8" self.headers["content-type"] = assemble_content_type(*ct) enc = "utf8" @@ -509,7 +515,7 @@ def encode(self, encoding: str) -> None: self.headers["content-encoding"] = encoding self.content = self.raw_content if "content-encoding" not in self.headers: - raise ValueError("Invalid content encoding {}".format(repr(encoding))) + raise ValueError(f"Invalid content encoding {repr(encoding)}") def json(self, **kwargs: Any) -> Any: """ @@ -526,7 +532,7 @@ def json(self, **kwargs: Any) -> Any: """ content = self.get_content(strict=False) if content is None: - raise TypeError('Message content is not available.') + raise TypeError("Message content is not available.") else: return json.loads(content, **kwargs) @@ -535,6 +541,7 @@ class Request(Message): """ An HTTP request. """ + data: RequestData def __init__( @@ -546,9 +553,9 @@ def __init__( authority: bytes, path: bytes, http_version: bytes, - headers: Union[Headers, Tuple[Tuple[bytes, bytes], ...]], + headers: Union[Headers, tuple[tuple[bytes, bytes], ...]], content: Optional[bytes], - trailers: Union[Headers, Tuple[Tuple[bytes, bytes], ...], None], + trailers: Union[Headers, tuple[tuple[bytes, bytes], ...], None], timestamp_start: float, timestamp_end: Optional[float], ): @@ -602,7 +609,11 @@ def make( method: str, url: str, content: Union[bytes, str] = "", - headers: Union[Headers, Dict[Union[str, bytes], Union[str, bytes]], Iterable[Tuple[bytes, bytes]]] = () + headers: Union[ + Headers, + dict[Union[str, bytes], Union[str, bytes]], + Iterable[tuple[bytes, bytes]], + ] = (), ) -> "Request": """ Simplified API for creating request objects. @@ -612,16 +623,20 @@ def make( pass elif isinstance(headers, dict): headers = Headers( - (always_bytes(k, "utf-8", "surrogateescape"), - always_bytes(v, "utf-8", "surrogateescape")) + ( + always_bytes(k, "utf-8", "surrogateescape"), + always_bytes(v, "utf-8", "surrogateescape"), + ) for k, v in headers.items() ) elif isinstance(headers, Iterable): headers = Headers(headers) # type: ignore else: - raise TypeError("Expected headers to be an iterable or dict, but is {}.".format( - type(headers).__name__ - )) + raise TypeError( + "Expected headers to be an iterable or dict, but is {}.".format( + type(headers).__name__ + ) + ) req = cls( "", @@ -645,7 +660,9 @@ def make( elif isinstance(content, str): req.text = content else: - raise TypeError(f"Expected content to be str or bytes, but is {type(content).__name__}.") + raise TypeError( + f"Expected content to be str or bytes, but is {type(content).__name__}." + ) return req @@ -851,10 +868,7 @@ def query(self) -> multidict.MultiDictView[str, str]: For the most part, this behaves like a dictionary. Modifications to the MultiDictView update `Request.path`, and vice versa. """ - return multidict.MultiDictView( - self._get_query, - self._set_query - ) + return multidict.MultiDictView(self._get_query, self._set_query) @query.setter def query(self, value): @@ -874,17 +888,14 @@ def cookies(self) -> multidict.MultiDictView[str, str]: For the most part, this behaves like a dictionary. Modifications to the MultiDictView update `Request.headers`, and vice versa. """ - return multidict.MultiDictView( - self._get_cookies, - self._set_cookies - ) + return multidict.MultiDictView(self._get_cookies, self._set_cookies) @cookies.setter def cookies(self, value): self._set_cookies(value) @property - def path_components(self) -> Tuple[str, ...]: + def path_components(self) -> tuple[str, ...]: """ The URL's path components as a tuple of strings. Components are unquoted. @@ -925,16 +936,17 @@ def constrain_encoding(self) -> None: """ accept_encoding = self.headers.get("accept-encoding") if accept_encoding: - self.headers["accept-encoding"] = ( - ', '.join( - e - for e in {"gzip", "identity", "deflate", "br", "zstd"} - if e in accept_encoding - ) + self.headers["accept-encoding"] = ", ".join( + e + for e in {"gzip", "identity", "deflate", "br", "zstd"} + if e in accept_encoding ) def _get_urlencoded_form(self): - is_valid_content_type = "application/x-www-form-urlencoded" in self.headers.get("content-type", "").lower() + is_valid_content_type = ( + "application/x-www-form-urlencoded" + in self.headers.get("content-type", "").lower() + ) if is_valid_content_type: return tuple(url.decode(self.get_text(strict=False))) return () @@ -958,8 +970,7 @@ def urlencoded_form(self) -> multidict.MultiDictView[str, str]: Modifications to the MultiDictView update `Request.content`, and vice versa. """ return multidict.MultiDictView( - self._get_urlencoded_form, - self._set_urlencoded_form + self._get_urlencoded_form, self._set_urlencoded_form ) @urlencoded_form.setter @@ -967,7 +978,9 @@ def urlencoded_form(self, value): self._set_urlencoded_form(value) def _get_multipart_form(self): - is_valid_content_type = "multipart/form-data" in self.headers.get("content-type", "").lower() + is_valid_content_type = ( + "multipart/form-data" in self.headers.get("content-type", "").lower() + ) if is_valid_content_type: try: return multipart.decode(self.headers.get("content-type"), self.content) @@ -976,7 +989,11 @@ def _get_multipart_form(self): return () def _set_multipart_form(self, value): - is_valid_content_type = self.headers.get("content-type", "").lower().startswith("multipart/form-data") + is_valid_content_type = ( + self.headers.get("content-type", "") + .lower() + .startswith("multipart/form-data") + ) if not is_valid_content_type: """ Generate a random boundary here. @@ -999,8 +1016,7 @@ def multipart_form(self) -> multidict.MultiDictView[bytes, bytes]: Modifications to the MultiDictView update `Request.content`, and vice versa. """ return multidict.MultiDictView( - self._get_multipart_form, - self._set_multipart_form + self._get_multipart_form, self._set_multipart_form ) @multipart_form.setter @@ -1012,6 +1028,7 @@ class Response(Message): """ An HTTP response. """ + data: ResponseData def __init__( @@ -1019,9 +1036,9 @@ def __init__( http_version: bytes, status_code: int, reason: bytes, - headers: Union[Headers, Tuple[Tuple[bytes, bytes], ...]], + headers: Union[Headers, tuple[tuple[bytes, bytes], ...]], content: Optional[bytes], - trailers: Union[None, Headers, Tuple[Tuple[bytes, bytes], ...]], + trailers: Union[None, Headers, tuple[tuple[bytes, bytes], ...]], timestamp_start: float, timestamp_end: Optional[float], ): @@ -1032,7 +1049,7 @@ def __init__( reason = reason.encode("ascii", "strict") if isinstance(content, str): - raise ValueError("Content must be bytes, not {}".format(type(content).__name__)) + raise ValueError(f"Content must be bytes, not {type(content).__name__}") if not isinstance(headers, Headers): headers = Headers(headers) if trailers is not None and not isinstance(trailers, Headers): @@ -1063,7 +1080,9 @@ def make( cls, status_code: int = 200, content: Union[bytes, str] = b"", - headers: Union[Headers, Mapping[str, Union[str, bytes]], Iterable[Tuple[bytes, bytes]]] = () + headers: Union[ + Headers, Mapping[str, Union[str, bytes]], Iterable[tuple[bytes, bytes]] + ] = (), ) -> "Response": """ Simplified API for creating response objects. @@ -1072,16 +1091,20 @@ def make( headers = headers elif isinstance(headers, dict): headers = Headers( - (always_bytes(k, "utf-8", "surrogateescape"), # type: ignore - always_bytes(v, "utf-8", "surrogateescape")) + ( + always_bytes(k, "utf-8", "surrogateescape"), # type: ignore + always_bytes(v, "utf-8", "surrogateescape"), + ) for k, v in headers.items() ) elif isinstance(headers, Iterable): headers = Headers(headers) # type: ignore else: - raise TypeError("Expected headers to be an iterable or dict, but is {}.".format( - type(headers).__name__ - )) + raise TypeError( + "Expected headers to be an iterable or dict, but is {}.".format( + type(headers).__name__ + ) + ) resp = cls( b"HTTP/1.1", @@ -1100,7 +1123,9 @@ def make( elif isinstance(content, str): resp.text = content else: - raise TypeError(f"Expected content to be str or bytes, but is {type(content).__name__}.") + raise TypeError( + f"Expected content to be str or bytes, but is {type(content).__name__}." + ) return resp @@ -1132,10 +1157,7 @@ def reason(self, reason: Union[str, bytes]) -> None: def _get_cookies(self): h = self.headers.get_all("set-cookie") all_cookies = cookies.parse_set_cookie_headers(h) - return tuple( - (name, (value, attrs)) - for name, value, attrs in all_cookies - ) + return tuple((name, (value, attrs)) for name, value, attrs in all_cookies) def _set_cookies(self, value): cookie_headers = [] @@ -1145,7 +1167,11 @@ def _set_cookies(self, value): self.headers.set_all("set-cookie", cookie_headers) @property - def cookies(self) -> multidict.MultiDictView[str, Tuple[str, multidict.MultiDict[str, Optional[str]]]]: + def cookies( + self, + ) -> multidict.MultiDictView[ + str, tuple[str, multidict.MultiDict[str, Optional[str]]] + ]: """ The response cookies. A possibly empty `MultiDictView`, where the keys are cookie name strings, and values are `(cookie value, attributes)` tuples. Within @@ -1155,10 +1181,7 @@ def cookies(self) -> multidict.MultiDictView[str, Tuple[str, multidict.MultiDict *Warning:* Changes to `attributes` will not be picked up unless you also reassign the `(cookie value, attributes)` tuple directly in the `MultiDictView`. """ - return multidict.MultiDictView( - self._get_cookies, - self._set_cookies - ) + return multidict.MultiDictView(self._get_cookies, self._set_cookies) @cookies.setter def cookies(self, value): @@ -1185,7 +1208,10 @@ def refresh(self, now=None): d = parsedate_tz(self.headers[i]) if d: new = mktime_tz(d) + delta - self.headers[i] = formatdate(new, usegmt=True) + try: + self.headers[i] = formatdate(new, usegmt=True) + except OSError: # pragma: no cover + pass # value out of bounds on Windows only (which is why we exclude it from coverage). c = [] for set_cookie_header in self.headers.get_all("set-cookie"): try: @@ -1202,6 +1228,7 @@ class HTTPFlow(flow.Flow): An HTTPFlow is a collection of objects representing a single HTTP transaction. """ + request: Request """The client's HTTP request.""" response: Optional[Response] = None @@ -1220,22 +1247,22 @@ class HTTPFlow(flow.Flow): If this HTTP flow initiated a WebSocket connection, this attribute contains all associated WebSocket data. """ - def __init__(self, client_conn, server_conn, live=None, mode="regular"): - super().__init__("http", client_conn, server_conn, live) - self.mode = mode - _stateobject_attributes = flow.Flow._stateobject_attributes.copy() # mypy doesn't support update with kwargs - _stateobject_attributes.update(dict( - request=Request, - response=Response, - websocket=WebSocketData, - mode=str - )) + _stateobject_attributes.update( + dict(request=Request, response=Response, websocket=WebSocketData) + ) def __repr__(self): s = " float: """*Read-only:* An alias for `Request.timestamp_start`.""" return self.request.timestamp_start + @property + def mode(self) -> str: # pragma: no cover + warnings.warn("HTTPFlow.mode is deprecated.", DeprecationWarning, stacklevel=2) + return getattr(self, "_mode", "regular") + + @mode.setter + def mode(self, val: str) -> None: # pragma: no cover + warnings.warn("HTTPFlow.mode is deprecated.", DeprecationWarning, stacklevel=2) + self._mode = val + def copy(self): f = super().copy() if self.request: diff --git a/mitmproxy/io/__init__.py b/mitmproxy/io/__init__.py index c440d43375..8d068d5699 100644 --- a/mitmproxy/io/__init__.py +++ b/mitmproxy/io/__init__.py @@ -1,6 +1,4 @@ from .io import FlowWriter, FlowReader, FilteredFlowWriter, read_flows_from_paths -__all__ = [ - "FlowWriter", "FlowReader", "FilteredFlowWriter", "read_flows_from_paths" -] +__all__ = ["FlowWriter", "FlowReader", "FilteredFlowWriter", "read_flows_from_paths"] diff --git a/mitmproxy/io/compat.py b/mitmproxy/io/compat.py index 61d436d994..3b77818f6c 100644 --- a/mitmproxy/io/compat.py +++ b/mitmproxy/io/compat.py @@ -6,7 +6,7 @@ version number, this prevents issues with developer builds and snapshots. """ import uuid -from typing import Any, Dict, Mapping, Union # noqa +from typing import Any, Mapping, Union from mitmproxy import version from mitmproxy.utils import strutils @@ -24,10 +24,14 @@ def convert_012_013(data): def convert_013_014(data): data[b"request"][b"first_line_format"] = data[b"request"].pop(b"form_in") - data[b"request"][b"http_version"] = b"HTTP/" + ".".join( - str(x) for x in data[b"request"].pop(b"httpversion")).encode() - data[b"response"][b"http_version"] = b"HTTP/" + ".".join( - str(x) for x in data[b"response"].pop(b"httpversion")).encode() + data[b"request"][b"http_version"] = ( + b"HTTP/" + + ".".join(str(x) for x in data[b"request"].pop(b"httpversion")).encode() + ) + data[b"response"][b"http_version"] = ( + b"HTTP/" + + ".".join(str(x) for x in data[b"response"].pop(b"httpversion")).encode() + ) data[b"response"][b"status_code"] = data[b"response"].pop(b"code") data[b"response"][b"body"] = data[b"response"].pop(b"content") data[b"server_conn"].pop(b"state") @@ -99,15 +103,23 @@ def convert_100_200(data): data["version"] = (2, 0, 0) data["client_conn"]["address"] = data["client_conn"]["address"]["address"] data["server_conn"]["address"] = data["server_conn"]["address"]["address"] - data["server_conn"]["source_address"] = data["server_conn"]["source_address"]["address"] + data["server_conn"]["source_address"] = data["server_conn"]["source_address"][ + "address" + ] if data["server_conn"]["ip_address"]: data["server_conn"]["ip_address"] = data["server_conn"]["ip_address"]["address"] if data["server_conn"]["via"]: - data["server_conn"]["via"]["address"] = data["server_conn"]["via"]["address"]["address"] - data["server_conn"]["via"]["source_address"] = data["server_conn"]["via"]["source_address"]["address"] + data["server_conn"]["via"]["address"] = data["server_conn"]["via"]["address"][ + "address" + ] + data["server_conn"]["via"]["source_address"] = data["server_conn"]["via"][ + "source_address" + ]["address"] if data["server_conn"]["via"]["ip_address"]: - data["server_conn"]["via"]["ip_address"] = data["server_conn"]["via"]["ip_address"]["address"] + data["server_conn"]["via"]["ip_address"] = data["server_conn"]["via"][ + "ip_address" + ]["address"] return data @@ -135,21 +147,27 @@ def convert_4_5(data): data["version"] = 5 client_conn_key = ( data["client_conn"]["timestamp_start"], - *data["client_conn"]["address"] + *data["client_conn"]["address"], ) server_conn_key = ( data["server_conn"]["timestamp_start"], - *data["server_conn"]["source_address"] + *data["server_conn"]["source_address"], + ) + data["client_conn"]["id"] = client_connections.setdefault( + client_conn_key, str(uuid.uuid4()) + ) + data["server_conn"]["id"] = server_connections.setdefault( + server_conn_key, str(uuid.uuid4()) ) - data["client_conn"]["id"] = client_connections.setdefault(client_conn_key, str(uuid.uuid4())) - data["server_conn"]["id"] = server_connections.setdefault(server_conn_key, str(uuid.uuid4())) if data["server_conn"]["via"]: server_conn_key = ( data["server_conn"]["via"]["timestamp_start"], - *data["server_conn"]["via"]["source_address"] + *data["server_conn"]["via"]["source_address"], + ) + data["server_conn"]["via"]["id"] = server_connections.setdefault( + server_conn_key, str(uuid.uuid4()) ) - data["server_conn"]["via"]["id"] = server_connections.setdefault(server_conn_key, str(uuid.uuid4())) return data @@ -157,12 +175,20 @@ def convert_4_5(data): def convert_5_6(data): data["version"] = 6 data["client_conn"]["tls_established"] = data["client_conn"].pop("ssl_established") - data["client_conn"]["timestamp_tls_setup"] = data["client_conn"].pop("timestamp_ssl_setup") + data["client_conn"]["timestamp_tls_setup"] = data["client_conn"].pop( + "timestamp_ssl_setup" + ) data["server_conn"]["tls_established"] = data["server_conn"].pop("ssl_established") - data["server_conn"]["timestamp_tls_setup"] = data["server_conn"].pop("timestamp_ssl_setup") + data["server_conn"]["timestamp_tls_setup"] = data["server_conn"].pop( + "timestamp_ssl_setup" + ) if data["server_conn"]["via"]: - data["server_conn"]["via"]["tls_established"] = data["server_conn"]["via"].pop("ssl_established") - data["server_conn"]["via"]["timestamp_tls_setup"] = data["server_conn"]["via"].pop("timestamp_ssl_setup") + data["server_conn"]["via"]["tls_established"] = data["server_conn"]["via"].pop( + "ssl_established" + ) + data["server_conn"]["via"]["timestamp_tls_setup"] = data["server_conn"][ + "via" + ].pop("timestamp_ssl_setup") return data @@ -266,21 +292,32 @@ def convert_11_12(data): except KeyError: # The handshake flow is missing, which should never really happen. We make up a dummy. data = { - 'client_conn': data["client_conn"], - 'error': data["error"], - 'id': data["id"], - 'intercepted': data["intercepted"], - 'is_replay': data["is_replay"], - 'marked': data["marked"], - 'metadata': {}, - 'mode': 'transparent', - 'request': {'authority': b'', 'content': None, 'headers': [], 'host': b'unknown', - 'http_version': b'HTTP/1.1', 'method': b'GET', 'path': b'/', 'port': 80, 'scheme': b'http', - 'timestamp_end': 0, 'timestamp_start': 0, 'trailers': None, }, - 'response': None, - 'server_conn': data["server_conn"], - 'type': 'http', - 'version': 12 + "client_conn": data["client_conn"], + "error": data["error"], + "id": data["id"], + "intercepted": data["intercepted"], + "is_replay": data["is_replay"], + "marked": data["marked"], + "metadata": {}, + "mode": "transparent", + "request": { + "authority": b"", + "content": None, + "headers": [], + "host": b"unknown", + "http_version": b"HTTP/1.1", + "method": b"GET", + "path": b"/", + "port": 80, + "scheme": b"http", + "timestamp_end": 0, + "timestamp_start": 0, + "trailers": None, + }, + "response": None, + "server_conn": data["server_conn"], + "type": "http", + "version": 12, } data["metadata"]["duplicated"] = ( "This WebSocket flow has been migrated from an old file format version " @@ -319,6 +356,30 @@ def convert_13_14(data): return data +def convert_14_15(data): + data["version"] = 15 + if data.get("websocket", None): + # Add "injected" attribute. + data["websocket"]["messages"] = [ + msg + [False] for msg in data["websocket"]["messages"] + ] + return data + + +def convert_15_16(data): + data["version"] = 16 + data["timestamp_created"] = data.get("request", data["client_conn"])[ + "timestamp_start" + ] + return data + + +def convert_16_17(data): + data["version"] = 17 + data.pop("mode", None) + return data + + def _convert_dict_keys(o: Any) -> Any: if isinstance(o, dict): return {strutils.always_str(k): _convert_dict_keys(v) for k, v in o.items()} @@ -343,16 +404,13 @@ def convert_unicode(data: dict) -> dict: """ data = _convert_dict_keys(data) data = _convert_dict_vals( - data, { + data, + { "type": True, "id": True, - "request": { - "first_line_format": True - }, - "error": { - "msg": True - } - } + "request": {"first_line_format": True}, + "error": {"msg": True}, + }, ) return data @@ -380,10 +438,15 @@ def convert_unicode(data: dict) -> dict: 11: convert_11_12, 12: convert_12_13, 13: convert_13_14, + 14: convert_14_15, + 15: convert_15_16, + 16: convert_16_17, } -def migrate_flow(flow_data: Dict[Union[bytes, str], Any]) -> Dict[Union[bytes, str], Any]: +def migrate_flow( + flow_data: dict[Union[bytes, str], Any] +) -> dict[Union[bytes, str], Any]: while True: flow_version = flow_data.get(b"version", flow_data.get("version")) @@ -404,7 +467,7 @@ def migrate_flow(flow_data: Dict[Union[bytes, str], Any]) -> Dict[Union[bytes, s "{} cannot read files with flow format version {}{}.".format( version.MITMPROXY, flow_version, - ", please update mitmproxy" if should_upgrade else "" + ", please update mitmproxy" if should_upgrade else "", ) ) return flow_data diff --git a/mitmproxy/io/io.py b/mitmproxy/io/io.py index 1ac6a8ad37..402d3630c0 100644 --- a/mitmproxy/io/io.py +++ b/mitmproxy/io/io.py @@ -1,51 +1,41 @@ import os -from typing import Any, Dict, IO, Iterable, Type, Union, cast +from typing import Any, BinaryIO, Iterable, Union, cast from mitmproxy import exceptions from mitmproxy import flow from mitmproxy import flowfilter -from mitmproxy import http -from mitmproxy import tcp from mitmproxy.io import compat from mitmproxy.io import tnetstring -FLOW_TYPES: Dict[str, Type[flow.Flow]] = dict( - http=http.HTTPFlow, - tcp=tcp.TCPFlow, -) - class FlowWriter: def __init__(self, fo): self.fo = fo - def add(self, flow): - d = flow.get_state() + def add(self, f: flow.Flow) -> None: + d = f.get_state() tnetstring.dump(d, self.fo) class FlowReader: - def __init__(self, fo: IO[bytes]): - self.fo: IO[bytes] = fo + def __init__(self, fo: BinaryIO): + self.fo: BinaryIO = fo def stream(self) -> Iterable[flow.Flow]: """ - Yields Flow objects from the dump. + Yields Flow objects from the dump. """ try: while True: # FIXME: This cast hides a lack of dynamic type checking loaded = cast( - Dict[Union[bytes, str], Any], + dict[Union[bytes, str], Any], tnetstring.load(self.fo), ) try: - mdata = compat.migrate_flow(loaded) + yield flow.Flow.from_state(compat.migrate_flow(loaded)) except ValueError as e: - raise exceptions.FlowReadException(str(e)) - if mdata["type"] not in FLOW_TYPES: - raise exceptions.FlowReadException("Unknown flow type: {}".format(mdata["type"])) - yield FLOW_TYPES[mdata["type"]].from_state(mdata) + raise exceptions.FlowReadException(e) except (ValueError, TypeError, IndexError) as e: if str(e) == "not a tnetstring: empty file": return # Error is due to EOF @@ -57,14 +47,14 @@ def __init__(self, fo, flt): self.fo = fo self.flt = flt - def add(self, f: flow.Flow): + def add(self, f: flow.Flow) -> None: if self.flt and not flowfilter.match(self.flt, f): return d = f.get_state() tnetstring.dump(d, self.fo) -def read_flows_from_paths(paths): +def read_flows_from_paths(paths) -> list[flow.Flow]: """ Given a list of filepaths, read all flows and return a list of them. From a performance perspective, streaming would be advisable - @@ -74,7 +64,7 @@ def read_flows_from_paths(paths): FlowReadException, if any error occurs. """ try: - flows = [] + flows: list[flow.Flow] = [] for path in paths: path = os.path.expanduser(path) with open(path, "rb") as f: diff --git a/mitmproxy/io/tnetstring.py b/mitmproxy/io/tnetstring.py index ac57bf1831..e08a729eba 100644 --- a/mitmproxy/io/tnetstring.py +++ b/mitmproxy/io/tnetstring.py @@ -41,9 +41,9 @@ """ import collections -import typing +from typing import BinaryIO, Union -TSerializable = typing.Union[None, str, bool, int, float, bytes, list, tuple, dict] +TSerializable = Union[None, str, bool, int, float, bytes, list, tuple, dict] def dumps(value: TSerializable) -> bytes: @@ -55,10 +55,10 @@ def dumps(value: TSerializable) -> bytes: # than creating all the intermediate strings. q: collections.deque = collections.deque() _rdumpq(q, 0, value) - return b''.join(q) + return b"".join(q) -def dump(value: TSerializable, file_handle: typing.IO[bytes]) -> None: +def dump(value: TSerializable, file_handle: BinaryIO) -> None: """ This function dumps a python object as a tnetstring and writes it to the given file. @@ -84,19 +84,19 @@ def _rdumpq(q: collections.deque, size: int, value: TSerializable) -> int: """ write = q.appendleft if value is None: - write(b'0:~') + write(b"0:~") return size + 3 elif value is True: - write(b'4:true!') + write(b"4:true!") return size + 7 elif value is False: - write(b'5:false!') + write(b"5:false!") return size + 8 elif isinstance(value, int): data = str(value).encode() ldata = len(data) span = str(ldata).encode() - write(b'%s:%s#' % (span, data)) + write(b"%s:%s#" % (span, data)) return size + 2 + len(span) + ldata elif isinstance(value, float): # Use repr() for float rather than str(). @@ -106,47 +106,47 @@ def _rdumpq(q: collections.deque, size: int, value: TSerializable) -> int: data = repr(value).encode() ldata = len(data) span = str(ldata).encode() - write(b'%s:%s^' % (span, data)) + write(b"%s:%s^" % (span, data)) return size + 2 + len(span) + ldata elif isinstance(value, bytes): data = value ldata = len(data) span = str(ldata).encode() - write(b',') + write(b",") write(data) - write(b':') + write(b":") write(span) return size + 2 + len(span) + ldata elif isinstance(value, str): data = value.encode("utf8") ldata = len(data) span = str(ldata).encode() - write(b';') + write(b";") write(data) - write(b':') + write(b":") write(span) return size + 2 + len(span) + ldata elif isinstance(value, (list, tuple)): - write(b']') + write(b"]") init_size = size = size + 1 for item in reversed(value): size = _rdumpq(q, size, item) span = str(size - init_size).encode() - write(b':') + write(b":") write(span) return size + 1 + len(span) elif isinstance(value, dict): - write(b'}') + write(b"}") init_size = size = size + 1 for (k, v) in value.items(): size = _rdumpq(q, size, v) size = _rdumpq(q, size, k) span = str(size - init_size).encode() - write(b':') + write(b":") write(span) return size + 1 + len(span) else: - raise ValueError("unserializable object: {} ({})".format(value, type(value))) + raise ValueError(f"unserializable object: {value} ({type(value)})") def loads(string: bytes) -> TSerializable: @@ -156,7 +156,7 @@ def loads(string: bytes) -> TSerializable: return pop(string)[0] -def load(file_handle: typing.IO[bytes]) -> TSerializable: +def load(file_handle: BinaryIO) -> TSerializable: """load(file) -> object This function reads a tnetstring from a file and parses it into a @@ -171,7 +171,7 @@ def load(file_handle: typing.IO[bytes]) -> TSerializable: data_length = b"" while c.isdigit(): data_length += c - if len(data_length) > 9: + if len(data_length) > 12: raise ValueError("not a tnetstring: absurdly large length prefix") c = file_handle.read(1) if c != b":": @@ -184,38 +184,38 @@ def load(file_handle: typing.IO[bytes]) -> TSerializable: def parse(data_type: int, data: bytes) -> TSerializable: - if data_type == ord(b','): + if data_type == ord(b","): return data - if data_type == ord(b';'): + if data_type == ord(b";"): return data.decode("utf8") - if data_type == ord(b'#'): + if data_type == ord(b"#"): try: return int(data) except ValueError: raise ValueError(f"not a tnetstring: invalid integer literal: {data!r}") - if data_type == ord(b'^'): + if data_type == ord(b"^"): try: return float(data) except ValueError: raise ValueError(f"not a tnetstring: invalid float literal: {data!r}") - if data_type == ord(b'!'): - if data == b'true': + if data_type == ord(b"!"): + if data == b"true": return True - elif data == b'false': + elif data == b"false": return False else: raise ValueError(f"not a tnetstring: invalid boolean literal: {data!r}") - if data_type == ord(b'~'): + if data_type == ord(b"~"): if data: raise ValueError(f"not a tnetstring: invalid null literal: {data!r}") return None - if data_type == ord(b']'): + if data_type == ord(b"]"): l = [] while data: item, data = pop(data) l.append(item) # type: ignore return l - if data_type == ord(b'}'): + if data_type == ord(b"}"): d = {} while data: key, data = pop(data) @@ -225,7 +225,7 @@ def parse(data_type: int, data: bytes) -> TSerializable: raise ValueError(f"unknown type tag: {data_type}") -def pop(data: bytes) -> typing.Tuple[TSerializable, bytes]: +def pop(data: bytes) -> tuple[TSerializable, bytes]: """ This function parses a tnetstring into a python object. It returns a tuple giving the parsed object and a string @@ -233,12 +233,14 @@ def pop(data: bytes) -> typing.Tuple[TSerializable, bytes]: """ # Parse out data length, type and remaining string. try: - blength, data = data.split(b':', 1) + blength, data = data.split(b":", 1) length = int(blength) except ValueError: - raise ValueError(f"not a tnetstring: missing or invalid length prefix: {data!r}") + raise ValueError( + f"not a tnetstring: missing or invalid length prefix: {data!r}" + ) try: - data, data_type, remain = data[:length], data[length], data[length + 1:] + data, data_type, remain = data[:length], data[length], data[length + 1 :] except IndexError: # This fires if len(data) < dlen, meaning we don't need # to further validate that data is the right length. diff --git a/mitmproxy/log.py b/mitmproxy/log.py index ca2404852e..2456be3cf8 100644 --- a/mitmproxy/log.py +++ b/mitmproxy/log.py @@ -21,47 +21,49 @@ def __repr__(self): class Log: """ - The central logger, exposed to scripts as mitmproxy.ctx.log. + The central logger, exposed to scripts as mitmproxy.ctx.log. """ + def __init__(self, master): self.master = master def debug(self, txt): """ - Log with level debug. + Log with level debug. """ self(txt, "debug") def info(self, txt): """ - Log with level info. + Log with level info. """ self(txt, "info") def alert(self, txt): """ - Log with level alert. Alerts have the same urgency as info, but - signals to interactive tools that the user's attention should be - drawn to the output even if they're not currently looking at the - event log. + Log with level alert. Alerts have the same urgency as info, but + signals to interactive tools that the user's attention should be + drawn to the output even if they're not currently looking at the + event log. """ self(txt, "alert") def warn(self, txt): """ - Log with level warn. + Log with level warn. """ self(txt, "warn") def error(self, txt): """ - Log with level error. + Log with level error. """ self(txt, "error") def __call__(self, text, level="info"): self.master.event_loop.call_soon_threadsafe( - self.master.addons.trigger, AddLogHook(LogEntry(text, level)), + self.master.addons.trigger, + AddLogHook(LogEntry(text, level)), ) @@ -72,6 +74,7 @@ class AddLogHook(hooks.Hook): context. Be careful not to log from this event, which will cause an infinite loop! """ + entry: LogEntry diff --git a/mitmproxy/master.py b/mitmproxy/master.py index 888406ae77..ec10032c0f 100644 --- a/mitmproxy/master.py +++ b/mitmproxy/master.py @@ -1,12 +1,9 @@ import asyncio -import logging -import sys -import threading import traceback +from typing import Optional from mitmproxy import addonmanager, hooks from mitmproxy import command -from mitmproxy import controller from mitmproxy import eventsequence from mitmproxy import http from mitmproxy import log @@ -14,98 +11,79 @@ from mitmproxy.net import server_spec from . import ctx as mitmproxy_ctx -# Conclusively preventing cross-thread races on proxy shutdown turns out to be -# very hard. We could build a thread sync infrastructure for this, or we could -# wait until we ditch threads and move all the protocols into the async loop. -# Until then, silence non-critical errors. -logging.getLogger('asyncio').setLevel(logging.CRITICAL) - class Master: """ - The master handles mitmproxy's main event loop. + The master handles mitmproxy's main event loop. """ - def __init__(self, opts): - self.should_exit = threading.Event() - self.event_loop = asyncio.get_event_loop() + event_loop: asyncio.AbstractEventLoop + + def __init__(self, opts, event_loop: Optional[asyncio.AbstractEventLoop] = None): self.options: options.Options = opts or options.Options() self.commands = command.CommandManager(self) self.addons = addonmanager.AddonManager(self) - self._server = None self.log = log.Log(self) + # We expect an active event loop here already because some addons + # may want to spawn tasks during the initial configuration phase, + # which happens before run(). + self.event_loop = event_loop or asyncio.get_running_loop() + try: + self.should_exit = asyncio.Event() + except RuntimeError: + self.should_exit = asyncio.Event(loop=self.event_loop) mitmproxy_ctx.master = self mitmproxy_ctx.log = self.log mitmproxy_ctx.options = self.options - def start(self): - self.should_exit.clear() - - async def running(self): - self.addons.trigger(hooks.RunningHook()) - - def run_loop(self, loop): - self.start() - asyncio.ensure_future(self.running()) - - exc = None + async def run(self) -> None: + old_handler = self.event_loop.get_exception_handler() + self.event_loop.set_exception_handler(self._asyncio_exception_handler) try: - loop() - except Exception: # pragma: no cover - exc = traceback.format_exc() + self.should_exit.clear() + + # Handle scheduled tasks (configure()) first. + await asyncio.sleep(0) + await self.running() + try: + await self.should_exit.wait() + finally: + # .wait might be cancelled (e.g. by sys.exit) + await self.done() finally: - if not self.should_exit.is_set(): # pragma: no cover - self.shutdown() - loop = asyncio.get_event_loop() - tasks = asyncio.all_tasks(loop) - for p in tasks: - p.cancel() - loop.close() - - if exc: # pragma: no cover - print(exc, file=sys.stderr) - print("mitmproxy has crashed!", file=sys.stderr) - print("Please lodge a bug report at:", file=sys.stderr) - print("\thttps://github.com/mitmproxy/mitmproxy/issues", file=sys.stderr) - - self.addons.trigger(hooks.DoneHook()) - - def run(self): - loop = asyncio.get_event_loop() - self.run_loop(loop.run_forever) - - async def _shutdown(self): - self.should_exit.set() - loop = asyncio.get_event_loop() - loop.stop() + self.event_loop.set_exception_handler(old_handler) def shutdown(self): """ - Shut down the proxy. This method is thread-safe. + Shut down the proxy. This method is thread-safe. """ - if not self.should_exit.is_set(): - self.should_exit.set() - ret = asyncio.run_coroutine_threadsafe(self._shutdown(), loop=self.event_loop) - # Weird band-aid to make sure that self._shutdown() is actually executed, - # which otherwise hangs the process as the proxy server is threaded. - # This all needs to be simplified when the proxy server runs on asyncio as well. - if not self.event_loop.is_running(): # pragma: no cover - try: - self.event_loop.run_until_complete(asyncio.wrap_future(ret)) - except RuntimeError: - pass # Event loop stopped before Future completed. + # We may add an exception argument here. + self.event_loop.call_soon_threadsafe(self.should_exit.set) - def _change_reverse_host(self, f): - """ - When we load flows in reverse proxy mode, we adjust the target host to - the reverse proxy destination for all flows we load. This makes it very - easy to replay saved flows against a different host. - """ - if self.options.mode.startswith("reverse:"): - _, upstream_spec = server_spec.parse_with_mode(self.options.mode) - f.request.host, f.request.port = upstream_spec.address - f.request.scheme = upstream_spec.scheme + async def running(self) -> None: + await self.addons.trigger_event(hooks.RunningHook()) + + async def done(self) -> None: + await self.addons.trigger_event(hooks.DoneHook()) + + def _asyncio_exception_handler(self, loop, context): + try: + exc: Exception = context["exception"] + except KeyError: + self.log.error( + f"Unhandled asyncio error: {context}" + "\nPlease lodge a bug report at:" + + "\n\thttps://github.com/mitmproxy/mitmproxy/issues" + ) + else: + if isinstance(exc, OSError) and exc.errno == 10038: + return # suppress https://bugs.python.org/issue43253 + self.log.error( + "\n".join(traceback.format_exception(type(exc), exc, exc.__traceback__)) + + "\nPlease lodge a bug report at:" + + "\n\thttps://github.com/mitmproxy/mitmproxy/issues" + ) async def load_flow(self, f): """ @@ -113,8 +91,13 @@ async def load_flow(self, f): """ if isinstance(f, http.HTTPFlow): - self._change_reverse_host(f) + if self.options.mode.startswith("reverse:"): + # When we load flows in reverse proxy mode, we adjust the target host to + # the reverse proxy destination for all flows we load. This makes it very + # easy to replay saved flows against a different host. + _, upstream_spec = server_spec.parse_with_mode(self.options.mode) + f.request.host, f.request.port = upstream_spec.address + f.request.scheme = upstream_spec.scheme - f.reply = controller.DummyReply() for e in eventsequence.iterate(f): await self.addons.handle_lifecycle(e) diff --git a/mitmproxy/net/check.py b/mitmproxy/net/check.py index 133521a4dd..9a0bdec496 100644 --- a/mitmproxy/net/check.py +++ b/mitmproxy/net/check.py @@ -33,7 +33,7 @@ def is_valid_host(host: AnyStr) -> bool: return True # IPv4/IPv6 address try: - ipaddress.ip_address(host_bytes.decode('idna')) + ipaddress.ip_address(host_bytes.decode("idna")) return True except ValueError: return False diff --git a/mitmproxy/net/dns/__init__.py b/mitmproxy/net/dns/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/mitmproxy/net/dns/classes.py b/mitmproxy/net/dns/classes.py new file mode 100644 index 0000000000..3f17defdef --- /dev/null +++ b/mitmproxy/net/dns/classes.py @@ -0,0 +1,11 @@ +IN = 1 +CH = 3 +HS = 4 +NONE = 254 +ANY = 255 + +_STRINGS = {IN: "IN", CH: "CH", HS: "HS", NONE: "NONE", ANY: "ANY"} + + +def to_str(class_: int) -> str: + return _STRINGS.get(class_, f"CLASS({class_})") diff --git a/mitmproxy/net/dns/domain_names.py b/mitmproxy/net/dns/domain_names.py new file mode 100644 index 0000000000..cbb666ece0 --- /dev/null +++ b/mitmproxy/net/dns/domain_names.py @@ -0,0 +1,108 @@ +import struct +from typing import Optional + + +_LABEL_SIZE = struct.Struct("!B") +_POINTER_OFFSET = struct.Struct("!H") +_POINTER_INDICATOR = 0b11000000 + + +Cache = dict[int, Optional[tuple[str, int]]] + + +def cache() -> Cache: + return dict() + + +def _unpack_label_into(labels: list[str], buffer: bytes, offset: int) -> int: + (size,) = _LABEL_SIZE.unpack_from(buffer, offset) + if size >= 64: + raise struct.error(f"unpack encountered a label of length {size}") + elif size == 0: + return _LABEL_SIZE.size + else: + offset += _LABEL_SIZE.size + end_label = offset + size + if len(buffer) < end_label: + raise struct.error(f"unpack requires a label buffer of {size} bytes") + try: + labels.append(buffer[offset:end_label].decode("idna")) + except UnicodeDecodeError: + raise struct.error( + f"unpack encountered a illegal characters at offset {offset}" + ) + return _LABEL_SIZE.size + size + + +def unpack_from_with_compression( + buffer: bytes, offset: int, cache: Cache +) -> tuple[str, int]: + if offset in cache: + result = cache[offset] + if result is None: + raise struct.error(f"unpack encountered domain name loop") + else: + cache[offset] = None # this will indicate that the offset is being unpacked + start_offset = offset + labels = [] + while True: + (size,) = _LABEL_SIZE.unpack_from(buffer, offset) + if size & _POINTER_INDICATOR == _POINTER_INDICATOR: + (pointer,) = _POINTER_OFFSET.unpack_from(buffer, offset) + offset += _POINTER_OFFSET.size + label, _ = unpack_from_with_compression( + buffer, pointer & ~(_POINTER_INDICATOR << 8), cache + ) + labels.append(label) + break + else: + offset += _unpack_label_into(labels, buffer, offset) + if size == 0: + break + result = ".".join(labels), (offset - start_offset) + cache[start_offset] = result + return result + + +def unpack_from(buffer: bytes, offset: int) -> tuple[str, int]: + """Converts RDATA into a domain name without pointer compression from a given offset and also returns the binary size.""" + labels: list[str] = [] + while True: + (size,) = _LABEL_SIZE.unpack_from(buffer, offset) + if size & _POINTER_INDICATOR == _POINTER_INDICATOR: + raise struct.error( + f"unpack encountered a pointer which is not supported in RDATA" + ) + else: + offset += _unpack_label_into(labels, buffer, offset) + if size == 0: + break + return ".".join(labels), offset + + +def unpack(buffer: bytes) -> str: + """Converts RDATA into a domain name without pointer compression.""" + name, length = unpack_from(buffer, 0) + if length != len(buffer): + raise struct.error(f"unpack requires a buffer of {length} bytes") + return name + + +def pack(name: str) -> bytes: + """Converts a domain name into RDATA without pointer compression.""" + buffer = bytearray() + if len(name) > 0: + for part in name.split("."): + label = part.encode("idna") + size = len(label) + if size == 0: + raise ValueError(f"domain name '{name}' contains empty labels") + if size >= 64: # pragma: no cover + # encoding with 'idna' will already have raised an exception earlier + raise ValueError( + f"encoded label '{part}' of domain name '{name}' is too long ({size} bytes)" + ) + buffer.extend(_LABEL_SIZE.pack(size)) + buffer.extend(label) + buffer.extend(_LABEL_SIZE.pack(0)) + return bytes(buffer) diff --git a/mitmproxy/net/dns/op_codes.py b/mitmproxy/net/dns/op_codes.py new file mode 100644 index 0000000000..1e1e1dc931 --- /dev/null +++ b/mitmproxy/net/dns/op_codes.py @@ -0,0 +1,19 @@ +QUERY = 0 +IQUERY = 1 +STATUS = 2 +NOTIFY = 4 +UPDATE = 5 +DSO = 6 + +_STRINGS = { + QUERY: "QUERY", + IQUERY: "IQUERY", + STATUS: "STATUS", + NOTIFY: "NOTIFY", + UPDATE: "UPDATE", + DSO: "DSO", +} + + +def to_str(op_code: int) -> str: + return _STRINGS.get(op_code, f"OPCODE({op_code})") diff --git a/mitmproxy/net/dns/response_codes.py b/mitmproxy/net/dns/response_codes.py new file mode 100644 index 0000000000..8a5024e0ed --- /dev/null +++ b/mitmproxy/net/dns/response_codes.py @@ -0,0 +1,50 @@ +NOERROR = 0 +FORMERR = 1 +SERVFAIL = 2 +NXDOMAIN = 3 +NOTIMP = 4 +REFUSED = 5 +YXDOMAIN = 6 +YXRRSET = 7 +NXRRSET = 8 +NOTAUTH = 9 +NOTZONE = 10 +DSOTYPENI = 11 + +_CODES = { + NOERROR: 200, + FORMERR: 400, + SERVFAIL: 500, + NXDOMAIN: 404, + NOTIMP: 501, + REFUSED: 403, + YXDOMAIN: 409, + YXRRSET: 409, + NXRRSET: 410, + NOTAUTH: 401, + NOTZONE: 404, + DSOTYPENI: 501, +} + +_STRINGS = { + NOERROR: "NOERROR", + FORMERR: "FORMERR", + SERVFAIL: "SERVFAIL", + NXDOMAIN: "NXDOMAIN", + NOTIMP: "NOTIMP", + REFUSED: "REFUSED", + YXDOMAIN: "YXDOMAIN", + YXRRSET: "YXRRSET", + NXRRSET: "NXRRSET", + NOTAUTH: "NOTAUTH", + NOTZONE: "NOTZONE", + DSOTYPENI: "DSOTYPENI", +} + + +def http_equiv_status_code(response_code: int) -> int: + return _CODES.get(response_code, 500) + + +def to_str(response_code: int) -> str: + return _STRINGS.get(response_code, f"RCODE({response_code})") diff --git a/mitmproxy/net/dns/types.py b/mitmproxy/net/dns/types.py new file mode 100644 index 0000000000..1ad44d194a --- /dev/null +++ b/mitmproxy/net/dns/types.py @@ -0,0 +1,185 @@ +A = 1 +NS = 2 +MD = 3 +MF = 4 +CNAME = 5 +SOA = 6 +MB = 7 +MG = 8 +MR = 9 +NULL = 10 +WKS = 11 +PTR = 12 +HINFO = 13 +MINFO = 14 +MX = 15 +TXT = 16 +RP = 17 +AFSDB = 18 +X25 = 19 +ISDN = 20 +RT = 21 +NSAP = 22 +NSAP_PTR = 23 +SIG = 24 +KEY = 25 +PX = 26 +GPOS = 27 +AAAA = 28 +LOC = 29 +NXT = 30 +EID = 31 +NIMLOC = 32 +SRV = 33 +ATMA = 34 +NAPTR = 35 +KX = 36 +CERT = 37 +A6 = 38 +DNAME = 39 +SINK = 40 +OPT = 41 +APL = 42 +DS = 43 +SSHFP = 44 +IPSECKEY = 45 +RRSIG = 46 +NSEC = 47 +DNSKEY = 48 +DHCID = 49 +NSEC3 = 50 +NSEC3PARAM = 51 +TLSA = 52 +SMIMEA = 53 +HIP = 55 +NINFO = 56 +RKEY = 57 +TALINK = 58 +CDS = 59 +CDNSKEY = 60 +OPENPGPKEY = 61 +CSYNC = 62 +ZONEMD = 63 +SVCB = 64 +HTTPS = 65 +SPF = 99 +UINFO = 100 +UID = 101 +GID = 102 +UNSPEC = 103 +NID = 104 +L32 = 105 +L64 = 106 +LP = 107 +EUI48 = 108 +EUI64 = 109 +TKEY = 249 +TSIG = 250 +IXFR = 251 +AXFR = 252 +MAILB = 253 +MAILA = 254 +ANY = 255 +URI = 256 +CAA = 257 +AVC = 258 +DOA = 259 +AMTRELAY = 260 +TA = 32768 +DLV = 32769 + +_STRINGS = { + A: "A", + NS: "NS", + MD: "MD", + MF: "MF", + CNAME: "CNAME", + SOA: "SOA", + MB: "MB", + MG: "MG", + MR: "MR", + NULL: "NULL", + WKS: "WKS", + PTR: "PTR", + HINFO: "HINFO", + MINFO: "MINFO", + MX: "MX", + TXT: "TXT", + RP: "RP", + AFSDB: "AFSDB", + X25: "X25", + ISDN: "ISDN", + RT: "RT", + NSAP: "NSAP", + NSAP_PTR: "NSAP_PTR", + SIG: "SIG", + KEY: "KEY", + PX: "PX", + GPOS: "GPOS", + AAAA: "AAAA", + LOC: "LOC", + NXT: "NXT", + EID: "EID", + NIMLOC: "NIMLOC", + SRV: "SRV", + ATMA: "ATMA", + NAPTR: "NAPTR", + KX: "KX", + CERT: "CERT", + A6: "A6", + DNAME: "DNAME", + SINK: "SINK", + OPT: "OPT", + APL: "APL", + DS: "DS", + SSHFP: "SSHFP", + IPSECKEY: "IPSECKEY", + RRSIG: "RRSIG", + NSEC: "NSEC", + DNSKEY: "DNSKEY", + DHCID: "DHCID", + NSEC3: "NSEC3", + NSEC3PARAM: "NSEC3PARAM", + TLSA: "TLSA", + SMIMEA: "SMIMEA", + HIP: "HIP", + NINFO: "NINFO", + RKEY: "RKEY", + TALINK: "TALINK", + CDS: "CDS", + CDNSKEY: "CDNSKEY", + OPENPGPKEY: "OPENPGPKEY", + CSYNC: "CSYNC", + ZONEMD: "ZONEMD", + SVCB: "SVCB", + HTTPS: "HTTPS", + SPF: "SPF", + UINFO: "UINFO", + UID: "UID", + GID: "GID", + UNSPEC: "UNSPEC", + NID: "NID", + L32: "L32", + L64: "L64", + LP: "LP", + EUI48: "EUI48", + EUI64: "EUI64", + TKEY: "TKEY", + TSIG: "TSIG", + IXFR: "IXFR", + AXFR: "AXFR", + MAILB: "MAILB", + MAILA: "MAILA", + ANY: "ANY", + URI: "URI", + CAA: "CAA", + AVC: "AVC", + DOA: "DOA", + AMTRELAY: "AMTRELAY", + TA: "TA", + DLV: "DLV", +} + + +def to_str(type: int) -> str: + return _STRINGS.get(type, f"TYPE({type})") diff --git a/mitmproxy/net/encoding.py b/mitmproxy/net/encoding.py index 854931e604..32e61b62c6 100644 --- a/mitmproxy/net/encoding.py +++ b/mitmproxy/net/encoding.py @@ -4,42 +4,39 @@ import codecs import collections -from io import BytesIO - import gzip import zlib +from io import BytesIO +from typing import Union, overload + import brotli import zstandard as zstd -from typing import Union, Optional, AnyStr, overload # noqa - # We have a shared single-element cache for encoding and decoding. # This is quite useful in practice, e.g. # flow.request.content = flow.request.content.replace(b"foo", b"bar") # does not require an .encode() call if content does not contain b"foo" -CachedDecode = collections.namedtuple( - "CachedDecode", "encoded encoding errors decoded" -) +CachedDecode = collections.namedtuple("CachedDecode", "encoded encoding errors decoded") _cache = CachedDecode(None, None, None, None) @overload -def decode(encoded: None, encoding: str, errors: str = 'strict') -> None: +def decode(encoded: None, encoding: str, errors: str = "strict") -> None: ... @overload -def decode(encoded: str, encoding: str, errors: str = 'strict') -> str: +def decode(encoded: str, encoding: str, errors: str = "strict") -> str: ... @overload -def decode(encoded: bytes, encoding: str, errors: str = 'strict') -> Union[str, bytes]: +def decode(encoded: bytes, encoding: str, errors: str = "strict") -> Union[str, bytes]: ... def decode( - encoded: Union[None, str, bytes], encoding: str, errors: str = 'strict' + encoded: Union[None, str, bytes], encoding: str, errors: str = "strict" ) -> Union[None, str, bytes]: """ Decode the given input object @@ -56,10 +53,10 @@ def decode( global _cache cached = ( - isinstance(encoded, bytes) and - _cache.encoded == encoded and - _cache.encoding == encoding and - _cache.errors == errors + isinstance(encoded, bytes) + and _cache.encoded == encoded + and _cache.encoding == encoding + and _cache.errors == errors ) if cached: return _cache.decoded @@ -74,30 +71,34 @@ def decode( except TypeError: raise except Exception as e: - raise ValueError("{} when decoding {} with {}: {}".format( - type(e).__name__, - repr(encoded)[:10], - repr(encoding), - repr(e), - )) + raise ValueError( + "{} when decoding {} with {}: {}".format( + type(e).__name__, + repr(encoded)[:10], + repr(encoding), + repr(e), + ) + ) @overload -def encode(decoded: None, encoding: str, errors: str = 'strict') -> None: +def encode(decoded: None, encoding: str, errors: str = "strict") -> None: ... @overload -def encode(decoded: str, encoding: str, errors: str = 'strict') -> Union[str, bytes]: +def encode(decoded: str, encoding: str, errors: str = "strict") -> Union[str, bytes]: ... @overload -def encode(decoded: bytes, encoding: str, errors: str = 'strict') -> bytes: +def encode(decoded: bytes, encoding: str, errors: str = "strict") -> bytes: ... -def encode(decoded: Union[None, str, bytes], encoding, errors='strict') -> Union[None, str, bytes]: +def encode( + decoded: Union[None, str, bytes], encoding, errors="strict" +) -> Union[None, str, bytes]: """ Encode the given input object @@ -113,10 +114,10 @@ def encode(decoded: Union[None, str, bytes], encoding, errors='strict') -> Union global _cache cached = ( - isinstance(decoded, bytes) and - _cache.decoded == decoded and - _cache.encoding == encoding and - _cache.errors == errors + isinstance(decoded, bytes) + and _cache.decoded == decoded + and _cache.encoding == encoding + and _cache.errors == errors ) if cached: return _cache.encoded @@ -131,18 +132,20 @@ def encode(decoded: Union[None, str, bytes], encoding, errors='strict') -> Union except TypeError: raise except Exception as e: - raise ValueError("{} when encoding {} with {}: {}".format( - type(e).__name__, - repr(decoded)[:10], - repr(encoding), - repr(e), - )) + raise ValueError( + "{} when encoding {} with {}: {}".format( + type(e).__name__, + repr(decoded)[:10], + repr(encoding), + repr(e), + ) + ) def identity(content): """ - Returns content unchanged. Identity is the default value of - Accept-Encoding headers. + Returns content unchanged. Identity is the default value of + Accept-Encoding headers. """ return content @@ -156,7 +159,7 @@ def decode_gzip(content: bytes) -> bytes: def encode_gzip(content: bytes) -> bytes: s = BytesIO() - gf = gzip.GzipFile(fileobj=s, mode='wb') + gf = gzip.GzipFile(fileobj=s, mode="wb") gf.write(content) gf.close() return s.getvalue() @@ -191,12 +194,12 @@ def encode_zstd(content: bytes) -> bytes: def decode_deflate(content: bytes) -> bytes: """ - Returns decompressed data for DEFLATE. Some servers may respond with - compressed data without a zlib header or checksum. An undocumented - feature of zlib permits the lenient decompression of data missing both - values. + Returns decompressed data for DEFLATE. Some servers may respond with + compressed data without a zlib header or checksum. An undocumented + feature of zlib permits the lenient decompression of data missing both + values. - http://bugs.python.org/issue5784 + http://bugs.python.org/issue5784 """ if not content: return b"" @@ -208,7 +211,7 @@ def decode_deflate(content: bytes) -> bytes: def encode_deflate(content: bytes) -> bytes: """ - Returns compressed content, always including zlib header and checksum. + Returns compressed content, always including zlib header and checksum. """ return zlib.compress(content) diff --git a/mitmproxy/net/http/cookies.py b/mitmproxy/net/http/cookies.py index 24580e3d25..4b2ddd9413 100644 --- a/mitmproxy/net/http/cookies.py +++ b/mitmproxy/net/http/cookies.py @@ -1,7 +1,7 @@ import email.utils import re import time -from typing import Tuple, List, Iterable +from typing import Iterable from mitmproxy.coretypes import multidict @@ -23,7 +23,15 @@ http://tools.ietf.org/html/rfc2965 """ -_cookie_params = {'expires', 'path', 'comment', 'max-age', 'secure', 'httponly', 'version'} +_cookie_params = { + "expires", + "path", + "comment", + "max-age", + "secure", + "httponly", + "version", +} ESCAPE = re.compile(r"([\"\\])") @@ -40,31 +48,31 @@ def _reduce_values(values): return values[-1] -TSetCookie = Tuple[str, str, CookieAttrs] -TPairs = List[List[str]] # TODO: Should be List[Tuple[str,str]]? +TSetCookie = tuple[str, str, CookieAttrs] +TPairs = list[list[str]] # TODO: Should be List[Tuple[str,str]]? def _read_until(s, start, term): """ - Read until one of the characters in term is reached. + Read until one of the characters in term is reached. """ if start == len(s): return "", start + 1 for i in range(start, len(s)): if s[i] in term: return s[start:i], i - return s[start:i + 1], i + 1 + return s[start : i + 1], i + 1 def _read_quoted_string(s, start): """ - start: offset to the first quote of the string to be read + start: offset to the first quote of the string to be read - A sort of loose super-set of the various quoted string specifications. + A sort of loose super-set of the various quoted string specifications. - RFC6265 disallows backslashes or double quotes within quoted strings. - Prior RFCs use backslashes to escape. This leaves us free to apply - backslash escaping by default and be compatible with everything. + RFC6265 disallows backslashes or double quotes within quoted strings. + Prior RFCs use backslashes to escape. This leaves us free to apply + backslash escaping by default and be compatible with everything. """ escaping = False ret = [] @@ -85,14 +93,14 @@ def _read_quoted_string(s, start): def _read_key(s, start, delims=";="): """ - Read a key - the LHS of a token/value pair in a cookie. + Read a key - the LHS of a token/value pair in a cookie. """ return _read_until(s, start, delims) def _read_value(s, start, delims): """ - Reads a value - the RHS of a token/value pair in a cookie. + Reads a value - the RHS of a token/value pair in a cookie. """ if start >= len(s): return "", start @@ -104,9 +112,9 @@ def _read_value(s, start, delims): def _read_cookie_pairs(s, off=0): """ - Read pairs of lhs=rhs values from Cookie headers. + Read pairs of lhs=rhs values from Cookie headers. - off: start offset + off: start offset """ pairs = [] @@ -128,14 +136,14 @@ def _read_cookie_pairs(s, off=0): return pairs, off -def _read_set_cookie_pairs(s: str, off=0) -> Tuple[List[TPairs], int]: +def _read_set_cookie_pairs(s: str, off=0) -> tuple[list[TPairs], int]: """ - Read pairs of lhs=rhs values from SetCookie headers while handling multiple cookies. + Read pairs of lhs=rhs values from SetCookie headers while handling multiple cookies. - off: start offset - specials: attributes that are treated specially + off: start offset + specials: attributes that are treated specially """ - cookies: List[TPairs] = [] + cookies: list[TPairs] = [] pairs: TPairs = [] while True: @@ -187,14 +195,14 @@ def _has_special(s: str) -> bool: if i in '",;\\': return True o = ord(i) - if o < 0x21 or o > 0x7e: + if o < 0x21 or o > 0x7E: return True return False def _format_pairs(pairs, specials=(), sep="; "): """ - specials: A lower-cased list of keys that will not be quoted. + specials: A lower-cased list of keys that will not be quoted. """ vals = [] for k, v in pairs: @@ -206,16 +214,13 @@ def _format_pairs(pairs, specials=(), sep="; "): def _format_set_cookie_pairs(lst): - return _format_pairs( - lst, - specials=("expires", "path") - ) + return _format_pairs(lst, specials=("expires", "path")) def parse_cookie_header(line): """ - Parse a Cookie header value. - Returns a list of (lhs, rhs) tuples. + Parse a Cookie header value. + Returns a list of (lhs, rhs) tuples. """ pairs, off_ = _read_cookie_pairs(line) return pairs @@ -230,12 +235,12 @@ def parse_cookie_headers(cookie_headers): def format_cookie_header(lst): """ - Formats a Cookie header value. + Formats a Cookie header value. """ return _format_pairs(lst) -def parse_set_cookie_header(line: str) -> List[TSetCookie]: +def parse_set_cookie_header(line: str) -> list[TSetCookie]: """ Parse a Set-Cookie header value @@ -249,15 +254,11 @@ def parse_set_cookie_header(line: str) -> List[TSetCookie]: for pairs in cookie_pairs: if pairs: cookie, *attrs = pairs - cookies.append(( - cookie[0], - cookie[1], - CookieAttrs(attrs) - )) + cookies.append((cookie[0], cookie[1], CookieAttrs(attrs))) return cookies -def parse_set_cookie_headers(headers: Iterable[str]) -> List[TSetCookie]: +def parse_set_cookie_headers(headers: Iterable[str]) -> list[TSetCookie]: rv = [] for header in headers: cookies = parse_set_cookie_header(header) @@ -265,9 +266,9 @@ def parse_set_cookie_headers(headers: Iterable[str]) -> List[TSetCookie]: return rv -def format_set_cookie_header(set_cookies: List[TSetCookie]) -> str: +def format_set_cookie_header(set_cookies: list[TSetCookie]) -> str: """ - Formats a Set-Cookie header value. + Formats a Set-Cookie header value. """ rv = [] @@ -275,9 +276,7 @@ def format_set_cookie_header(set_cookies: List[TSetCookie]) -> str: for name, value, attrs in set_cookies: pairs = [(name, value)] - pairs.extend( - attrs.fields if hasattr(attrs, "fields") else attrs - ) + pairs.extend(attrs.fields if hasattr(attrs, "fields") else attrs) rv.append(_format_set_cookie_pairs(pairs)) @@ -318,21 +317,21 @@ def refresh_set_cookie_header(c: str, delta: int) -> str: def get_expiration_ts(cookie_attrs): """ - Determines the time when the cookie will be expired. + Determines the time when the cookie will be expired. - Considering both 'expires' and 'max-age' parameters. + Considering both 'expires' and 'max-age' parameters. - Returns: timestamp of when the cookie will expire. - None, if no expiration time is set. + Returns: timestamp of when the cookie will expire. + None, if no expiration time is set. """ - if 'expires' in cookie_attrs: + if "expires" in cookie_attrs: e = email.utils.parsedate_tz(cookie_attrs["expires"]) if e: return email.utils.mktime_tz(e) - elif 'max-age' in cookie_attrs: + elif "max-age" in cookie_attrs: try: - max_age = int(cookie_attrs['Max-Age']) + max_age = int(cookie_attrs["Max-Age"]) except ValueError: pass else: @@ -344,9 +343,9 @@ def get_expiration_ts(cookie_attrs): def is_expired(cookie_attrs): """ - Determines whether a cookie has expired. + Determines whether a cookie has expired. - Returns: boolean + Returns: boolean """ exp_ts = get_expiration_ts(cookie_attrs) diff --git a/mitmproxy/net/http/headers.py b/mitmproxy/net/http/headers.py index 6673996259..e3c00994a7 100644 --- a/mitmproxy/net/http/headers.py +++ b/mitmproxy/net/http/headers.py @@ -1,20 +1,20 @@ import collections -from typing import Dict, Optional, Tuple +from typing import Optional -def parse_content_type(c: str) -> Optional[Tuple[str, str, Dict[str, str]]]: +def parse_content_type(c: str) -> Optional[tuple[str, str, dict[str, str]]]: """ - A simple parser for content-type values. Returns a (type, subtype, - parameters) tuple, where type and subtype are strings, and parameters - is a dict. If the string could not be parsed, return None. + A simple parser for content-type values. Returns a (type, subtype, + parameters) tuple, where type and subtype are strings, and parameters + is a dict. If the string could not be parsed, return None. - E.g. the following string: + E.g. the following string: - text/html; charset=UTF-8 + text/html; charset=UTF-8 - Returns: + Returns: - ("text", "html", {"charset": "UTF-8"}) + ("text", "html", {"charset": "UTF-8"}) """ parts = c.split(";", 1) ts = parts[0].split("/", 1) @@ -32,10 +32,5 @@ def parse_content_type(c: str) -> Optional[Tuple[str, str, Dict[str, str]]]: def assemble_content_type(type, subtype, parameters): if not parameters: return f"{type}/{subtype}" - params = "; ".join( - f"{k}={v}" - for k, v in parameters.items() - ) - return "{}/{}; {}".format( - type, subtype, params - ) + params = "; ".join(f"{k}={v}" for k, v in parameters.items()) + return "{}/{}; {}".format(type, subtype, params) diff --git a/mitmproxy/net/http/http1/__init__.py b/mitmproxy/net/http/http1/__init__.py index d05ef2fdc4..3049e02fb9 100644 --- a/mitmproxy/net/http/http1/__init__.py +++ b/mitmproxy/net/http/http1/__init__.py @@ -3,10 +3,13 @@ read_response_head, connection_close, expected_http_body_size, + validate_headers, ) from .assemble import ( - assemble_request, assemble_request_head, - assemble_response, assemble_response_head, + assemble_request, + assemble_request_head, + assemble_response, + assemble_response_head, assemble_body, ) @@ -16,7 +19,10 @@ "read_response_head", "connection_close", "expected_http_body_size", - "assemble_request", "assemble_request_head", - "assemble_response", "assemble_response_head", + "validate_headers", + "assemble_request", + "assemble_request_head", + "assemble_response", + "assemble_response_head", "assemble_body", ] diff --git a/mitmproxy/net/http/http1/assemble.py b/mitmproxy/net/http/http1/assemble.py index 51225f320c..4760a25824 100644 --- a/mitmproxy/net/http/http1/assemble.py +++ b/mitmproxy/net/http/http1/assemble.py @@ -2,7 +2,11 @@ def assemble_request(request): if request.data.content is None: raise ValueError("Cannot assemble flow with missing content") head = assemble_request_head(request) - body = b"".join(assemble_body(request.data.headers, [request.data.content], request.data.trailers)) + body = b"".join( + assemble_body( + request.data.headers, [request.data.content], request.data.trailers + ) + ) return head + body @@ -16,7 +20,11 @@ def assemble_response(response): if response.data.content is None: raise ValueError("Cannot assemble flow with missing content") head = assemble_response_head(response) - body = b"".join(assemble_body(response.data.headers, [response.data.content], response.data.trailers)) + body = b"".join( + assemble_body( + response.data.headers, [response.data.content], response.data.trailers + ) + ) return head + body @@ -37,7 +45,9 @@ def assemble_body(headers, body_chunks, trailers): yield b"0\r\n\r\n" else: if trailers: - raise ValueError("Sending HTTP/1.1 trailer headers requires transfer-encoding: chunked") + raise ValueError( + "Sending HTTP/1.1 trailer headers requires transfer-encoding: chunked" + ) for chunk in body_chunks: yield chunk @@ -51,7 +61,7 @@ def _assemble_request_line(request_data): return b"%s %s %s" % ( request_data.method, request_data.authority, - request_data.http_version + request_data.http_version, ) elif request_data.authority: return b"%s %s://%s%s %s" % ( @@ -59,13 +69,13 @@ def _assemble_request_line(request_data): request_data.scheme, request_data.authority, request_data.path, - request_data.http_version + request_data.http_version, ) else: return b"%s %s %s" % ( request_data.method, request_data.path, - request_data.http_version + request_data.http_version, ) diff --git a/mitmproxy/net/http/http1/read.py b/mitmproxy/net/http/http1/read.py index 4ac21698b1..1da4583e1d 100644 --- a/mitmproxy/net/http/http1/read.py +++ b/mitmproxy/net/http/http1/read.py @@ -1,6 +1,6 @@ import re import time -from typing import List, Tuple, Iterable, Optional +from typing import Iterable, Optional from mitmproxy.http import Request, Headers, Response from mitmproxy.net.http import url @@ -8,9 +8,9 @@ def get_header_tokens(headers, key): """ - Retrieve all tokens for a header key. A number of different headers - follow a pattern where each header line can containe comma-separated - tokens, and headers can be set multiple times. + Retrieve all tokens for a header key. A number of different headers + follow a pattern where each header line can containe comma-separated + tokens, and headers can be set multiple times. """ if key not in headers: return [] @@ -20,10 +20,10 @@ def get_header_tokens(headers, key): def connection_close(http_version, headers): """ - Checks the message to see if the client connection should be closed - according to RFC 2616 Section 8.1. - If we don't have a Connection header, HTTP 1.1 connections are assumed - to be persistent. + Checks the message to see if the client connection should be closed + according to RFC 2616 Section 8.1. + If we don't have a Connection header, HTTP 1.1 connections are assumed + to be persistent. """ if "connection" in headers: tokens = get_header_tokens(headers, "connection") @@ -33,24 +33,59 @@ def connection_close(http_version, headers): return False return http_version not in ( - "HTTP/1.1", b"HTTP/1.1", - "HTTP/2.0", b"HTTP/2.0", + "HTTP/1.1", + b"HTTP/1.1", + "HTTP/2.0", + b"HTTP/2.0", ) +# https://datatracker.ietf.org/doc/html/rfc7230#section-3.2: Header fields are tokens. +# "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +_valid_header_name = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-zA-Z]+$") + + +def validate_headers(headers: Headers) -> None: + """ + Validate headers to avoid request smuggling attacks. Raises a ValueError if they are malformed. + """ + + te_found = False + cl_found = False + + for (name, value) in headers.fields: + if not _valid_header_name.match(name): + raise ValueError( + f"Received an invalid header name: {name!r}. Invalid header names may introduce " + f"request smuggling vulnerabilities. Disable the validate_inbound_headers option " + f"to skip this security check." + ) + + name_lower = name.lower() + te_found = te_found or name_lower == b"transfer-encoding" + cl_found = cl_found or name_lower == b"content-length" + + if te_found and cl_found: + raise ValueError( + "Received both a Transfer-Encoding and a Content-Length header, " + "refusing as recommended in RFC 7230 Section 3.3.3. " + "See https://github.com/mitmproxy/mitmproxy/issues/4799 for details. " + "Disable the validate_inbound_headers option to skip this security check." + ) + + def expected_http_body_size( - request: Request, - response: Optional[Response] = None + request: Request, response: Optional[Response] = None ) -> Optional[int]: """ - Returns: - The expected body length: - - a positive integer, if the size is known in advance - - None, if the size in unknown in advance (chunked encoding) - - -1, if all data should be read until end of stream. - - Raises: - ValueError, if the content length header is invalid + Returns: + The expected body length: + - a positive integer, if the size is known in advance + - None, if the size in unknown in advance (chunked encoding) + - -1, if all data should be read until end of stream. + + Raises: + ValueError, if the content length header is invalid """ # Determine response size according to http://tools.ietf.org/html/rfc7230#section-3.3, which is inlined below. if not response: @@ -101,10 +136,8 @@ def expected_http_body_size( # a message downstream. # if "transfer-encoding" in headers: - if "content-length" in headers: - raise ValueError("Received both a Transfer-Encoding and a Content-Length header, " - "refusing as recommended in RFC 7230 Section 3.3.3. " - "See https://github.com/mitmproxy/mitmproxy/issues/4799 for details.") + # we should make sure that there isn't also a content-length header. + # this is already handled in validate_headers. te: str = headers["transfer-encoding"] if not te.isascii(): @@ -128,9 +161,13 @@ def expected_http_body_size( if response: return -1 else: - raise ValueError(f"Invalid request transfer encoding, message body cannot be determined reliably.") + raise ValueError( + f"Invalid request transfer encoding, message body cannot be determined reliably." + ) else: - raise ValueError(f"Unknown transfer encoding: {headers['transfer-encoding']!r}") + raise ValueError( + f"Unknown transfer encoding: {headers['transfer-encoding']!r}" + ) # 4. If a message is received without Transfer-Encoding and with # either multiple Content-Length header fields having differing @@ -181,7 +218,9 @@ def raise_if_http_version_unknown(http_version: bytes) -> None: raise ValueError(f"Unknown HTTP version: {http_version!r}") -def _read_request_line(line: bytes) -> Tuple[str, int, bytes, bytes, bytes, bytes, bytes]: +def _read_request_line( + line: bytes, +) -> tuple[str, int, bytes, bytes, bytes, bytes, bytes]: try: method, target, http_version = line.split() port: Optional[int] @@ -212,7 +251,7 @@ def _read_request_line(line: bytes) -> Tuple[str, int, bytes, bytes, bytes, byte return host, port, method, scheme, authority, path, http_version -def _read_response_line(line: bytes) -> Tuple[bytes, int, bytes]: +def _read_response_line(line: bytes) -> tuple[bytes, int, bytes]: try: parts = line.split(None, 2) if len(parts) == 2: # handle missing message gracefully @@ -229,22 +268,22 @@ def _read_response_line(line: bytes) -> Tuple[bytes, int, bytes]: def _read_headers(lines: Iterable[bytes]) -> Headers: """ - Read a set of headers. - Stop once a blank line is reached. + Read a set of headers. + Stop once a blank line is reached. - Returns: - A headers object + Returns: + A headers object - Raises: - exceptions.HttpSyntaxException + Raises: + exceptions.HttpSyntaxException """ - ret: List[Tuple[bytes, bytes]] = [] + ret: list[tuple[bytes, bytes]] = [] for line in lines: if line[0] in b" \t": if not ret: raise ValueError("Invalid headers") # continued header - ret[-1] = (ret[-1][0], ret[-1][1] + b'\r\n ' + line.strip()) + ret[-1] = (ret[-1][0], ret[-1][1] + b"\r\n " + line.strip()) else: try: name, value = line.split(b":", 1) @@ -257,7 +296,7 @@ def _read_headers(lines: Iterable[bytes]) -> Headers: return Headers(ret) -def read_request_head(lines: List[bytes]) -> Request: +def read_request_head(lines: list[bytes]) -> Request: """ Parse an HTTP request head (request line + headers) from an iterable of lines @@ -270,7 +309,9 @@ def read_request_head(lines: List[bytes]) -> Request: Raises: ValueError: The input is malformed. """ - host, port, method, scheme, authority, path, http_version = _read_request_line(lines[0]) + host, port, method, scheme, authority, path, http_version = _read_request_line( + lines[0] + ) headers = _read_headers(lines[1:]) return Request( @@ -285,11 +326,11 @@ def read_request_head(lines: List[bytes]) -> Request: content=None, trailers=None, timestamp_start=time.time(), - timestamp_end=None + timestamp_end=None, ) -def read_response_head(lines: List[bytes]) -> Response: +def read_response_head(lines: list[bytes]) -> Response: """ Parse an HTTP response head (response line + headers) from an iterable of lines diff --git a/mitmproxy/net/http/multipart.py b/mitmproxy/net/http/multipart.py index bfb9f61392..0079995875 100644 --- a/mitmproxy/net/http/multipart.py +++ b/mitmproxy/net/http/multipart.py @@ -1,6 +1,6 @@ import mimetypes import re -from typing import Tuple, List, Optional +from typing import Optional from urllib.parse import quote from mitmproxy.net.http import headers @@ -18,30 +18,34 @@ def encode(head, l): return b"" hdrs = [] for key, value in l: - file_type = mimetypes.guess_type(str(key))[0] or "text/plain; charset=utf-8" + file_type = ( + mimetypes.guess_type(str(key))[0] or "text/plain; charset=utf-8" + ) if key: - hdrs.append(b"--%b" % boundary.encode('utf-8')) + hdrs.append(b"--%b" % boundary.encode("utf-8")) disposition = b'form-data; name="%b"' % key hdrs.append(b"Content-Disposition: %b" % disposition) - hdrs.append(b"Content-Type: %b" % file_type.encode('utf-8')) - hdrs.append(b'') + hdrs.append(b"Content-Type: %b" % file_type.encode("utf-8")) + hdrs.append(b"") hdrs.append(value) - hdrs.append(b'') + hdrs.append(b"") if value is not None: # If boundary is found in value then raise ValueError - if re.search(rb"^--%b$" % re.escape(boundary.encode('utf-8')), value): + if re.search( + rb"^--%b$" % re.escape(boundary.encode("utf-8")), value + ): raise ValueError(b"boundary found in encoded string") - hdrs.append(b"--%b--\r\n" % boundary.encode('utf-8')) + hdrs.append(b"--%b--\r\n" % boundary.encode("utf-8")) temp = b"\r\n".join(hdrs) return temp -def decode(content_type: Optional[str], content: bytes) -> List[Tuple[bytes, bytes]]: +def decode(content_type: Optional[str], content: bytes) -> list[tuple[bytes, bytes]]: """ - Takes a multipart boundary encoded string and returns list of (key, value) tuples. + Takes a multipart boundary encoded string and returns list of (key, value) tuples. """ if content_type: ct = headers.parse_content_type(content_type) @@ -61,7 +65,7 @@ def decode(content_type: Optional[str], content: bytes) -> List[Tuple[bytes, byt match = rx.search(parts[1]) if match: key = match.group(1) - value = b"".join(parts[3 + parts[2:].index(b""):]) + value = b"".join(parts[3 + parts[2:].index(b"") :]) r.append((key, value)) return r return [] diff --git a/mitmproxy/net/http/status_codes.py b/mitmproxy/net/http/status_codes.py index aac38ba0b3..66e7d49ab6 100644 --- a/mitmproxy/net/http/status_codes.py +++ b/mitmproxy/net/http/status_codes.py @@ -1,5 +1,8 @@ CONTINUE = 100 SWITCHING = 101 +PROCESSING = 102 +EARLY_HINTS = 103 + OK = 200 CREATED = 201 ACCEPTED = 202 @@ -52,7 +55,8 @@ # 100 CONTINUE: "Continue", SWITCHING: "Switching Protocols", - + PROCESSING: "Processing", + EARLY_HINTS: "Early Hints", # 200 OK: "OK", CREATED: "Created", @@ -62,7 +66,6 @@ RESET_CONTENT: "Reset Content.", PARTIAL_CONTENT: "Partial Content", MULTI_STATUS: "Multi-Status", - # 300 MULTIPLE_CHOICE: "Multiple Choices", MOVED_PERMANENTLY: "Moved Permanently", @@ -72,7 +75,6 @@ USE_PROXY: "Use Proxy", # 306 not defined?? TEMPORARY_REDIRECT: "Temporary Redirect", - # 400 BAD_REQUEST: "Bad Request", UNAUTHORIZED: "Unauthorized", @@ -95,8 +97,6 @@ IM_A_TEAPOT: "I'm a teapot", NO_RESPONSE: "No Response", CLIENT_CLOSED_REQUEST: "Client Closed Request", - - # 500 INTERNAL_SERVER_ERROR: "Internal Server Error", NOT_IMPLEMENTED: "Not Implemented", @@ -105,5 +105,5 @@ GATEWAY_TIMEOUT: "Gateway Time-out", HTTP_VERSION_NOT_SUPPORTED: "HTTP Version not supported", INSUFFICIENT_STORAGE_SPACE: "Insufficient Storage Space", - NOT_EXTENDED: "Not Extended" + NOT_EXTENDED: "Not Extended", } diff --git a/mitmproxy/net/http/url.py b/mitmproxy/net/http/url.py index 17a300c27d..9468302e12 100644 --- a/mitmproxy/net/http/url.py +++ b/mitmproxy/net/http/url.py @@ -1,10 +1,10 @@ import re import urllib.parse +from collections.abc import Sequence from typing import AnyStr, Optional -from typing import Sequence -from typing import Tuple from mitmproxy.net import check + # This regex extracts & splits the host header into host and port. # Handles the edge case of IPv6 addresses containing colons. # https://bugzilla.mozilla.org/show_bug.cgi?id=45891 @@ -16,19 +16,19 @@ def parse(url): """ - URL-parsing function that checks that - - port is an integer 0-65535 - - host is a valid IDNA-encoded hostname with no null-bytes - - path is valid ASCII + URL-parsing function that checks that + - port is an integer 0-65535 + - host is a valid IDNA-encoded hostname with no null-bytes + - path is valid ASCII - Args: - A URL (as bytes or as unicode) + Args: + A URL (as bytes or as unicode) - Returns: - A (scheme, host, port, path) tuple + Returns: + A (scheme, host, port, path) tuple - Raises: - ValueError, if the URL is not properly formatted. + Raises: + ValueError, if the URL is not properly formatted. """ # FIXME: We shouldn't rely on urllib here. @@ -85,10 +85,10 @@ def unparse(scheme: str, host: str, port: int, path: str = "") -> str: return f"{scheme}://{authority}{path}" -def encode(s: Sequence[Tuple[str, str]], similar_to: str = None) -> str: +def encode(s: Sequence[tuple[str, str]], similar_to: str = None) -> str: """ - Takes a list of (key, value) tuples and returns a urlencoded string. - If similar_to is passed, the output is formatted similar to the provided urlencoded string. + Takes a list of (key, value) tuples and returns a urlencoded string. + If similar_to is passed, the output is formatted similar to the provided urlencoded string. """ remove_trailing_equal = False @@ -99,7 +99,7 @@ def encode(s: Sequence[Tuple[str, str]], similar_to: str = None) -> str: if encoded and remove_trailing_equal: encoded = encoded.replace("=&", "&") - if encoded[-1] == '=': + if encoded[-1] == "=": encoded = encoded[:-1] return encoded @@ -107,9 +107,9 @@ def encode(s: Sequence[Tuple[str, str]], similar_to: str = None) -> str: def decode(s): """ - Takes a urlencoded string and returns a list of surrogate-escaped (key, value) tuples. + Takes a urlencoded string and returns a list of surrogate-escaped (key, value) tuples. """ - return urllib.parse.parse_qsl(s, keep_blank_values=True, errors='surrogateescape') + return urllib.parse.parse_qsl(s, keep_blank_values=True, errors="surrogateescape") def quote(b: str, safe: str = "/") -> str: @@ -132,7 +132,7 @@ def unquote(s: str) -> str: def hostport(scheme: AnyStr, host: AnyStr, port: int) -> AnyStr: """ - Returns the host component, with a port specification if needed. + Returns the host component, with a port specification if needed. """ if default_port(scheme) == port: return host @@ -152,7 +152,7 @@ def default_port(scheme: AnyStr) -> Optional[int]: }.get(scheme, None) -def parse_authority(authority: AnyStr, check: bool) -> Tuple[str, Optional[int]]: +def parse_authority(authority: AnyStr, check: bool) -> tuple[str, Optional[int]]: """Extract the host and port from host header/authority information Raises: diff --git a/mitmproxy/net/http/user_agents.py b/mitmproxy/net/http/user_agents.py index d0ca2f215e..58aa21eab9 100644 --- a/mitmproxy/net/http/user_agents.py +++ b/mitmproxy/net/http/user_agents.py @@ -8,42 +8,54 @@ # A collection of (name, shortcut, string) tuples. UASTRINGS = [ - ("android", - "a", - "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Nexus 7 Build/JRO03D) AFL/01.04.02"), # noqa - ("blackberry", - "l", - "Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+"), # noqa - ("bingbot", - "b", - "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"), # noqa - ("chrome", - "c", - "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1"), # noqa - ("firefox", - "f", - "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1"), # noqa - ("googlebot", - "g", - "Googlebot/2.1 (+http://www.googlebot.com/bot.html)"), # noqa - ("ie9", - "i", - "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US)"), # noqa - ("ipad", - "p", - "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B176 Safari/7534.48.3"), # noqa - ("iphone", - "h", - "Mozilla/5.0 (iPhone; CPU iPhone OS 4_2_1 like Mac OS X) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a Safari/6533.18.5"), # noqa - ("safari", - "s", - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10"), # noqa + ( + "android", + "a", + "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Nexus 7 Build/JRO03D) AFL/01.04.02", + ), + ( + "blackberry", + "l", + "Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+", + ), + ( + "bingbot", + "b", + "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)", + ), + ( + "chrome", + "c", + "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1", + ), + ( + "firefox", + "f", + "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:14.0) Gecko/20120405 Firefox/14.0a1", + ), + ("googlebot", "g", "Googlebot/2.1 (+http://www.googlebot.com/bot.html)"), + ("ie9", "i", "Mozilla/5.0 (Windows; U; MSIE 9.0; WIndows NT 9.0; en-US)"), + ( + "ipad", + "p", + "Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko) Version/5.1 Mobile/9B176 Safari/7534.48.3", + ), + ( + "iphone", + "h", + "Mozilla/5.0 (iPhone; CPU iPhone OS 4_2_1 like Mac OS X) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a Safari/6533.18.5", # noqa + ), + ( + "safari", + "s", + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_7_3) AppleWebKit/534.55.3 (KHTML, like Gecko) Version/5.1.3 Safari/534.53.10", + ), ] def get_by_shortcut(s): """ - Retrieve a user agent entry by shortcut. + Retrieve a user agent entry by shortcut. """ for i in UASTRINGS: if s == i[1]: diff --git a/mitmproxy/net/server_spec.py b/mitmproxy/net/server_spec.py index f117774f5b..ee95ad6471 100644 --- a/mitmproxy/net/server_spec.py +++ b/mitmproxy/net/server_spec.py @@ -3,14 +3,14 @@ """ import functools import re -from typing import Tuple, Literal, NamedTuple +from typing import Literal, NamedTuple from mitmproxy.net import check class ServerSpec(NamedTuple): scheme: Literal["http", "https"] - address: Tuple[str, int] + address: tuple[str, int] server_spec_re = re.compile( @@ -22,7 +22,7 @@ class ServerSpec(NamedTuple): /? # we allow a trailing backslash, but no path $ """, - re.VERBOSE + re.VERBOSE, ) @@ -59,17 +59,14 @@ def parse(server_spec: str) -> ServerSpec: if m.group("port"): port = int(m.group("port")) else: - port = { - "http": 80, - "https": 443 - }[scheme] + port = {"http": 80, "https": 443}[scheme] if not check.is_valid_port(port): raise ValueError(f"Invalid port: {port}") return ServerSpec(scheme, (host, port)) # type: ignore -def parse_with_mode(mode: str) -> Tuple[str, ServerSpec]: +def parse_with_mode(mode: str) -> tuple[str, ServerSpec]: """ Parse a proxy mode specification, which is usually just `(reverse|upstream):server-spec`. diff --git a/mitmproxy/net/tls.py b/mitmproxy/net/tls.py index 7e85fd55e9..e2f7e35224 100644 --- a/mitmproxy/net/tls.py +++ b/mitmproxy/net/tls.py @@ -1,49 +1,40 @@ -import io -import ipaddress import os import threading from enum import Enum from functools import lru_cache from pathlib import Path -from typing import Iterable, Callable, Optional, Tuple, List, Any, BinaryIO +from typing import Any, BinaryIO, Callable, Iterable, Optional import certifi from OpenSSL.crypto import X509 from cryptography.hazmat.primitives.asymmetric import rsa -from kaitaistruct import KaitaiStream from OpenSSL import SSL, crypto from mitmproxy import certs -from mitmproxy.contrib.kaitaistruct import tls_client_hello -from mitmproxy.net import check # redeclared here for strict type checking class Method(Enum): - # TODO: just SSL attributes once https://github.com/pyca/pyopenssl/pull/985 has landed. - TLS_SERVER_METHOD = getattr(SSL, "TLS_SERVER_METHOD", 8) - TLS_CLIENT_METHOD = getattr(SSL, "TLS_CLIENT_METHOD", 9) + TLS_SERVER_METHOD = SSL.TLS_SERVER_METHOD + TLS_CLIENT_METHOD = SSL.TLS_CLIENT_METHOD -# TODO: remove once https://github.com/pyca/pyopenssl/pull/985 has landed. try: SSL._lib.TLS_server_method # type: ignore except AttributeError as e: # pragma: no cover - raise RuntimeError("Your installation of the cryptography Python package is outdated.") from e - -SSL.Context._methods.setdefault(Method.TLS_SERVER_METHOD.value, SSL._lib.TLS_server_method) # type: ignore -SSL.Context._methods.setdefault(Method.TLS_CLIENT_METHOD.value, SSL._lib.TLS_client_method) # type: ignore + raise RuntimeError( + "Your installation of the cryptography Python package is outdated." + ) from e class Version(Enum): UNBOUNDED = 0 - # TODO: just SSL attributes once https://github.com/pyca/pyopenssl/pull/985 has landed. - SSL3 = getattr(SSL, "SSL3_VERSION", 768) - TLS1 = getattr(SSL, "TLS1_VERSION", 769) - TLS1_1 = getattr(SSL, "TLS1_1_VERSION", 770) - TLS1_2 = getattr(SSL, "TLS1_2_VERSION", 771) - TLS1_3 = getattr(SSL, "TLS1_3_VERSION", 772) + SSL3 = SSL.SSL3_VERSION + TLS1 = SSL.TLS1_VERSION + TLS1_1 = SSL.TLS1_1_VERSION + TLS1_2 = SSL.TLS1_2_VERSION + TLS1_3 = SSL.TLS1_3_VERSION class Verify(Enum): @@ -53,10 +44,7 @@ class Verify(Enum): DEFAULT_MIN_VERSION = Version.TLS1_2 DEFAULT_MAX_VERSION = Version.UNBOUNDED -DEFAULT_OPTIONS = ( - SSL.OP_CIPHER_SERVER_PREFERENCE - | SSL.OP_NO_COMPRESSION -) +DEFAULT_OPTIONS = SSL.OP_CIPHER_SERVER_PREFERENCE | SSL.OP_NO_COMPRESSION class MasterSecretLogger: @@ -95,11 +83,11 @@ def make_master_secret_logger(filename: Optional[str]) -> Optional[MasterSecretL def _create_ssl_context( - *, - method: Method, - min_version: Version, - max_version: Version, - cipher_list: Optional[Iterable[str]], + *, + method: Method, + min_version: Version, + max_version: Version, + cipher_list: Optional[Iterable[str]], ) -> SSL.Context: context = SSL.Context(method.value) @@ -119,7 +107,7 @@ def _create_ssl_context( try: context.set_cipher_list(b":".join(x.encode() for x in cipher_list)) except SSL.Error as e: - raise RuntimeError("SSL cipher specification error: {e}") from e + raise RuntimeError(f"SSL cipher specification error: {e}") from e # SSLKEYLOGFILE if log_master_secret: @@ -130,16 +118,14 @@ def _create_ssl_context( @lru_cache(256) def create_proxy_server_context( - *, - min_version: Version, - max_version: Version, - cipher_list: Optional[Tuple[str, ...]], - verify: Verify, - hostname: Optional[str], - ca_path: Optional[str], - ca_pemfile: Optional[str], - client_cert: Optional[str], - alpn_protos: Optional[Tuple[bytes, ...]], + *, + min_version: Version, + max_version: Version, + cipher_list: Optional[tuple[str, ...]], + verify: Verify, + ca_path: Optional[str], + ca_pemfile: Optional[str], + client_cert: Optional[str], ) -> SSL.Context: context: SSL.Context = _create_ssl_context( method=Method.TLS_CLIENT_METHOD, @@ -147,39 +133,16 @@ def create_proxy_server_context( max_version=max_version, cipher_list=cipher_list, ) - - if verify is not Verify.VERIFY_NONE and hostname is None: - raise ValueError("Cannot validate certificate hostname without SNI") - context.set_verify(verify.value, None) - if hostname is not None: - assert isinstance(hostname, str) - # Manually enable hostname verification on the context object. - # https://wiki.openssl.org/index.php/Hostname_validation - param = SSL._lib.SSL_CTX_get0_param(context._context) # type: ignore - # Matching on the CN is disabled in both Chrome and Firefox, so we disable it, too. - # https://www.chromestatus.com/feature/4981025180483584 - SSL._lib.X509_VERIFY_PARAM_set_hostflags( # type: ignore - param, - SSL._lib.X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS | SSL._lib.X509_CHECK_FLAG_NEVER_CHECK_SUBJECT # type: ignore - ) - try: - ip: bytes = ipaddress.ip_address(hostname).packed - except ValueError: - SSL._openssl_assert( # type: ignore - SSL._lib.X509_VERIFY_PARAM_set1_host(param, hostname.encode(), 0) == 1 # type: ignore - ) - else: - SSL._openssl_assert( # type: ignore - SSL._lib.X509_VERIFY_PARAM_set1_ip(param, ip, len(ip)) == 1 # type: ignore - ) if ca_path is None and ca_pemfile is None: ca_pemfile = certifi.where() try: context.load_verify_locations(ca_pemfile, ca_path) except SSL.Error as e: - raise RuntimeError(f"Cannot load trusted certificates ({ca_pemfile=}, {ca_path=}).") from e + raise RuntimeError( + f"Cannot load trusted certificates ({ca_pemfile=}, {ca_path=})." + ) from e # Client Certs if client_cert: @@ -189,26 +152,22 @@ def create_proxy_server_context( except SSL.Error as e: raise RuntimeError(f"Cannot load TLS client certificate: {e}") from e - if alpn_protos: - # advertise application layer protocols - context.set_alpn_protos(alpn_protos) - return context @lru_cache(256) def create_client_proxy_context( - *, - min_version: Version, - max_version: Version, - cipher_list: Optional[Tuple[str, ...]], - cert: certs.Cert, - key: rsa.RSAPrivateKey, - chain_file: Optional[Path], - alpn_select_callback: Optional[Callable[[SSL.Connection, List[bytes]], Any]], - request_client_cert: bool, - extra_chain_certs: Tuple[certs.Cert, ...], - dhparams: certs.DHParams, + *, + min_version: Version, + max_version: Version, + cipher_list: Optional[tuple[str, ...]], + cert: certs.Cert, + key: rsa.RSAPrivateKey, + chain_file: Optional[Path], + alpn_select_callback: Optional[Callable[[SSL.Connection, list[bytes]], Any]], + request_client_cert: bool, + extra_chain_certs: tuple[certs.Cert, ...], + dhparams: certs.DHParams, ) -> SSL.Context: context: SSL.Context = _create_ssl_context( method=Method.TLS_SERVER_METHOD, @@ -246,17 +205,18 @@ def create_client_proxy_context( context.add_extra_chain_cert(i.to_pyopenssl()) if dhparams: - SSL._lib.SSL_CTX_set_tmp_dh(context._context, dhparams) # type: ignore + res = SSL._lib.SSL_CTX_set_tmp_dh(context._context, dhparams) # type: ignore + SSL._openssl_assert(res == 1) # type: ignore return context def accept_all( - conn_: SSL.Connection, - x509: X509, - errno: int, - err_depth: int, - is_cert_verified: int, + conn_: SSL.Connection, + x509: X509, + errno: int, + err_depth: int, + is_cert_verified: int, ) -> bool: # Return true to prevent cert verification error return True @@ -273,55 +233,4 @@ def is_tls_record_magic(d): # TLS ClientHello magic, works for SSLv3, TLSv1.0, TLSv1.1, TLSv1.2, and TLSv1.3 # http://www.moserware.com/2009/06/first-few-milliseconds-of-https.html#client-hello # https://tls13.ulfheim.net/ - return ( - len(d) == 3 and - d[0] == 0x16 and - d[1] == 0x03 and - 0x0 <= d[2] <= 0x03 - ) - - -class ClientHello: - - def __init__(self, raw_client_hello): - self._client_hello = tls_client_hello.TlsClientHello( - KaitaiStream(io.BytesIO(raw_client_hello)) - ) - - @property - def cipher_suites(self) -> List[int]: - return self._client_hello.cipher_suites.cipher_suites - - @property - def sni(self) -> Optional[str]: - if self._client_hello.extensions: - for extension in self._client_hello.extensions.extensions: - is_valid_sni_extension = ( - extension.type == 0x00 and - len(extension.body.server_names) == 1 and - extension.body.server_names[0].name_type == 0 and - check.is_valid_host(extension.body.server_names[0].host_name) - ) - if is_valid_sni_extension: - return extension.body.server_names[0].host_name.decode("ascii") - return None - - @property - def alpn_protocols(self) -> List[bytes]: - if self._client_hello.extensions: - for extension in self._client_hello.extensions.extensions: - if extension.type == 0x10: - return list(x.name for x in extension.body.alpn_protocols) - return [] - - @property - def extensions(self) -> List[Tuple[int, bytes]]: - ret = [] - if self._client_hello.extensions: - for extension in self._client_hello.extensions.extensions: - body = getattr(extension, "_raw_body", extension.body) - ret.append((extension.type, body)) - return ret - - def __repr__(self): - return f"ClientHello(sni: {self.sni}, alpn_protocols: {self.alpn_protocols})" + return len(d) == 3 and d[0] == 0x16 and d[1] == 0x03 and 0x0 <= d[2] <= 0x03 diff --git a/mitmproxy/net/udp.py b/mitmproxy/net/udp.py new file mode 100644 index 0000000000..c70647800e --- /dev/null +++ b/mitmproxy/net/udp.py @@ -0,0 +1,394 @@ +from __future__ import annotations + +import asyncio +import ipaddress +import socket +import struct +from typing import Any, Callable, Optional, Union, cast +from mitmproxy import ctx +from mitmproxy.connection import Address +from mitmproxy.utils import human + + +MAX_DATAGRAM_SIZE = 65535 - 20 + +DatagramReceivedCallback = Callable[ + [asyncio.DatagramTransport, bytes, Address, Address], None +] +""" +Callable that gets invoked when a datagram is received. +The first argument is the outgoing transport. +The second argument is the received payload. +The third argument is the source address, also referred to as `remote_addr` or `peername`. +The fourth argument is the destination address, also referred to as `local_addr` or `sockname`. +In the case of transparent server, the last argument is the original destination address. +""" + +# to make mypy happy +SockAddress = Union[tuple[str, int], tuple[str, int, int, int]] + + +class TransparentSocket(socket.socket): + SOL_IP = getattr(socket, "SOL_IP", 0) + IP_TRANSPARENT = getattr(socket, "IP_TRANSPARENT", 19) + IP_RECVORIGDSTADDR = getattr(socket, "IP_RECVORIGDSTADDR", 20) + + def __init__(self, family: socket.AddressFamily, local_addr: SockAddress) -> None: + self._recvmsg = getattr(self, "recvmsg") + if not self._recvmsg: + raise NotImplementedError( + "Transparent UDP sockets are only supporting on platforms providing recvmsg." + ) + super().__init__( + family=family, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP + ) + try: + self.setblocking(False) + self.setsockopt( + TransparentSocket.SOL_IP, TransparentSocket.IP_TRANSPARENT, 1 + ) + self.setsockopt( + TransparentSocket.SOL_IP, TransparentSocket.IP_RECVORIGDSTADDR, 1 + ) + self.bind(local_addr) + except: + self.close() + raise + + @staticmethod + def _unpack_addr(sockaddr_in: bytes) -> SockAddress: + """Converts a native sockaddr into a python tuple.""" + + (family,) = struct.unpack_from("h", sockaddr_in, 0) + if family == socket.AF_INET: + port, in4_addr, _ = struct.unpack_from("!H4s8s", sockaddr_in, 2) + return str(ipaddress.IPv4Address(in4_addr)), port + elif family == socket.AF_INET6: + port, flowinfo, in6_addr, scopeid = struct.unpack_from( + "!HL16sL", sockaddr_in, 2 + ) + return str(ipaddress.IPv6Address(in6_addr)), port, flowinfo, scopeid + else: + raise NotImplementedError(f"family {family} not implemented") + + def recvfrom( + self, bufsize: int, flags: int = 0 + ) -> tuple[bytes, tuple[SockAddress, SockAddress]]: + """Same as recvfrom, but always returns source and destination addresses.""" + + data, ancdata, _, client_addr = self._recvmsg( + bufsize, socket.CMSG_SPACE(1024), flags + ) + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if ( + cmsg_level == TransparentSocket.SOL_IP + and cmsg_type == TransparentSocket.IP_RECVORIGDSTADDR + ): + server_addr = TransparentSocket._unpack_addr(cmsg_data) + break + else: + raise OSError("recvmsg did not return th original destination address") + return data, (client_addr, server_addr) + + +class DrainableDatagramProtocol(asyncio.DatagramProtocol): + + _loop: asyncio.AbstractEventLoop + _closed: asyncio.Event + _paused: int + _can_write: asyncio.Event + _sock: socket.socket | None + + def __init__(self, loop: asyncio.AbstractEventLoop | None) -> None: + self._loop = asyncio.get_running_loop() if loop is None else loop + self._closed = asyncio.Event() + self._paused = 0 + self._can_write = asyncio.Event() + self._can_write.set() + self._sock = None + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} socket={self._sock!r}>" + + @property + def sockets(self) -> tuple[socket.socket, ...]: + return () if self._sock is None else (self._sock,) + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + self._sock = transport.get_extra_info("socket") + + def connection_lost(self, exc: Exception | None) -> None: + self._closed.set() + if exc: + ctx.log.warn(f"Connection lost on {self!r}: {exc!r}") + + def pause_writing(self) -> None: + self._paused = self._paused + 1 + if self._paused == 1: + self._can_write.clear() + + def resume_writing(self) -> None: + assert self._paused > 0 + self._paused = self._paused - 1 + if self._paused == 0: + self._can_write.set() + + async def drain(self) -> None: + await self._can_write.wait() + + def error_received(self, exc: Exception) -> None: + ctx.log.warn(f"Send/receive on {self!r} failed: {exc!r}") + + async def wait_closed(self) -> None: + await self._closed.wait() + + +class UdpServer(DrainableDatagramProtocol): + """UDP server similar to base_events.Server""" + + # _datagram_received_cb: DatagramReceivedCallback + _transport: asyncio.DatagramTransport | None + _transparent_transports: dict[Address, asyncio.DatagramTransport] | None + _local_addr: Address | None + + def __init__( + self, + datagram_received_cb: DatagramReceivedCallback, + loop: asyncio.AbstractEventLoop | None, + transparent: bool, + ) -> None: + super().__init__(loop) + self._datagram_received_cb = datagram_received_cb + self._transport = None + self._transparent_transports = {} if transparent else None + self._local_addr = None + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + if self._transport is None: + self._transport = cast(asyncio.DatagramTransport, transport) + self._local_addr = transport.get_extra_info("sockname") + super().connection_made(transport) + + async def _datagram_received_for_new_transparent_addr( + self, data: bytes, remote_addr: Address, local_addr: Address + ) -> None: + assert self._sock is not None + assert self._transparent_transports is not None + sock = socket.socket( + family=self._sock.family, type=socket.SOCK_DGRAM, proto=socket.IPPROTO_UDP + ) + try: + sock.setblocking(False) + sock.setsockopt( + TransparentSocket.SOL_IP, TransparentSocket.IP_TRANSPARENT, 1 + ) + sock.shutdown(socket.SHUT_RD) + sock.bind(local_addr) + except: + sock.close() + raise + transport, _ = await self._loop.create_datagram_endpoint( + lambda: self, sock=sock + ) + self._transparent_transports[local_addr] = cast( + asyncio.DatagramTransport, transport + ) + self._datagram_received_cb( + self._transparent_transports[local_addr], data, remote_addr, local_addr + ) + + def datagram_received(self, data: bytes, addr: Any) -> None: + assert self._transport is not None + if self._transparent_transports is None: + assert self._local_addr is not None + self._datagram_received_cb(self._transport, data, addr, self._local_addr) + else: + remote_addr, local_addr = addr + if local_addr in self._transparent_transports: + self._datagram_received_cb( + self._transparent_transports[local_addr], + data, + remote_addr, + local_addr, + ) + else: + self._loop.create_task( + self._datagram_received_for_new_transparent_addr( + data, remote_addr, local_addr + ) + ) + + def close(self) -> None: + if self._transport is not None: + self._transport.close() + if self._transparent_transports is not None: + for transport in self._transparent_transports.values(): + transport.close() + + +class DatagramReader: + + _packets: asyncio.Queue + _eof: bool + + def __init__(self) -> None: + self._packets = asyncio.Queue(42) # ~2.75MB + self._eof = False + + def feed_data(self, data: bytes, remote_addr: Address) -> None: + assert len(data) <= MAX_DATAGRAM_SIZE + if self._eof: + ctx.log.info( + f"Received UDP packet from {human.format_address(remote_addr)} after EOF." + ) + else: + try: + self._packets.put_nowait(data) + except asyncio.QueueFull: + ctx.log.debug( + f"Dropped UDP packet from {human.format_address(remote_addr)}." + ) + + def feed_eof(self) -> None: + self._eof = True + try: + self._packets.put_nowait(b"") + except asyncio.QueueFull: + pass + + async def read(self, n: int) -> bytes: + assert n >= MAX_DATAGRAM_SIZE + if self._eof: + try: + return self._packets.get_nowait() + except asyncio.QueueEmpty: + return b"" + else: + return await self._packets.get() + + +class DatagramWriter: + + _transport: asyncio.DatagramTransport + _remote_addr: Address + _reader: DatagramReader | None + _closed: asyncio.Event | None + + def __init__( + self, + transport: asyncio.DatagramTransport, + remote_addr: Address, + reader: DatagramReader | None = None, + ) -> None: + """ + Create a new datagram writer around the given transport. + Specify a reader to prevent closing the transport and instead only feed EOF to the reader. + """ + self._transport = transport + self._remote_addr = remote_addr + proto = transport.get_protocol() + assert isinstance(proto, DrainableDatagramProtocol) + self._reader = reader + self._closed = asyncio.Event() if reader is not None else None + + @property + def _protocol(self) -> DrainableDatagramProtocol: + return cast(DrainableDatagramProtocol, self._transport.get_protocol()) + + def write(self, data: bytes) -> None: + self._transport.sendto(data, self._remote_addr) + + def write_eof(self) -> None: + raise NotImplementedError("UDP does not support half-closing.") + + def get_extra_info(self, name: str, default: Any = None) -> Any: + if name == "peername": + return self._remote_addr + else: + return self._transport.get_extra_info(name, default) + + def close(self) -> None: + if self._closed is None: + self._transport.close() + else: + self._closed.set() + if self._reader is not None: + self._reader.feed_eof() + + async def wait_closed(self) -> None: + if self._closed is None: + await self._protocol.wait_closed() + else: + await self._closed.wait() + + async def drain(self) -> None: + await self._protocol.drain() + + +class UdpClient(DrainableDatagramProtocol): + """UDP protocol for upstream connections.""" + + _reader: DatagramReader + + def __init__(self, reader: DatagramReader, loop: asyncio.AbstractEventLoop | None): + super().__init__(loop) + self._reader = reader + + def datagram_received(self, data: bytes, remote_addr: Address) -> None: + self._reader.feed_data(data, remote_addr) + + def connection_lost(self, exc: Exception | None) -> None: + self._reader.feed_eof() + super().connection_lost(exc) + + +async def start_server( + datagram_received_cb: DatagramReceivedCallback, + host: str, + port: int, + *, + transparent: bool = False, +) -> UdpServer: + """UDP variant of asyncio.start_server.""" + + loop = asyncio.get_running_loop() + + if transparent: + addrinfos = await loop.getaddrinfo(host, port) + exception = OSError(f"getaddrinfo for host '{host}' failed") + for family, _, _, _, addr in addrinfos: + try: + sock = TransparentSocket(family=family, local_addr=addr) + except OSError as exc: + exception = exc + else: + break + else: + raise exception + else: + sock = None + + _, protocol = await loop.create_datagram_endpoint( + lambda: UdpServer(datagram_received_cb, loop, transparent), + local_addr=(host, port), + sock=sock, + ) + assert isinstance(protocol, UdpServer) + return protocol + + +async def open_connection( + host: str, port: int, *, local_addr: Optional[Address] = None +) -> tuple[DatagramReader, DatagramWriter]: + """UDP variant of asyncio.open_connection.""" + + loop = asyncio.get_running_loop() + reader = DatagramReader() + transport, _ = await loop.create_datagram_endpoint( + lambda: UdpClient(reader, loop), local_addr=local_addr, remote_addr=(host, port) + ) + writer = DatagramWriter( + cast(asyncio.DatagramTransport, transport), + remote_addr=transport.get_extra_info("peername"), + ) + return reader, writer diff --git a/mitmproxy/options.py b/mitmproxy/options.py index 14c90d2200..c9aa9c3335 100644 --- a/mitmproxy/options.py +++ b/mitmproxy/options.py @@ -1,4 +1,5 @@ -from typing import Optional, Sequence +from collections.abc import Sequence +from typing import Optional from mitmproxy import optmanager @@ -10,32 +11,38 @@ class Options(optmanager.OptManager): - def __init__(self, **kwargs) -> None: super().__init__() self.add_option( - "server", bool, True, - "Start a proxy server. Enabled by default." + "server", bool, True, "Start a proxy server. Enabled by default." ) self.add_option( - "showhost", bool, False, - "Use the Host header to construct URLs for display." + "showhost", + bool, + False, + "Use the Host header to construct URLs for display.", ) # Proxy options self.add_option( - "add_upstream_certs_to_client_chain", bool, False, + "add_upstream_certs_to_client_chain", + bool, + False, """ Add all certificates of the upstream server to the certificate chain that will be served to the proxy client, as extras. - """ + """, ) self.add_option( - "confdir", str, CONF_DIR, - "Location of the default mitmproxy configuration files." + "confdir", + str, + CONF_DIR, + "Location of the default mitmproxy configuration files.", ) self.add_option( - "certs", Sequence[str], [], + "certs", + Sequence[str], + [], """ SSL certificates of the form "[domain=]path". The domain may include a wildcard, and is equal to "*" if not specified. The file at path @@ -43,114 +50,143 @@ def __init__(self, **kwargs) -> None: PEM, it is used, else the default key in the conf dir is used. The PEM file should contain the full certificate chain, with the leaf certificate as the first entry. - """ + """, ) self.add_option( - "cert_passphrase", Optional[str], None, + "cert_passphrase", + Optional[str], + None, """ Passphrase for decrypting the private key provided in the --cert option. Note that passing cert_passphrase on the command line makes your passphrase visible in your system's process list. Specify it in config.yaml to avoid this. - """ + """, ) self.add_option( - "ciphers_client", Optional[str], None, - "Set supported ciphers for client <-> mitmproxy connections using OpenSSL syntax." + "ciphers_client", + Optional[str], + None, + "Set supported ciphers for client <-> mitmproxy connections using OpenSSL syntax.", ) self.add_option( - "ciphers_server", Optional[str], None, - "Set supported ciphers for mitmproxy <-> server connections using OpenSSL syntax." + "ciphers_server", + Optional[str], + None, + "Set supported ciphers for mitmproxy <-> server connections using OpenSSL syntax.", ) self.add_option( - "client_certs", Optional[str], None, - "Client certificate file or directory." + "client_certs", Optional[str], None, "Client certificate file or directory." ) self.add_option( - "ignore_hosts", Sequence[str], [], + "ignore_hosts", + Sequence[str], + [], """ Ignore host and forward all traffic without processing it. In transparent mode, it is recommended to use an IP address (range), not the hostname. In regular mode, only SSL traffic is ignored and the hostname should be used. The supplied value is interpreted as a regular expression and matched on the ip or the hostname. - """ + """, ) + self.add_option("allow_hosts", Sequence[str], [], "Opposite of --ignore-hosts.") + self.add_option("listen_host", str, "", "Address to bind proxy to.") + self.add_option("listen_port", int, LISTEN_PORT, "Proxy service port.") self.add_option( - "allow_hosts", Sequence[str], [], - "Opposite of --ignore-hosts." - ) - self.add_option( - "listen_host", str, "", - "Address to bind proxy to." - ) - self.add_option( - "listen_port", int, LISTEN_PORT, - "Proxy service port." - ) - self.add_option( - "mode", str, "regular", + "mode", + str, + "regular", """ Mode can be "regular", "transparent", "socks5", "reverse:SPEC", or "upstream:SPEC". For reverse and upstream proxy modes, SPEC is host specification in the form of "http[s]://host[:port]". - """ + """, ) self.add_option( - "upstream_cert", bool, True, - "Connect to upstream server to look up certificate details." + "upstream_cert", + bool, + True, + "Connect to upstream server to look up certificate details.", ) self.add_option( - "http2", bool, True, - "Enable/disable HTTP/2 support. " - "HTTP/2 support is enabled by default.", + "http2", + bool, + True, + "Enable/disable HTTP/2 support. " "HTTP/2 support is enabled by default.", + ) + self.add_option( + "http2_ping_keepalive", + int, + 58, + """ + Send a PING frame if an HTTP/2 connection is idle for more than + the specified number of seconds to prevent the remote site from closing it. + Set to 0 to disable this feature. + """, ) self.add_option( - "websocket", bool, True, + "websocket", + bool, + True, "Enable/disable WebSocket support. " "WebSocket support is enabled by default.", ) self.add_option( - "rawtcp", bool, True, + "rawtcp", + bool, + True, "Enable/disable raw TCP connections. " - "TCP connections are enabled by default. " + "TCP connections are enabled by default. ", ) self.add_option( - "ssl_insecure", bool, False, - "Do not verify upstream server SSL/TLS certificates." + "ssl_insecure", + bool, + False, + "Do not verify upstream server SSL/TLS certificates.", ) self.add_option( - "ssl_verify_upstream_trusted_confdir", Optional[str], None, + "ssl_verify_upstream_trusted_confdir", + Optional[str], + None, """ Path to a directory of trusted CA certificates for upstream server verification prepared using the c_rehash tool. - """ + """, ) self.add_option( - "ssl_verify_upstream_trusted_ca", Optional[str], None, - "Path to a PEM formatted trusted CA certificate." + "ssl_verify_upstream_trusted_ca", + Optional[str], + None, + "Path to a PEM formatted trusted CA certificate.", ) self.add_option( - "tcp_hosts", Sequence[str], [], + "tcp_hosts", + Sequence[str], + [], """ Generic TCP SSL proxy mode for all hosts that match the pattern. Similar to --ignore-hosts, but SSL connections are intercepted. The communication contents are printed to the log in verbose mode. - """ + """, ) self.add_option( - "content_view_lines_cutoff", int, CONTENT_VIEW_LINES_CUTOFF, + "content_view_lines_cutoff", + int, + CONTENT_VIEW_LINES_CUTOFF, """ Flow content view lines limit. Limit is enabled by default to speedup flows browsing. - """ + """, ) self.add_option( - "key_size", int, KEY_SIZE, + "key_size", + int, + KEY_SIZE, """ TLS key size for certificates and CA. - """ + """, ) self.update(**kwargs) diff --git a/mitmproxy/optmanager.py b/mitmproxy/optmanager.py index 00374c4ea4..6264ce6f9e 100644 --- a/mitmproxy/optmanager.py +++ b/mitmproxy/optmanager.py @@ -1,14 +1,15 @@ -import collections import contextlib -import blinker -import blinker._saferef -import pprint import copy +from collections.abc import Sequence +from dataclasses import dataclass import functools import os -import typing +import pprint import textwrap +from typing import Any, Optional, TextIO, Union +import blinker +import blinker._saferef import ruamel.yaml from mitmproxy import exceptions @@ -27,10 +28,10 @@ class _Option: def __init__( self, name: str, - typespec: typing.Union[type, object], # object for Optional[x], which is not a type. - default: typing.Any, + typespec: Union[type, object], # object for Optional[x], which is not a type. + default: Any, help: str, - choices: typing.Optional[typing.Sequence[str]] + choices: Optional[Sequence[str]], ) -> None: typecheck.check_option_type(name, default, typespec) self.name = name @@ -47,14 +48,14 @@ def __repr__(self): def default(self): return copy.deepcopy(self._default) - def current(self) -> typing.Any: + def current(self) -> Any: if self.value is unset: v = self.default else: v = self.value return copy.deepcopy(v) - def set(self, value: typing.Any) -> None: + def set(self, value: Any) -> None: typecheck.check_option_type(self.name, value, self.typespec) self.value = value @@ -71,41 +72,45 @@ def __eq__(self, other) -> bool: return True def __deepcopy__(self, _): - o = _Option( - self.name, self.typespec, self.default, self.help, self.choices - ) + o = _Option(self.name, self.typespec, self.default, self.help, self.choices) if self.has_changed(): o.value = self.current() return o +@dataclass +class _UnconvertedStrings: + val: list[str] + + class OptManager: """ - OptManager is the base class from which Options objects are derived. + OptManager is the base class from which Options objects are derived. - .changed is a blinker Signal that triggers whenever options are - updated. If any handler in the chain raises an exceptions.OptionsError - exception, all changes are rolled back, the exception is suppressed, - and the .errored signal is notified. + .changed is a blinker Signal that triggers whenever options are + updated. If any handler in the chain raises an exceptions.OptionsError + exception, all changes are rolled back, the exception is suppressed, + and the .errored signal is notified. - Optmanager always returns a deep copy of options to ensure that - mutation doesn't change the option state inadvertently. + Optmanager always returns a deep copy of options to ensure that + mutation doesn't change the option state inadvertently. """ + def __init__(self): - self.deferred: typing.Dict[str, typing.List[str]] = {} + self.deferred: dict[str, Any] = {} self.changed = blinker.Signal() self.errored = blinker.Signal() # Options must be the last attribute here - after that, we raise an # error for attribute assignment to unknown options. - self._options: typing.Dict[str, typing.Any] = {} + self._options: dict[str, Any] = {} def add_option( self, name: str, - typespec: typing.Union[type, object], - default: typing.Any, + typespec: Union[type, object], + default: Any, help: str, - choices: typing.Optional[typing.Sequence[str]] = None + choices: Optional[Sequence[str]] = None, ) -> None: self._options[name] = _Option(name, typespec, default, help, choices) self.changed.send(self, updated={name}) @@ -126,11 +131,11 @@ def rollback(self, updated, reraise=False): def subscribe(self, func, opts): """ - Subscribe a callable to the .changed signal, but only for a - specified list of options. The callable should accept arguments - (options, updated), and may raise an OptionsError. + Subscribe a callable to the .changed signal, but only for a + specified list of options. The callable should accept arguments + (options, updated), and may raise an OptionsError. - The event will automatically be unsubscribed if the callable goes out of scope. + The event will automatically be unsubscribed if the callable goes out of scope. """ for i in opts: if i not in self._options: @@ -159,7 +164,7 @@ def __eq__(self, other): return self._options == other._options return False - def __deepcopy__(self, memodict = None): + def __deepcopy__(self, memodict=None): o = OptManager() o.__dict__["_options"] = copy.deepcopy(self._options, memodict) return o @@ -193,7 +198,7 @@ def __contains__(self, k): def reset(self): """ - Restore defaults for all options. + Restore defaults for all options. """ for o in self._options.values(): o.reset() @@ -201,8 +206,8 @@ def reset(self): def update_known(self, **kwargs): """ - Update and set all known options from kwargs. Returns a dictionary - of unknown options. + Update and set all known options from kwargs. Returns a dictionary + of unknown options. """ known, unknown = {}, {} for k, v in kwargs.items(): @@ -229,20 +234,21 @@ def update(self, **kwargs): def setter(self, attr): """ - Generate a setter for a given attribute. This returns a callable - taking a single argument. + Generate a setter for a given attribute. This returns a callable + taking a single argument. """ if attr not in self._options: raise KeyError("No such option: %s" % attr) def setter(x): setattr(self, attr, x) + return setter def toggler(self, attr): """ - Generate a toggler for a boolean attribute. This returns a callable - that takes no arguments. + Generate a toggler for a boolean attribute. This returns a callable + that takes no arguments. """ if attr not in self._options: raise KeyError("No such option: %s" % attr) @@ -252,22 +258,23 @@ def toggler(self, attr): def toggle(): setattr(self, attr, not getattr(self, attr)) + return toggle - def default(self, option: str) -> typing.Any: + def default(self, option: str) -> Any: return self._options[option].default def has_changed(self, option): """ - Has the option changed from the default? + Has the option changed from the default? """ return self._options[option].has_changed() def merge(self, opts): """ - Merge a dict of options into this object. Options that have None - value are ignored. Lists and tuples are appended to the current - option value. + Merge a dict of options into this object. Options that have None + value are ignored. Lists and tuples are appended to the current + option value. """ toset = {} for k, v in opts.items(): @@ -283,65 +290,92 @@ def __repr__(self): if "\n" in options: options = "\n " + options + "\n" return "{mod}.{cls}({{{options}}})".format( - mod=type(self).__module__, - cls=type(self).__name__, - options=options + mod=type(self).__module__, cls=type(self).__name__, options=options ) - def set(self, *spec, defer=False): + def set(self, *specs: str, defer: bool = False) -> None: """ - Takes a list of set specification in standard form (option=value). - Options that are known are updated immediately. If defer is true, - options that are not known are deferred, and will be set once they - are added. + Takes a list of set specification in standard form (option=value). + Options that are known are updated immediately. If defer is true, + options that are not known are deferred, and will be set once they + are added. + + May raise an `OptionsError` if a value is malformed or an option is unknown and defer is False. """ - vals = {} - unknown: typing.Dict[str, typing.List[str]] = collections.defaultdict(list) - for i in spec: - parts = i.split("=", maxsplit=1) - if len(parts) == 1: - optname, optval = parts[0], None + # First, group specs by option name. + unprocessed: dict[str, list[str]] = {} + for spec in specs: + if "=" in spec: + name, value = spec.split("=", maxsplit=1) + unprocessed.setdefault(name, []).append(value) else: - optname, optval = parts[0], parts[1] - if optname in self._options: - vals[optname] = self.parse_setval(self._options[optname], optval, vals.get(optname)) - else: - unknown[optname].append(optval) + unprocessed.setdefault(spec, []) + + # Second, convert values to the correct type. + processed: dict[str, Any] = {} + for name in list(unprocessed.keys()): + if name in self._options: + processed[name] = self._parse_setval( + self._options[name], unprocessed.pop(name) + ) + + # Third, stash away unrecognized options or complain about them. if defer: - self.deferred.update(unknown) - elif unknown: - raise exceptions.OptionsError("Unknown options: %s" % ", ".join(unknown.keys())) - self.update(**vals) + self.deferred.update( + {k: _UnconvertedStrings(v) for k, v in unprocessed.items()} + ) + elif unprocessed: + raise exceptions.OptionsError( + f"Unknown option(s): {', '.join(unprocessed)}" + ) + + # Finally, apply updated options. + self.update(**processed) - def process_deferred(self): + def process_deferred(self) -> None: """ - Processes options that were deferred in previous calls to set, and - have since been added. + Processes options that were deferred in previous calls to set, and + have since been added. """ - update = {} - for optname, optvals in self.deferred.items(): + update: dict[str, Any] = {} + for optname, value in self.deferred.items(): if optname in self._options: - for optval in optvals: - optval = self.parse_setval(self._options[optname], optval, update.get(optname)) - update[optname] = optval + if isinstance(value, _UnconvertedStrings): + value = self._parse_setval(self._options[optname], value.val) + update[optname] = value self.update(**update) for k in update.keys(): del self.deferred[k] - def parse_setval(self, o: _Option, optstr: typing.Optional[str], currentvalue: typing.Any) -> typing.Any: + def _parse_setval(self, o: _Option, values: list[str]) -> Any: """ - Convert a string to a value appropriate for the option type. + Convert a string to a value appropriate for the option type. """ - if o.typespec in (str, typing.Optional[str]): + if o.typespec == Sequence[str]: + return values + if len(values) > 1: + raise exceptions.OptionsError( + f"Received multiple values for {o.name}: {values}" + ) + + optstr: Optional[str] + if values: + optstr = values[0] + else: + optstr = None + + if o.typespec in (str, Optional[str]): + if o.typespec == str and optstr is None: + raise exceptions.OptionsError(f"Option is required: {o.name}") return optstr - elif o.typespec in (int, typing.Optional[int]): + elif o.typespec in (int, Optional[int]): if optstr: try: return int(optstr) except ValueError: - raise exceptions.OptionsError("Not an integer: %s" % optstr) + raise exceptions.OptionsError(f"Not an integer: {optstr}") elif o.typespec == int: - raise exceptions.OptionsError("Option is required: %s" % o.name) + raise exceptions.OptionsError(f"Option is required: {o.name}") else: return None elif o.typespec == bool: @@ -353,22 +387,14 @@ def parse_setval(self, o: _Option, optstr: typing.Optional[str], currentvalue: t return False else: raise exceptions.OptionsError( - "Boolean must be \"true\", \"false\", or have the value " "omitted (a synonym for \"true\")." + 'Boolean must be "true", "false", or have the value omitted (a synonym for "true").' ) - elif o.typespec == typing.Sequence[str]: - if not optstr: - return [] - else: - if currentvalue: - return currentvalue + [optstr] - else: - return [optstr] - raise NotImplementedError("Unsupported option type: %s", o.typespec) + raise NotImplementedError(f"Unsupported option type: {o.typespec}") def make_parser(self, parser, optname, metavar=None, short=None): """ - Auto-Create a command-line parser entry for a named option. If the - option does not exist, it is ignored. + Auto-Create a command-line parser entry for a named option. If the + option does not exist, it is ignored. """ if optname not in self._options: return @@ -399,14 +425,9 @@ def mkf(l, s): action="store_false", dest=optname, ) - g.add_argument( - *onf, - action="store_true", - dest=optname, - help=o.help - ) + g.add_argument(*onf, action="store_true", dest=optname, help=o.help) parser.set_defaults(**{optname: None}) - elif o.typespec in (int, typing.Optional[int]): + elif o.typespec in (int, Optional[int]): parser.add_argument( *flags, action="store", @@ -415,7 +436,7 @@ def mkf(l, s): help=o.help, metavar=metavar, ) - elif o.typespec in (str, typing.Optional[str]): + elif o.typespec in (str, Optional[str]): parser.add_argument( *flags, action="store", @@ -423,9 +444,9 @@ def mkf(l, s): dest=optname, help=o.help, metavar=metavar, - choices=o.choices + choices=o.choices, ) - elif o.typespec == typing.Sequence[str]: + elif o.typespec == Sequence[str]: parser.add_argument( *flags, action="append", @@ -439,9 +460,9 @@ def mkf(l, s): raise ValueError("Unsupported option type: %s", o.typespec) -def dump_defaults(opts, out: typing.TextIO): +def dump_defaults(opts, out: TextIO): """ - Dumps an annotated file with all options. + Dumps an annotated file with all options. """ # Sort data s = ruamel.yaml.comments.CommentedMap() @@ -461,11 +482,11 @@ def dump_defaults(opts, out: typing.TextIO): return ruamel.yaml.YAML().dump(s, out) -def dump_dicts(opts, keys: typing.List[str]=None): +def dump_dicts(opts, keys: list[str] = None): """ - Dumps the options into a list of dict object. + Dumps the options into a list of dict object. - Return: A list like: { "anticache": { type: "bool", default: false, value: true, help: "help text"} } + Return: A list like: { "anticache": { type: "bool", default: false, value: true, help: "help text"} } """ options_dict = {} keys = keys if keys else opts.keys() @@ -473,11 +494,11 @@ def dump_dicts(opts, keys: typing.List[str]=None): o = opts._options[k] t = typecheck.typespec_to_str(o.typespec) option = { - 'type': t, - 'default': o.default, - 'value': o.current(), - 'help': o.help, - 'choices': o.choices + "type": t, + "default": o.default, + "value": o.current(), + "help": o.help, + "choices": o.choices, } options_dict[k] = option return options_dict @@ -487,14 +508,14 @@ def parse(text): if not text: return {} try: - yaml = ruamel.yaml.YAML(typ='unsafe', pure=True) + yaml = ruamel.yaml.YAML(typ="unsafe", pure=True) data = yaml.load(text) except ruamel.yaml.error.YAMLError as v: if hasattr(v, "problem_mark"): snip = v.problem_mark.get_snippet() raise exceptions.OptionsError( - "Config error at line %s:\n%s\n%s" % - (v.problem_mark.line + 1, snip, v.problem) + "Config error at line %s:\n%s\n%s" + % (v.problem_mark.line + 1, snip, v.problem) ) else: raise exceptions.OptionsError("Could not parse options.") @@ -507,8 +528,8 @@ def parse(text): def load(opts: OptManager, text: str) -> None: """ - Load configuration from text, over-writing options already set in - this object. May raise OptionsError if the config file is invalid. + Load configuration from text, over-writing options already set in + this object. May raise OptionsError if the config file is invalid. """ data = parse(text) opts.update_defer(**data) @@ -516,9 +537,9 @@ def load(opts: OptManager, text: str) -> None: def load_paths(opts: OptManager, *paths: str) -> None: """ - Load paths in order. Each path takes precedence over the previous - path. Paths that don't exist are ignored, errors raise an - OptionsError. + Load paths in order. Each path takes precedence over the previous + path. Paths that don't exist are ignored, errors raise an + OptionsError. """ for p in paths: p = os.path.expanduser(p) @@ -527,27 +548,25 @@ def load_paths(opts: OptManager, *paths: str) -> None: try: txt = f.read() except UnicodeDecodeError as e: - raise exceptions.OptionsError( - f"Error reading {p}: {e}" - ) + raise exceptions.OptionsError(f"Error reading {p}: {e}") try: load(opts, txt) except exceptions.OptionsError as e: - raise exceptions.OptionsError( - f"Error reading {p}: {e}" - ) + raise exceptions.OptionsError(f"Error reading {p}: {e}") -def serialize(opts: OptManager, file: typing.TextIO, text: str, defaults: bool = False) -> None: +def serialize( + opts: OptManager, file: TextIO, text: str, defaults: bool = False +) -> None: """ - Performs a round-trip serialization. If text is not None, it is - treated as a previous serialization that should be modified - in-place. - - - If "defaults" is False, only options with non-default values are - serialized. Default values in text are preserved. - - Unknown options in text are removed. - - Raises OptionsError if text is invalid. + Performs a round-trip serialization. If text is not None, it is + treated as a previous serialization that should be modified + in-place. + + - If "defaults" is False, only options with non-default values are + serialized. Default values in text are preserved. + - Unknown options in text are removed. + - Raises OptionsError if text is invalid. """ data = parse(text) for k in opts.keys(): @@ -560,11 +579,11 @@ def serialize(opts: OptManager, file: typing.TextIO, text: str, defaults: bool = ruamel.yaml.YAML().dump(data, file) -def save(opts: OptManager, path: str, defaults: bool =False) -> None: +def save(opts: OptManager, path: str, defaults: bool = False) -> None: """ - Save to path. If the destination file exists, modify it in-place. + Save to path. If the destination file exists, modify it in-place. - Raises OptionsError if the existing data is corrupt. + Raises OptionsError if the existing data is corrupt. """ path = os.path.expanduser(path) if os.path.exists(path) and os.path.isfile(path): @@ -572,9 +591,7 @@ def save(opts: OptManager, path: str, defaults: bool =False) -> None: try: data = f.read() except UnicodeDecodeError as e: - raise exceptions.OptionsError( - f"Error trying to modify {path}: {e}" - ) + raise exceptions.OptionsError(f"Error trying to modify {path}: {e}") else: data = "" diff --git a/mitmproxy/platform/__init__.py b/mitmproxy/platform/__init__.py index 7e690789a3..e6fdcd7c8d 100644 --- a/mitmproxy/platform/__init__.py +++ b/mitmproxy/platform/__init__.py @@ -1,7 +1,7 @@ import re import socket import sys -from typing import Callable, Optional, Tuple +from typing import Callable, Optional def init_transparent_mode() -> None: @@ -10,7 +10,7 @@ def init_transparent_mode() -> None: """ -original_addr: Optional[Callable[[socket.socket], Tuple[str, int]]] +original_addr: Optional[Callable[[socket.socket], tuple[str, int]]] """ Get the original destination for the given socket. This function will be None if transparent mode is not supported. @@ -37,7 +37,4 @@ def init_transparent_mode() -> None: else: original_addr = None -__all__ = [ - "original_addr", - "init_transparent_mode" -] +__all__ = ["original_addr", "init_transparent_mode"] diff --git a/mitmproxy/platform/linux.py b/mitmproxy/platform/linux.py index 2ab6922ccd..ffbaa82775 100644 --- a/mitmproxy/platform/linux.py +++ b/mitmproxy/platform/linux.py @@ -1,6 +1,5 @@ import socket import struct -import typing import os # Python's socket module does not have these constants @@ -8,7 +7,7 @@ SOL_IPV6 = 41 -def original_addr(csock: socket.socket) -> typing.Tuple[str, int]: +def original_addr(csock: socket.socket) -> tuple[str, int]: # Get the original destination on Linux. # In theory, this can be done using the following syscalls: # sock.getsockopt(socket.SOL_IP, SO_ORIGINAL_DST, 16) diff --git a/mitmproxy/platform/osx.py b/mitmproxy/platform/osx.py index 9b150679c5..b2ed9068c3 100644 --- a/mitmproxy/platform/osx.py +++ b/mitmproxy/platform/osx.py @@ -27,10 +27,13 @@ def original_addr(csock): else: raise RuntimeError("Error getting pfctl state: " + repr(e)) else: - insufficient_priv = "sudo: a password is required" in stxt.decode(errors="replace") + insufficient_priv = "sudo: a password is required" in stxt.decode( + errors="replace" + ) if insufficient_priv: raise RuntimeError( "Insufficient privileges to access pfctl. " - "See https://mitmproxy.org/docs/latest/howto-transparent/#macos for details.") + "See https://mitmproxy.org/docs/latest/howto-transparent/#macos for details." + ) return pf.lookup(peer[0], peer[1], stxt) diff --git a/mitmproxy/platform/pf.py b/mitmproxy/platform/pf.py index e274202436..baf674030f 100644 --- a/mitmproxy/platform/pf.py +++ b/mitmproxy/platform/pf.py @@ -4,10 +4,10 @@ def lookup(address, port, s): """ - Parse the pfctl state output s, to look up the destination host - matching the client (address, port). + Parse the pfctl state output s, to look up the destination host + matching the client (address, port). - Returns an (address, port) tuple, or None. + Returns an (address, port) tuple, or None. """ # We may get an ipv4-mapped ipv6 address here, e.g. ::ffff:127.0.0.1. # Those still appear as "127.0.0.1" in the table, so we need to strip the prefix. diff --git a/mitmproxy/platform/windows.py b/mitmproxy/platform/windows.py index b447a75179..969c655415 100644 --- a/mitmproxy/platform/windows.py +++ b/mitmproxy/platform/windows.py @@ -1,3 +1,5 @@ +import collections +import collections.abc import contextlib import ctypes import ctypes.wintypes @@ -9,20 +11,12 @@ import socketserver import threading import time -import typing +from collections.abc import Callable +from typing import Any, ClassVar, Optional -import click -import collections -import collections.abc import pydivert import pydivert.consts -if typing.TYPE_CHECKING: - class WindowsError(OSError): - @property - def winerror(self) -> int: - return 42 - REDIRECT_API_HOST = "127.0.0.1" REDIRECT_API_PORT = 8085 @@ -30,8 +24,11 @@ def winerror(self) -> int: ########################## # Resolver -def read(rfile: io.BufferedReader) -> typing.Any: + +def read(rfile: io.BufferedReader) -> Any: x = rfile.readline().strip() + if not x: + return None return json.loads(x) @@ -59,8 +56,8 @@ def _connect(self): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((REDIRECT_API_HOST, REDIRECT_API_PORT)) - self.wfile = self.sock.makefile('wb') - self.rfile = self.sock.makefile('rb') + self.wfile = self.sock.makefile("wb") + self.rfile = self.sock.makefile("rb") write(os.getpid(), self.wfile) def original_addr(self, csock: socket.socket): @@ -74,7 +71,7 @@ def original_addr(self, csock: socket.socket): if addr is None: raise RuntimeError("Cannot resolve original destination.") return tuple(addr) - except (EOFError, OSError): + except (EOFError, OSError, AttributeError): self._connect() return self.original_addr(csock) @@ -89,11 +86,15 @@ def handle(self): proxifier: TransparentProxy = self.server.proxifier try: pid: int = read(self.rfile) + if pid is None: + return with proxifier.exempt(pid): while True: - client = tuple(read(self.rfile)) + c = read(self.rfile) + if c is None: + return try: - server = proxifier.client_server_map[client] + server = proxifier.client_server_map[tuple(c)] except KeyError: server = None write(server, self.wfile) @@ -102,7 +103,6 @@ def handle(self): class APIServer(socketserver.ThreadingMixIn, socketserver.TCPServer): - def __init__(self, proxifier, *args, **kwargs): super().__init__(*args, **kwargs) self.proxifier = proxifier @@ -126,14 +126,14 @@ def __init__(self, proxifier, *args, **kwargs): # https://msdn.microsoft.com/en-us/library/windows/desktop/aa366896(v=vs.85).aspx class MIB_TCP6ROW_OWNER_PID(ctypes.Structure): _fields_ = [ - ('ucLocalAddr', IN6_ADDR), - ('dwLocalScopeId', ctypes.wintypes.DWORD), - ('dwLocalPort', ctypes.wintypes.DWORD), - ('ucRemoteAddr', IN6_ADDR), - ('dwRemoteScopeId', ctypes.wintypes.DWORD), - ('dwRemotePort', ctypes.wintypes.DWORD), - ('dwState', ctypes.wintypes.DWORD), - ('dwOwningPid', ctypes.wintypes.DWORD), + ("ucLocalAddr", IN6_ADDR), + ("dwLocalScopeId", ctypes.wintypes.DWORD), + ("dwLocalPort", ctypes.wintypes.DWORD), + ("ucRemoteAddr", IN6_ADDR), + ("dwRemoteScopeId", ctypes.wintypes.DWORD), + ("dwRemotePort", ctypes.wintypes.DWORD), + ("dwState", ctypes.wintypes.DWORD), + ("dwOwningPid", ctypes.wintypes.DWORD), ] @@ -141,8 +141,8 @@ class MIB_TCP6ROW_OWNER_PID(ctypes.Structure): def MIB_TCP6TABLE_OWNER_PID(size): class _MIB_TCP6TABLE_OWNER_PID(ctypes.Structure): _fields_ = [ - ('dwNumEntries', ctypes.wintypes.DWORD), - ('table', MIB_TCP6ROW_OWNER_PID * size) + ("dwNumEntries", ctypes.wintypes.DWORD), + ("table", MIB_TCP6ROW_OWNER_PID * size), ] return _MIB_TCP6TABLE_OWNER_PID() @@ -155,12 +155,12 @@ class _MIB_TCP6TABLE_OWNER_PID(ctypes.Structure): # https://msdn.microsoft.com/en-us/library/windows/desktop/aa366913(v=vs.85).aspx class MIB_TCPROW_OWNER_PID(ctypes.Structure): _fields_ = [ - ('dwState', ctypes.wintypes.DWORD), - ('ucLocalAddr', IN4_ADDR), - ('dwLocalPort', ctypes.wintypes.DWORD), - ('ucRemoteAddr', IN4_ADDR), - ('dwRemotePort', ctypes.wintypes.DWORD), - ('dwOwningPid', ctypes.wintypes.DWORD), + ("dwState", ctypes.wintypes.DWORD), + ("ucLocalAddr", IN4_ADDR), + ("dwLocalPort", ctypes.wintypes.DWORD), + ("ucRemoteAddr", IN4_ADDR), + ("dwRemotePort", ctypes.wintypes.DWORD), + ("dwOwningPid", ctypes.wintypes.DWORD), ] @@ -168,8 +168,8 @@ class MIB_TCPROW_OWNER_PID(ctypes.Structure): def MIB_TCPTABLE_OWNER_PID(size): class _MIB_TCPTABLE_OWNER_PID(ctypes.Structure): _fields_ = [ - ('dwNumEntries', ctypes.wintypes.DWORD), - ('table', MIB_TCPROW_OWNER_PID * size) + ("dwNumEntries", ctypes.wintypes.DWORD), + ("table", MIB_TCPROW_OWNER_PID * size), ] return _MIB_TCPTABLE_OWNER_PID() @@ -209,10 +209,10 @@ def _refresh_ipv4(self): False, socket.AF_INET, TCP_TABLE_OWNER_PID_CONNECTIONS, - 0 + 0, ) if ret == 0: - for row in self._tcp.table[:self._tcp.dwNumEntries]: + for row in self._tcp.table[: self._tcp.dwNumEntries]: local_ip = socket.inet_ntop(socket.AF_INET, bytes(row.ucLocalAddr)) local_port = socket.htons(row.dwLocalPort) self._map[(local_ip, local_port)] = row.dwOwningPid @@ -221,7 +221,9 @@ def _refresh_ipv4(self): # no need to update size, that's already done. self._refresh_ipv4() else: - raise RuntimeError("[IPv4] Unknown GetExtendedTcpTable return code: %s" % ret) + raise RuntimeError( + "[IPv4] Unknown GetExtendedTcpTable return code: %s" % ret + ) def _refresh_ipv6(self): ret = ctypes.windll.iphlpapi.GetExtendedTcpTable( @@ -230,10 +232,10 @@ def _refresh_ipv6(self): False, socket.AF_INET6, TCP_TABLE_OWNER_PID_CONNECTIONS, - 0 + 0, ) if ret == 0: - for row in self._tcp6.table[:self._tcp6.dwNumEntries]: + for row in self._tcp6.table[: self._tcp6.dwNumEntries]: local_ip = socket.inet_ntop(socket.AF_INET6, bytes(row.ucLocalAddr)) local_port = socket.htons(row.dwLocalPort) self._map[(local_ip, local_port)] = row.dwOwningPid @@ -242,10 +244,12 @@ def _refresh_ipv6(self): # no need to update size, that's already done. self._refresh_ipv6() else: - raise RuntimeError("[IPv6] Unknown GetExtendedTcpTable return code: %s" % ret) + raise RuntimeError( + "[IPv6] Unknown GetExtendedTcpTable return code: %s" % ret + ) -def get_local_ip() -> typing.Optional[str]: +def get_local_ip() -> Optional[str]: # Auto-Detect local IP. This is required as re-injecting to 127.0.0.1 does not work. # https://stackoverflow.com/questions/166506/finding-local-ip-addresses-using-pythons-stdlib s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) @@ -258,7 +262,7 @@ def get_local_ip() -> typing.Optional[str]: s.close() -def get_local_ip6(reachable: str) -> typing.Optional[str]: +def get_local_ip6(reachable: str) -> Optional[str]: # The same goes for IPv6, with the added difficulty that .connect() fails if # the target network is not reachable. s = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) @@ -277,10 +281,10 @@ class Redirect(threading.Thread): def __init__( self, - handle: typing.Callable[[pydivert.Packet], None], + handle: Callable[[pydivert.Packet], None], filter: str, layer: pydivert.Layer = pydivert.Layer.NETWORK, - flags: pydivert.Flag = 0 + flags: pydivert.Flag = 0, ) -> None: self.handle = handle self.windivert = pydivert.WinDivert(filter, layer, flags=flags) @@ -294,7 +298,7 @@ def run(self): while True: try: packet = self.windivert.recv() - except WindowsError as e: + except OSError as e: if e.winerror == 995: return else: @@ -305,27 +309,25 @@ def run(self): def shutdown(self): self.windivert.close() - def recv(self) -> typing.Optional[pydivert.Packet]: + def recv(self) -> Optional[pydivert.Packet]: """ Convenience function that receives a packet from the passed handler and handles error codes. If the process has been shut down, None is returned. """ try: return self.windivert.recv() - except WindowsError as e: - if e.winerror == 995: + except OSError as e: + if e.winerror == 995: # type: ignore return None else: raise class RedirectLocal(Redirect): - trusted_pids: typing.Set[int] + trusted_pids: set[int] def __init__( - self, - redirect_request: typing.Callable[[pydivert.Packet], None], - filter: str + self, redirect_request: Callable[[pydivert.Packet], None], filter: str ) -> None: self.tcp_connections = TcpConnectionTable() self.trusted_pids = set() @@ -350,12 +352,13 @@ def handle(self, packet): self.windivert.send(packet, recalculate_checksum=True) -TConnection = typing.Tuple[str, int] +TConnection = tuple[str, int] class ClientServerMap: """A thread-safe LRU dict.""" - connection_cache_size: typing.ClassVar[int] = 65536 + + connection_cache_size: ClassVar[int] = 65536 def __init__(self): self._lock = threading.Lock() @@ -413,9 +416,10 @@ class TransparentProxy: 192.168.0.42:4242 simultaneously. This could be mitigated by introducing unique "meta-addresses" which mitmproxy sees, but this would remove the correct client info from mitmproxy. """ - local: typing.Optional[RedirectLocal] = None + + local: Optional[RedirectLocal] = None # really weird linting error here. - forward: typing.Optional[Redirect] = None # noqa + forward: Optional[Redirect] = None # noqa response: Redirect icmp: Redirect @@ -429,13 +433,12 @@ def __init__( local: bool = True, forward: bool = True, proxy_port: int = 8080, - filter: typing.Optional[str] = "tcp.DstPort == 80 or tcp.DstPort == 443", + filter: Optional[str] = "tcp.DstPort == 80 or tcp.DstPort == 443", ) -> None: self.proxy_port = proxy_port self.filter = ( filter - or - f"tcp.DstPort != {proxy_port} and tcp.DstPort != {REDIRECT_API_PORT} and tcp.DstPort < 49152" + or f"tcp.DstPort != {proxy_port} and tcp.DstPort != {REDIRECT_API_PORT} and tcp.DstPort < 49152" ) self.ipv4_address = get_local_ip() @@ -443,21 +446,18 @@ def __init__( # print(f"IPv4: {self.ipv4_address}, IPv6: {self.ipv6_address}") self.client_server_map = ClientServerMap() - self.api = APIServer(self, (REDIRECT_API_HOST, REDIRECT_API_PORT), APIRequestHandler) + self.api = APIServer( + self, (REDIRECT_API_HOST, REDIRECT_API_PORT), APIRequestHandler + ) self.api_thread = threading.Thread(target=self.api.serve_forever) self.api_thread.daemon = True if forward: self.forward = Redirect( - self.redirect_request, - self.filter, - pydivert.Layer.NETWORK_FORWARD + self.redirect_request, self.filter, pydivert.Layer.NETWORK_FORWARD ) if local: - self.local = RedirectLocal( - self.redirect_request, - self.filter - ) + self.local = RedirectLocal(self.redirect_request, self.filter) # The proxy server responds to the client. To the client, # this response should look like it has been sent by the real target @@ -470,11 +470,7 @@ def __init__( # If we don't do this, our proxy machine may send an ICMP redirect to the client, # which instructs the client to directly connect to the real gateway # if they are on the same network. - self.icmp = Redirect( - lambda _: None, - "icmp", - flags=pydivert.Flag.DROP - ) + self.icmp = Redirect(lambda _: None, "icmp", flags=pydivert.Flag.DROP) @classmethod def setup(cls): @@ -556,43 +552,56 @@ def exempt(self, pid: int): self.local.trusted_pids.remove(pid) -@click.group() -def cli(): - pass - - -@cli.command() -@click.option("--local/--no-local", default=True, - help="Redirect the host's own traffic.") -@click.option("--forward/--no-forward", default=True, - help="Redirect traffic that's forwarded by the host.") -@click.option("--filter", type=str, metavar="WINDIVERT_FILTER", - help="Custom WinDivert interception rule.") -@click.option("-p", "--proxy-port", type=int, metavar="8080", default=8080, - help="The port mitmproxy is listening on.") -def redirect(**options): - """Redirect flows to mitmproxy.""" - proxy = TransparentProxy(**options) - proxy.start() - print(f" * Redirection active.") - print(f" Filter: {proxy.request_filter}") - try: - while True: - time.sleep(1) - except KeyboardInterrupt: - print(" * Shutting down...") - proxy.shutdown() - print(" * Shut down.") - - -@cli.command() -def connections(): - """List all TCP connections and the associated PIDs.""" - connections = TcpConnectionTable() - connections.refresh() - for (ip, port), pid in connections.items(): - print(f"{ip}:{port} -> {pid}") - - if __name__ == "__main__": + import click + + @click.group() + def cli(): + pass + + @cli.command() + @click.option( + "--local/--no-local", default=True, help="Redirect the host's own traffic." + ) + @click.option( + "--forward/--no-forward", + default=True, + help="Redirect traffic that's forwarded by the host.", + ) + @click.option( + "--filter", + type=str, + metavar="WINDIVERT_FILTER", + help="Custom WinDivert interception rule.", + ) + @click.option( + "-p", + "--proxy-port", + type=int, + metavar="8080", + default=8080, + help="The port mitmproxy is listening on.", + ) + def redirect(**options): + """Redirect flows to mitmproxy.""" + proxy = TransparentProxy(**options) + proxy.start() + print(f" * Redirection active.") + print(f" Filter: {proxy.filter}") + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + print(" * Shutting down...") + proxy.shutdown() + print(" * Shut down.") + + @cli.command() + def connections(): + """List all TCP connections and the associated PIDs.""" + connections = TcpConnectionTable() + connections.refresh() + for (ip, port), pid in connections.items(): + print(f"{ip}:{port} -> {pid}") + cli() diff --git a/mitmproxy/proxy/commands.py b/mitmproxy/proxy/commands.py index 8f07990052..388abf9fe8 100644 --- a/mitmproxy/proxy/commands.py +++ b/mitmproxy/proxy/commands.py @@ -39,10 +39,22 @@ def __repr__(self): return f"{type(self).__name__}({repr(x)})" +class RequestWakeup(Command): + """ + Request a `Wakeup` event after the specified amount of seconds. + """ + + delay: float + + def __init__(self, delay: float): + self.delay = delay + + class ConnectionCommand(Command): """ Commands involving a specific connection """ + connection: Connection def __init__(self, connection: Connection): @@ -53,6 +65,7 @@ class SendData(ConnectionCommand): """ Send data to a remote peer """ + data: bytes def __init__(self, connection: Connection, data: bytes): @@ -68,6 +81,7 @@ class OpenConnection(ConnectionCommand): """ Open a new connection """ + connection: Server blocking = True @@ -77,6 +91,7 @@ class CloseConnection(ConnectionCommand): Close a connection. If the client connection is closed, all other connections will ultimately be closed during cleanup. """ + half_close: bool """ If True, only close our half of the connection by sending a FIN packet. @@ -94,6 +109,7 @@ class StartHook(Command, mitmproxy.hooks.Hook): Start an event hook in the mitmproxy core. This triggers a particular function (derived from the class name) in all addons. """ + name = "" blocking = True @@ -108,6 +124,7 @@ class GetSocket(ConnectionCommand): Get the underlying socket. This should really never be used, but is required to implement transparent mode. """ + blocking = True @@ -115,7 +132,11 @@ class Log(Command): message: str level: str - def __init__(self, message: str, level: Literal["error", "warn", "info", "alert", "debug"] = "info"): + def __init__( + self, + message: str, + level: Literal["error", "warn", "info", "alert", "debug"] = "info", + ): self.message = message self.level = level diff --git a/mitmproxy/proxy/context.py b/mitmproxy/proxy/context.py index 0114c00c50..5edb977c25 100644 --- a/mitmproxy/proxy/context.py +++ b/mitmproxy/proxy/context.py @@ -1,4 +1,4 @@ -from typing import List, TYPE_CHECKING +from typing import TYPE_CHECKING from mitmproxy import connection from mitmproxy.options import Options @@ -9,13 +9,26 @@ class Context: """ - The context object provided to each `mitmproxy.proxy.layer.Layer` by its parent layer. + The context object provided to each protocol layer in the proxy core. """ client: connection.Client + """The client connection.""" server: connection.Server + """ + The server connection. + + For practical reasons this attribute is always set, even if there is not server connection yet. + In this case the server address is `None`. + """ options: Options - layers: List["mitmproxy.proxy.layer.Layer"] + """ + Provides access to options for proxy layers. Not intended for use by addons, use `mitmproxy.ctx.options` instead. + """ + layers: list["mitmproxy.proxy.layer.Layer"] + """ + The protocol layer stack. + """ def __init__( self, @@ -24,7 +37,9 @@ def __init__( ) -> None: self.client = client self.options = options - self.server = connection.Server(None) + self.server = connection.Server( + None, transport_protocol=client.transport_protocol + ) self.layers = [] def fork(self) -> "Context": diff --git a/mitmproxy/proxy/events.py b/mitmproxy/proxy/events.py index 7cbf266618..b767483f0e 100644 --- a/mitmproxy/proxy/events.py +++ b/mitmproxy/proxy/events.py @@ -4,9 +4,9 @@ The counterpart to events are commands. """ import socket -import typing import warnings from dataclasses import dataclass, is_dataclass +from typing import Any, Generic, Optional, TypeVar from mitmproxy import flow from mitmproxy.proxy import commands @@ -27,7 +27,6 @@ class Start(Event): Every layer initially receives a start event. This is useful to emit events on startup. """ - pass @dataclass @@ -35,6 +34,7 @@ class ConnectionEvent(Event): """ All events involving connection IO. """ + connection: Connection @@ -43,6 +43,7 @@ class DataReceived(ConnectionEvent): """ Remote has sent some data. """ + data: bytes def __repr__(self): @@ -54,7 +55,6 @@ class ConnectionClosed(ConnectionEvent): """ Remote has closed a connection. """ - pass class CommandCompleted(Event): @@ -62,8 +62,9 @@ class CommandCompleted(Event): Emitted when a command has been finished, e.g. when the master has replied or when we have established a server connection. """ + command: commands.Command - reply: typing.Any + reply: Any def __new__(cls, *args, **kwargs): if cls is CommandCompleted: @@ -74,28 +75,34 @@ def __new__(cls, *args, **kwargs): def __init_subclass__(cls, **kwargs): command_cls = cls.__annotations__.get("command", None) valid_command_subclass = ( - isinstance(command_cls, type) - and issubclass(command_cls, commands.Command) - and command_cls is not commands.Command + isinstance(command_cls, type) + and issubclass(command_cls, commands.Command) + and command_cls is not commands.Command ) if not valid_command_subclass: - warnings.warn(f"{command_cls} needs a properly annotated command attribute.", RuntimeWarning) + warnings.warn( + f"{command_cls} needs a properly annotated command attribute.", + RuntimeWarning, + ) if command_cls in command_reply_subclasses: other = command_reply_subclasses[command_cls] - warnings.warn(f"Two conflicting subclasses for {command_cls}: {cls} and {other}", RuntimeWarning) + warnings.warn( + f"Two conflicting subclasses for {command_cls}: {cls} and {other}", + RuntimeWarning, + ) command_reply_subclasses[command_cls] = cls def __repr__(self): return f"Reply({repr(self.command)}, {repr(self.reply)})" -command_reply_subclasses: typing.Dict[commands.Command, typing.Type[CommandCompleted]] = {} +command_reply_subclasses: dict[commands.Command, type[CommandCompleted]] = {} @dataclass(repr=False) class OpenConnectionCompleted(CommandCompleted): command: commands.OpenConnection - reply: typing.Optional[str] + reply: Optional[str] """error message""" @@ -111,13 +118,23 @@ class GetSocketCompleted(CommandCompleted): reply: socket.socket -T = typing.TypeVar('T') +T = TypeVar("T") @dataclass -class MessageInjected(Event, typing.Generic[T]): +class MessageInjected(Event, Generic[T]): """ The user has injected a custom WebSocket/TCP/... message. """ + flow: flow.Flow message: T + + +@dataclass +class Wakeup(CommandCompleted): + """ + Event sent to layers that requested a wakeup using RequestWakeup. + """ + + command: commands.RequestWakeup diff --git a/mitmproxy/proxy/layer.py b/mitmproxy/proxy/layer.py index ef0881d629..c1a1c3d0f4 100644 --- a/mitmproxy/proxy/layer.py +++ b/mitmproxy/proxy/layer.py @@ -5,14 +5,14 @@ import textwrap from abc import abstractmethod from dataclasses import dataclass -from typing import Optional, List, ClassVar, Deque, NamedTuple, Generator, Any, TypeVar +from typing import Any, ClassVar, Generator, NamedTuple, Optional, TypeVar from mitmproxy.connection import Connection from mitmproxy.proxy import commands, events from mitmproxy.proxy.commands import Command, StartHook from mitmproxy.proxy.context import Context -T = TypeVar('T') +T = TypeVar("T") CommandGenerator = Generator[Command, Any, T] """ A function annotated with CommandGenerator[bool] may yield commands and ultimately return a boolean value. @@ -23,6 +23,7 @@ class Paused(NamedTuple): """ State of a layer that's paused because it is waiting for a command reply. """ + command: commands.Command generator: CommandGenerator @@ -47,6 +48,7 @@ def _handle_event(self, event): Technically this is very similar to how coroutines are implemented. """ + __last_debug_message: ClassVar[str] = "" context: Context _paused: Optional[Paused] @@ -54,7 +56,7 @@ def _handle_event(self, event): If execution is currently paused, this attribute stores the paused coroutine and the command for which we are expecting a reply. """ - _paused_event_queue: Deque[events.Event] + _paused_event_queue: collections.deque[events.Event] """ All events that have occurred since execution was paused. These will be replayed to ._child_layer once we resume. @@ -95,10 +97,7 @@ def __debug(self, message): message = message[:256] + "…" else: Layer.__last_debug_message = message - return commands.Log( - textwrap.indent(message, self.debug), - "debug" - ) + return commands.Log(textwrap.indent(message, self.debug), "debug") @property def stack_pos(self) -> str: @@ -108,7 +107,7 @@ def stack_pos(self) -> str: except ValueError: return repr(self) else: - return " >> ".join(repr(x) for x in self.context.layers[:idx + 1]) + return " >> ".join(repr(x) for x in self.context.layers[: idx + 1]) @abstractmethod def _handle_event(self, event: events.Event) -> CommandGenerator[None]: @@ -119,8 +118,8 @@ def handle_event(self, event: events.Event) -> CommandGenerator[None]: if self._paused: # did we just receive the reply we were waiting for? pause_finished = ( - isinstance(event, events.CommandCompleted) and - event.command is self._paused.command + isinstance(event, events.CommandCompleted) + and event.command is self._paused.command ) if self.debug is not None: yield self.__debug(f"{'>>' if pause_finished else '>!'} {event}") @@ -227,14 +226,16 @@ def __continue(self, event: events.CommandCompleted): yield from self.__process(command_generator) -mevents = events # alias here because autocomplete above should not have aliased version. +mevents = ( + events # alias here because autocomplete above should not have aliased version. +) class NextLayer(Layer): layer: Optional[Layer] """The next layer. To be set by an addon.""" - events: List[mevents.Event] + events: list[mevents.Event] """All events that happened before a decision was made.""" _ask_on_start: bool @@ -262,7 +263,10 @@ def _handle_event(self, event: mevents.Event): # We receive new data. Let's find out if we can determine the next layer now? if self._ask_on_start and isinstance(event, events.Start): yield from self._ask() - elif isinstance(event, mevents.ConnectionClosed) and event.connection == self.context.client: + elif ( + isinstance(event, mevents.ConnectionClosed) + and event.connection == self.context.client + ): # If we have not determined the next protocol yet and the client already closes the connection, # we abort everything. yield commands.CloseConnection(self.context.client) @@ -304,7 +308,8 @@ def data_server(self): def _data(self, connection: Connection): data = ( - e.data for e in self.events + e.data + for e in self.events if isinstance(e, mevents.DataReceived) and e.connection == connection ) return b"".join(data) @@ -317,4 +322,5 @@ class NextLayerHook(StartHook): (by default, this is done by mitmproxy.addons.NextLayer) """ + data: NextLayer diff --git a/mitmproxy/proxy/layers/__init__.py b/mitmproxy/proxy/layers/__init__.py index a6b837bab3..55553b258c 100644 --- a/mitmproxy/proxy/layers/__init__.py +++ b/mitmproxy/proxy/layers/__init__.py @@ -1,4 +1,5 @@ from . import modes +from .dns import DNSLayer from .http import HttpLayer from .tcp import TCPLayer from .tls import ClientTLSLayer, ServerTLSLayer @@ -6,8 +7,10 @@ __all__ = [ "modes", + "DNSLayer", "HttpLayer", "TCPLayer", - "ClientTLSLayer", "ServerTLSLayer", + "ClientTLSLayer", + "ServerTLSLayer", "WebsocketLayer", ] diff --git a/mitmproxy/proxy/layers/dns.py b/mitmproxy/proxy/layers/dns.py new file mode 100644 index 0000000000..d76c1abfd8 --- /dev/null +++ b/mitmproxy/proxy/layers/dns.py @@ -0,0 +1,113 @@ +from dataclasses import dataclass +import struct + +from mitmproxy import dns, flow +from mitmproxy import connection +from mitmproxy.proxy import commands, events, layer +from mitmproxy.proxy.context import Context +from mitmproxy.proxy.utils import expect + + +@dataclass +class DnsRequestHook(commands.StartHook): + """ + A DNS query has been received. + """ + + flow: dns.DNSFlow + + +@dataclass +class DnsResponseHook(commands.StartHook): + """ + A DNS response has been received or set. + """ + + flow: dns.DNSFlow + + +@dataclass +class DnsErrorHook(commands.StartHook): + """ + A DNS error has occurred. + """ + + flow: dns.DNSFlow + + +class DNSLayer(layer.Layer): + """ + Layer that handles resolving DNS queries. + """ + + flow: dns.DNSFlow + + def __init__(self, context: Context): + super().__init__(context) + self.flow = dns.DNSFlow(self.context.client, self.context.server, live=True) + + def handle_request(self, msg: dns.Message) -> layer.CommandGenerator[None]: + self.flow.request = msg # if already set, continue and query upstream again + yield DnsRequestHook( + self.flow + ) # give hooks a chance to change the request or produce a response + if self.flow.response: + yield from self.handle_response(self.flow.response) + elif not self.flow.server_conn.address: + yield from self.handle_error("No hook has set a response.") + else: + if ( + self.flow.server_conn.state is connection.ConnectionState.CLOSED + ): # we need an upstream connection + err = yield commands.OpenConnection(self.flow.server_conn) + if err: + yield from self.handle_error(str(err)) + return # cannot recover from this + yield commands.SendData(self.flow.server_conn, self.flow.request.packed) + + def handle_response(self, msg: dns.Message) -> layer.CommandGenerator[None]: + self.flow.response = msg + yield DnsResponseHook(self.flow) + if self.flow.response: # allows the response hook to suppress an answer + yield commands.SendData(self.context.client, self.flow.response.packed) + + def handle_error(self, err: str) -> layer.CommandGenerator[None]: + self.flow.error = flow.Error(err) + yield DnsErrorHook(self.flow) + + @expect(events.Start) + def state_start(self, _) -> layer.CommandGenerator[None]: + self._handle_event = self.state_query + yield from () + + @expect(events.DataReceived, events.ConnectionClosed) + def state_query(self, event: events.Event) -> layer.CommandGenerator[None]: + assert isinstance(event, events.ConnectionEvent) + from_client = event.connection is self.context.client + + if isinstance(event, events.DataReceived): + try: + msg = dns.Message.unpack(event.data) + except struct.error as e: + yield commands.Log(f"{event.connection} sent an invalid message: {e}") + else: + if from_client: + yield from self.handle_request(msg) + else: + yield from self.handle_response(msg) + + elif isinstance(event, events.ConnectionClosed): + other_conn = self.flow.server_conn if from_client else self.context.client + if other_conn.state is not connection.ConnectionState.CLOSED: + yield commands.CloseConnection(other_conn) + self._handle_event = self.state_done + self.flow.live = False + + else: + raise AssertionError(f"Unexpected event: {event}") + + @expect(events.DataReceived, events.ConnectionClosed) + def state_done(self, _) -> layer.CommandGenerator[None]: + yield from () + + _handle_event = state_start diff --git a/mitmproxy/proxy/layers/http/__init__.py b/mitmproxy/proxy/layers/http/__init__.py index 319b394e86..08d6d7fb3b 100644 --- a/mitmproxy/proxy/layers/http/__init__.py +++ b/mitmproxy/proxy/layers/http/__init__.py @@ -2,7 +2,7 @@ import enum import time from dataclasses import dataclass -from typing import DefaultDict, Dict, List, Optional, Tuple, Union +from typing import Optional, Union import wsproto.handshake from mitmproxy import flow, http @@ -17,10 +17,28 @@ from mitmproxy.utils import human from mitmproxy.websocket import WebSocketData from ._base import HttpCommand, HttpConnection, ReceiveHttp, StreamId -from ._events import HttpEvent, RequestData, RequestEndOfMessage, RequestHeaders, RequestProtocolError, RequestTrailers, \ - ResponseData, ResponseEndOfMessage, ResponseHeaders, ResponseProtocolError, ResponseTrailers -from ._hooks import HttpConnectHook, HttpErrorHook, HttpRequestHeadersHook, HttpRequestHook, HttpResponseHeadersHook, \ - HttpResponseHook, HttpConnectUpstreamHook # noqa +from ._events import ( + HttpEvent, + RequestData, + RequestEndOfMessage, + RequestHeaders, + RequestProtocolError, + RequestTrailers, + ResponseData, + ResponseEndOfMessage, + ResponseHeaders, + ResponseProtocolError, + ResponseTrailers, +) +from ._hooks import ( # noqa + HttpConnectHook, + HttpConnectUpstreamHook, + HttpErrorHook, + HttpRequestHeadersHook, + HttpRequestHook, + HttpResponseHeadersHook, + HttpResponseHook, +) from ._http1 import Http1Client, Http1Connection, Http1Server from ._http2 import Http2Client, Http2Server from ...context import Context @@ -48,8 +66,9 @@ class GetHttpConnection(HttpCommand): """ Open an HTTP Connection. This may not actually open a connection, but return an existing HTTP connection instead. """ + blocking = True - address: Tuple[str, int] + address: tuple[str, int] tls: bool via: Optional[server_spec.ServerSpec] @@ -59,19 +78,16 @@ def __hash__(self): def connection_spec_matches(self, connection: Connection) -> bool: return ( isinstance(connection, Server) - and - self.address == connection.address - and - self.tls == connection.tls - and - self.via == connection.via + and self.address == connection.address + and self.tls == connection.tls + and self.via == connection.via ) @dataclass class GetHttpConnectionCompleted(events.CommandCompleted): command: GetHttpConnection - reply: Union[Tuple[None, str], Tuple[Connection, None]] + reply: Union[tuple[None, str], tuple[Connection, None]] """connection object, error message""" @@ -80,6 +96,7 @@ class RegisterHttpConnection(HttpCommand): """ Register that a HTTP connection attempt has been completed. """ + connection: Connection err: Optional[str] @@ -93,6 +110,13 @@ def __repr__(self) -> str: return f"Send({self.event})" +@dataclass +class DropStream(HttpCommand): + """Signal to the HTTP layer that this stream is done processing and can be dropped from memory.""" + + stream_id: StreamId + + class HttpStream(layer.Layer): request_body_buf: bytes response_body_buf: bytes @@ -132,22 +156,24 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: self.client_state = self.state_wait_for_request_headers elif isinstance(event, (RequestProtocolError, ResponseProtocolError)): yield from self.handle_protocol_error(event) - elif isinstance(event, (RequestHeaders, RequestData, RequestTrailers, RequestEndOfMessage)): + elif isinstance( + event, (RequestHeaders, RequestData, RequestTrailers, RequestEndOfMessage) + ): yield from self.client_state(event) else: yield from self.server_state(event) @expect(RequestHeaders) - def state_wait_for_request_headers(self, event: RequestHeaders) -> layer.CommandGenerator[None]: + def state_wait_for_request_headers( + self, event: RequestHeaders + ) -> layer.CommandGenerator[None]: if not event.replay_flow: - self.flow = http.HTTPFlow( - self.context.client, - self.context.server - ) + self.flow = http.HTTPFlow(self.context.client, self.context.server) else: self.flow = event.replay_flow self.flow.request = event.request + self.flow.live = True if err := validate_request(self.mode, self.flow.request): self.flow.response = http.Response.make(502, str(err)) @@ -166,11 +192,17 @@ def state_wait_for_request_headers(self, event: RequestHeaders) -> layer.Command elif not self.flow.request.host: # We need to extract destination information from the host header. try: - host, port = url.parse_authority(self.flow.request.host_header or "", check=True) + host, port = url.parse_authority( + self.flow.request.host_header or "", check=True + ) except ValueError: yield SendHttp( - ResponseProtocolError(self.stream_id, "HTTP request has no host header, destination unknown.", 400), - self.context.client + ResponseProtocolError( + self.stream_id, + "HTTP request has no host header, destination unknown.", + 400, + ), + self.context.client, ) self.client_state = self.state_errored return @@ -179,7 +211,9 @@ def state_wait_for_request_headers(self, event: RequestHeaders) -> layer.Command port = 443 if self.context.client.tls else 80 self.flow.request.data.host = host self.flow.request.data.port = port - self.flow.request.scheme = "https" if self.context.client.tls else "http" + self.flow.request.scheme = ( + "https" if self.context.client.tls else "http" + ) if self.mode is HTTPMode.regular and not self.flow.request.is_http2: # Set the request target to origin-form for HTTP/1, some servers don't support absolute-form requests. @@ -187,7 +221,10 @@ def state_wait_for_request_headers(self, event: RequestHeaders) -> layer.Command self.flow.request.authority = "" # update host header in reverse proxy mode - if self.context.options.mode.startswith("reverse:") and not self.context.options.keep_host_header: + if ( + self.context.options.mode.startswith("reverse:") + and not self.context.options.keep_host_header + ): assert self.context.server.address self.flow.request.host_header = url.hostport( "https" if self.context.server.tls else "http", @@ -205,7 +242,9 @@ def state_wait_for_request_headers(self, event: RequestHeaders) -> layer.Command if self.flow.request.headers.get("expect", "").lower() == "100-continue": continue_response = http.Response.make(100) continue_response.headers.clear() - yield SendHttp(ResponseHeaders(self.stream_id, continue_response), self.context.client) + yield SendHttp( + ResponseHeaders(self.stream_id, continue_response), self.context.client + ) self.flow.request.headers.pop("expect") if self.flow.request.stream: @@ -216,19 +255,24 @@ def state_wait_for_request_headers(self, event: RequestHeaders) -> layer.Command def start_request_stream(self) -> layer.CommandGenerator[None]: if self.flow.response: - raise NotImplementedError("Can't set a response and enable streaming at the same time.") + raise NotImplementedError( + "Can't set a response and enable streaming at the same time." + ) ok = yield from self.make_server_connection() if not ok: + self.client_state = self.state_errored return yield SendHttp( RequestHeaders(self.stream_id, self.flow.request, end_stream=False), - self.context.server + self.context.server, ) yield commands.Log(f"Streaming request to {self.flow.request.host}.") self.client_state = self.state_stream_request_body @expect(RequestData, RequestTrailers, RequestEndOfMessage) - def state_stream_request_body(self, event: Union[RequestData, RequestEndOfMessage]) -> layer.CommandGenerator[None]: + def state_stream_request_body( + self, event: Union[RequestData, RequestEndOfMessage] + ) -> layer.CommandGenerator[None]: if isinstance(event, RequestData): if callable(self.flow.request.stream): chunks = self.flow.request.stream(event.data) @@ -244,10 +288,14 @@ def state_stream_request_body(self, event: Union[RequestData, RequestEndOfMessag elif isinstance(event, RequestEndOfMessage): if callable(self.flow.request.stream): chunks = self.flow.request.stream(b"") - if isinstance(chunks, bytes): + if chunks == b"": + chunks = [] + elif isinstance(chunks, bytes): chunks = [chunks] for chunk in chunks: - yield SendHttp(RequestData(self.stream_id, chunk), self.context.server) + yield SendHttp( + RequestData(self.stream_id, chunk), self.context.server + ) self.flow.request.timestamp_end = time.time() yield HttpRequestHook(self.flow) @@ -255,11 +303,19 @@ def state_stream_request_body(self, event: Union[RequestData, RequestEndOfMessag if self.flow.request.trailers: # we've delayed sending trailers until after `request` has been triggered. - yield SendHttp(RequestTrailers(self.stream_id, self.flow.request.trailers), self.context.server) + yield SendHttp( + RequestTrailers(self.stream_id, self.flow.request.trailers), + self.context.server, + ) yield SendHttp(event, self.context.server) + if self.server_state == self.state_done: + yield from self.flow_done() + @expect(RequestData, RequestTrailers, RequestEndOfMessage) - def state_consume_request_body(self, event: events.Event) -> layer.CommandGenerator[None]: + def state_consume_request_body( + self, event: events.Event + ) -> layer.CommandGenerator[None]: if isinstance(event, RequestData): self.request_body_buf += event.data yield from self.check_body_size(True) @@ -289,15 +345,27 @@ def state_consume_request_body(self, event: events.Event) -> layer.CommandGenera content = self.flow.request.raw_content done_after_headers = not (content or self.flow.request.trailers) - yield SendHttp(RequestHeaders(self.stream_id, self.flow.request, done_after_headers), self.context.server) + yield SendHttp( + RequestHeaders( + self.stream_id, self.flow.request, done_after_headers + ), + self.context.server, + ) if content: - yield SendHttp(RequestData(self.stream_id, content), self.context.server) + yield SendHttp( + RequestData(self.stream_id, content), self.context.server + ) if self.flow.request.trailers: - yield SendHttp(RequestTrailers(self.stream_id, self.flow.request.trailers), self.context.server) + yield SendHttp( + RequestTrailers(self.stream_id, self.flow.request.trailers), + self.context.server, + ) yield SendHttp(RequestEndOfMessage(self.stream_id), self.context.server) @expect(ResponseHeaders) - def state_wait_for_response_headers(self, event: ResponseHeaders) -> layer.CommandGenerator[None]: + def state_wait_for_response_headers( + self, event: ResponseHeaders + ) -> layer.CommandGenerator[None]: self.flow.response = event.response if not event.end_stream and (yield from self.check_body_size(False)): @@ -314,12 +382,17 @@ def state_wait_for_response_headers(self, event: ResponseHeaders) -> layer.Comma def start_response_stream(self) -> layer.CommandGenerator[None]: assert self.flow.response - yield SendHttp(ResponseHeaders(self.stream_id, self.flow.response, end_stream=False), self.context.client) + yield SendHttp( + ResponseHeaders(self.stream_id, self.flow.response, end_stream=False), + self.context.client, + ) yield commands.Log(f"Streaming response from {self.flow.request.host}.") self.server_state = self.state_stream_response_body @expect(ResponseData, ResponseTrailers, ResponseEndOfMessage) - def state_stream_response_body(self, event: events.Event) -> layer.CommandGenerator[None]: + def state_stream_response_body( + self, event: events.Event + ) -> layer.CommandGenerator[None]: assert self.flow.response if isinstance(event, ResponseData): if callable(self.flow.response.stream): @@ -336,14 +409,20 @@ def state_stream_response_body(self, event: events.Event) -> layer.CommandGenera elif isinstance(event, ResponseEndOfMessage): if callable(self.flow.response.stream): chunks = self.flow.response.stream(b"") - if isinstance(chunks, bytes): + if chunks == b"": + chunks = [] + elif isinstance(chunks, bytes): chunks = [chunks] for chunk in chunks: - yield SendHttp(ResponseData(self.stream_id, chunk), self.context.client) + yield SendHttp( + ResponseData(self.stream_id, chunk), self.context.client + ) yield from self.send_response(already_streamed=True) @expect(ResponseData, ResponseTrailers, ResponseEndOfMessage) - def state_consume_response_body(self, event: events.Event) -> layer.CommandGenerator[None]: + def state_consume_response_body( + self, event: events.Event + ) -> layer.CommandGenerator[None]: if isinstance(event, ResponseData): self.response_body_buf += event.data yield from self.check_body_size(False) @@ -363,12 +442,10 @@ def send_response(self, already_streamed: bool = False): is_websocket = ( self.flow.response.status_code == 101 - and - self.flow.response.headers.get("upgrade", "").lower() == "websocket" - and - self.flow.request.headers.get("Sec-WebSocket-Version", "").encode() == wsproto.handshake.WEBSOCKET_VERSION - and - self.context.options.websocket + and self.flow.response.headers.get("upgrade", "").lower() == "websocket" + and self.flow.request.headers.get("Sec-WebSocket-Version", "").encode() + == wsproto.handshake.WEBSOCKET_VERSION + and self.context.options.websocket ) if is_websocket: # We need to set this before calling the response hook @@ -383,29 +460,53 @@ def send_response(self, already_streamed: bool = False): if not already_streamed: content = self.flow.response.raw_content done_after_headers = not (content or self.flow.response.trailers) - yield SendHttp(ResponseHeaders(self.stream_id, self.flow.response, done_after_headers), self.context.client) + yield SendHttp( + ResponseHeaders(self.stream_id, self.flow.response, done_after_headers), + self.context.client, + ) if content: - yield SendHttp(ResponseData(self.stream_id, content), self.context.client) + yield SendHttp( + ResponseData(self.stream_id, content), self.context.client + ) if self.flow.response.trailers: - yield SendHttp(ResponseTrailers(self.stream_id, self.flow.response.trailers), self.context.client) - yield SendHttp(ResponseEndOfMessage(self.stream_id), self.context.client) + yield SendHttp( + ResponseTrailers(self.stream_id, self.flow.response.trailers), + self.context.client, + ) + + if self.client_state == self.state_done: + yield from self.flow_done() + + def flow_done(self): + if not self.flow.websocket: + self.flow.live = False if self.flow.response.status_code == 101: - if is_websocket: + if self.flow.websocket: self.child_layer = websocket.WebsocketLayer(self.context, self.flow) elif self.context.options.rawtcp: self.child_layer = tcp.TCPLayer(self.context) else: - yield commands.Log(f"Sent HTTP 101 response, but no protocol is enabled to upgrade to.", "warn") + yield commands.Log( + f"Sent HTTP 101 response, but no protocol is enabled to upgrade to.", + "warn", + ) yield commands.CloseConnection(self.context.client) self.client_state = self.server_state = self.state_errored return if self.debug: - yield commands.Log(f"{self.debug}[http] upgrading to {self.child_layer}", "debug") + yield commands.Log( + f"{self.debug}[http] upgrading to {self.child_layer}", "debug" + ) yield from self.child_layer.handle_event(events.Start()) self._handle_event = self.passthrough - return + else: + yield DropStream(self.stream_id) + + # delay sending EOM until the child layer is set up, + # we may get data immediately and need to be prepared to handle it. + yield SendHttp(ResponseEndOfMessage(self.stream_id), self.context.client) def check_body_size(self, request: bool) -> layer.CommandGenerator[bool]: """ @@ -413,7 +514,10 @@ def check_body_size(self, request: bool) -> layer.CommandGenerator[bool]: Returns `True` if the body size exceeds body_size_limit and further processing should be stopped. """ - if not (self.context.options.stream_large_bodies or self.context.options.body_size_limit): + if not ( + self.context.options.stream_large_bodies + or self.context.options.body_size_limit + ): return False # Step 1: Determine the expected body size. This can either come from a known content-length header, @@ -428,7 +532,9 @@ def check_body_size(self, request: bool) -> layer.CommandGenerator[bool]: else: # the 'early' case: we have not started consuming the body try: - expected_size = expected_http_body_size(self.flow.request, self.flow.response if response else None) + expected_size = expected_http_body_size( + self.flow.request, self.flow.response if response else None + ) except ValueError: # pragma: no cover # we just don't stream/kill malformed content-length headers. expected_size = None @@ -449,11 +555,18 @@ def check_body_size(self, request: bool) -> layer.CommandGenerator[bool]: self.flow.error = flow.Error(err_msg) yield HttpErrorHook(self.flow) - yield SendHttp(ResponseProtocolError(self.stream_id, err_msg, err_code), self.context.client) + yield SendHttp( + ResponseProtocolError(self.stream_id, err_msg, err_code), + self.context.client, + ) self.client_state = self.state_errored if response: - yield SendHttp(RequestProtocolError(self.stream_id, err_msg, err_code), self.context.server) + yield SendHttp( + RequestProtocolError(self.stream_id, err_msg, err_code), + self.context.server, + ) self.server_state = self.state_errored + self.flow.live = False return True # Step 3: Do we need to stream this? @@ -497,16 +610,18 @@ def check_killed(self, emit_error_hook: bool) -> layer.CommandGenerator[bool]: yield HttpErrorHook(self.flow) # Use the special NO_RESPONSE status code to make sure that no error message is sent to the client. yield SendHttp( - ResponseProtocolError(self.stream_id, "killed", status_codes.NO_RESPONSE), - self.context.client + ResponseProtocolError( + self.stream_id, "killed", status_codes.NO_RESPONSE + ), + self.context.client, ) - self._handle_event = self.state_errored + self.flow.live = False + self.client_state = self.server_state = self.state_errored return True return False def handle_protocol_error( - self, - event: Union[RequestProtocolError, ResponseProtocolError] + self, event: Union[RequestProtocolError, ResponseProtocolError] ) -> layer.CommandGenerator[None]: is_client_error_but_we_already_talk_upstream = ( isinstance(event, RequestProtocolError) @@ -514,9 +629,8 @@ def handle_protocol_error( and self.server_state not in (self.state_done, self.state_errored) ) need_error_hook = not ( - self.client_state in (self.state_wait_for_request_headers, self.state_errored) - or - self.server_state in (self.state_done, self.state_errored) + self.client_state == self.state_errored + or self.server_state in (self.state_done, self.state_errored) ) if is_client_error_but_we_already_talk_upstream: @@ -537,14 +651,19 @@ def handle_protocol_error( yield SendHttp(event, self.context.client) self.server_state = self.state_errored + self.flow.live = False + yield DropStream(self.stream_id) + def make_server_connection(self) -> layer.CommandGenerator[bool]: connection, err = yield GetHttpConnection( (self.flow.request.host, self.flow.request.port), self.flow.request.scheme == "https", - self.context.server.via, + self.flow.server_conn.via, ) if err: - yield from self.handle_protocol_error(ResponseProtocolError(self.stream_id, err)) + yield from self.handle_protocol_error( + ResponseProtocolError(self.stream_id, err) + ) return False else: self.context.server = self.flow.server_conn = connection @@ -563,11 +682,15 @@ def handle_connect(self) -> layer.CommandGenerator[None]: yield from self.handle_connect_upstream() def handle_connect_regular(self): - if not self.flow.response and self.context.options.connection_strategy == "eager": + if ( + not self.flow.response + and self.context.options.connection_strategy == "eager" + ): err = yield commands.OpenConnection(self.context.server) if err: self.flow.response = http.Response.make( - 502, f"Cannot connect to {human.format_address(self.context.server.address)}: {err}" + 502, + f"Cannot connect to {human.format_address(self.context.server.address)}: {err}", ) self.child_layer = layer.NextLayer(self.context) yield from self.handle_connect_finish() @@ -595,7 +718,10 @@ def handle_connect_finish(self): self.child_layer = self.child_layer or layer.NextLayer(self.context) yield from self.child_layer.handle_event(events.Start()) self._handle_event = self.passthrough - yield SendHttp(ResponseHeaders(self.stream_id, self.flow.response, True), self.context.client) + yield SendHttp( + ResponseHeaders(self.stream_id, self.flow.response, True), + self.context.client, + ) yield SendHttp(ResponseEndOfMessage(self.stream_id), self.context.client) else: yield from self.send_response() @@ -618,18 +744,33 @@ def passthrough(self, event: events.Event) -> layer.CommandGenerator[None]: # normal connection events -> HTTP events if isinstance(command, commands.SendData): if command.connection == self.context.client: - yield SendHttp(ResponseData(self.stream_id, command.data), self.context.client) - elif command.connection == self.context.server and self.flow.response.status_code == 101: + yield SendHttp( + ResponseData(self.stream_id, command.data), self.context.client + ) + elif ( + command.connection == self.context.server + and self.flow.response.status_code == 101 + ): # there only is a HTTP server connection if we have switched protocols, # not if a connection is established via CONNECT. - yield SendHttp(RequestData(self.stream_id, command.data), self.context.server) + yield SendHttp( + RequestData(self.stream_id, command.data), self.context.server + ) else: yield command elif isinstance(command, commands.CloseConnection): if command.connection == self.context.client: - yield SendHttp(ResponseProtocolError(self.stream_id, "EOF"), self.context.client) - elif command.connection == self.context.server and self.flow.response.status_code == 101: - yield SendHttp(RequestProtocolError(self.stream_id, "EOF"), self.context.server) + yield SendHttp( + ResponseProtocolError(self.stream_id, "EOF"), + self.context.client, + ) + elif ( + command.connection == self.context.server + and self.flow.response.status_code == 101 + ): + yield SendHttp( + RequestProtocolError(self.stream_id, "EOF"), self.context.server + ) else: # If we are running TCP over HTTP we want to be consistent with half-closes. # The easiest approach for this is to just always full close for now. @@ -662,11 +803,14 @@ class HttpLayer(layer.Layer): ConnectionEvent -> HttpEvent -> HttpCommand -> ConnectionCommand """ + mode: HTTPMode - command_sources: Dict[commands.Command, layer.Layer] - streams: Dict[int, HttpStream] - connections: Dict[Connection, layer.Layer] - waiting_for_establishment: DefaultDict[Connection, List[GetHttpConnection]] + command_sources: dict[commands.Command, layer.Layer] + streams: dict[int, HttpStream] + connections: dict[Connection, layer.Layer] + waiting_for_establishment: collections.defaultdict[ + Connection, list[GetHttpConnection] + ] def __init__(self, context: Context, mode: HTTPMode): super().__init__(context) @@ -682,9 +826,7 @@ def __init__(self, context: Context, mode: HTTPMode): else: http_conn = Http1Server(context.fork()) - self.connections = { - context.client: http_conn - } + self.connections = {context.client: http_conn} def __repr__(self): return f"HttpLayer({self.mode.name}, conns: {len(self.connections)})" @@ -693,7 +835,12 @@ def _handle_event(self, event: events.Event): if isinstance(event, events.Start): yield from self.event_to_child(self.connections[self.context.client], event) if self.mode is HTTPMode.upstream: - self.context.server.via = server_spec.parse_with_mode(self.context.options.mode)[1] + self.context.server.via = server_spec.parse_with_mode( + self.context.options.mode + )[1] + elif isinstance(event, events.Wakeup): + stream = self.command_sources.pop(event.command) + yield from self.event_to_child(stream, event) elif isinstance(event, events.CommandCompleted): stream = self.command_sources.pop(event.command) yield from self.event_to_child(stream, event) @@ -718,7 +865,10 @@ def _handle_event(self, event: events.Event): stream_id = conn.stream_id yield from self.event_to_child(self.streams[stream_id], event) elif isinstance(event, events.ConnectionEvent): - if event.connection == self.context.server and self.context.server not in self.connections: + if ( + event.connection == self.context.server + and self.context.server not in self.connections + ): # We didn't do anything with this connection yet, now the peer is doing something. if isinstance(event, events.ConnectionClosed): # The peer has closed it - let's close it too! @@ -751,17 +901,25 @@ def event_to_child( # Streams may yield blocking commands, which ultimately generate CommandCompleted events. # Those need to be routed back to the correct stream, so we need to keep track of that. - if command.blocking: + if command.blocking or isinstance(command, commands.RequestWakeup): self.command_sources[command] = child if isinstance(command, ReceiveHttp): if isinstance(command.event, RequestHeaders): yield from self.make_stream(command.event.stream_id) - stream = self.streams[command.event.stream_id] - yield from self.event_to_child(stream, command.event) + try: + stream = self.streams[command.event.stream_id] + except KeyError: + # We may be getting data or errors for a stream even though we've already finished handling it, + # see for example https://github.com/mitmproxy/mitmproxy/issues/5343. + pass + else: + yield from self.event_to_child(stream, command.event) elif isinstance(command, SendHttp): conn = self.connections[command.connection] yield from self.event_to_child(conn, command.event) + elif isinstance(command, DropStream): + self.streams.pop(command.stream_id, None) elif isinstance(command, GetHttpConnection): yield from self.get_connection(command) elif isinstance(command, RegisterHttpConnection): @@ -779,44 +937,53 @@ def make_stream(self, stream_id: int) -> layer.CommandGenerator[None]: self.streams[stream_id] = HttpStream(ctx, stream_id) yield from self.event_to_child(self.streams[stream_id], events.Start()) - def get_connection(self, event: GetHttpConnection, *, reuse: bool = True) -> layer.CommandGenerator[None]: + def get_connection( + self, event: GetHttpConnection, *, reuse: bool = True + ) -> layer.CommandGenerator[None]: # Do we already have a connection we can re-use? if reuse: for connection in self.connections: - connection_suitable = ( - event.connection_spec_matches(connection) - ) + connection_suitable = event.connection_spec_matches(connection) if connection_suitable: if connection in self.waiting_for_establishment: self.waiting_for_establishment[connection].append(event) return elif connection.error: stream = self.command_sources.pop(event) - yield from self.event_to_child(stream, - GetHttpConnectionCompleted(event, (None, connection.error))) + yield from self.event_to_child( + stream, + GetHttpConnectionCompleted(event, (None, connection.error)), + ) return elif connection.connected: # see "tricky multiplexing edge case" in make_http_connection for an explanation - h2_to_h1 = self.context.client.alpn == b"h2" and connection.alpn != b"h2" + h2_to_h1 = ( + self.context.client.alpn == b"h2" + and connection.alpn != b"h2" + ) if not h2_to_h1: stream = self.command_sources.pop(event) - yield from self.event_to_child(stream, - GetHttpConnectionCompleted(event, (connection, None))) + yield from self.event_to_child( + stream, + GetHttpConnectionCompleted(event, (connection, None)), + ) return else: pass # the connection is at least half-closed already, we want a new one. context_connection_matches = ( - self.context.server not in self.connections and - event.connection_spec_matches(self.context.server) + self.context.server not in self.connections + and event.connection_spec_matches(self.context.server) ) can_use_context_connection = ( - context_connection_matches - and self.context.server.connected + context_connection_matches and self.context.server.connected ) if context_connection_matches and self.context.server.error: stream = self.command_sources.pop(event) - yield from self.event_to_child(stream, GetHttpConnectionCompleted(event, (None, self.context.server.error))) + yield from self.event_to_child( + stream, + GetHttpConnectionCompleted(event, (None, self.context.server.error)), + ) return context = self.context.fork() @@ -836,7 +1003,10 @@ def get_connection(self, event: GetHttpConnection, *, reuse: bool = True) -> lay if event.tls: # Assume that we are in transparent mode and lazily did not open a connection yet. # We don't want the IP (which is the address) as the upstream SNI, but the client's SNI instead. - if self.mode == HTTPMode.transparent and event.address == self.context.server.address: + if ( + self.mode == HTTPMode.transparent + and event.address == self.context.server.address + ): context.server.sni = self.context.client.sni or event.address[0] else: context.server.sni = event.address[0] @@ -849,10 +1019,12 @@ def get_connection(self, event: GetHttpConnection, *, reuse: bool = True) -> lay yield from self.event_to_child(stack[0], events.Start()) - def register_connection(self, command: RegisterHttpConnection) -> layer.CommandGenerator[None]: + def register_connection( + self, command: RegisterHttpConnection + ) -> layer.CommandGenerator[None]: waiting = self.waiting_for_establishment.pop(command.connection) - reply: Union[Tuple[None, str], Tuple[Connection, None]] + reply: Union[tuple[None, str], tuple[Connection, None]] if command.err: reply = (None, command.err) else: @@ -860,12 +1032,18 @@ def register_connection(self, command: RegisterHttpConnection) -> layer.CommandG for cmd in waiting: stream = self.command_sources.pop(cmd) - yield from self.event_to_child(stream, GetHttpConnectionCompleted(cmd, reply)) + yield from self.event_to_child( + stream, GetHttpConnectionCompleted(cmd, reply) + ) # Tricky multiplexing edge case: Assume we are doing HTTP/2 -> HTTP/1 proxying and the destination server # only serves responses with HTTP read-until-EOF semantics. In this case we can't process two flows on the # same connection. The only workaround left is to open a separate connection for each flow. - if not command.err and self.context.client.alpn == b"h2" and command.connection.alpn != b"h2": + if ( + not command.err + and self.context.client.alpn == b"h2" + and command.connection.alpn != b"h2" + ): for cmd in waiting[1:]: yield from self.get_connection(cmd, reuse=False) break diff --git a/mitmproxy/proxy/layers/http/_base.py b/mitmproxy/proxy/layers/http/_base.py index 5e1946b8b9..b5f66d46ba 100644 --- a/mitmproxy/proxy/layers/http/_base.py +++ b/mitmproxy/proxy/layers/http/_base.py @@ -40,7 +40,9 @@ def __repr__(self) -> str: def format_error(status_code: int, message: str) -> bytes: reason = http.status_codes.RESPONSES.get(status_code, "Unknown") - return textwrap.dedent(f""" + return ( + textwrap.dedent( + f""" {status_code} {reason} @@ -50,4 +52,8 @@ def format_error(status_code: int, message: str) -> bytes:

{html.escape(message)}

- """).strip().encode("utf8", "replace") + """ + ) + .strip() + .encode("utf8", "replace") + ) diff --git a/mitmproxy/proxy/layers/http/_events.py b/mitmproxy/proxy/layers/http/_events.py index edddf5b6ec..f67217b03b 100644 --- a/mitmproxy/proxy/layers/http/_events.py +++ b/mitmproxy/proxy/layers/http/_events.py @@ -27,6 +27,7 @@ class ResponseHeaders(HttpEvent): # explicit constructors below to facilitate type checking in _http1/_http2 + @dataclass class RequestData(HttpEvent): data: bytes diff --git a/mitmproxy/proxy/layers/http/_hooks.py b/mitmproxy/proxy/layers/http/_hooks.py index 0c3912c891..949c9a7e7d 100644 --- a/mitmproxy/proxy/layers/http/_hooks.py +++ b/mitmproxy/proxy/layers/http/_hooks.py @@ -9,6 +9,7 @@ class HttpRequestHeadersHook(commands.StartHook): """ HTTP request headers were successfully read. At this point, the body is empty. """ + name = "requestheaders" flow: http.HTTPFlow @@ -23,6 +24,7 @@ class HttpRequestHook(commands.StartHook): Enabling streaming may cause unexpected event sequences: For example, `response` may now occur before `request` because the server replied with "413 Payload Too Large" during upload. """ + name = "request" flow: http.HTTPFlow @@ -32,6 +34,7 @@ class HttpResponseHeadersHook(commands.StartHook): """ HTTP response headers were successfully read. At this point, the body is empty. """ + name = "responseheaders" flow: http.HTTPFlow @@ -44,6 +47,7 @@ class HttpResponseHook(commands.StartHook): Note: If response streaming is active, this event fires after the entire body has been streamed. HTTP trailers, if present, have not been transmitted to the client yet and can still be modified. """ + name = "response" flow: http.HTTPFlow @@ -57,6 +61,7 @@ class HttpErrorHook(commands.StartHook): Every flow will receive either an error or an response event, but not both. """ + name = "error" flow: http.HTTPFlow @@ -74,6 +79,7 @@ class HttpConnectHook(commands.StartHook): and not forwarded. They do not generate the usual HTTP handler events, but all requests going over the newly opened connection will. """ + flow: http.HTTPFlow @@ -88,4 +94,5 @@ class HttpConnectUpstreamHook(commands.StartHook): CONNECT requests do not generate the usual HTTP handler events, but all requests going over the newly opened connection will. """ + flow: http.HTTPFlow diff --git a/mitmproxy/proxy/layers/http/_http1.py b/mitmproxy/proxy/layers/http/_http1.py index 3fff8faeae..4cab5bd9b6 100644 --- a/mitmproxy/proxy/layers/http/_http1.py +++ b/mitmproxy/proxy/layers/http/_http1.py @@ -1,5 +1,5 @@ import abc -from typing import Callable, Optional, Type, Union +from typing import Callable, Optional, Union import h11 from h11._readers import ChunkedReader, ContentLengthReader, Http10Reader @@ -13,8 +13,17 @@ from mitmproxy.proxy.utils import expect from mitmproxy.utils import human from ._base import HttpConnection, format_error -from ._events import HttpEvent, RequestData, RequestEndOfMessage, RequestHeaders, RequestProtocolError, ResponseData, \ - ResponseEndOfMessage, ResponseHeaders, ResponseProtocolError +from ._events import ( + HttpEvent, + RequestData, + RequestEndOfMessage, + RequestHeaders, + RequestProtocolError, + ResponseData, + ResponseEndOfMessage, + ResponseHeaders, + ResponseProtocolError, +) from ...context import Context TBodyReader = Union[ChunkedReader, Http10Reader, ContentLengthReader] @@ -31,9 +40,9 @@ class Http1Connection(HttpConnection, metaclass=abc.ABCMeta): body_reader: TBodyReader buf: ReceiveBuffer - ReceiveProtocolError: Type[Union[RequestProtocolError, ResponseProtocolError]] - ReceiveData: Type[Union[RequestData, ResponseData]] - ReceiveEndOfMessage: Type[Union[RequestEndOfMessage, ResponseEndOfMessage]] + ReceiveProtocolError: type[Union[RequestProtocolError, ResponseProtocolError]] + ReceiveData: type[Union[RequestData, ResponseData]] + ReceiveEndOfMessage: type[Union[RequestEndOfMessage, ResponseEndOfMessage]] def __init__(self, context: Context, conn: Connection): super().__init__(context, conn) @@ -44,14 +53,19 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: yield from () # pragma: no cover @abc.abstractmethod - def read_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: + def read_headers( + self, event: events.ConnectionEvent + ) -> layer.CommandGenerator[None]: yield from () # pragma: no cover def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: if isinstance(event, HttpEvent): yield from self.send(event) else: - if isinstance(event, events.DataReceived) and self.state != self.passthrough: + if ( + isinstance(event, events.DataReceived) + and self.state != self.passthrough + ): self.buf += event.data yield from self.state(event) @@ -74,7 +88,11 @@ def read_body(self, event: events.Event) -> layer.CommandGenerator[None]: raise AssertionError(f"Unexpected event: {event}") except h11.ProtocolError as e: yield commands.CloseConnection(self.conn) - yield ReceiveHttp(self.ReceiveProtocolError(self.stream_id, f"HTTP/1 protocol error: {e}")) + yield ReceiveHttp( + self.ReceiveProtocolError( + self.stream_id, f"HTTP/1 protocol error: {e}" + ) + ) return if h11_event is None: @@ -90,10 +108,7 @@ def read_body(self, event: events.Event) -> layer.CommandGenerator[None]: if self.request.data.method.upper() != b"CONNECT": yield ReceiveHttp(self.ReceiveEndOfMessage(self.stream_id)) is_request = isinstance(self, Http1Server) - yield from self.mark_done( - request=is_request, - response=not is_request - ) + yield from self.mark_done(request=is_request, response=not is_request) return def wait(self, event: events.Event) -> layer.CommandGenerator[None]: @@ -110,8 +125,13 @@ def wait(self, event: events.Event) -> layer.CommandGenerator[None]: # see https://github.com/httpwg/http-core/issues/22 if event.connection.state is not ConnectionState.CLOSED: yield commands.CloseConnection(event.connection) - yield ReceiveHttp(self.ReceiveProtocolError(self.stream_id, f"Client disconnected.", - code=status_codes.CLIENT_CLOSED_REQUEST)) + yield ReceiveHttp( + self.ReceiveProtocolError( + self.stream_id, + f"Client disconnected.", + code=status_codes.CLIENT_CLOSED_REQUEST, + ) + ) else: # pragma: no cover raise AssertionError(f"Unexpected event: {event}") @@ -137,7 +157,9 @@ def passthrough(self, event: events.Event) -> layer.CommandGenerator[None]: else: yield ReceiveHttp(ResponseEndOfMessage(self.stream_id)) - def mark_done(self, *, request: bool = False, response: bool = False) -> layer.CommandGenerator[None]: + def mark_done( + self, *, request: bool = False, response: bool = False + ) -> layer.CommandGenerator[None]: if request: self.request_done = True if response: @@ -149,15 +171,21 @@ def mark_done(self, *, request: bool = False, response: bool = False) -> layer.C yield from self.make_pipe() return try: - read_until_eof_semantics = http1.expected_http_body_size(self.request, self.response) == -1 + read_until_eof_semantics = ( + http1.expected_http_body_size(self.request, self.response) == -1 + ) except ValueError: # this may raise only now (and not earlier) because an addon set invalid headers, # in which case it's not really clear what we are supposed to do. read_until_eof_semantics = False connection_done = ( read_until_eof_semantics - or http1.connection_close(self.request.http_version, self.request.headers) - or http1.connection_close(self.response.http_version, self.response.headers) + or http1.connection_close( + self.request.http_version, self.request.headers + ) + or http1.connection_close( + self.response.http_version, self.response.headers + ) # If we proxy HTTP/2 to HTTP/1, we only use upstream connections for one request. # This simplifies our connection management quite a bit as we can rely on # the proxyserver's max-connection-per-server throttling. @@ -215,38 +243,57 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: if raw: yield commands.SendData(self.conn, raw) elif isinstance(event, ResponseEndOfMessage): + assert self.request assert self.response - if "chunked" in self.response.headers.get("transfer-encoding", "").lower(): + if self.request.method.upper() != "HEAD" and "chunked" in self.response.headers.get("transfer-encoding", "").lower(): yield commands.SendData(self.conn, b"0\r\n\r\n") yield from self.mark_done(response=True) elif isinstance(event, ResponseProtocolError): if not self.response and event.code != status_codes.NO_RESPONSE: - yield commands.SendData(self.conn, make_error_response(event.code, event.message)) + yield commands.SendData( + self.conn, make_error_response(event.code, event.message) + ) if self.conn.state & ConnectionState.CAN_WRITE: yield commands.CloseConnection(self.conn) else: raise AssertionError(f"Unexpected event: {event}") - def read_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: + def read_headers( + self, event: events.ConnectionEvent + ) -> layer.CommandGenerator[None]: if isinstance(event, events.DataReceived): request_head = self.buf.maybe_extract_lines() if request_head: - request_head = [bytes(x) for x in request_head] # TODO: Make url.parse compatible with bytearrays + request_head = [ + bytes(x) for x in request_head + ] # TODO: Make url.parse compatible with bytearrays try: self.request = http1.read_request_head(request_head) + if self.context.options.validate_inbound_headers: + http1.validate_headers(self.request.headers) expected_body_size = http1.expected_http_body_size(self.request) except ValueError as e: yield commands.SendData(self.conn, make_error_response(400, str(e))) yield commands.CloseConnection(self.conn) if self.request: # we have headers that we can show in the ui - yield ReceiveHttp(RequestHeaders(self.stream_id, self.request, False)) - yield ReceiveHttp(RequestProtocolError(self.stream_id, str(e), 400)) + yield ReceiveHttp( + RequestHeaders(self.stream_id, self.request, False) + ) + yield ReceiveHttp( + RequestProtocolError(self.stream_id, str(e), 400) + ) else: - yield commands.Log(f"{human.format_address(self.conn.peername)}: {e}") + yield commands.Log( + f"{human.format_address(self.conn.peername)}: {e}" + ) self.state = self.done return - yield ReceiveHttp(RequestHeaders(self.stream_id, self.request, expected_body_size == 0)) + yield ReceiveHttp( + RequestHeaders( + self.stream_id, self.request, expected_body_size == 0 + ) + ) self.body_reader = make_body_reader(expected_body_size) self.state = self.read_body yield from self.state(event) @@ -255,12 +302,16 @@ def read_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[ elif isinstance(event, events.ConnectionClosed): buf = bytes(self.buf) if buf.strip(): - yield commands.Log(f"Client closed connection before completing request headers: {buf!r}") + yield commands.Log( + f"Client closed connection before completing request headers: {buf!r}" + ) yield commands.CloseConnection(self.conn) else: raise AssertionError(f"Unexpected event: {event}") - def mark_done(self, *, request: bool = False, response: bool = False) -> layer.CommandGenerator[None]: + def mark_done( + self, *, request: bool = False, response: bool = False + ) -> layer.CommandGenerator[None]: yield from super().mark_done(request=request, response=response) if self.request_done and not self.response_done: self.state = self.wait @@ -291,11 +342,19 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: request = event.request if request.is_http2: # Convert to an HTTP/1 request. - request = request.copy() # (we could probably be a bit more efficient here.) + request = ( + request.copy() + ) # (we could probably be a bit more efficient here.) request.http_version = "HTTP/1.1" if "Host" not in request.headers and request.authority: request.headers.insert(0, "Host", request.authority) request.authority = "" + cookie_headers = request.headers.get_all("Cookie") + if len(cookie_headers) > 1: + # Only HTTP/2 supports multiple cookie headers, HTTP/1.x does not. + # see: https://www.rfc-editor.org/rfc/rfc6265#section-5.4 + # https://www.rfc-editor.org/rfc/rfc7540#section-8.1.2.5 + request.headers["Cookie"] = "; ".join(cookie_headers) raw = http1.assemble_request_head(request) yield commands.SendData(self.conn, raw) elif isinstance(event, RequestData): @@ -316,7 +375,9 @@ def send(self, event: HttpEvent) -> layer.CommandGenerator[None]: else: raise AssertionError(f"Unexpected event: {event}") - def read_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[None]: + def read_headers( + self, event: events.ConnectionEvent + ) -> layer.CommandGenerator[None]: if isinstance(event, events.DataReceived): if not self.request: # we just received some data for an unknown request. @@ -327,15 +388,27 @@ def read_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[ response_head = self.buf.maybe_extract_lines() if response_head: - response_head = [bytes(x) for x in response_head] # TODO: Make url.parse compatible with bytearrays + response_head = [ + bytes(x) for x in response_head + ] # TODO: Make url.parse compatible with bytearrays try: self.response = http1.read_response_head(response_head) - expected_size = http1.expected_http_body_size(self.request, self.response) + if self.context.options.validate_inbound_headers: + http1.validate_headers(self.response.headers) + expected_size = http1.expected_http_body_size( + self.request, self.response + ) except ValueError as e: yield commands.CloseConnection(self.conn) - yield ReceiveHttp(ResponseProtocolError(self.stream_id, f"Cannot parse HTTP response: {e}")) + yield ReceiveHttp( + ResponseProtocolError( + self.stream_id, f"Cannot parse HTTP response: {e}" + ) + ) return - yield ReceiveHttp(ResponseHeaders(self.stream_id, self.response, expected_size == 0)) + yield ReceiveHttp( + ResponseHeaders(self.stream_id, self.response, expected_size == 0) + ) self.body_reader = make_body_reader(expected_size) self.state = self.read_body @@ -347,13 +420,21 @@ def read_headers(self, event: events.ConnectionEvent) -> layer.CommandGenerator[ yield commands.CloseConnection(self.conn) if self.stream_id: if self.buf: - yield ReceiveHttp(ResponseProtocolError(self.stream_id, - f"unexpected server response: {bytes(self.buf)!r}")) + yield ReceiveHttp( + ResponseProtocolError( + self.stream_id, + f"unexpected server response: {bytes(self.buf)!r}", + ) + ) else: # The server has closed the connection to prevent us from continuing. # We need to signal that to the stream. # https://tools.ietf.org/html/rfc7231#section-6.5.11 - yield ReceiveHttp(ResponseProtocolError(self.stream_id, "server closed connection")) + yield ReceiveHttp( + ResponseProtocolError( + self.stream_id, "server closed connection" + ) + ) else: return else: @@ -389,7 +470,7 @@ def make_error_response( Server=version.MITMPROXY, Connection="close", Content_Type="text/html", - ) + ), ) return http1.assemble_response(resp) diff --git a/mitmproxy/proxy/layers/http/_http2.py b/mitmproxy/proxy/layers/http/_http2.py index 8e477c34f4..a014c3efbb 100644 --- a/mitmproxy/proxy/layers/http/_http2.py +++ b/mitmproxy/proxy/layers/http/_http2.py @@ -1,7 +1,8 @@ import collections import time +from collections.abc import Sequence from enum import Enum -from typing import ClassVar, DefaultDict, Dict, List, Optional, Sequence, Tuple, Type, Union +from typing import ClassVar, Optional, Union import h2.config import h2.connection @@ -16,13 +17,23 @@ from mitmproxy.connection import Connection from mitmproxy.net.http import status_codes, url from mitmproxy.utils import human -from . import RequestData, RequestEndOfMessage, RequestHeaders, RequestProtocolError, ResponseData, \ - ResponseEndOfMessage, ResponseHeaders, RequestTrailers, ResponseTrailers, ResponseProtocolError +from . import ( + RequestData, + RequestEndOfMessage, + RequestHeaders, + RequestProtocolError, + ResponseData, + ResponseEndOfMessage, + ResponseHeaders, + RequestTrailers, + ResponseTrailers, + ResponseProtocolError, +) from ._base import HttpConnection, HttpEvent, ReceiveHttp, format_error from ._http_h2 import BufferedH2Connection, H2ConnectionLogger -from ...commands import CloseConnection, Log, SendData +from ...commands import CloseConnection, Log, SendData, RequestWakeup from ...context import Context -from ...events import ConnectionClosed, DataReceived, Event, Start +from ...events import ConnectionClosed, DataReceived, Event, Start, Wakeup from ...layer import CommandGenerator from ...utils import expect @@ -40,24 +51,29 @@ class Http2Connection(HttpConnection): h2_conf_defaults = dict( header_encoding=False, validate_outbound_headers=False, - validate_inbound_headers=True, + # validate_inbound_headers is controlled by the validate_inbound_headers option. normalize_inbound_headers=False, # changing this to True is required to pass h2spec normalize_outbound_headers=False, ) h2_conn: BufferedH2Connection - streams: Dict[int, StreamState] + streams: dict[int, StreamState] """keep track of all active stream ids to send protocol errors on teardown""" - ReceiveProtocolError: Type[Union[RequestProtocolError, ResponseProtocolError]] - ReceiveData: Type[Union[RequestData, ResponseData]] - ReceiveTrailers: Type[Union[RequestTrailers, ResponseTrailers]] - ReceiveEndOfMessage: Type[Union[RequestEndOfMessage, ResponseEndOfMessage]] + ReceiveProtocolError: type[Union[RequestProtocolError, ResponseProtocolError]] + ReceiveData: type[Union[RequestData, ResponseData]] + ReceiveTrailers: type[Union[RequestTrailers, ResponseTrailers]] + ReceiveEndOfMessage: type[Union[RequestEndOfMessage, ResponseEndOfMessage]] def __init__(self, context: Context, conn: Connection): super().__init__(context, conn) if self.debug: - self.h2_conf.logger = H2ConnectionLogger(f"{human.format_address(self.context.client.peername)}: " - f"{self.__class__.__name__}") + self.h2_conf.logger = H2ConnectionLogger( + f"{human.format_address(self.context.client.peername)}: " + f"{self.__class__.__name__}" + ) + self.h2_conf.validate_inbound_headers = ( + self.context.options.validate_inbound_headers + ) self.h2_conn = BufferedH2Connection(self.h2_conf) self.streams = {} @@ -66,10 +82,9 @@ def is_closed(self, stream_id: int) -> bool: stream = self.h2_conn.streams.get(stream_id, None) if ( stream is not None - and - stream.state_machine.state is not h2.stream.StreamState.CLOSED - and - self.h2_conn.state_machine.state is not h2.connection.ConnectionState.CLOSED + and stream.state_machine.state is not h2.stream.StreamState.CLOSED + and self.h2_conn.state_machine.state + is not h2.connection.ConnectionState.CLOSED ): return False else: @@ -80,12 +95,11 @@ def is_open_for_us(self, stream_id: int) -> bool: stream = self.h2_conn.streams.get(stream_id, None) if ( stream is not None - and - stream.state_machine.state is not h2.stream.StreamState.HALF_CLOSED_LOCAL - and - stream.state_machine.state is not h2.stream.StreamState.CLOSED - and - self.h2_conn.state_machine.state is not h2.connection.ConnectionState.CLOSED + and stream.state_machine.state + is not h2.stream.StreamState.HALF_CLOSED_LOCAL + and stream.state_machine.state is not h2.stream.StreamState.CLOSED + and self.h2_conn.state_machine.state + is not h2.connection.ConnectionState.CLOSED ): return True else: @@ -103,7 +117,7 @@ def _handle_event(self, event: Event) -> CommandGenerator[None]: elif isinstance(event, (RequestTrailers, ResponseTrailers)): if self.is_open_for_us(event.stream_id): trailers = [*event.trailers.fields] - self.h2_conn.send_headers(event.stream_id, trailers, end_stream=True) + self.h2_conn.send_trailers(event.stream_id, trailers) elif isinstance(event, (RequestEndOfMessage, ResponseEndOfMessage)): if self.is_open_for_us(event.stream_id): self.h2_conn.end_stream(event.stream_id) @@ -120,15 +134,18 @@ def _handle_event(self, event: Event) -> CommandGenerator[None]: and event.code != status_codes.NO_RESPONSE ) if send_error_message: - self.h2_conn.send_headers(event.stream_id, [ - (b":status", b"%d" % event.code), - (b"server", version.MITMPROXY.encode()), - (b"content-type", b"text/html"), - ]) + self.h2_conn.send_headers( + event.stream_id, + [ + (b":status", b"%d" % event.code), + (b"server", version.MITMPROXY.encode()), + (b"content-type", b"text/html"), + ], + ) self.h2_conn.send_data( event.stream_id, format_error(event.code, event.message), - end_stream=True + end_stream=True, ) else: self.h2_conn.reset_stream(event.stream_id, code) @@ -146,7 +163,9 @@ def _handle_event(self, event: Event) -> CommandGenerator[None]: # this should never raise a ValueError, but we triggered one while fuzzing: # https://github.com/python-hyper/hyper-h2/issues/1231 # this stays here as defense-in-depth. - raise h2.exceptions.ProtocolError(f"uncaught hyper-h2 error: {e}") from e + raise h2.exceptions.ProtocolError( + f"uncaught hyper-h2 error: {e}" + ) from e except h2.exceptions.ProtocolError as e: events = [e] @@ -174,9 +193,13 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: if state is StreamState.HEADERS_RECEIVED: yield ReceiveHttp(self.ReceiveData(event.stream_id, event.data)) elif state is StreamState.EXPECTING_HEADERS: - yield from self.protocol_error(f"Received HTTP/2 data frame, expected headers.") + yield from self.protocol_error( + f"Received HTTP/2 data frame, expected headers." + ) return True - self.h2_conn.acknowledge_received_data(event.flow_controlled_length, event.stream_id) + self.h2_conn.acknowledge_received_data( + event.flow_controlled_length, event.stream_id + ) elif isinstance(event, h2.events.TrailersReceived): trailers = http.Headers(event.headers) yield ReceiveHttp(self.ReceiveTrailers(event.stream_id, trailers)) @@ -197,8 +220,13 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: err_code = { h2.errors.ErrorCodes.CANCEL: status_codes.CLIENT_CLOSED_REQUEST, }.get(event.error_code, self.ReceiveProtocolError.code) - yield ReceiveHttp(self.ReceiveProtocolError(event.stream_id, f"stream reset by client ({err_str})", - code=err_code)) + yield ReceiveHttp( + self.ReceiveProtocolError( + event.stream_id, + f"stream reset by client ({err_str})", + code=err_code, + ) + ) self.streams.pop(event.stream_id) else: pass # We don't track priority frames which could be followed by a stream reset here. @@ -225,7 +253,10 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: elif isinstance(event, h2.events.PingAckReceived): pass elif isinstance(event, h2.events.PushedStreamReceived): - yield Log("Received HTTP/2 push promise, even though we signalled no support.", "error") + yield Log( + "Received HTTP/2 push promise, even though we signalled no support.", + "error", + ) elif isinstance(event, h2.events.UnknownFrameReceived): # https://http2.github.io/http2-spec/#rfc.section.4.1 # Implementations MUST ignore and discard any frame that has a type that is unknown. @@ -251,17 +282,19 @@ def close_connection(self, msg: str) -> CommandGenerator[None]: self.streams.clear() self._handle_event = self.done # type: ignore - @expect(DataReceived, HttpEvent, ConnectionClosed) + @expect(DataReceived, HttpEvent, ConnectionClosed, Wakeup) def done(self, _) -> CommandGenerator[None]: yield from () -def normalize_h1_headers(headers: List[Tuple[bytes, bytes]], is_client: bool) -> List[Tuple[bytes, bytes]]: +def normalize_h1_headers( + headers: list[tuple[bytes, bytes]], is_client: bool +) -> list[tuple[bytes, bytes]]: # HTTP/1 servers commonly send capitalized headers (Content-Length vs content-length), # which isn't valid HTTP/2. As such we normalize. headers = h2.utilities.normalize_outbound_headers( headers, - h2.utilities.HeaderValidationFlags(is_client, False, not is_client, False) + h2.utilities.HeaderValidationFlags(is_client, False, not is_client, False), ) # make sure that this is not just an iterator but an iterable, # otherwise hyper-h2 will silently drop headers. @@ -269,6 +302,15 @@ def normalize_h1_headers(headers: List[Tuple[bytes, bytes]], is_client: bool) -> return headers +def normalize_h2_headers(headers: list[tuple[bytes, bytes]]) -> CommandGenerator[None]: + for i in range(len(headers)): + if not headers[i][0].islower(): + yield Log( + f"Lowercased {repr(headers[i][0]).lstrip('b')} header as uppercase is not allowed with HTTP/2." + ) + headers[i] = (headers[i][0].lower(), headers[i][1]) + + class Http2Server(Http2Connection): h2_conf = h2.config.H2Configuration( **Http2Connection.h2_conf_defaults, @@ -288,9 +330,12 @@ def _handle_event(self, event: Event) -> CommandGenerator[None]: if self.is_open_for_us(event.stream_id): headers = [ (b":status", b"%d" % event.response.status_code), - *event.response.headers.fields + *event.response.headers.fields, ] - if not event.response.is_http2: + if event.response.is_http2: + if self.context.options.normalize_outbound_headers: + yield from normalize_h2_headers(headers) + else: headers = normalize_h1_headers(headers, False) self.h2_conn.send_headers( @@ -305,7 +350,15 @@ def _handle_event(self, event: Event) -> CommandGenerator[None]: def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: if isinstance(event, h2.events.RequestReceived): try: - host, port, method, scheme, authority, path, headers = parse_h2_request_headers(event.headers) + ( + host, + port, + method, + scheme, + authority, + path, + headers, + ) = parse_h2_request_headers(event.headers) except ValueError as e: yield from self.protocol_error(f"Invalid HTTP/2 request headers: {e}") return True @@ -324,7 +377,11 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: timestamp_end=None, ) self.streams[event.stream_id] = StreamState.HEADERS_RECEIVED - yield ReceiveHttp(RequestHeaders(event.stream_id, request, end_stream=bool(event.stream_ended))) + yield ReceiveHttp( + RequestHeaders( + event.stream_id, request, end_stream=bool(event.stream_ended) + ) + ) return False else: return (yield from super().handle_h2_event(event)) @@ -341,12 +398,14 @@ class Http2Client(Http2Connection): ReceiveTrailers = ResponseTrailers ReceiveEndOfMessage = ResponseEndOfMessage - our_stream_id: Dict[int, int] - their_stream_id: Dict[int, int] - stream_queue: DefaultDict[int, List[Event]] + our_stream_id: dict[int, int] + their_stream_id: dict[int, int] + stream_queue: collections.defaultdict[int, list[Event]] """Queue of streams that we haven't sent yet because we have reached MAX_CONCURRENT_STREAMS""" provisional_max_concurrency: Optional[int] = 10 """A provisional currency limit before we get the server's first settings frame.""" + last_activity: float + """Timestamp of when we've last seen network activity on this connection.""" def __init__(self, context: Context): super().__init__(context, context.server) @@ -366,9 +425,9 @@ def _handle_event(self, event: Event) -> CommandGenerator[None]: if isinstance(event, HttpEvent): ours = self.our_stream_id.get(event.stream_id, None) if ours is None: - no_free_streams = ( - self.h2_conn.open_outbound_streams >= - (self.provisional_max_concurrency or self.h2_conn.remote_settings.max_concurrent_streams) + no_free_streams = self.h2_conn.open_outbound_streams >= ( + self.provisional_max_concurrency + or self.h2_conn.remote_settings.max_concurrent_streams ) if no_free_streams: self.stream_queue[event.stream_id].append(event) @@ -383,11 +442,9 @@ def _handle_event(self, event: Event) -> CommandGenerator[None]: cmd.event.stream_id = self.their_stream_id[cmd.event.stream_id] yield cmd - can_resume_queue = ( - self.stream_queue and - self.h2_conn.open_outbound_streams < ( - self.provisional_max_concurrency or self.h2_conn.remote_settings.max_concurrent_streams - ) + can_resume_queue = self.stream_queue and self.h2_conn.open_outbound_streams < ( + self.provisional_max_concurrency + or self.h2_conn.remote_settings.max_concurrent_streams ) if can_resume_queue: # popitem would be LIFO, but we want FIFO. @@ -396,17 +453,48 @@ def _handle_event(self, event: Event) -> CommandGenerator[None]: yield from self._handle_event(event) def _handle_event2(self, event: Event) -> CommandGenerator[None]: - if isinstance(event, RequestHeaders): + if isinstance(event, Wakeup): + send_ping_now = ( + # add one second to avoid unnecessary roundtrip, we don't need to be super correct here. + time.time() - self.last_activity + 1 + > self.context.options.http2_ping_keepalive + ) + if send_ping_now: + # PING frames MUST contain 8 octets of opaque data in the payload. + # A sender can include any value it chooses and use those octets in any fashion. + self.last_activity = time.time() + self.h2_conn.ping(b"0" * 8) + data = self.h2_conn.data_to_send() + if data is not None: + yield Log( + f"Send HTTP/2 keep-alive PING to {human.format_address(self.conn.peername)}", + "debug", + ) + yield SendData(self.conn, data) + time_until_next_ping = self.context.options.http2_ping_keepalive - ( + time.time() - self.last_activity + ) + yield RequestWakeup(time_until_next_ping) + return + + self.last_activity = time.time() + if isinstance(event, Start): + if self.context.options.http2_ping_keepalive > 0: + yield RequestWakeup(self.context.options.http2_ping_keepalive) + yield from super()._handle_event(event) + elif isinstance(event, RequestHeaders): pseudo_headers = [ - (b':method', event.request.data.method), - (b':scheme', event.request.data.scheme), - (b':path', event.request.data.path), + (b":method", event.request.data.method), + (b":scheme", event.request.data.scheme), + (b":path", event.request.data.path), ] if event.request.authority: pseudo_headers.append((b":authority", event.request.data.authority)) if event.request.is_http2: hdrs = list(event.request.headers.fields) + if self.context.options.normalize_outbound_headers: + yield from normalize_h2_headers(hdrs) else: headers = event.request.headers if not event.request.authority and "host" in headers: @@ -426,7 +514,10 @@ def _handle_event2(self, event: Event) -> CommandGenerator[None]: def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: if isinstance(event, h2.events.ResponseReceived): - if self.streams.get(event.stream_id, None) is not StreamState.EXPECTING_HEADERS: + if ( + self.streams.get(event.stream_id, None) + is not StreamState.EXPECTING_HEADERS + ): yield from self.protocol_error(f"Received unexpected HTTP/2 response.") return True @@ -447,10 +538,30 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: timestamp_end=None, ) self.streams[event.stream_id] = StreamState.HEADERS_RECEIVED - yield ReceiveHttp(ResponseHeaders(event.stream_id, response, bool(event.stream_ended))) + yield ReceiveHttp( + ResponseHeaders(event.stream_id, response, bool(event.stream_ended)) + ) + return False + elif isinstance(event, h2.events.InformationalResponseReceived): + # We violate the spec here ("A proxy MUST forward 1xx responses", RFC 7231), + # but that's probably fine: + # - 100 Continue is sent by mitmproxy to clients (irrespective of what the server does). + # - 101 Switching Protocols is not allowed for HTTP/2. + # - 102 Processing is WebDAV only and also ignorable. + # - 103 Early Hints is not mission-critical. + headers = http.Headers(event.headers) + status: Union[str, int] = "" + try: + status = int(headers[":status"]) + reason = status_codes.RESPONSES.get(status, "") + except (KeyError, ValueError): + reason = "" + yield Log(f"Swallowing HTTP/2 informational response: {status} {reason}") return False elif isinstance(event, h2.events.RequestReceived): - yield from self.protocol_error(f"HTTP/2 protocol error: received request from server") + yield from self.protocol_error( + f"HTTP/2 protocol error: received request from server" + ) return True elif isinstance(event, h2.events.RemoteSettingsChanged): # We have received at least one settings from now, @@ -461,8 +572,10 @@ def handle_h2_event(self, event: h2.events.Event) -> CommandGenerator[bool]: return (yield from super().handle_h2_event(event)) -def split_pseudo_headers(h2_headers: Sequence[Tuple[bytes, bytes]]) -> Tuple[Dict[bytes, bytes], http.Headers]: - pseudo_headers: Dict[bytes, bytes] = {} +def split_pseudo_headers( + h2_headers: Sequence[tuple[bytes, bytes]] +) -> tuple[dict[bytes, bytes], http.Headers]: + pseudo_headers: dict[bytes, bytes] = {} i = 0 for (header, value) in h2_headers: if header.startswith(b":"): @@ -480,14 +593,16 @@ def split_pseudo_headers(h2_headers: Sequence[Tuple[bytes, bytes]]) -> Tuple[Dic def parse_h2_request_headers( - h2_headers: Sequence[Tuple[bytes, bytes]] -) -> Tuple[str, int, bytes, bytes, bytes, bytes, http.Headers]: + h2_headers: Sequence[tuple[bytes, bytes]] +) -> tuple[str, int, bytes, bytes, bytes, bytes, http.Headers]: """Split HTTP/2 pseudo-headers from the actual headers and parse them.""" pseudo_headers, headers = split_pseudo_headers(h2_headers) try: method: bytes = pseudo_headers.pop(b":method") - scheme: bytes = pseudo_headers.pop(b":scheme") # this raises for HTTP/2 CONNECT requests + scheme: bytes = pseudo_headers.pop( + b":scheme" + ) # this raises for HTTP/2 CONNECT requests path: bytes = pseudo_headers.pop(b":path") authority: bytes = pseudo_headers.pop(b":authority", b"") except KeyError as e: @@ -499,7 +614,7 @@ def parse_h2_request_headers( if authority: host, port = url.parse_authority(authority, check=True) if port is None: - port = 80 if scheme == b'http' else 443 + port = 80 if scheme == b"http" else 443 else: host = "" port = 0 @@ -507,7 +622,9 @@ def parse_h2_request_headers( return host, port, method, scheme, authority, path, headers -def parse_h2_response_headers(h2_headers: Sequence[Tuple[bytes, bytes]]) -> Tuple[int, http.Headers]: +def parse_h2_response_headers( + h2_headers: Sequence[tuple[bytes, bytes]] +) -> tuple[int, http.Headers]: """Split HTTP/2 pseudo-headers from the actual headers and parse them.""" pseudo_headers, headers = split_pseudo_headers(h2_headers) diff --git a/mitmproxy/proxy/layers/http/_http_h2.py b/mitmproxy/proxy/layers/http/_http_h2.py index a44a0adae8..abcce65798 100644 --- a/mitmproxy/proxy/layers/http/_http_h2.py +++ b/mitmproxy/proxy/layers/http/_http_h2.py @@ -1,5 +1,5 @@ import collections -from typing import DefaultDict, Deque, NamedTuple +from typing import Dict, List, NamedTuple, Tuple import h2.config import h2.connection @@ -32,18 +32,21 @@ class BufferedH2Connection(h2.connection.H2Connection): To simplify implementation, padding is unsupported. """ - stream_buffers: DefaultDict[int, Deque[SendH2Data]] + + stream_buffers: collections.defaultdict[int, collections.deque[SendH2Data]] + stream_trailers: Dict[int, List[Tuple[bytes, bytes]]] def __init__(self, config: h2.config.H2Configuration): super().__init__(config) self.stream_buffers = collections.defaultdict(collections.deque) + self.stream_trailers = {} def send_data( self, stream_id: int, data: bytes, end_stream: bool = False, - pad_length: None = None + pad_length: None = None, ) -> None: """ Send data on a given stream. @@ -55,17 +58,15 @@ def send_data( assert pad_length is None while frame_size > self.max_outbound_frame_size: - chunk_data = data[:self.max_outbound_frame_size] + chunk_data = data[: self.max_outbound_frame_size] self.send_data(stream_id, chunk_data, end_stream=False) - data = data[self.max_outbound_frame_size:] + data = data[self.max_outbound_frame_size :] frame_size -= len(chunk_data) if self.stream_buffers.get(stream_id, None): # We already have some data buffered, let's append. - self.stream_buffers[stream_id].append( - SendH2Data(data, end_stream) - ) + self.stream_buffers[stream_id].append(SendH2Data(data, end_stream)) else: available_window = self.local_flow_control_window(stream_id) if frame_size <= available_window: @@ -76,11 +77,18 @@ def send_data( super().send_data(stream_id, can_send_now, end_stream=False) data = data[available_window:] # We can't send right now, so we buffer. - self.stream_buffers[stream_id].append( - SendH2Data(data, end_stream) - ) + self.stream_buffers[stream_id].append(SendH2Data(data, end_stream)) + + def send_trailers(self, stream_id: int, trailers: List[Tuple[bytes, bytes]]): + if self.stream_buffers.get(stream_id, None): + # Though trailers are not subject to flow control, we need to queue them and send strictly after data frames + self.stream_trailers[stream_id] = trailers + else: + self.send_headers(stream_id, trailers, end_stream=True) - def end_stream(self, stream_id) -> None: + def end_stream(self, stream_id: int) -> None: + if stream_id in self.stream_trailers: + return # we already have trailers queued up that will end the stream. self.send_data(stream_id, b"", end_stream=True) def reset_stream(self, stream_id: int, error_code: int = 0) -> None: @@ -98,7 +106,10 @@ def receive_data(self, data: bytes): self.stream_window_updated(event.stream_id) continue elif isinstance(event, h2.events.RemoteSettingsChanged): - if h2.settings.SettingCodes.INITIAL_WINDOW_SIZE in event.changed_settings: + if ( + h2.settings.SettingCodes.INITIAL_WINDOW_SIZE + in event.changed_settings + ): self.connection_window_updated() elif isinstance(event, h2.events.StreamReset): self.stream_buffers.pop(event.stream_id, None) @@ -117,8 +128,9 @@ def stream_window_updated(self, stream_id: int) -> bool: except KeyError: stream_was_reset = True else: - stream_was_reset = ( - stream.state_machine.state not in (h2.stream.StreamState.OPEN, h2.stream.StreamState.HALF_CLOSED_REMOTE) + stream_was_reset = stream.state_machine.state not in ( + h2.stream.StreamState.OPEN, + h2.stream.StreamState.HALF_CLOSED_REMOTE, ) if stream_was_reset: self.stream_buffers.pop(stream_id, None) @@ -146,6 +158,8 @@ def stream_window_updated(self, stream_id: int) -> bool: available_window -= len(chunk.data) if not self.stream_buffers[stream_id]: del self.stream_buffers[stream_id] + if stream_id in self.stream_trailers: + self.send_headers(stream_id, self.stream_trailers.pop(stream_id), end_stream=True) sent_any_data = True return sent_any_data @@ -158,7 +172,9 @@ def connection_window_updated(self) -> None: while sent_any_data: sent_any_data = False for stream_id in list(self.stream_buffers): - self.stream_buffers[stream_id] = self.stream_buffers.pop(stream_id) # move to end of dict + self.stream_buffers[stream_id] = self.stream_buffers.pop( + stream_id + ) # move to end of dict if self.stream_window_updated(stream_id): sent_any_data = True if self.outbound_flow_control_window == 0: diff --git a/mitmproxy/proxy/layers/http/_upstream_proxy.py b/mitmproxy/proxy/layers/http/_upstream_proxy.py index dc0c60ec49..cdbde2d67d 100644 --- a/mitmproxy/proxy/layers/http/_upstream_proxy.py +++ b/mitmproxy/proxy/layers/http/_upstream_proxy.py @@ -1,5 +1,5 @@ import time -from typing import Optional, Tuple +from typing import Optional from h11._receivebuffer import ReceiveBuffer @@ -18,16 +18,9 @@ class HttpUpstreamProxy(tunnel.TunnelLayer): tunnel_connection: connection.Server def __init__( - self, - ctx: context.Context, - tunnel_conn: connection.Server, - send_connect: bool + self, ctx: context.Context, tunnel_conn: connection.Server, send_connect: bool ): - super().__init__( - ctx, - tunnel_connection=tunnel_conn, - conn=ctx.server - ) + super().__init__(ctx, tunnel_connection=tunnel_conn, conn=ctx.server) self.buf = ReceiveBuffer() self.send_connect = send_connect @@ -53,12 +46,13 @@ def start_handshake(self) -> layer.CommandGenerator[None]: return (yield from super().start_handshake()) assert self.conn.address flow = http.HTTPFlow(self.context.client, self.tunnel_connection) + authority = self.conn.address[0].encode("idna") + f":{self.conn.address[1]}".encode() flow.request = http.Request( host=self.conn.address[0], port=self.conn.address[1], method=b"CONNECT", scheme=b"", - authority=f"{self.conn.address[0]}:{self.conn.address[1]}".encode(), + authority=authority, path=b"", http_version=b"HTTP/1.1", headers=http.Headers(), @@ -71,13 +65,17 @@ def start_handshake(self) -> layer.CommandGenerator[None]: raw = http1.assemble_request(flow.request) yield commands.SendData(self.tunnel_connection, raw) - def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: + def receive_handshake_data( + self, data: bytes + ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: if not self.send_connect: return (yield from super().receive_handshake_data(data)) self.buf += data response_head = self.buf.maybe_extract_lines() if response_head: - response_head = [bytes(x) for x in response_head] # TODO: Make url.parse compatible with bytearrays + response_head = [ + bytes(x) for x in response_head + ] # TODO: Make url.parse compatible with bytearrays try: response = http1.read_response_head(response_head) except ValueError as e: @@ -92,8 +90,10 @@ def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bo else: proxyaddr = human.format_address(self.tunnel_connection.address) raw_resp = b"\n".join(response_head) - yield commands.Log(f"{proxyaddr}: {raw_resp!r}", - level="debug") - return False, f"Upstream proxy {proxyaddr} refused HTTP CONNECT request: {response.status_code} {response.reason}" + yield commands.Log(f"{proxyaddr}: {raw_resp!r}", level="debug") + return ( + False, + f"Upstream proxy {proxyaddr} refused HTTP CONNECT request: {response.status_code} {response.reason}", + ) else: return False, None diff --git a/mitmproxy/proxy/layers/modes.py b/mitmproxy/proxy/layers/modes.py index 40ebfda12f..cf9bf960a5 100644 --- a/mitmproxy/proxy/layers/modes.py +++ b/mitmproxy/proxy/layers/modes.py @@ -22,10 +22,14 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: class DestinationKnown(layer.Layer, metaclass=ABCMeta): """Base layer for layers that gather connection destination info and then delegate.""" + child_layer: layer.Layer def finish_start(self) -> layer.CommandGenerator[Optional[str]]: - if self.context.options.connection_strategy == "eager" and self.context.server.address: + if ( + self.context.options.connection_strategy == "eager" + and self.context.server.address + ): err = yield commands.OpenConnection(self.context.server) if err: self._handle_event = self.done # type: ignore @@ -105,6 +109,7 @@ class Socks5AuthHook(StartHook): This hook decides whether they are valid by setting `data.valid`. """ + data: Socks5AuthData @@ -119,7 +124,8 @@ def socks_err( if reply_code is not None: yield commands.SendData( self.context.client, - bytes([SOCKS5_VERSION, reply_code]) + b"\x00\x01\x00\x00\x00\x00\x00\x00" + bytes([SOCKS5_VERSION, reply_code]) + + b"\x00\x01\x00\x00\x00\x00\x00\x00", ) yield commands.CloseConnection(self.context.client) yield commands.Log(message) @@ -134,7 +140,9 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: yield from self.state() elif isinstance(event, events.ConnectionClosed): if self.buf: - yield commands.Log(f"Client closed connection before completing SOCKS5 handshake: {self.buf!r}") + yield commands.Log( + f"Client closed connection before completing SOCKS5 handshake: {self.buf!r}" + ) yield commands.CloseConnection(event.connection) else: raise AssertionError(f"Unknown event: {event}") @@ -148,7 +156,9 @@ def state_greet(self): guess = "Probably not a SOCKS request but a regular HTTP request. " else: guess = "" - yield from self.socks_err(guess + "Invalid SOCKS version. Expected 0x05, got 0x%x" % self.buf[0]) + yield from self.socks_err( + guess + "Invalid SOCKS version. Expected 0x05, got 0x%x" % self.buf[0] + ) return n_methods = self.buf[1] @@ -162,15 +172,19 @@ def state_greet(self): method = SOCKS5_METHOD_NO_AUTHENTICATION_REQUIRED self.state = self.state_connect - if method not in self.buf[2:2 + n_methods]: - method_str = "user/password" if method == SOCKS5_METHOD_USER_PASSWORD_AUTHENTICATION else "no" + if method not in self.buf[2 : 2 + n_methods]: + method_str = ( + "user/password" + if method == SOCKS5_METHOD_USER_PASSWORD_AUTHENTICATION + else "no" + ) yield from self.socks_err( f"Client does not support SOCKS5 with {method_str} authentication.", - SOCKS5_METHOD_NO_ACCEPTABLE_METHODS + SOCKS5_METHOD_NO_ACCEPTABLE_METHODS, ) return yield commands.SendData(self.context.client, bytes([SOCKS5_VERSION, method])) - self.buf = self.buf[2 + n_methods:] + self.buf = self.buf[2 + n_methods :] yield from self.state() state = state_greet @@ -186,8 +200,10 @@ def state_auth(self): pass_len = self.buf[2 + user_len] if len(self.buf) < 3 + user_len + pass_len: return - user = self.buf[2:(2 + user_len)].decode("utf-8", "backslashreplace") - password = self.buf[(3 + user_len):(3 + user_len + pass_len)].decode("utf-8", "backslashreplace") + user = self.buf[2 : (2 + user_len)].decode("utf-8", "backslashreplace") + password = self.buf[(3 + user_len) : (3 + user_len + pass_len)].decode( + "utf-8", "backslashreplace" + ) data = Socks5AuthData(self.context.client, user, password) yield Socks5AuthHook(data) @@ -198,17 +214,20 @@ def state_auth(self): return yield commands.SendData(self.context.client, b"\x01\x00") - self.buf = self.buf[3 + user_len + pass_len:] + self.buf = self.buf[3 + user_len + pass_len :] self.state = self.state_connect yield from self.state() def state_connect(self): # Parse Connect Request - if len(self.buf) < 4: + if len(self.buf) < 5: return if self.buf[:3] != b"\x05\x01\x00": - yield from self.socks_err(f"Unsupported SOCKS5 request: {self.buf!r}", SOCKS5_REP_COMMAND_NOT_SUPPORTED) + yield from self.socks_err( + f"Unsupported SOCKS5 request: {self.buf!r}", + SOCKS5_REP_COMMAND_NOT_SUPPORTED, + ) return # Determine message length @@ -221,7 +240,9 @@ def state_connect(self): elif atyp == SOCKS5_ATYP_DOMAINNAME: message_len = 4 + 1 + self.buf[4] + 2 else: - yield from self.socks_err(f"Unknown address type: {atyp}", SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED) + yield from self.socks_err( + f"Unknown address type: {atyp}", SOCKS5_REP_ADDRESS_TYPE_NOT_SUPPORTED + ) return # Do we have enough bytes yet? @@ -240,7 +261,7 @@ def state_connect(self): host_bytes = msg[5:-2] host = host_bytes.decode("ascii", "replace") - port, = struct.unpack("!H", msg[-2:]) + (port,) = struct.unpack("!H", msg[-2:]) # We now have all we need, let's get going. self.context.server.address = (host, port) @@ -250,10 +271,16 @@ def state_connect(self): # but that's not a problem in practice... err = yield from self.finish_start() if err: - yield commands.SendData(self.context.client, b"\x05\x04\x00\x01\x00\x00\x00\x00\x00\x00") + yield commands.SendData( + self.context.client, b"\x05\x04\x00\x01\x00\x00\x00\x00\x00\x00" + ) yield commands.CloseConnection(self.context.client) else: - yield commands.SendData(self.context.client, b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00") + yield commands.SendData( + self.context.client, b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00" + ) if self.buf: - yield from self.child_layer.handle_event(events.DataReceived(self.context.client, self.buf)) + yield from self.child_layer.handle_event( + events.DataReceived(self.context.client, self.buf) + ) del self.buf diff --git a/mitmproxy/proxy/layers/tcp.py b/mitmproxy/proxy/layers/tcp.py index 4332d8415c..296adb80b3 100644 --- a/mitmproxy/proxy/layers/tcp.py +++ b/mitmproxy/proxy/layers/tcp.py @@ -25,6 +25,7 @@ class TcpMessageHook(StartHook): A TCP connection has received a message. The most recent message will be flow.messages[-1]. The message is user-modifiable. """ + flow: tcp.TCPFlow @@ -33,6 +34,7 @@ class TcpEndHook(StartHook): """ A TCP connection has ended. """ + flow: tcp.TCPFlow @@ -43,6 +45,7 @@ class TcpErrorHook(StartHook): Every TCP flow will receive either a tcp_error or a tcp_end event, but not both. """ + flow: tcp.TCPFlow @@ -56,6 +59,7 @@ class TCPLayer(layer.Layer): """ Simple TCP layer that just relays messages right now. """ + flow: Optional[tcp.TCPFlow] def __init__(self, context: Context, ignore: bool = False): @@ -89,7 +93,9 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: if isinstance(event, TcpMessageInjected): # we just spoof that we received data here and then process that regularly. event = events.DataReceived( - self.context.client if event.message.from_client else self.context.server, + self.context.client + if event.message.from_client + else self.context.server, event.message.content, ) @@ -113,9 +119,8 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: elif isinstance(event, events.ConnectionClosed): all_done = not ( - (self.context.client.state & ConnectionState.CAN_READ) - or - (self.context.server.state & ConnectionState.CAN_READ) + (self.context.client.state & ConnectionState.CAN_READ) + or (self.context.server.state & ConnectionState.CAN_READ) ) if all_done: if self.context.server.state is not ConnectionState.CLOSED: @@ -125,6 +130,7 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: self._handle_event = self.done if self.flow: yield TcpEndHook(self.flow) + self.flow.live = False else: yield commands.CloseConnection(send_to, half_close=True) else: diff --git a/mitmproxy/proxy/layers/tls.py b/mitmproxy/proxy/layers/tls.py index 47b813be10..580a39bbb5 100644 --- a/mitmproxy/proxy/layers/tls.py +++ b/mitmproxy/proxy/layers/tls.py @@ -1,14 +1,16 @@ import struct import time from dataclasses import dataclass -from typing import Iterator, Literal, Optional, Tuple +from typing import Iterator, Literal, Optional from OpenSSL import SSL + from mitmproxy import certs, connection -from mitmproxy.net import tls as net_tls from mitmproxy.proxy import commands, events, layer, tunnel from mitmproxy.proxy import context from mitmproxy.proxy.commands import StartHook +from mitmproxy.proxy.layers import tcp +from mitmproxy.tls import ClientHello, ClientHelloData, TlsData from mitmproxy.utils import human @@ -21,12 +23,7 @@ def is_tls_handshake_record(d: bytes) -> bool: # TLS ClientHello magic, works for SSLv3, TLSv1.0, TLSv1.1, TLSv1.2. # TLS 1.3 mandates legacy_record_version to be 0x0301. # http://www.moserware.com/2009/06/first-few-milliseconds-of-https.html#client-hello - return ( - len(d) >= 3 and - d[0] == 0x16 and - d[1] == 0x03 and - 0x0 <= d[2] <= 0x03 - ) + return len(d) >= 3 and d[0] == 0x16 and d[1] == 0x03 and 0x0 <= d[2] <= 0x03 def handshake_record_contents(data: bytes) -> Iterator[bytes]: @@ -39,7 +36,7 @@ def handshake_record_contents(data: bytes) -> Iterator[bytes]: while True: if len(data) < offset + 5: return - record_header = data[offset:offset + 5] + record_header = data[offset : offset + 5] if not is_tls_handshake_record(record_header): raise ValueError(f"Expected TLS record, got {record_header!r} instead.") record_size = struct.unpack("!H", record_header[3:])[0] @@ -49,7 +46,7 @@ def handshake_record_contents(data: bytes) -> Iterator[bytes]: if len(data) < offset + record_size: return - record_body = data[offset:offset + record_size] + record_body = data[offset : offset + record_size] yield record_body offset += record_size @@ -63,13 +60,13 @@ def get_client_hello(data: bytes) -> Optional[bytes]: for d in handshake_record_contents(data): client_hello += d if len(client_hello) >= 4: - client_hello_size = struct.unpack("!I", b'\x00' + client_hello[1:4])[0] + 4 + client_hello_size = struct.unpack("!I", b"\x00" + client_hello[1:4])[0] + 4 if len(client_hello) >= client_hello_size: return client_hello[:client_hello_size] return None -def parse_client_hello(data: bytes) -> Optional[net_tls.ClientHello]: +def parse_client_hello(data: bytes) -> Optional[ClientHello]: """ Check if the supplied bytes contain a full ClientHello message, and if so, parse it. @@ -85,7 +82,7 @@ def parse_client_hello(data: bytes) -> Optional[net_tls.ClientHello]: client_hello = get_client_hello(data) if client_hello: try: - return net_tls.ClientHello(client_hello[4:]) + return ClientHello(client_hello[4:]) except EOFError as e: raise ValueError("Invalid ClientHello") from e return None @@ -97,18 +94,6 @@ def parse_client_hello(data: bytes) -> Optional[net_tls.ClientHello]: # We need these classes as hooks can only have one argument at the moment. -@dataclass -class ClientHelloData: - context: context.Context - """The context object for this connection.""" - client_hello: net_tls.ClientHello - """The entire parsed TLS ClientHello.""" - establish_server_tls_first: bool = False - """ - If set to `True`, pause this handshake and establish TLS with an upstream server first. - This makes it possible to process the server certificate when generating an interception certificate. - """ - @dataclass class TlsClienthelloHook(StartHook): @@ -118,36 +103,68 @@ class TlsClienthelloHook(StartHook): This hook decides whether a server connection is needed to negotiate TLS with the client (data.establish_server_tls_first) """ - data: ClientHelloData - -@dataclass -class TlsStartData: - conn: connection.Connection - context: context.Context - ssl_conn: Optional[SSL.Connection] = None + data: ClientHelloData @dataclass class TlsStartClientHook(StartHook): """ - TLS Negotation between mitmproxy and a client is about to start. + TLS negotation between mitmproxy and a client is about to start. An addon is expected to initialize data.ssl_conn. - (by default, this is done by mitmproxy.addons.TlsConfig) + (by default, this is done by `mitmproxy.addons.tlsconfig`) """ - data: TlsStartData + + data: TlsData @dataclass class TlsStartServerHook(StartHook): """ - TLS Negotation between mitmproxy and a server is about to start. + TLS negotation between mitmproxy and a server is about to start. An addon is expected to initialize data.ssl_conn. - (by default, this is done by mitmproxy.addons.TlsConfig) + (by default, this is done by `mitmproxy.addons.tlsconfig`) + """ + + data: TlsData + + +@dataclass +class TlsEstablishedClientHook(StartHook): + """ + The TLS handshake with the client has been completed successfully. """ - data: TlsStartData + + data: TlsData + + +@dataclass +class TlsEstablishedServerHook(StartHook): + """ + The TLS handshake with the server has been completed successfully. + """ + + data: TlsData + + +@dataclass +class TlsFailedClientHook(StartHook): + """ + The TLS handshake with the client has failed. + """ + + data: TlsData + + +@dataclass +class TlsFailedServerHook(StartHook): + """ + The TLS handshake with the server has failed. + """ + + data: TlsData class _TLSLayer(tunnel.TunnelLayer): @@ -169,13 +186,15 @@ def __repr__(self): def start_tls(self) -> layer.CommandGenerator[None]: assert not self.tls - tls_start = TlsStartData(self.conn, self.context) - if tls_start.conn == tls_start.context.client: + tls_start = TlsData(self.conn, self.context) + if self.conn == self.context.client: yield TlsStartClientHook(tls_start) else: yield TlsStartServerHook(tls_start) if not tls_start.ssl_conn: - yield commands.Log("No TLS context was provided, failing connection.", "error") + yield commands.Log( + "No TLS context was provided, failing connection.", "error" + ) yield commands.CloseConnection(self.conn) return assert tls_start.ssl_conn @@ -190,7 +209,9 @@ def tls_interact(self) -> layer.CommandGenerator[None]: else: yield commands.SendData(self.conn, data) - def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: + def receive_handshake_data( + self, data: bytes + ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: # bio_write errors for b"", so we need to check first if we actually received something. if data: self.tls.bio_write(data) @@ -201,27 +222,47 @@ def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bo return False, None except SSL.Error as e: # provide more detailed information for some errors. - last_err = e.args and isinstance(e.args[0], list) and e.args[0] and e.args[0][-1] - if last_err == ('SSL routines', 'tls_process_server_certificate', 'certificate verify failed'): + last_err = ( + e.args and isinstance(e.args[0], list) and e.args[0] and e.args[0][-1] + ) + if last_err in [ + ( + "SSL routines", + "tls_process_server_certificate", + "certificate verify failed", + ), + ("SSL routines", "", "certificate verify failed"), # OpenSSL 3+ + ]: verify_result = SSL._lib.SSL_get_verify_result(self.tls._ssl) # type: ignore error = SSL._ffi.string(SSL._lib.X509_verify_cert_error_string(verify_result)).decode() # type: ignore err = f"Certificate verify failed: {error}" elif last_err in [ - ('SSL routines', 'ssl3_read_bytes', 'tlsv1 alert unknown ca'), - ('SSL routines', 'ssl3_read_bytes', 'sslv3 alert bad certificate') + ("SSL routines", "ssl3_read_bytes", "tlsv1 alert unknown ca"), + ("SSL routines", "ssl3_read_bytes", "sslv3 alert bad certificate"), + ("SSL routines", "", "tlsv1 alert unknown ca"), # OpenSSL 3+ + ("SSL routines", "", "sslv3 alert bad certificate"), # OpenSSL 3+ ]: assert isinstance(last_err, tuple) err = last_err[2] - elif last_err == ('SSL routines', 'ssl3_get_record', 'wrong version number') and data[:4].isascii(): + elif ( + last_err + in [ + ("SSL routines", "ssl3_get_record", "wrong version number"), + ("SSL routines", "", "wrong version number"), # OpenSSL 3+ + ] + and data[:4].isascii() + ): err = f"The remote server does not speak TLS." - elif last_err == ('SSL routines', 'ssl3_read_bytes', 'tlsv1 alert protocol version'): + elif last_err in [ + ("SSL routines", "ssl3_read_bytes", "tlsv1 alert protocol version"), + ("SSL routines", "", "tlsv1 alert protocol version"), # OpenSSL 3+ + ]: err = ( f"The remote server and mitmproxy cannot agree on a TLS version to use. " f"You may need to adjust mitmproxy's tls_version_server_min option." ) else: err = f"OpenSSL {e!r}" - self.conn.error = err return False, err else: # Here we set all attributes that are only known *after* the handshake. @@ -238,14 +279,34 @@ def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bo self.conn.timestamp_tls_setup = time.time() self.conn.alpn = self.tls.get_alpn_proto_negotiated() - self.conn.certificate_list = [certs.Cert.from_pyopenssl(x) for x in all_certs] + self.conn.certificate_list = [ + certs.Cert.from_pyopenssl(x) for x in all_certs + ] self.conn.cipher = self.tls.get_cipher_name() self.conn.tls_version = self.tls.get_protocol_version_name() if self.debug: - yield commands.Log(f"{self.debug}[tls] tls established: {self.conn}", "debug") + yield commands.Log( + f"{self.debug}[tls] tls established: {self.conn}", "debug" + ) + if self.conn == self.context.client: + yield TlsEstablishedClientHook( + TlsData(self.conn, self.context, self.tls) + ) + else: + yield TlsEstablishedServerHook( + TlsData(self.conn, self.context, self.tls) + ) yield from self.receive_data(b"") return True, None + def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: + self.conn.error = err + if self.conn == self.context.client: + yield TlsFailedClientHook(TlsData(self.conn, self.context, self.tls)) + else: + yield TlsFailedServerHook(TlsData(self.conn, self.context, self.tls)) + yield from super().on_handshake_error(err) + def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: if data: self.tls.bio_write(data) @@ -261,6 +322,14 @@ def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: except SSL.ZeroReturnError: close = True break + except SSL.Error as e: + # This may be happening because the other side send an alert. + # There's somewhat ugly behavior with Firefox on Android here, + # which upon mistrusting a certificate still completes the handshake + # and then sends an alert in the next packet. At this point we have unfortunately + # already fired out `tls_established_client` hook. + yield commands.Log(f"TLS Error: {e}", "warn") + break if plaintext: yield from self.event_to_child( @@ -269,10 +338,10 @@ def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: if close: self.conn.state &= ~connection.ConnectionState.CAN_READ if self.debug: - yield commands.Log(f"{self.debug}[tls] close_notify {self.conn}", level="debug") - yield from self.event_to_child( - events.ConnectionClosed(self.conn) - ) + yield commands.Log( + f"{self.debug}[tls] close_notify {self.conn}", level="debug" + ) + yield from self.event_to_child(events.ConnectionClosed(self.conn)) def receive_close(self) -> layer.CommandGenerator[None]: if self.tls.get_shutdown() & SSL.RECEIVED_SHUTDOWN: @@ -283,7 +352,7 @@ def receive_close(self) -> layer.CommandGenerator[None]: def send_data(self, data: bytes) -> layer.CommandGenerator[None]: try: self.tls.sendall(data) - except SSL.ZeroReturnError: + except (SSL.ZeroReturnError, SSL.SysCallError): # The other peer may still be trying to send data over, which we discard here. pass yield from self.tls_interact() @@ -297,9 +366,12 @@ class ServerTLSLayer(_TLSLayer): """ This layer establishes TLS for a single server connection. """ + wait_for_clienthello: bool = False - def __init__(self, context: context.Context, conn: Optional[connection.Server] = None): + def __init__( + self, context: context.Context, conn: Optional[connection.Server] = None + ): super().__init__(context, conn or context.server) def start_handshake(self) -> layer.CommandGenerator[None]: @@ -318,12 +390,16 @@ def start_handshake(self) -> layer.CommandGenerator[None]: self.tunnel_state = tunnel.TunnelState.CLOSED else: yield from self.start_tls() - yield from self.receive_handshake_data(b"") + if self.tls: + yield from self.receive_handshake_data(b"") def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: if self.wait_for_clienthello: for command in super().event_to_child(event): - if isinstance(command, commands.OpenConnection) and command.connection == self.conn: + if ( + isinstance(command, commands.OpenConnection) + and command.connection == self.conn + ): self.wait_for_clienthello = False # swallow OpenConnection here by not re-yielding it. else: @@ -353,6 +429,7 @@ class ClientTLSLayer(_TLSLayer): └────────────────┘ """ + recv_buffer: bytearray server_tls_available: bool client_hello_parsed: bool = False @@ -382,7 +459,9 @@ def __init__(self, context: context.Context): def start_handshake(self) -> layer.CommandGenerator[None]: yield from () - def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: + def receive_handshake_data( + self, data: bytes + ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: if self.client_hello_parsed: return (yield from super().receive_handshake_data(data)) self.recv_buffer.extend(data) @@ -401,11 +480,33 @@ def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bo tls_clienthello = ClientHelloData(self.context, client_hello) yield TlsClienthelloHook(tls_clienthello) - if tls_clienthello.establish_server_tls_first and not self.context.server.tls_established: + if tls_clienthello.ignore_connection: + # we've figured out that we don't want to intercept this connection, so we assign fake connection objects + # to all TLS layers. This makes the real connection contents just go through. + self.conn = self.tunnel_connection = connection.Client( + ("ignore-conn", 0), ("ignore-conn", 0), time.time() + ) + parent_layer = self.context.layers[self.context.layers.index(self) - 1] + if isinstance(parent_layer, ServerTLSLayer): + parent_layer.conn = parent_layer.tunnel_connection = connection.Server( + None + ) + self.child_layer = tcp.TCPLayer(self.context, ignore=True) + yield from self.event_to_child( + events.DataReceived(self.context.client, bytes(self.recv_buffer)) + ) + self.recv_buffer.clear() + return True, None + if ( + tls_clienthello.establish_server_tls_first + and not self.context.server.tls_established + ): err = yield from self.start_server_tls() if err: - yield commands.Log(f"Unable to establish TLS connection with server ({err}). " - f"Trying to establish TLS with client anyway.") + yield commands.Log( + f"Unable to establish TLS connection with server ({err}). " + f"Trying to establish TLS with client anyway." + ) yield from self.start_tls() if not self.conn.connected: @@ -433,13 +534,23 @@ def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: level: Literal["warn", "info"] = "warn" if err.startswith("Cannot parse ClientHello"): pass - elif "('SSL routines', 'tls_early_post_process_client_hello', 'unsupported protocol')" in err: + elif ( + "('SSL routines', 'tls_early_post_process_client_hello', 'unsupported protocol')" + in err + or "('SSL routines', '', 'unsupported protocol')" in err # OpenSSL 3+ + ): err = ( f"Client and mitmproxy cannot agree on a TLS version to use. " f"You may need to adjust mitmproxy's tls_version_client_min option." ) - elif "unknown ca" in err or "bad certificate" in err or "certificate unknown" in err: - err = f"The client does not trust the proxy's certificate for {dest} ({err})" + elif ( + "unknown ca" in err + or "bad certificate" in err + or "certificate unknown" in err + ): + err = ( + f"The client does not trust the proxy's certificate for {dest} ({err})" + ) elif err == "connection closed": err = ( f"The client disconnected during the handshake. If this happens consistently for {dest}, " diff --git a/mitmproxy/proxy/layers/websocket.py b/mitmproxy/proxy/layers/websocket.py index b616eb9408..a2c57fee91 100644 --- a/mitmproxy/proxy/layers/websocket.py +++ b/mitmproxy/proxy/layers/websocket.py @@ -1,6 +1,6 @@ import time from dataclasses import dataclass -from typing import Iterator, List +from typing import Iterator import wsproto import wsproto.extensions @@ -21,6 +21,7 @@ class WebsocketStartHook(StartHook): """ A WebSocket connection has commenced. """ + flow: http.HTTPFlow @@ -32,6 +33,7 @@ class WebsocketMessageHook(StartHook): message is user-modifiable. Currently there are two types of messages, corresponding to the BINARY and TEXT frame types. """ + flow: http.HTTPFlow @@ -59,11 +61,12 @@ class WebsocketConnection(wsproto.Connection): - we add a framebuffer for incomplete messages - we wrap .send() so that we can directly yield it. """ + conn: connection.Connection - frame_buf: List[bytes] + frame_buf: list[bytes] def __init__(self, *args, conn: connection.Connection, **kwargs): - super(WebsocketConnection, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.conn = conn self.frame_buf = [b""] @@ -79,6 +82,7 @@ class WebsocketLayer(layer.Layer): """ WebSocket layer that intercepts and relays messages. """ + flow: http.HTTPFlow client_ws: WebsocketConnection server_ws: WebsocketConnection @@ -86,7 +90,6 @@ class WebsocketLayer(layer.Layer): def __init__(self, context: Context, flow: http.HTTPFlow): super().__init__(context) self.flow = flow - assert context.server.connected @expect(events.Start) def start(self, _) -> layer.CommandGenerator[None]: @@ -98,7 +101,9 @@ def start(self, _) -> layer.CommandGenerator[None]: assert self.flow.response # satisfy type checker ext_header = self.flow.response.headers.get("Sec-WebSocket-Extensions", "") if ext_header: - for ext in wsproto.utilities.split_comma_header(ext_header.encode("ascii", "replace")): + for ext in wsproto.utilities.split_comma_header( + ext_header.encode("ascii", "replace") + ): ext_name = ext.split(";", 1)[0].strip() if ext_name == wsproto.extensions.PerMessageDeflate.name: client_deflate = wsproto.extensions.PerMessageDeflate() @@ -108,10 +113,16 @@ def start(self, _) -> layer.CommandGenerator[None]: server_deflate.finalize(ext) server_extensions.append(server_deflate) else: - yield commands.Log(f"Ignoring unknown WebSocket extension {ext_name!r}.") + yield commands.Log( + f"Ignoring unknown WebSocket extension {ext_name!r}." + ) - self.client_ws = WebsocketConnection(wsproto.ConnectionType.SERVER, client_extensions, conn=self.context.client) - self.server_ws = WebsocketConnection(wsproto.ConnectionType.CLIENT, server_extensions, conn=self.context.server) + self.client_ws = WebsocketConnection( + wsproto.ConnectionType.SERVER, client_extensions, conn=self.context.client + ) + self.server_ws = WebsocketConnection( + wsproto.ConnectionType.CLIENT, server_extensions, conn=self.context.server + ) yield WebsocketStartHook(self.flow) @@ -125,12 +136,14 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: if isinstance(event, events.ConnectionEvent): from_client = event.connection == self.context.client + injected = False elif isinstance(event, WebSocketMessageInjected): from_client = event.message.from_client + injected = True else: raise AssertionError(f"Unexpected event: {event}") - from_str = 'client' if from_client else 'server' + from_str = "client" if from_client else "server" if from_client: src_ws = self.client_ws dst_ws = self.server_ws @@ -144,9 +157,7 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: src_ws.receive_data(None) elif isinstance(event, WebSocketMessageInjected): fragmentizer = Fragmentizer([], event.message.type == Opcode.TEXT) - src_ws._events.extend( - fragmentizer(event.message.content) - ) + src_ws._events.extend(fragmentizer(event.message.content)) else: # pragma: no cover raise AssertionError(f"Unexpected event: {event}") @@ -166,7 +177,9 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: fragmentizer = Fragmentizer(src_ws.frame_buf, is_text) src_ws.frame_buf = [b""] - message = websocket.WebSocketMessage(typ, from_client, content) + message = websocket.WebSocketMessage( + typ, from_client, content, injected=injected + ) self.flow.websocket.messages.append(message) yield WebsocketMessageHook(self.flow) @@ -190,11 +203,15 @@ def relay_messages(self, event: events.Event) -> layer.CommandGenerator[None]: self.flow.websocket.close_reason = ws_event.reason for ws in [self.server_ws, self.client_ws]: - if ws.state in {ConnectionState.OPEN, ConnectionState.REMOTE_CLOSING}: + if ws.state in { + ConnectionState.OPEN, + ConnectionState.REMOTE_CLOSING, + }: # response == original event, so no need to differentiate here. yield ws.send2(ws_event) yield commands.CloseConnection(ws.conn) yield WebsocketEndHook(self.flow) + self.flow.live = False self._handle_event = self.done else: # pragma: no cover raise AssertionError(f"Unexpected WebSocket event: {ws_event}") @@ -218,17 +235,20 @@ class Fragmentizer: If one deals with web servers that do not support CONTINUATION frames, addons need to monkeypatch FRAGMENT_SIZE if they need to modify the message. """ + # A bit less than 4kb to accommodate for headers. FRAGMENT_SIZE = 4000 - def __init__(self, fragments: List[bytes], is_text: bool): + def __init__(self, fragments: list[bytes], is_text: bool): self.fragment_lengths = [len(x) for x in fragments] self.is_text = is_text def msg(self, data: bytes, message_finished: bool): if self.is_text: data_str = data.decode(errors="replace") - return wsproto.events.TextMessage(data_str, message_finished=message_finished) + return wsproto.events.TextMessage( + data_str, message_finished=message_finished + ) else: return wsproto.events.BytesMessage(data, message_finished=message_finished) @@ -237,13 +257,13 @@ def __call__(self, content: bytes) -> Iterator[wsproto.events.Message]: # message has the same length, we can reuse the same sizes offset = 0 for fl in self.fragment_lengths[:-1]: - yield self.msg(content[offset:offset + fl], False) + yield self.msg(content[offset : offset + fl], False) offset += fl yield self.msg(content[offset:], True) else: offset = 0 total = len(content) - self.FRAGMENT_SIZE while offset < total: - yield self.msg(content[offset:offset + self.FRAGMENT_SIZE], False) + yield self.msg(content[offset : offset + self.FRAGMENT_SIZE], False) offset += self.FRAGMENT_SIZE yield self.msg(content[offset:], True) diff --git a/mitmproxy/proxy/server.py b/mitmproxy/proxy/server.py index cb434f650c..20fd4233bf 100644 --- a/mitmproxy/proxy/server.py +++ b/mitmproxy/proxy/server.py @@ -11,17 +11,18 @@ import collections import time import traceback -import typing +from collections.abc import Awaitable, Callable, MutableMapping from contextlib import contextmanager from dataclasses import dataclass +from typing import Optional, Union from OpenSSL import SSL -from mitmproxy import http, options as moptions +from mitmproxy import http, options as moptions, tls from mitmproxy.proxy.context import Context from mitmproxy.proxy.layers.http import HTTPMode from mitmproxy.proxy import commands, events, layer, layers, server_hooks from mitmproxy.connection import Address, Client, Connection, ConnectionState -from mitmproxy.proxy.layers import tls +from mitmproxy.net import udp from mitmproxy.utils import asyncio_utils from mitmproxy.utils import human from mitmproxy.utils.data import pkg_data @@ -33,7 +34,7 @@ class TimeoutWatchdog: can_timeout: asyncio.Event blocker: int - def __init__(self, callback: typing.Callable[[], typing.Any]): + def __init__(self, callback: Callable[[], Awaitable]): self.callback = callback self.last_activity = time.time() self.can_timeout = asyncio.Event() @@ -44,12 +45,17 @@ def register_activity(self): self.last_activity = time.time() async def watch(self): - while True: - await self.can_timeout.wait() - await asyncio.sleep(self.CONNECTION_TIMEOUT - (time.time() - self.last_activity)) - if self.last_activity + self.CONNECTION_TIMEOUT < time.time(): - await self.callback() - return + try: + while True: + await self.can_timeout.wait() + await asyncio.sleep( + self.CONNECTION_TIMEOUT - (time.time() - self.last_activity) + ) + if self.last_activity + self.CONNECTION_TIMEOUT < time.time(): + await self.callback() + return + except asyncio.CancelledError: + return @contextmanager def disarm(self): @@ -66,22 +72,24 @@ def disarm(self): @dataclass class ConnectionIO: - handler: typing.Optional[asyncio.Task] = None - reader: typing.Optional[asyncio.StreamReader] = None - writer: typing.Optional[asyncio.StreamWriter] = None + handler: Optional[asyncio.Task] = None + reader: Optional[Union[asyncio.StreamReader, udp.DatagramReader]] = None + writer: Optional[Union[asyncio.StreamWriter, udp.DatagramWriter]] = None class ConnectionHandler(metaclass=abc.ABCMeta): - transports: typing.MutableMapping[Connection, ConnectionIO] + transports: MutableMapping[Connection, ConnectionIO] timeout_watchdog: TimeoutWatchdog client: Client - max_conns: typing.DefaultDict[Address, asyncio.Semaphore] + max_conns: collections.defaultdict[Address, asyncio.Semaphore] layer: layer.Layer + wakeup_timer: set[asyncio.Task] def __init__(self, context: Context) -> None: self.client = context.client self.transports = {} self.max_conns = collections.defaultdict(lambda: asyncio.Semaphore(5)) + self.wakeup_timer = set() # Ask for the first layer right away. # In a reverse proxy scenario, this is necessary as we would otherwise hang @@ -89,14 +97,19 @@ def __init__(self, context: Context) -> None: self.layer = layer.NextLayer(context, ask_on_start=True) self.timeout_watchdog = TimeoutWatchdog(self.on_timeout) + # workaround for https://bugs.python.org/issue40124 / https://bugs.python.org/issue29930 + self._drain_lock = asyncio.Lock() + async def handle_client(self) -> None: + asyncio_utils.set_current_task_debug_info( + name=f"client handler", + client=self.client.peername, + ) watch = asyncio_utils.create_task( self.timeout_watchdog.watch(), name="timeout watchdog", client=self.client.peername, ) - if not watch: - return # this should not be needed, see asyncio_utils.create_task self.log("client connect") await self.handle_hook(server_hooks.ClientConnectedHook(self.client)) @@ -111,13 +124,14 @@ async def handle_client(self) -> None: name=f"client connection handler", client=self.client.peername, ) - if not handler: - return # this should not be needed, see asyncio_utils.create_task self.transports[self.client].handler = handler self.server_event(events.Start()) await asyncio.wait([handler]) watch.cancel() + while self.wakeup_timer: + timer = self.wakeup_timer.pop() + timer.cancel() self.log("client disconnect") self.client.timestamp_end = time.time() @@ -127,31 +141,53 @@ async def handle_client(self) -> None: self.log("closing transports...", "debug") for io in self.transports.values(): if io.handler: - asyncio_utils.cancel_task(io.handler, "client disconnected") - await asyncio.wait([x.handler for x in self.transports.values() if x.handler]) + io.handler.cancel("client disconnected") + await asyncio.wait( + [x.handler for x in self.transports.values() if x.handler] + ) self.log("transports closed!", "debug") async def open_connection(self, command: commands.OpenConnection) -> None: if not command.connection.address: self.log(f"Cannot open connection, no hostname given.") - self.server_event(events.OpenConnectionCompleted(command, f"Cannot open connection, no hostname given.")) + self.server_event( + events.OpenConnectionCompleted( + command, f"Cannot open connection, no hostname given." + ) + ) return hook_data = server_hooks.ServerConnectionHookData( - client=self.client, - server=command.connection + client=self.client, server=command.connection ) await self.handle_hook(server_hooks.ServerConnectHook(hook_data)) if err := command.connection.error: - self.log(f"server connection to {human.format_address(command.connection.address)} killed before connect: {err}") - self.server_event(events.OpenConnectionCompleted(command, f"Connection killed: {err}")) + self.log( + f"server connection to {human.format_address(command.connection.address)} killed before connect: {err}" + ) + self.server_event( + events.OpenConnectionCompleted(command, f"Connection killed: {err}") + ) return async with self.max_conns[command.connection.address]: + reader: Union[asyncio.StreamReader, udp.DatagramReader] + writer: Union[asyncio.StreamWriter, udp.DatagramWriter] try: command.connection.timestamp_start = time.time() - reader, writer = await asyncio.open_connection(*command.connection.address) - except (IOError, asyncio.CancelledError) as e: + if command.connection.transport_protocol == "tcp": + reader, writer = await asyncio.open_connection( + *command.connection.address, + local_addr=command.connection.sockname, + ) + elif command.connection.transport_protocol == "udp": + reader, writer = await udp.open_connection( + *command.connection.address, + local_addr=command.connection.sockname, + ) + else: + raise AssertionError(command.connection.transport_protocol) + except (OSError, asyncio.CancelledError) as e: err = str(e) if not err: # str(CancelledError()) returns empty string. err = "connection cancelled" @@ -164,10 +200,12 @@ async def open_connection(self, command: commands.OpenConnection) -> None: # It is not really defined what almost means here, but we play safe. raise else: - command.connection.timestamp_tcp_setup = time.time() + if command.connection.transport_protocol == "tcp": + # TODO: Rename to `timestamp_setup` and make it agnostic for both TCP (SYN/ACK) and UDP (DNS resl.) + command.connection.timestamp_tcp_setup = time.time() command.connection.state = ConnectionState.OPEN - command.connection.peername = writer.get_extra_info('peername') - command.connection.sockname = writer.get_extra_info('sockname') + command.connection.peername = writer.get_extra_info("peername") + command.connection.sockname = writer.get_extra_info("sockname") self.transports[command.connection].reader = reader self.transports[command.connection].writer = writer @@ -177,14 +215,7 @@ async def open_connection(self, command: commands.OpenConnection) -> None: else: addr = human.format_address(command.connection.address) self.log(f"server connect {addr}") - connected_hook = asyncio_utils.create_task( - self.handle_hook(server_hooks.ServerConnectedHook(hook_data)), - name=f"handle_hook(server_connected) {addr}", - client=self.client.peername, - ) - if not connected_hook: - return # this should not be needed, see asyncio_utils.create_task - + await self.handle_hook(server_hooks.ServerConnectedHook(hook_data)) self.server_event(events.OpenConnectionCompleted(command, None)) # during connection opening, this function is the designated handler that can be cancelled. @@ -195,16 +226,20 @@ async def open_connection(self, command: commands.OpenConnection) -> None: name=f"server connection handler for {addr}", client=self.client.peername, ) - if not new_handler: - return # this should not be needed, see asyncio_utils.create_task self.transports[command.connection].handler = new_handler await asyncio.wait([new_handler]) self.log(f"server disconnect {addr}") command.connection.timestamp_end = time.time() - await connected_hook # wait here for this so that closed always comes after connected. await self.handle_hook(server_hooks.ServerDisconnectedHook(hook_data)) + async def wakeup(self, request: commands.RequestWakeup) -> None: + await asyncio.sleep(request.delay) + task = asyncio.current_task() + assert task is not None + self.wakeup_timer.discard(task) + self.server_event(events.Wakeup(request)) + async def handle_connection(self, connection: Connection) -> None: """ Handle a connection for its entire lifetime. @@ -224,11 +259,14 @@ async def handle_connection(self, connection: Connection) -> None: except asyncio.CancelledError as e: cancelled = e break - else: - self.server_event(events.DataReceived(connection, data)) - for transport in self.transports.values(): - if transport.writer is not None: - await transport.writer.drain() + + self.server_event(events.DataReceived(connection, data)) + + try: + await self.drain_writers() + except asyncio.CancelledError as e: + cancelled = e + break if cancelled is None: connection.state &= ~ConnectionState.CAN_READ @@ -254,11 +292,25 @@ async def handle_connection(self, connection: Connection) -> None: if cancelled: raise cancelled + async def drain_writers(self): + """ + Drain all writers to create some backpressure. We won't continue reading until there's space available in our + write buffers, so if we cannot write fast enough our own read buffers run full and the TCP recv stream is throttled. + """ + async with self._drain_lock: + for transport in self.transports.values(): + if transport.writer is not None: + try: + await transport.writer.drain() + except OSError as e: + if transport.handler is not None: + transport.handler.cancel(f"Error sending data: {e}") + async def on_timeout(self) -> None: self.log(f"Closing connection due to inactivity: {self.client}") handler = self.transports[self.client].handler assert handler - asyncio_utils.cancel_task(handler, "timeout") + handler.cancel("timeout") async def hook_task(self, hook: commands.StartHook) -> None: await self.handle_hook(hook) @@ -286,7 +338,18 @@ def server_event(self, event: events.Event) -> None: client=self.client.peername, ) self.transports[command.connection] = ConnectionIO(handler=handler) - elif isinstance(command, commands.ConnectionCommand) and command.connection not in self.transports: + elif isinstance(command, commands.RequestWakeup): + task = asyncio_utils.create_task( + self.wakeup(command), + name=f"wakeup timer ({command.delay:.1f}s)", + client=self.client.peername, + ) + assert task is not None + self.wakeup_timer.add(task) + elif ( + isinstance(command, commands.ConnectionCommand) + and command.connection not in self.transports + ): pass # The connection has already been closed. elif isinstance(command, commands.SendData): writer = self.transports[command.connection].writer @@ -312,7 +375,9 @@ def server_event(self, event: events.Event) -> None: except Exception: self.log(f"mitmproxy has crashed!\n{traceback.format_exc()}", level="error") - def close_connection(self, connection: Connection, half_close: bool = False) -> None: + def close_connection( + self, connection: Connection, half_close: bool = False + ) -> None: if half_close: if not connection.state & ConnectionState.CAN_WRITE: return @@ -332,34 +397,38 @@ def close_connection(self, connection: Connection, half_close: bool = False) -> if connection.state is ConnectionState.CLOSED: handler = self.transports[connection].handler assert handler - asyncio_utils.cancel_task(handler, "closed by command") + handler.cancel("closed by command") -class StreamConnectionHandler(ConnectionHandler, metaclass=abc.ABCMeta): - def __init__(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, options: moptions.Options) -> None: +class LiveConnectionHandler(ConnectionHandler, metaclass=abc.ABCMeta): + def __init__( + self, + reader: asyncio.StreamReader, + writer: asyncio.StreamWriter, + options: moptions.Options, + ) -> None: client = Client( - writer.get_extra_info('peername'), - writer.get_extra_info('sockname'), + writer.get_extra_info("peername"), + writer.get_extra_info("sockname"), time.time(), ) context = Context(client, options) super().__init__(context) - self.transports[client] = ConnectionIO(handler=None, reader=reader, writer=writer) + self.transports[client] = ConnectionIO( + handler=None, reader=reader, writer=writer + ) -class SimpleConnectionHandler(StreamConnectionHandler): # pragma: no cover +class SimpleConnectionHandler(LiveConnectionHandler): # pragma: no cover """Simple handler that does not really process any hooks.""" - hook_handlers: typing.Dict[str, typing.Callable] + hook_handlers: dict[str, Callable] def __init__(self, reader, writer, options, hooks): super().__init__(reader, writer, options) self.hook_handlers = hooks - async def handle_hook( - self, - hook: commands.StartHook - ) -> None: + async def handle_hook(self, hook: commands.StartHook) -> None: if hook.name in self.hook_handlers: self.hook_handlers[hook.name](*hook.args()) @@ -375,16 +444,20 @@ def log(self, message: str, level: str = "info"): opts = moptions.Options() # options duplicated here to simplify testing setup opts.add_option( - "connection_strategy", str, "lazy", + "connection_strategy", + str, + "lazy", "Determine when server connections should be established.", - choices=("eager", "lazy") + choices=("eager", "lazy"), ) opts.add_option( - "keep_host_header", bool, False, + "keep_host_header", + bool, + False, """ Reverse Proxy: Keep the original host header instead of rewriting it to the reverse proxy target. - """ + """, ) opts.mode = "reverse:http://127.0.0.1:3000/" @@ -395,7 +468,7 @@ async def handle(reader, writer): # lambda ctx: setattr(ctx.server, "tls", True) or layers.ServerTLSLayer(ctx), # lambda ctx: layers.ClientTLSLayer(ctx), lambda ctx: layers.modes.ReverseProxy(ctx), - lambda ctx: layers.HttpLayer(ctx, HTTPMode.transparent) + lambda ctx: layers.HttpLayer(ctx, HTTPMode.transparent), ] def next_layer(nl: layer.NextLayer): @@ -414,34 +487,45 @@ def request(flow: http.HTTPFlow): if "redirect" in flow.request.path: flow.request.host = "httpbin.org" - def tls_start_client(tls_start: tls.TlsStartData): + def tls_start_client(tls_start: tls.TlsData): # INSECURE ssl_context = SSL.Context(SSL.SSLv23_METHOD) ssl_context.use_privatekey_file( - pkg_data.path("../test/mitmproxy/data/verificationcerts/trusted-leaf.key") + pkg_data.path( + "../test/mitmproxy/data/verificationcerts/trusted-leaf.key" + ) ) ssl_context.use_certificate_chain_file( - pkg_data.path("../test/mitmproxy/data/verificationcerts/trusted-leaf.crt") + pkg_data.path( + "../test/mitmproxy/data/verificationcerts/trusted-leaf.crt" + ) ) tls_start.ssl_conn = SSL.Connection(ssl_context) tls_start.ssl_conn.set_accept_state() - def tls_start_server(tls_start: tls.TlsStartData): + def tls_start_server(tls_start: tls.TlsData): # INSECURE ssl_context = SSL.Context(SSL.SSLv23_METHOD) tls_start.ssl_conn = SSL.Connection(ssl_context) tls_start.ssl_conn.set_connect_state() if tls_start.context.client.sni is not None: - tls_start.ssl_conn.set_tlsext_host_name(tls_start.context.client.sni.encode()) - - await SimpleConnectionHandler(reader, writer, opts, { - "next_layer": next_layer, - "request": request, - "tls_start_client": tls_start_client, - "tls_start_server": tls_start_server, - }).handle_client() + tls_start.ssl_conn.set_tlsext_host_name( + tls_start.context.client.sni.encode() + ) - coro = asyncio.start_server(handle, '127.0.0.1', 8080, loop=loop) + await SimpleConnectionHandler( + reader, + writer, + opts, + { + "next_layer": next_layer, + "request": request, + "tls_start_client": tls_start_client, + "tls_start_server": tls_start_server, + }, + ).handle_client() + + coro = asyncio.start_server(handle, "127.0.0.1", 8080, loop=loop) server = loop.run_until_complete(coro) # Serve requests until Ctrl+C is pressed diff --git a/mitmproxy/proxy/server_hooks.py b/mitmproxy/proxy/server_hooks.py index 7afba799f9..22e1e5418b 100644 --- a/mitmproxy/proxy/server_hooks.py +++ b/mitmproxy/proxy/server_hooks.py @@ -12,6 +12,7 @@ class ClientConnectedHook(commands.StartHook): Setting client.error kills the connection. """ + client: connection.Client @@ -20,7 +21,7 @@ class ClientDisconnectedHook(commands.StartHook): """ A client connection has been closed (either by us or the client). """ - blocking = False + client: connection.Client @@ -42,6 +43,7 @@ class ServerConnectHook(commands.StartHook): Setting data.server.error kills the connection. """ + data: ServerConnectionHookData @@ -50,7 +52,7 @@ class ServerConnectedHook(commands.StartHook): """ Mitmproxy has connected to a server. """ - blocking = False + data: ServerConnectionHookData @@ -59,5 +61,5 @@ class ServerDisconnectedHook(commands.StartHook): """ A server connection has been closed (either by us or the server). """ - blocking = False + data: ServerConnectionHookData diff --git a/mitmproxy/proxy/tunnel.py b/mitmproxy/proxy/tunnel.py index b033366a0a..d14023a742 100644 --- a/mitmproxy/proxy/tunnel.py +++ b/mitmproxy/proxy/tunnel.py @@ -1,5 +1,6 @@ +import time from enum import Enum, auto -from typing import List, Optional, Tuple, Union +from typing import Optional, Union from mitmproxy import connection from mitmproxy.proxy import commands, context, events, layer @@ -18,6 +19,7 @@ class TunnelLayer(layer.Layer): A specialized layer that simplifies the implementation of tunneling protocols such as SOCKS, upstream HTTP proxies, or TLS. """ + child_layer: layer.Layer tunnel_connection: connection.Connection """The 'outer' connection which provides the tunnel protocol I/O""" @@ -25,17 +27,17 @@ class TunnelLayer(layer.Layer): """The 'inner' connection which provides data I/O""" tunnel_state: TunnelState = TunnelState.INACTIVE command_to_reply_to: Optional[commands.OpenConnection] = None - _event_queue: List[events.Event] + _event_queue: list[events.Event] """ If the connection already exists when we receive the start event, we buffer commands until we have established the tunnel. """ def __init__( - self, - context: context.Context, - tunnel_connection: connection.Connection, - conn: connection.Connection, + self, + context: context.Context, + tunnel_connection: connection.Connection, + conn: connection.Connection, ): super().__init__(context) self.tunnel_connection = tunnel_connection @@ -55,23 +57,30 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: self.tunnel_state = TunnelState.ESTABLISHING yield from self.start_handshake() yield from self.event_to_child(event) - elif isinstance(event, events.ConnectionEvent) and event.connection == self.tunnel_connection: + elif ( + isinstance(event, events.ConnectionEvent) + and event.connection == self.tunnel_connection + ): if isinstance(event, events.DataReceived): if self.tunnel_state is TunnelState.ESTABLISHING: done, err = yield from self.receive_handshake_data(event.data) if done: if self.conn != self.tunnel_connection: self.conn.state = connection.ConnectionState.OPEN + self.conn.timestamp_start = time.time() if err: if self.conn != self.tunnel_connection: self.conn.state = connection.ConnectionState.CLOSED + self.conn.timestamp_start = time.time() yield from self.on_handshake_error(err) if done or err: yield from self._handshake_finished(err) else: yield from self.receive_data(event.data) elif isinstance(event, events.ConnectionClosed): - self.conn.state &= ~connection.ConnectionState.CAN_READ + if self.conn != self.tunnel_connection: + self.conn.state &= ~connection.ConnectionState.CAN_READ + self.conn.timestamp_end = time.time() if self.tunnel_state is TunnelState.OPEN: yield from self.receive_close() elif self.tunnel_state is TunnelState.ESTABLISHING: @@ -90,7 +99,9 @@ def _handshake_finished(self, err: Optional[str]): else: self.tunnel_state = TunnelState.OPEN if self.command_to_reply_to: - yield from self.event_to_child(events.OpenConnectionCompleted(self.command_to_reply_to, err)) + yield from self.event_to_child( + events.OpenConnectionCompleted(self.command_to_reply_to, err) + ) self.command_to_reply_to = None else: for evt in self._event_queue: @@ -98,11 +109,17 @@ def _handshake_finished(self, err: Optional[str]): self._event_queue.clear() def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: - if self.tunnel_state is TunnelState.ESTABLISHING and not self.command_to_reply_to: + if ( + self.tunnel_state is TunnelState.ESTABLISHING + and not self.command_to_reply_to + ): self._event_queue.append(event) return for command in self.child_layer.handle_event(event): - if isinstance(command, commands.ConnectionCommand) and command.connection == self.conn: + if ( + isinstance(command, commands.ConnectionCommand) + and command.connection == self.conn + ): if isinstance(command, commands.SendData): yield from self.send_data(command.data) elif isinstance(command, commands.CloseConnection): @@ -118,7 +135,9 @@ def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: self.tunnel_state = TunnelState.ESTABLISHING err = yield commands.OpenConnection(self.tunnel_connection) if err: - yield from self.event_to_child(events.OpenConnectionCompleted(command, err)) + yield from self.event_to_child( + events.OpenConnectionCompleted(command, err) + ) self.tunnel_state = TunnelState.CLOSED else: yield from self.start_handshake() @@ -130,7 +149,9 @@ def event_to_child(self, event: events.Event) -> layer.CommandGenerator[None]: def start_handshake(self) -> layer.CommandGenerator[None]: yield from self._handle_event(events.DataReceived(self.tunnel_connection, b"")) - def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: + def receive_handshake_data( + self, data: bytes + ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: """returns a (done, err) tuple""" yield from () return True, None @@ -140,14 +161,10 @@ def on_handshake_error(self, err: str) -> layer.CommandGenerator[None]: yield commands.CloseConnection(self.tunnel_connection) def receive_data(self, data: bytes) -> layer.CommandGenerator[None]: - yield from self.event_to_child( - events.DataReceived(self.conn, data) - ) + yield from self.event_to_child(events.DataReceived(self.conn, data)) def receive_close(self) -> layer.CommandGenerator[None]: - yield from self.event_to_child( - events.ConnectionClosed(self.conn) - ) + yield from self.event_to_child(events.ConnectionClosed(self.conn)) def send_data(self, data: bytes) -> layer.CommandGenerator[None]: yield commands.SendData(self.tunnel_connection, data) @@ -158,7 +175,7 @@ def send_close(self, half_close: bool) -> layer.CommandGenerator[None]: class LayerStack: def __init__(self) -> None: - self._stack: List[Layer] = [] + self._stack: list[Layer] = [] def __getitem__(self, item: int) -> Layer: return self._stack.__getitem__(item) diff --git a/mitmproxy/proxy/utils.py b/mitmproxy/proxy/utils.py index 03cd3b5f1f..d2b2aa23aa 100644 --- a/mitmproxy/proxy/utils.py +++ b/mitmproxy/proxy/utils.py @@ -14,12 +14,15 @@ def expect(*event_types): def decorator(f): if __debug__ is True: + @functools.wraps(f) def _check_event_type(self, event: events.Event): if isinstance(event, event_types): return f(self, event) else: - event_types_str = '|'.join(e.__name__ for e in event_types) or "no events" + event_types_str = ( + "|".join(e.__name__ for e in event_types) or "no events" + ) raise AssertionError( f"Unexpected event type at {f.__qualname__}: " f"Expected {event_types_str}, got {event}." diff --git a/mitmproxy/script/concurrent.py b/mitmproxy/script/concurrent.py index 39ff84b552..9d9546568d 100644 --- a/mitmproxy/script/concurrent.py +++ b/mitmproxy/script/concurrent.py @@ -3,12 +3,9 @@ offload computations from mitmproxy's main master thread. """ +import asyncio +import inspect from mitmproxy import hooks -from mitmproxy.coretypes import basethread - - -class ScriptThread(basethread.BaseThread): - name = "ScriptThread" def concurrent(fn): @@ -17,20 +14,18 @@ def concurrent(fn): "Concurrent decorator not supported for '%s' method." % fn.__name__ ) - def _concurrent(*args): - # When annotating classmethods, "self" is passed as the first argument. - # To support both class and static methods, we accept a variable number of arguments - # and take the last one as our actual hook object. - obj = args[-1] - + async def _concurrent(*args): def run(): - fn(*args) - if obj.reply.state == "taken": - obj.reply.commit() - obj.reply.take() - ScriptThread( - f"script.concurrent {fn.__name__}", - target=run - ).start() + if inspect.iscoroutinefunction(fn): + # Run the async function in a new event loop + loop = asyncio.new_event_loop() + try: + loop.run_until_complete(fn(*args)) + finally: + loop.close() + else: + fn(*args) + + await asyncio.get_running_loop().run_in_executor(None, run) return _concurrent diff --git a/mitmproxy/stateobject.py b/mitmproxy/stateobject.py index 1f392b1ce6..4f87a52279 100644 --- a/mitmproxy/stateobject.py +++ b/mitmproxy/stateobject.py @@ -1,6 +1,6 @@ import json +from collections import abc import typing - from mitmproxy.coretypes import serializable from mitmproxy.utils import typecheck @@ -13,7 +13,7 @@ class StateObject(serializable.Serializable): or StateObject instances themselves. """ - _stateobject_attributes: typing.ClassVar[typing.MutableMapping[str, typing.Any]] + _stateobject_attributes: typing.ClassVar[abc.MutableMapping[str, typing.Any]] """ An attribute-name -> class-or-type dict containing all attributes that should be serialized. If the attribute is a class, it must implement the @@ -57,25 +57,22 @@ def _process(typeinfo: typecheck.Type, val: typing.Any, make: bool) -> typing.An elif not make and hasattr(val, "get_state"): return val.get_state() - typename = str(typeinfo) + origin = typing.get_origin(typeinfo) - if typename.startswith("typing.List"): - T = typecheck.sequence_type(typeinfo) + if origin is list: + T = typing.get_args(typeinfo)[0] return [_process(T, x, make) for x in val] - elif typename.startswith("typing.Tuple"): - Ts = typecheck.tuple_types(typeinfo) + elif origin is tuple: + Ts = typing.get_args(typeinfo) if len(Ts) != len(val): raise ValueError(f"Invalid data. Expected {Ts}, got {val}.") - return tuple( - _process(T, x, make) for T, x in zip(Ts, val) - ) - elif typename.startswith("typing.Dict"): - k_cls, v_cls = typecheck.mapping_types(typeinfo) + return tuple(_process(T, x, make) for T, x in zip(Ts, val)) + elif origin is dict: + k_cls, v_cls = typing.get_args(typeinfo) return { - _process(k_cls, k, make): _process(v_cls, v, make) - for k, v in val.items() + _process(k_cls, k, make): _process(v_cls, v, make) for k, v in val.items() } - elif typename.startswith("typing.Any"): + elif typeinfo is typing.Any: # This requires a bit of explanation. We can't import our IO layer here, # because it causes a circular import. Rather than restructuring the # code for this, we use JSON serialization, which has similar primitive diff --git a/mitmproxy/tcp.py b/mitmproxy/tcp.py index ec3c78b674..a2512cfb83 100644 --- a/mitmproxy/tcp.py +++ b/mitmproxy/tcp.py @@ -1,7 +1,6 @@ import time -from typing import List -from mitmproxy import flow +from mitmproxy import connection, flow from mitmproxy.coretypes import serializable @@ -30,8 +29,7 @@ def set_state(self, state): def __repr__(self): return "{direction} {content}".format( - direction="->" if self.from_client else "<-", - content=repr(self.content) + direction="->" if self.from_client else "<-", content=repr(self.content) ) @@ -40,15 +38,27 @@ class TCPFlow(flow.Flow): A TCPFlow is a simplified representation of a TCP session. """ - def __init__(self, client_conn, server_conn, live=None): - super().__init__("tcp", client_conn, server_conn, live) - self.messages: List[TCPMessage] = [] + messages: list[TCPMessage] + """ + The messages transmitted over this connection. + + The latest message can be accessed as `flow.messages[-1]` in event hooks. + """ + + def __init__( + self, + client_conn: connection.Client, + server_conn: connection.Server, + live: bool = False, + ): + super().__init__(client_conn, server_conn, live) + self.messages = [] _stateobject_attributes = flow.Flow._stateobject_attributes.copy() - _stateobject_attributes["messages"] = List[TCPMessage] + _stateobject_attributes["messages"] = list[TCPMessage] def __repr__(self): - return "".format(len(self.messages)) + return f"" __all__ = [ diff --git a/mitmproxy/test/taddons.py b/mitmproxy/test/taddons.py index dc06dba427..a52bb5eef5 100644 --- a/mitmproxy/test/taddons.py +++ b/mitmproxy/test/taddons.py @@ -1,4 +1,3 @@ -import contextlib import asyncio import sys @@ -22,7 +21,11 @@ def trigger(self, event: hooks.Hook): class RecordingMaster(mitmproxy.master.Master): def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + super().__init__(*args, **kwargs, event_loop=loop) self.addons = TestAddons(self) self.logs = [] @@ -55,16 +58,14 @@ def clear(self): class context: """ - A context for testing addons, which sets up the mitmproxy.ctx module so - handlers can run as they would within mitmproxy. The context also - provides a number of helper methods for common testing scenarios. + A context for testing addons, which sets up the mitmproxy.ctx module so + handlers can run as they would within mitmproxy. The context also + provides a number of helper methods for common testing scenarios. """ def __init__(self, *addons, options=None, loadcore=True): options = options or mitmproxy.options.Options() - self.master = RecordingMaster( - options - ) + self.master = RecordingMaster(options) self.options = self.master.options if loadcore: @@ -79,26 +80,21 @@ def __enter__(self): def __exit__(self, exc_type, exc_value, traceback): return False - @contextlib.contextmanager - def cycle(self, addon, f): + async def cycle(self, addon, f): """ - Cycles the flow through the events for the flow. Stops if a reply - is taken (as in flow interception). + Cycles the flow through the events for the flow. Stops if the flow + is intercepted. """ - f.reply._state = "start" for evt in eventsequence.iterate(f): - self.master.addons.invoke_addon( - addon, - evt - ) - if f.reply.state == "taken": + await self.master.addons.invoke_addon(addon, evt) + if f.intercepted: return def configure(self, addon, **kwargs): """ - A helper for testing configure methods. Modifies the registered - Options object with the given keyword arguments, then calls the - configure method on the addon with the updated value. + A helper for testing configure methods. Modifies the registered + Options object with the given keyword arguments, then calls the + configure method on the addon with the updated value. """ if addon not in self.master.addons: self.master.addons.register(addon) @@ -106,24 +102,18 @@ def configure(self, addon, **kwargs): if kwargs: self.options.update(**kwargs) else: - self.master.addons.invoke_addon(addon, hooks.ConfigureHook(set())) + self.master.addons.invoke_addon_sync(addon, hooks.ConfigureHook(set())) def script(self, path): """ - Loads a script from path, and returns the enclosed addon. + Loads a script from path, and returns the enclosed addon. """ sc = script.Script(path, False) return sc.addons[0] if sc.addons else None - def invoke(self, addon, event: hooks.Hook): - """ - Recursively invoke an event on an addon and all its children. - """ - return self.master.addons.invoke_addon(addon, event) - def command(self, func, *args): """ - Invoke a command function with a list of string arguments within a command context, mimicking the actual command environment. + Invoke a command function with a list of string arguments within a command context, mimicking the actual command environment. """ cmd = command.Command(self.master.commands, "test.command", func) return cmd.call(args) diff --git a/mitmproxy/test/tflow.py b/mitmproxy/test/tflow.py index d00ea395b1..571a22375d 100644 --- a/mitmproxy/test/tflow.py +++ b/mitmproxy/test/tflow.py @@ -2,16 +2,19 @@ from typing import Optional, Union from mitmproxy import connection -from mitmproxy import controller +from mitmproxy import dns from mitmproxy import flow from mitmproxy import http from mitmproxy import tcp from mitmproxy import websocket +from mitmproxy.test.tutils import tdnsreq, tdnsresp from mitmproxy.test.tutils import treq, tresp from wsproto.frame_protocol import Opcode -def ttcpflow(client_conn=True, server_conn=True, messages=True, err=None) -> tcp.TCPFlow: +def ttcpflow( + client_conn=True, server_conn=True, messages=True, err=None +) -> tcp.TCPFlow: if client_conn is True: client_conn = tclient_conn() if server_conn is True: @@ -25,13 +28,16 @@ def ttcpflow(client_conn=True, server_conn=True, messages=True, err=None) -> tcp err = terr() f = tcp.TCPFlow(client_conn, server_conn) + f.timestamp_created = client_conn.timestamp_start f.messages = messages f.error = err - f.reply = controller.DummyReply() + f.live = True return f -def twebsocketflow(messages=True, err=None, close_code=None, close_reason='') -> http.HTTPFlow: +def twebsocketflow( + messages=True, err=None, close_code=None, close_reason="" +) -> http.HTTPFlow: flow = http.HTTPFlow(tclient_conn(), tserver_conn()) flow.request = http.Request( "example.com", @@ -47,22 +53,21 @@ def twebsocketflow(messages=True, err=None, close_code=None, close_reason='') -> sec_websocket_version="13", sec_websocket_key="1234", ), - content=b'', + content=b"", trailers=None, timestamp_start=946681200, timestamp_end=946681201, - ) flow.response = http.Response( b"HTTP/1.1", 101, reason=b"Switching Protocols", headers=http.Headers( - connection='upgrade', - upgrade='websocket', - sec_websocket_accept=b'', + connection="upgrade", + upgrade="websocket", + sec_websocket_accept=b"", ), - content=b'', + content=b"", trailers=None, timestamp_start=946681202, timestamp_end=946681203, @@ -82,10 +87,46 @@ def twebsocketflow(messages=True, err=None, close_code=None, close_reason='') -> # NORMAL_CLOSURE flow.websocket.close_code = 1000 - flow.reply = controller.DummyReply() + flow.live = True return flow +def tdnsflow( + *, + client_conn: Optional[connection.Client] = None, + server_conn: Optional[connection.Server] = None, + req: Optional[dns.Message] = None, + resp: Union[bool, dns.Message] = False, + err: Union[bool, flow.Error] = False, + live: bool = True, +) -> dns.DNSFlow: + """Create a DNS flow for testing.""" + if client_conn is None: + client_conn = tclient_conn() + client_conn.transport_protocol = "udp" + if server_conn is None: + server_conn = tserver_conn() + server_conn.transport_protocol = "udp" + if req is None: + req = tdnsreq() + + if resp is True: + resp = tdnsresp() + if err is True: + err = terr() + + assert resp is False or isinstance(resp, dns.Message) + assert err is False or isinstance(err, flow.Error) + + f = dns.DNSFlow(client_conn, server_conn) + f.timestamp_created = req.timestamp + f.request = req + f.response = resp or None + f.error = err or None + f.live = live + return f + + def tflow( *, client_conn: Optional[connection.Client] = None, @@ -94,6 +135,7 @@ def tflow( resp: Union[bool, http.Response] = False, err: Union[bool, flow.Error] = False, ws: Union[bool, websocket.WebSocketData] = False, + live: bool = True, ) -> http.HTTPFlow: """Create a flow for testing.""" if client_conn is None: @@ -115,20 +157,18 @@ def tflow( assert ws is False or isinstance(ws, websocket.WebSocketData) f = http.HTTPFlow(client_conn, server_conn) + f.timestamp_created = req.timestamp_start f.request = req f.response = resp or None f.error = err or None f.websocket = ws or None - f.reply = controller.DummyReply() + f.live = live return f class DummyFlow(flow.Flow): """A flow that is neither HTTP nor TCP.""" - def __init__(self, client_conn, server_conn, live=None): - super().__init__("dummy", client_conn, server_conn, live) - def tdummyflow(client_conn=True, server_conn=True, err=None) -> DummyFlow: if client_conn is True: @@ -140,61 +180,63 @@ def tdummyflow(client_conn=True, server_conn=True, err=None) -> DummyFlow: f = DummyFlow(client_conn, server_conn) f.error = err - f.reply = controller.DummyReply() + f.live = True return f def tclient_conn() -> connection.Client: - c = connection.Client.from_state(dict( - id=str(uuid.uuid4()), - address=("127.0.0.1", 22), - mitmcert=None, - tls_established=True, - timestamp_start=946681200, - timestamp_tls_setup=946681201, - timestamp_end=946681206, - sni="address", - cipher_name="cipher", - alpn=b"http/1.1", - tls_version="TLSv1.2", - tls_extensions=[(0x00, bytes.fromhex("000e00000b6578616d"))], - state=0, - sockname=("", 0), - error=None, - tls=False, - certificate_list=[], - alpn_offers=[], - cipher_list=[], - )) - c.reply = controller.DummyReply() # type: ignore + c = connection.Client.from_state( + dict( + id=str(uuid.uuid4()), + address=("127.0.0.1", 22), + mitmcert=None, + tls_established=True, + timestamp_start=946681200, + timestamp_tls_setup=946681201, + timestamp_end=946681206, + sni="address", + cipher_name="cipher", + alpn=b"http/1.1", + tls_version="TLSv1.2", + tls_extensions=[(0x00, bytes.fromhex("000e00000b6578616d"))], + state=0, + sockname=("", 0), + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher_list=[], + ) + ) return c def tserver_conn() -> connection.Server: - c = connection.Server.from_state(dict( - id=str(uuid.uuid4()), - address=("address", 22), - source_address=("address", 22), - ip_address=("192.168.0.1", 22), - timestamp_start=946681202, - timestamp_tcp_setup=946681203, - timestamp_tls_setup=946681204, - timestamp_end=946681205, - tls_established=True, - sni="address", - alpn=None, - tls_version="TLSv1.2", - via=None, - state=0, - error=None, - tls=False, - certificate_list=[], - alpn_offers=[], - cipher_name=None, - cipher_list=[], - via2=None, - )) - c.reply = controller.DummyReply() # type: ignore + c = connection.Server.from_state( + dict( + id=str(uuid.uuid4()), + address=("address", 22), + source_address=("address", 22), + ip_address=("192.168.0.1", 22), + timestamp_start=946681202, + timestamp_tcp_setup=946681203, + timestamp_tls_setup=946681204, + timestamp_end=946681205, + tls_established=True, + sni="address", + alpn=None, + tls_version="TLSv1.2", + via=None, + state=0, + error=None, + tls=False, + certificate_list=[], + alpn_offers=[], + cipher_name=None, + cipher_list=[], + via2=None, + ) + ) return c @@ -218,3 +260,15 @@ def twebsocket(messages: bool = True) -> websocket.WebSocketData: ws.timestamp_end = 946681205 return ws + + +def tflows() -> list[flow.Flow]: + return [ + tflow(resp=True), + tflow(err=True), + tflow(ws=True), + ttcpflow(), + ttcpflow(err=True), + tdnsflow(resp=True), + tdnsflow(err=True), + ] diff --git a/mitmproxy/test/tutils.py b/mitmproxy/test/tutils.py index 940732092d..4a2fb67f31 100644 --- a/mitmproxy/test/tutils.py +++ b/mitmproxy/test/tutils.py @@ -1,6 +1,64 @@ +from mitmproxy import dns from mitmproxy import http +def tdnsreq(**kwargs) -> dns.Message: + """ + Returns: + mitmproxy.dns.Message + """ + default = dict( + timestamp=946681200, + id=42, + query=True, + op_code=dns.op_codes.QUERY, + authoritative_answer=False, + truncation=False, + recursion_desired=True, + recursion_available=False, + reserved=0, + response_code=dns.response_codes.NOERROR, + questions=[dns.Question("dns.google", dns.types.A, dns.classes.IN)], + answers=[], + authorities=[], + additionals=[], + ) + default.update(kwargs) + return dns.Message(**default) # type: ignore + + +def tdnsresp(**kwargs) -> dns.Message: + """ + Returns: + mitmproxy.dns.Message + """ + default = dict( + timestamp=946681201, + id=42, + query=False, + op_code=dns.op_codes.QUERY, + authoritative_answer=False, + truncation=False, + recursion_desired=True, + recursion_available=True, + reserved=0, + response_code=dns.response_codes.NOERROR, + questions=[dns.Question("dns.google", dns.types.A, dns.classes.IN)], + answers=[ + dns.ResourceRecord( + "dns.google", dns.types.A, dns.classes.IN, 32, b"\x08\x08\x08\x08" + ), + dns.ResourceRecord( + "dns.google", dns.types.A, dns.classes.IN, 32, b"\x08\x08\x04\x04" + ), + ], + authorities=[], + additionals=[], + ) + default.update(kwargs) + return dns.Message(**default) # type: ignore + + def treq(**kwargs) -> http.Request: """ Returns: @@ -33,7 +91,9 @@ def tresp(**kwargs) -> http.Response: http_version=b"HTTP/1.1", status_code=200, reason=b"OK", - headers=http.Headers(((b"header-response", b"svalue"), (b"content-length", b"7"))), + headers=http.Headers( + ((b"header-response", b"svalue"), (b"content-length", b"7")) + ), content=b"message", trailers=None, timestamp_start=946681202, diff --git a/mitmproxy/tls.py b/mitmproxy/tls.py new file mode 100644 index 0000000000..f26e34816b --- /dev/null +++ b/mitmproxy/tls.py @@ -0,0 +1,140 @@ +import io +from dataclasses import dataclass +from typing import Optional + +from kaitaistruct import KaitaiStream + +from OpenSSL import SSL +from mitmproxy import connection +from mitmproxy.contrib.kaitaistruct import tls_client_hello +from mitmproxy.net import check +from mitmproxy.proxy import context + + +class ClientHello: + """ + A TLS ClientHello is the first message sent by the client when initiating TLS. + """ + + _raw_bytes: bytes + + def __init__(self, raw_client_hello: bytes): + """Create a TLS ClientHello object from raw bytes.""" + self._raw_bytes = raw_client_hello + self._client_hello = tls_client_hello.TlsClientHello( + KaitaiStream(io.BytesIO(raw_client_hello)) + ) + + def raw_bytes(self, wrap_in_record: bool = True) -> bytes: + """ + The raw ClientHello bytes as seen on the wire. + + If `wrap_in_record` is True, the ClientHello will be wrapped in a synthetic TLS record + (`0x160303 + len(chm) + 0x01 + len(ch)`), which is the format expected by some tools. + The synthetic record assumes TLS version (`0x0303`), which may be different from what has been sent over the + wire. JA3 hashes are unaffected by this as they only use the TLS version from the ClientHello data structure. + + A future implementation may return not just the exact ClientHello, but also the exact record(s) as seen on the + wire. + """ + if wrap_in_record: + return ( + # record layer + b"\x16\x03\x03" + + (len(self._raw_bytes) + 4).to_bytes(2, byteorder="big") + + + # handshake header + b"\x01" + + len(self._raw_bytes).to_bytes(3, byteorder="big") + + + # ClientHello as defined in https://datatracker.ietf.org/doc/html/rfc8446#section-4.1.2. + self._raw_bytes + ) + else: + return self._raw_bytes + + @property + def cipher_suites(self) -> list[int]: + """The cipher suites offered by the client (as raw ints).""" + return self._client_hello.cipher_suites.cipher_suites + + @property + def sni(self) -> Optional[str]: + """ + The [Server Name Indication](https://en.wikipedia.org/wiki/Server_Name_Indication), + which indicates which hostname the client wants to connect to. + """ + if self._client_hello.extensions: + for extension in self._client_hello.extensions.extensions: + is_valid_sni_extension = ( + extension.type == 0x00 + and len(extension.body.server_names) == 1 + and extension.body.server_names[0].name_type == 0 + and check.is_valid_host(extension.body.server_names[0].host_name) + ) + if is_valid_sni_extension: + return extension.body.server_names[0].host_name.decode("ascii") + return None + + @property + def alpn_protocols(self) -> list[bytes]: + """ + The application layer protocols offered by the client as part of the + [ALPN](https://en.wikipedia.org/wiki/Application-Layer_Protocol_Negotiation) TLS extension. + """ + if self._client_hello.extensions: + for extension in self._client_hello.extensions.extensions: + if extension.type == 0x10: + return list(x.name for x in extension.body.alpn_protocols) + return [] + + @property + def extensions(self) -> list[tuple[int, bytes]]: + """The raw list of extensions in the form of `(extension_type, raw_bytes)` tuples.""" + ret = [] + if self._client_hello.extensions: + for extension in self._client_hello.extensions.extensions: + body = getattr(extension, "_raw_body", extension.body) + ret.append((extension.type, body)) + return ret + + def __repr__(self): + return f"ClientHello(sni: {self.sni}, alpn_protocols: {self.alpn_protocols})" + + +@dataclass +class ClientHelloData: + """ + Event data for `tls_clienthello` event hooks. + """ + + context: context.Context + """The context object for this connection.""" + client_hello: ClientHello + """The entire parsed TLS ClientHello.""" + ignore_connection: bool = False + """ + If set to `True`, do not intercept this connection and forward encrypted contents unmodified. + """ + establish_server_tls_first: bool = False + """ + If set to `True`, pause this handshake and establish TLS with an upstream server first. + This makes it possible to process the server certificate when generating an interception certificate. + """ + + +@dataclass +class TlsData: + """ + Event data for `tls_start_client`, `tls_start_server`, and `tls_handshake` event hooks. + """ + + conn: connection.Connection + """The affected connection.""" + context: context.Context + """The context object for this connection.""" + ssl_conn: Optional[SSL.Connection] = None + """ + The associated pyOpenSSL `SSL.Connection` object. + This will be set by an addon in the `tls_start_*` event hooks. + """ diff --git a/mitmproxy/tools/cmdline.py b/mitmproxy/tools/cmdline.py index 471e026704..7f902a88a8 100644 --- a/mitmproxy/tools/cmdline.py +++ b/mitmproxy/tools/cmdline.py @@ -3,24 +3,26 @@ def common_options(parser, opts): parser.add_argument( - '--version', - action='store_true', + "--version", + action="store_true", help="show version number and exit", - dest='version', + dest="version", ) parser.add_argument( - '--options', - action='store_true', + "--options", + action="store_true", help="Show all options and their default values", ) parser.add_argument( - '--commands', - action='store_true', + "--commands", + action="store_true", help="Show all commands and their signatures", ) parser.add_argument( "--set", - type=str, dest="setoptions", default=[], + type=str, + dest="setoptions", + default=[], action="append", metavar="option[=value]", help=""" @@ -29,17 +31,18 @@ def common_options(parser, opts): are emptied. Boolean values can be true, false or toggle. Sequences are set using multiple invocations to set for the same option. - """ + """, ) parser.add_argument( - "-q", "--quiet", - action="store_true", dest="quiet", - help="Quiet." + "-q", "--quiet", action="store_true", dest="quiet", help="Quiet." ) parser.add_argument( - "-v", "--verbose", - action="store_const", dest="verbose", const='debug', - help="Increase log verbosity." + "-v", + "--verbose", + action="store_const", + dest="verbose", + const="debug", + help="Increase log verbosity.", ) # Basic options @@ -108,8 +111,7 @@ def mitmproxy(opts): opts.make_parser(parser, "console_layout") opts.make_parser(parser, "console_layout_headers") group = parser.add_argument_group( - "Filters", - "See help in mitmproxy for filter expression syntax." + "Filters", "See help in mitmproxy for filter expression syntax." ) opts.make_parser(group, "intercept", metavar="FILTER") opts.make_parser(group, "view_filter", metavar="FILTER") @@ -120,14 +122,14 @@ def mitmdump(opts): parser = argparse.ArgumentParser(usage="%(prog)s [options] [filter]") common_options(parser, opts) - opts.make_parser(parser, "flow_detail", metavar = "LEVEL") + opts.make_parser(parser, "flow_detail", metavar="LEVEL") parser.add_argument( - 'filter_args', + "filter_args", nargs="...", help=""" Filter expression, equivalent to setting both the view_filter and save_stream_filter options. - """ + """, ) return parser @@ -143,8 +145,7 @@ def mitmweb(opts): common_options(parser, opts) group = parser.add_argument_group( - "Filters", - "See help in mitmproxy for filter expression syntax." + "Filters", "See help in mitmproxy for filter expression syntax." ) opts.make_parser(group, "intercept", metavar="FILTER") return parser diff --git a/mitmproxy/tools/console/commander/commander.py b/mitmproxy/tools/console/commander/commander.py index 47cf1962a9..53acbf4bd2 100644 --- a/mitmproxy/tools/console/commander/commander.py +++ b/mitmproxy/tools/console/commander/commander.py @@ -1,5 +1,6 @@ import abc -import typing +from collections.abc import Sequence +from typing import NamedTuple, Optional import urwid from urwid.text_layout import calc_coords @@ -18,12 +19,12 @@ def cycle(self, forward: bool = True) -> str: class ListCompleter(Completer): def __init__( - self, - start: str, - options: typing.Sequence[str], + self, + start: str, + options: Sequence[str], ) -> None: self.start = start - self.options: typing.List[str] = [] + self.options: list[str] = [] for o in options: if o.startswith(start): self.options.append(o) @@ -41,9 +42,9 @@ def cycle(self, forward: bool = True) -> str: return self.options[self.pos] -class CompletionState(typing.NamedTuple): +class CompletionState(NamedTuple): completer: Completer - parsed: typing.Sequence[mitmproxy.command.ParseResult] + parsed: Sequence[mitmproxy.command.ParseResult] class CommandBuffer: @@ -52,7 +53,7 @@ def __init__(self, master: mitmproxy.master.Master, start: str = "") -> None: self.text = start # Cursor is always within the range [0:len(buffer)]. self._cursor = len(self.text) - self.completion: typing.Optional[CompletionState] = None + self.completion: Optional[CompletionState] = None @property def cursor(self) -> int: @@ -105,7 +106,9 @@ def right(self) -> None: def cycle_completion(self, forward: bool = True) -> None: if not self.completion: - parts, remaining = self.master.commands.parse_partial(self.text[:self.cursor]) + parts, remaining = self.master.commands.parse_partial( + self.text[: self.cursor] + ) if parts and parts[-1].type != mitmproxy.types.Space: type_to_complete = parts[-1].type cycle_prefix = parts[-1].value @@ -121,7 +124,9 @@ def cycle_completion(self, forward: bool = True) -> None: self.completion = CompletionState( completer=ListCompleter( cycle_prefix, - ct.completion(self.master.commands, type_to_complete, cycle_prefix) + ct.completion( + self.master.commands, type_to_complete, cycle_prefix + ), ), parsed=parsed, ) @@ -134,26 +139,26 @@ def cycle_completion(self, forward: bool = True) -> None: def backspace(self) -> None: if self.cursor == 0: return - self.text = self.text[:self.cursor - 1] + self.text[self.cursor:] + self.text = self.text[: self.cursor - 1] + self.text[self.cursor :] self.cursor = self.cursor - 1 self.completion = None def delete(self) -> None: if self.cursor == len(self.text): return - self.text = self.text[:self.cursor] + self.text[self.cursor + 1:] + self.text = self.text[: self.cursor] + self.text[self.cursor + 1 :] self.completion = None def insert(self, k: str) -> None: """ - Inserts text at the cursor. + Inserts text at the cursor. """ # We don't want to insert a space before the command - if k == ' ' and self.text[0:self.cursor].strip() == '': + if k == " " and self.text[0 : self.cursor].strip() == "": return - self.text = self.text[:self.cursor] + k + self.text[self.cursor:] + self.text = self.text[: self.cursor] + k + self.text[self.cursor :] self.cursor += len(k) self.completion = None @@ -165,32 +170,32 @@ def __init__(self, master: mitmproxy.master.Master, text: str) -> None: super().__init__(urwid.Text(self.leader)) self.master = master self.active_filter = False - self.filter_str = '' + self.filter_str = "" self.cbuf = CommandBuffer(master, text) self.update() def keypress(self, size, key) -> None: if key == "delete": self.cbuf.delete() - elif key == "ctrl a" or key == 'home': + elif key == "ctrl a" or key == "home": self.cbuf.cursor = 0 - elif key == "ctrl e" or key == 'end': + elif key == "ctrl e" or key == "end": self.cbuf.cursor = len(self.cbuf.text) elif key == "meta b": - self.cbuf.cursor = self.cbuf.text.rfind(' ', 0, self.cbuf.cursor) + self.cbuf.cursor = self.cbuf.text.rfind(" ", 0, self.cbuf.cursor) elif key == "meta f": - pos = self.cbuf.text.find(' ', self.cbuf.cursor + 1) + pos = self.cbuf.text.find(" ", self.cbuf.cursor + 1) if pos == -1: pos = len(self.cbuf.text) self.cbuf.cursor = pos elif key == "ctrl w": prev_cursor = self.cbuf.cursor - pos = self.cbuf.text.rfind(' ', 0, self.cbuf.cursor - 1) + pos = self.cbuf.text.rfind(" ", 0, self.cbuf.cursor - 1) if pos == -1: - new_text = self.cbuf.text[self.cbuf.cursor:] + new_text = self.cbuf.text[self.cbuf.cursor :] cursor_pos = 0 else: - txt_after = self.cbuf.text[self.cbuf.cursor:] + txt_after = self.cbuf.text[self.cbuf.cursor :] txt_before = self.cbuf.text[0:pos] new_text = f"{txt_before} {txt_after}" cursor_pos = prev_cursor - (prev_cursor - pos) + 1 @@ -198,10 +203,10 @@ def keypress(self, size, key) -> None: self.cbuf.cursor = cursor_pos elif key == "backspace": self.cbuf.backspace() - if self.cbuf.text == '': + if self.cbuf.text == "": self.active_filter = False self.master.commands.call("commands.history.filter", "") - self.filter_str = '' + self.filter_str = "" elif key == "left" or key == "ctrl b": self.cbuf.left() elif key == "right" or key == "ctrl f": @@ -217,14 +222,14 @@ def keypress(self, size, key) -> None: prev_cmd = self.cbuf.text cmd = self.master.commands.execute("commands.history.next") - if cmd == '': + if cmd == "": if prev_cmd == self.filter_str: self.cbuf = CommandBuffer(self.master, prev_cmd) else: self.active_filter = False self.master.commands.call("commands.history.filter", "") - self.filter_str = '' - self.cbuf = CommandBuffer(self.master, '') + self.filter_str = "" + self.cbuf = CommandBuffer(self.master, "") else: self.cbuf = CommandBuffer(self.master, cmd) elif key == "shift tab": @@ -245,7 +250,7 @@ def render(self, size, focus=False) -> urwid.Canvas: canv.cursor = self.get_cursor_coords((maxcol,)) return canv - def get_cursor_coords(self, size) -> typing.Tuple[int, int]: + def get_cursor_coords(self, size) -> tuple[int, int]: p = self.cbuf.cursor + len(self.leader) trans = self._w.get_line_translation(size[0]) x, y = calc_coords(self._w.get_text()[0], trans, p) diff --git a/mitmproxy/tools/console/commandexecutor.py b/mitmproxy/tools/console/commandexecutor.py index 1c6d5aa696..bc99db94c4 100644 --- a/mitmproxy/tools/console/commandexecutor.py +++ b/mitmproxy/tools/console/commandexecutor.py @@ -1,4 +1,4 @@ -import typing +from collections.abc import Sequence from mitmproxy import exceptions from mitmproxy import flow @@ -19,20 +19,18 @@ def __call__(self, cmd): except exceptions.CommandError as e: ctx.log.error(str(e)) else: - if ret: - if type(ret) == typing.Sequence[flow.Flow]: + if ret is not None: + if type(ret) == Sequence[flow.Flow]: signals.status_message.send( message="Command returned %s flows" % len(ret) ) elif type(ret) == flow.Flow: - signals.status_message.send( - message="Command returned 1 flow" - ) + signals.status_message.send(message="Command returned 1 flow") else: self.master.overlay( overlay.DataViewerOverlay( self.master, ret, ), - valign="top" + valign="top", ) diff --git a/mitmproxy/tools/console/commands.py b/mitmproxy/tools/console/commands.py index 26a99b14d5..3b8f60f3d5 100644 --- a/mitmproxy/tools/console/commands.py +++ b/mitmproxy/tools/console/commands.py @@ -18,10 +18,7 @@ def __init__(self, walker, cmd: command.Command, focused: bool): self._w = self.get_widget() def get_widget(self): - parts = [ - ("focus", ">> " if self.focused else " "), - ("title", self.cmd.name) - ] + parts = [("focus", ">> " if self.focused else " "), ("title", self.cmd.name)] if self.cmd.parameters: parts += [ ("text", " "), @@ -33,10 +30,7 @@ def get_widget(self): ("text", command.typename(self.cmd.return_type)), ] - return urwid.AttrMap( - urwid.Padding(urwid.Text(parts)), - "text" - ) + return urwid.AttrMap(urwid.Padding(urwid.Text(parts)), "text") def get_edit_text(self): return self._w[1].get_edit_text() @@ -121,9 +115,7 @@ def set_active(self, val): def widget(self, txt): cols, _ = self.master.ui.get_cols_rows() - return urwid.ListBox( - [urwid.Text(i) for i in textwrap.wrap(txt, cols)] - ) + return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)]) def sig_mod(self, txt): self.set_body(self.widget(txt)) @@ -148,9 +140,7 @@ def layout_pushed(self, prev): def keypress(self, size, key): if key == "m_next": - self.focus_position = ( - self.focus_position + 1 - ) % len(self.widget_list) + self.focus_position = (self.focus_position + 1) % len(self.widget_list) self.widget_list[1].set_active(self.focus_position == 1) key = None @@ -158,7 +148,7 @@ def keypress(self, size, key): # So much for "closed for modification, but open for extension". item_rows = None if len(size) == 2: - item_rows = self.get_item_rows(size, focus = True) + 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) diff --git a/mitmproxy/tools/console/common.py b/mitmproxy/tools/console/common.py index fe3f6469d7..18c2abf270 100644 --- a/mitmproxy/tools/console/common.py +++ b/mitmproxy/tools/console/common.py @@ -1,8 +1,10 @@ import enum import platform -import typing import math +from collections.abc import Iterable from functools import lru_cache +from typing import Optional, Union + from publicsuffix2 import get_sld, get_tld import urwid @@ -12,14 +14,18 @@ from mitmproxy.http import HTTPFlow from mitmproxy.utils import human, emoji from mitmproxy.tcp import TCPFlow +from mitmproxy import dns +from mitmproxy.dns import DNSFlow # Detect Windows Subsystem for Linux and Windows -IS_WINDOWS_OR_WSL = "Microsoft" in platform.platform() or "Windows" in platform.platform() +IS_WINDOWS_OR_WSL = ( + "Microsoft" in platform.platform() or "Windows" in platform.platform() +) def is_keypress(k): """ - Is this input event a keypress? + Is this input event a keypress? """ if isinstance(k, str): return True @@ -40,11 +46,11 @@ def highlight_key(str, key, textattr="text", keyattr="key"): def format_keyvals( - entries: typing.Iterable[typing.Tuple[str, typing.Union[None, str, urwid.Widget]]], - key_format: str = "key", - value_format: str = "text", - indent: int = 0 -) -> typing.List[urwid.Columns]: + entries: Iterable[tuple[str, Union[None, str, urwid.Widget]]], + key_format: str = "key", + value_format: str = "text", + indent: int = 0, +) -> list[urwid.Columns]: """ Format a list of (key, value) tuples. @@ -71,30 +77,18 @@ def format_keyvals( urwid.Columns( [ ("fixed", indent, urwid.Text("")), - ( - "fixed", - max_key_len, - urwid.Text([(key_format, k)]) - ), - v + ("fixed", max_key_len, urwid.Text([(key_format, k)])), + v, ], - dividechars=2 + dividechars=2, ) ) return ret -def fcol(s: str, attr: str) -> typing.Tuple[str, int, urwid.Text]: +def fcol(s: str, attr: str) -> tuple[str, int, urwid.Text]: s = str(s) - return ( - "fixed", - len(s), - urwid.Text( - [ - (attr, s) - ] - ) - ) + return ("fixed", len(s), urwid.Text([(attr, s)])) if urwid.util.detected_encoding: @@ -117,18 +111,19 @@ def fcol(s: str, attr: str) -> typing.Tuple[str, int, urwid.Text]: SYMBOL_TO_CLIENT = "<-" SCHEME_STYLES = { - 'http': 'scheme_http', - 'https': 'scheme_https', - 'ws': 'scheme_ws', - 'wss': 'scheme_wss', - 'tcp': 'scheme_tcp', + "http": "scheme_http", + "https": "scheme_https", + "ws": "scheme_ws", + "wss": "scheme_wss", + "tcp": "scheme_tcp", + "dns": "scheme_dns", } HTTP_REQUEST_METHOD_STYLES = { - 'GET': 'method_get', - 'POST': 'method_post', - 'DELETE': 'method_delete', - 'HEAD': 'method_head', - 'PUT': 'method_put' + "GET": "method_get", + "POST": "method_post", + "DELETE": "method_delete", + "HEAD": "method_head", + "PUT": "method_put", } HTTP_RESPONSE_CODE_STYLE = { 2: "code_200", @@ -151,14 +146,14 @@ def fixlen(s: str, maxlen: int) -> str: if len(s) <= maxlen: return s.ljust(maxlen) else: - return s[0:maxlen - len(SYMBOL_ELLIPSIS)] + SYMBOL_ELLIPSIS + return s[0 : maxlen - len(SYMBOL_ELLIPSIS)] + SYMBOL_ELLIPSIS def fixlen_r(s: str, maxlen: int) -> str: if len(s) <= maxlen: return s.rjust(maxlen) else: - return SYMBOL_ELLIPSIS + s[len(s) - maxlen + len(SYMBOL_ELLIPSIS):] + return SYMBOL_ELLIPSIS + s[len(s) - maxlen + len(SYMBOL_ELLIPSIS) :] def render_marker(marker: str) -> str: @@ -172,7 +167,7 @@ def render_marker(marker: str) -> str: class TruncatedText(urwid.Widget): - def __init__(self, text, attr, align='left'): + def __init__(self, text, attr, align="left"): self.text = text self.attr = attr self.align = align @@ -187,11 +182,11 @@ def rows(self, size, focus=False): def render(self, size, focus=False): text = self.text attr = self.attr - if self.align == 'right': + if self.align == "right": text = text[::-1] attr = attr[::-1] - text_len = len(text) # TODO: unicode? + text_len = urwid.util.calc_width(text, 0, len(text)) if size is not None and len(size) > 0: width = size[0] else: @@ -200,26 +195,29 @@ def render(self, size, focus=False): if width >= text_len: remaining = width - text_len if remaining > 0: - c_text = text + ' ' * remaining - c_attr = attr + [('text', remaining)] + c_text = text + " " * remaining + c_attr = attr + [("text", remaining)] else: c_text = text c_attr = attr else: - visible_len = width - len(SYMBOL_ELLIPSIS) - visible_text = text[0:visible_len] + trim = urwid.util.calc_trim_text(text, 0, width - 1, 0, width - 1) + visible_text = text[0 : trim[1]] + if trim[3] == 1: + visible_text += " " c_text = visible_text + SYMBOL_ELLIPSIS - c_attr = (urwid.util.rle_subseg(attr, 0, len(visible_text.encode())) + - [('focus', len(SYMBOL_ELLIPSIS.encode()))]) + c_attr = urwid.util.rle_subseg(attr, 0, len(visible_text.encode())) + [ + ("focus", len(SYMBOL_ELLIPSIS.encode())) + ] - if self.align == 'right': + if self.align == "right": c_text = c_text[::-1] c_attr = c_attr[::-1] return urwid.TextCanvas([c_text.encode()], [c_attr], maxcol=width) -def truncated_plain(text, attr, align='left'): +def truncated_plain(text, attr, align="left"): return TruncatedText(text, [(attr, len(text.encode()))], align) @@ -254,106 +252,111 @@ def colorize_host(host): for letter in reversed(range(len(host))): character = host[letter] if tld_size > 0: - style = 'url_domain' + style = "url_domain" tld_size -= 1 elif tld_size == 0: - style = 'text' + style = "text" tld_size -= 1 elif sld_size > 0: sld_size -= 1 - style = 'url_extension' + style = "url_extension" else: - style = 'text' + style = "text" rle_append_beginning_modify(attr, (style, len(character.encode()))) return attr def colorize_req(s): - path = s.split('?', 2)[0] + path = s.split("?", 2)[0] i_query = len(path) - i_last_slash = path.rfind('/') - i_ext = path[i_last_slash + 1:].rfind('.') + i_last_slash = path.rfind("/") + i_ext = path[i_last_slash + 1 :].rfind(".") i_ext = i_last_slash + i_ext if i_ext >= 0 else len(s) in_val = False attr = [] for i in range(len(s)): c = s[i] - if ((i < i_query and c == '/') or - (i < i_query and i > i_last_slash and c == '.') or - (i == i_query)): - a = 'url_punctuation' + if ( + (i < i_query and c == "/") + or (i < i_query and i > i_last_slash and c == ".") + or (i == i_query) + ): + a = "url_punctuation" elif i > i_query: if in_val: - if c == '&': + if c == "&": in_val = False - a = 'url_punctuation' + a = "url_punctuation" else: - a = 'url_query_value' + a = "url_query_value" else: - if c == '=': + if c == "=": in_val = True - a = 'url_punctuation' + a = "url_punctuation" else: - a = 'url_query_key' + a = "url_query_key" elif i > i_ext: - a = 'url_extension' + a = "url_extension" elif i > i_last_slash: - a = 'url_filename' + a = "url_filename" else: - a = 'text' + a = "text" urwid.util.rle_append_modify(attr, (a, len(c.encode()))) return attr def colorize_url(url): - parts = url.split('/', 3) - if len(parts) < 4 or len(parts[1]) > 0 or parts[0][-1:] != ':': - return [('error', len(url))] # bad URL - return [ - (SCHEME_STYLES.get(parts[0], "scheme_other"), len(parts[0]) - 1), - ('url_punctuation', 3), # :// - ] + colorize_host(parts[2]) + colorize_req('/' + parts[3]) + parts = url.split("/", 3) + if len(parts) < 4 or len(parts[1]) > 0 or parts[0][-1:] != ":": + return [("error", len(url))] # bad URL + return ( + [ + (SCHEME_STYLES.get(parts[0], "scheme_other"), len(parts[0]) - 1), + ("url_punctuation", 3), # :// + ] + + colorize_host(parts[2]) + + colorize_req("/" + parts[3]) + ) -def format_http_content_type(content_type: str) -> typing.Tuple[str, str]: +def format_http_content_type(content_type: str) -> tuple[str, str]: content_type = content_type.split(";")[0] - if content_type.endswith('/javascript'): - style = 'content_script' - elif content_type.startswith('text/'): - style = 'content_text' - elif (content_type.startswith('image/') or - content_type.startswith('video/') or - content_type.startswith('font/') or - "/x-font-" in content_type): - style = 'content_media' - elif content_type.endswith('/json') or content_type.endswith('/xml'): - style = 'content_data' - elif content_type.startswith('application/'): - style = 'content_raw' + if content_type.endswith("/javascript"): + style = "content_script" + elif content_type.startswith("text/"): + style = "content_text" + elif ( + content_type.startswith("image/") + or content_type.startswith("video/") + or content_type.startswith("font/") + or "/x-font-" in content_type + ): + style = "content_media" + elif content_type.endswith("/json") or content_type.endswith("/xml"): + style = "content_data" + elif content_type.startswith("application/"): + style = "content_raw" else: - style = 'content_other' + style = "content_other" return content_type, style -def format_duration(duration: float) -> typing.Tuple[str, str]: +def format_duration(duration: float) -> tuple[str, str]: pretty_duration = human.pretty_duration(duration) - style = 'gradient_%02d' % int(99 - 100 * min(math.log2(1 + 1000 * duration) / 12, 0.99)) + style = "gradient_%02d" % int( + 99 - 100 * min(math.log2(1 + 1000 * duration) / 12, 0.99) + ) return pretty_duration, style -def format_size(num_bytes: int) -> typing.Tuple[str, str]: +def format_size(num_bytes: int) -> tuple[str, str]: pretty_size = human.pretty_size(num_bytes) - style = 'gradient_%02d' % int(99 - 100 * min(math.log2(1 + num_bytes) / 20, 0.99)) + style = "gradient_%02d" % int(99 - 100 * min(math.log2(1 + num_bytes) / 20, 0.99)) return pretty_size, style -def format_left_indicators( - *, - focused: bool, - intercepted: bool, - timestamp: float -): - indicators: typing.List[typing.Union[str, typing.Tuple[str, str]]] = [] +def format_left_indicators(*, focused: bool, intercepted: bool, timestamp: float): + indicators: list[Union[str, tuple[str, str]]] = [] if focused: indicators.append(("focus", ">>")) else: @@ -367,11 +370,11 @@ def format_left_indicators( def format_right_indicators( - *, - replay: bool, - marked: str, + *, + replay: bool, + marked: str, ): - indicators: typing.List[typing.Union[str, typing.Tuple[str, str]]] = [] + indicators: list[Union[str, tuple[str, str]]] = [] if replay: indicators.append(("replay", SYMBOL_REPLAY)) else: @@ -385,26 +388,26 @@ def format_right_indicators( @lru_cache(maxsize=800) def format_http_flow_list( - *, - render_mode: RenderMode, - focused: bool, - marked: str, - is_replay: bool, - request_method: str, - request_scheme: str, - request_host: str, - request_path: str, - request_url: str, - request_http_version: str, - request_timestamp: float, - request_is_push_promise: bool, - intercepted: bool, - response_code: typing.Optional[int], - response_reason: typing.Optional[str], - response_content_length: typing.Optional[int], - response_content_type: typing.Optional[str], - duration: typing.Optional[float], - error_message: typing.Optional[str], + *, + render_mode: RenderMode, + focused: bool, + marked: str, + is_replay: bool, + request_method: str, + request_scheme: str, + request_host: str, + request_path: str, + request_url: str, + request_http_version: str, + request_timestamp: float, + request_is_push_promise: bool, + intercepted: bool, + response_code: Optional[int], + response_reason: Optional[str], + response_content_length: Optional[int], + response_content_type: Optional[str], + duration: Optional[float], + error_message: Optional[str], ) -> urwid.Widget: req = [] @@ -420,7 +423,7 @@ def format_http_flow_list( req.append(fcol(request_method, method_style)) if request_is_push_promise: - req.append(fcol('PUSH_PROMISE', 'method_http2_push')) + req.append(fcol("PUSH_PROMISE", "method_http2_push")) preamble_len = sum(x[1] for x in req) + len(req) - 1 @@ -434,24 +437,22 @@ def format_http_flow_list( url_style = "title" if render_mode is RenderMode.DETAILVIEW: - req.append( - urwid.Text([(url_style, request_url)]) - ) + req.append(urwid.Text([(url_style, request_url)])) else: req.append(truncated_plain(request_url, url_style)) req.append(format_right_indicators(replay=is_replay, marked=marked)) - resp = [ - ("fixed", preamble_len, urwid.Text("")) - ] + resp = [("fixed", preamble_len, urwid.Text(""))] if response_code: if intercepted: style = "intercept" else: style = "" - status_style = style or HTTP_RESPONSE_CODE_STYLE.get(response_code // 100, "code_other") + status_style = style or HTTP_RESPONSE_CODE_STYLE.get( + response_code // 100, "code_other" + ) resp.append(fcol(SYMBOL_RETURN, status_style)) resp.append(fcol(str(response_code), status_style)) if response_reason and render_mode is RenderMode.DETAILVIEW: @@ -478,40 +479,37 @@ def format_http_flow_list( resp.append(fcol(SYMBOL_RETURN, "error")) resp.append(urwid.Text([("error", error_message)])) - return urwid.Pile([ - urwid.Columns(req, dividechars=1), - urwid.Columns(resp, dividechars=1) - ]) + return urwid.Pile( + [urwid.Columns(req, dividechars=1), urwid.Columns(resp, dividechars=1)] + ) @lru_cache(maxsize=800) def format_http_flow_table( - *, - render_mode: RenderMode, - focused: bool, - marked: str, - is_replay: typing.Optional[str], - request_method: str, - request_scheme: str, - request_host: str, - request_path: str, - request_url: str, - request_http_version: str, - request_timestamp: float, - request_is_push_promise: bool, - intercepted: bool, - response_code: typing.Optional[int], - response_reason: typing.Optional[str], - response_content_length: typing.Optional[int], - response_content_type: typing.Optional[str], - duration: typing.Optional[float], - error_message: typing.Optional[str], + *, + render_mode: RenderMode, + focused: bool, + marked: str, + is_replay: Optional[str], + request_method: str, + request_scheme: str, + request_host: str, + request_path: str, + request_url: str, + request_http_version: str, + request_timestamp: float, + request_is_push_promise: bool, + intercepted: bool, + response_code: Optional[int], + response_reason: Optional[str], + response_content_length: Optional[int], + response_content_type: Optional[str], + duration: Optional[float], + error_message: Optional[str], ) -> urwid.Widget: items = [ format_left_indicators( - focused=focused, - intercepted=intercepted, - timestamp=request_timestamp + focused=focused, intercepted=intercepted, timestamp=request_timestamp ) ] @@ -524,13 +522,23 @@ def format_http_flow_table( items.append(fcol(fixlen(request_scheme.upper(), 5), scheme_style)) if request_is_push_promise: - method_style = 'method_http2_push' + method_style = "method_http2_push" else: - method_style = request_style or HTTP_REQUEST_METHOD_STYLES.get(request_method, "method_other") + method_style = request_style or HTTP_REQUEST_METHOD_STYLES.get( + request_method, "method_other" + ) items.append(fcol(fixlen(request_method, 4), method_style)) - items.append(('weight', 0.25, TruncatedText(request_host, colorize_host(request_host), 'right'))) - items.append(('weight', 1.0, TruncatedText(request_path, colorize_req(request_path), 'left'))) + items.append( + ( + "weight", + 0.25, + TruncatedText(request_host, colorize_host(request_host), "right"), + ) + ) + items.append( + ("weight", 1.0, TruncatedText(request_path, colorize_req(request_path), "left")) + ) if intercepted and response_code: response_style = "intercept" @@ -540,35 +548,37 @@ def format_http_flow_table( if response_code: status = str(response_code) - status_style = response_style or HTTP_RESPONSE_CODE_STYLE.get(response_code // 100, "code_other") + status_style = response_style or HTTP_RESPONSE_CODE_STYLE.get( + response_code // 100, "code_other" + ) if response_content_length and response_content_type: content, content_style = format_http_content_type(response_content_type) content_style = response_style or content_style elif response_content_length: - content = '' - content_style = 'content_none' + content = "" + content_style = "content_none" elif response_content_length == 0: content = "[no content]" - content_style = 'content_none' + content_style = "content_none" else: content = "[content missing]" - content_style = 'content_none' + content_style = "content_none" elif error_message: - status = 'err' - status_style = 'error' + status = "err" + status_style = "error" content = error_message - content_style = 'error' + content_style = "error" else: - status = '' - status_style = 'text' - content = '' - content_style = '' + status = "" + status_style = "text" + content = "" + content_style = "" items.append(fcol(fixlen(status, 3), status_style)) - items.append(('weight', 0.15, truncated_plain(content, content_style, 'right'))) + items.append(("weight", 0.15, truncated_plain(content, content_style, "right"))) if response_content_length: size, size_style = format_size(response_content_length) @@ -578,29 +588,33 @@ def format_http_flow_table( if duration: duration_pretty, duration_style = format_duration(duration) - items.append(fcol(fixlen_r(duration_pretty, 5), response_style or duration_style)) + items.append( + fcol(fixlen_r(duration_pretty, 5), response_style or duration_style) + ) else: items.append(("fixed", 5, urwid.Text(""))) - items.append(format_right_indicators( - replay=bool(is_replay), - marked=marked, - )) + items.append( + format_right_indicators( + replay=bool(is_replay), + marked=marked, + ) + ) return urwid.Columns(items, dividechars=1, min_width=15) @lru_cache(maxsize=800) def format_tcp_flow( - *, - render_mode: RenderMode, - focused: bool, - timestamp_start: float, - marked: str, - client_address, - server_address, - total_size: int, - duration: typing.Optional[float], - error_message: typing.Optional[str], + *, + render_mode: RenderMode, + focused: bool, + timestamp_start: float, + marked: str, + client_address, + server_address, + total_size: int, + duration: Optional[float], + error_message: Optional[str], ): conn = f"{human.format_address(client_address)} <-> {human.format_address(server_address)}" @@ -608,7 +622,9 @@ def format_tcp_flow( if render_mode in (RenderMode.TABLE, RenderMode.DETAILVIEW): items.append( - format_left_indicators(focused=focused, intercepted=False, timestamp=timestamp_start) + format_left_indicators( + focused=focused, intercepted=False, timestamp=timestamp_start + ) ) else: if focused: @@ -621,9 +637,9 @@ def format_tcp_flow( else: items.append(fcol("TCP", SCHEME_STYLES["tcp"])) - items.append(('weight', 1.0, truncated_plain(conn, "text", 'left'))) + items.append(("weight", 1.0, truncated_plain(conn, "text", "left"))) if error_message: - items.append(('weight', 1.0, truncated_plain(error_message, "error", 'left'))) + items.append(("weight", 1.0, truncated_plain(error_message, "error", "left"))) if total_size: size, size_style = format_size(total_size) @@ -639,17 +655,89 @@ def format_tcp_flow( items.append(format_right_indicators(replay=False, marked=marked)) - return urwid.Pile([ - urwid.Columns(items, dividechars=1, min_width=15) - ]) + return urwid.Pile([urwid.Columns(items, dividechars=1, min_width=15)]) + + +@lru_cache(maxsize=800) +def format_dns_flow( + *, + render_mode: RenderMode, + focused: bool, + intercepted: bool, + marked: str, + is_replay: Optional[str], + op_code: str, + request_timestamp: float, + domain: str, + type: str, + response_code: Optional[str], + response_code_http_equiv: int, + answer: Optional[str], + error_message: str, + duration: Optional[float], +): + items = [] + + if render_mode in (RenderMode.TABLE, RenderMode.DETAILVIEW): + items.append( + format_left_indicators( + focused=focused, intercepted=intercepted, timestamp=request_timestamp + ) + ) + else: + items.append(fcol(">>" if focused else " ", "focus")) + + scheme_style = "intercepted" if intercepted else SCHEME_STYLES["dns"] + t = f"DNS {op_code}" + if render_mode is RenderMode.TABLE: + t = fixlen(t, 10) + items.append(fcol(t, scheme_style)) + items.append(("weight", 0.5, TruncatedText(domain, colorize_host(domain), "right"))) + items.append(fcol("(" + fixlen(type, 5)[: len(type)] + ") =", "text")) + + items.append( + ( + "weight", + 1, + ( + truncated_plain( + "..." if answer is None else "?" if not answer else answer, "text" + ) + if error_message is None + else truncated_plain(error_message, "error") + ), + ) + ) + status_style = ( + "intercepted" + if intercepted + else HTTP_RESPONSE_CODE_STYLE.get(response_code_http_equiv // 100, "code_other") + ) + items.append( + fcol(fixlen("" if response_code is None else response_code, 9), status_style) + ) + + if duration: + duration_pretty, duration_style = format_duration(duration) + items.append(fcol(fixlen_r(duration_pretty, 5), duration_style)) + else: + items.append(("fixed", 5, urwid.Text(""))) + + items.append( + format_right_indicators( + replay=bool(is_replay), + marked=marked, + ) + ) + return urwid.Pile([urwid.Columns(items, dividechars=1, min_width=15)]) def format_flow( - f: flow.Flow, - *, - render_mode: RenderMode, - hostheader: bool = False, # pass options directly if we need more stuff from them - focused: bool = True, + f: flow.Flow, + *, + render_mode: RenderMode, + hostheader: bool = False, # pass options directly if we need more stuff from them + focused: bool = True, ) -> urwid.Widget: """ This functions calls the proper renderer depending on the flow type. @@ -657,8 +745,8 @@ def format_flow( relevant for display and call the render with only that. This assures that rows are updated if the flow is changed. """ - duration: typing.Optional[float] - error_message: typing.Optional[str] + duration: Optional[float] + error_message: Optional[str] if f.error: error_message = f.error.msg else: @@ -669,13 +757,13 @@ def format_flow( for message in f.messages: total_size += len(message.content) if f.messages: - duration = f.messages[-1].timestamp - f.timestamp_start + duration = f.messages[-1].timestamp - f.client_conn.timestamp_start else: duration = None return format_tcp_flow( render_mode=render_mode, focused=focused, - timestamp_start=f.timestamp_start, + timestamp_start=f.client_conn.timestamp_start, marked=f.marked, client_address=f.client_conn.peername, server_address=f.server_conn.address, @@ -683,21 +771,54 @@ def format_flow( duration=duration, error_message=error_message, ) - elif isinstance(f, HTTPFlow): - intercepted = ( - f.intercepted and not (f.reply and f.reply.state == "committed") + elif isinstance(f, DNSFlow): + if f.response: + duration = f.response.timestamp - f.request.timestamp + response_code_str: Optional[str] = dns.response_codes.to_str( + f.response.response_code + ) + response_code_http_equiv = dns.response_codes.http_equiv_status_code( + f.response.response_code + ) + answer = ", ".join(str(x) for x in f.response.answers) + else: + duration = None + response_code_str = None + response_code_http_equiv = 0 + answer = None + return format_dns_flow( + render_mode=render_mode, + focused=focused, + intercepted=f.intercepted, + marked=f.marked, + is_replay=f.is_replay, + op_code=dns.op_codes.to_str(f.request.op_code), + request_timestamp=f.request.timestamp, + domain=f.request.questions[0].name if f.request.questions else "", + type=dns.types.to_str(f.request.questions[0].type) + if f.request.questions + else "", + response_code=response_code_str, + response_code_http_equiv=response_code_http_equiv, + answer=answer, + error_message=error_message, + duration=duration, ) - response_content_length: typing.Optional[int] + elif isinstance(f, HTTPFlow): + intercepted = f.intercepted + response_content_length: Optional[int] if f.response: if f.response.raw_content is not None: response_content_length = len(f.response.raw_content) else: response_content_length = None - response_code: typing.Optional[int] = f.response.status_code - response_reason: typing.Optional[str] = f.response.reason + response_code: Optional[int] = f.response.status_code + response_reason: Optional[str] = f.response.reason response_content_type = f.response.headers.get("content-type") if f.response.timestamp_end: - duration = max([f.response.timestamp_end - f.request.timestamp_start, 0]) + duration = max( + [f.response.timestamp_end - f.request.timestamp_start, 0] + ) else: duration = None else: @@ -730,7 +851,7 @@ def format_flow( request_url=f.request.pretty_url if hostheader else f.request.url, request_http_version=f.request.http_version, request_timestamp=f.request.timestamp_start, - request_is_push_promise='h2-pushed-stream' in f.metadata, + request_is_push_promise="h2-pushed-stream" in f.metadata, intercepted=intercepted, response_code=response_code, response_reason=response_reason, diff --git a/mitmproxy/tools/console/consoleaddons.py b/mitmproxy/tools/console/consoleaddons.py index 61d614977a..f0eebbf08c 100644 --- a/mitmproxy/tools/console/consoleaddons.py +++ b/mitmproxy/tools/console/consoleaddons.py @@ -1,10 +1,11 @@ import csv -import typing +from collections.abc import Sequence import mitmproxy.types from mitmproxy import command, command_lexer from mitmproxy import contentviews from mitmproxy import ctx +from mitmproxy import dns from mitmproxy import exceptions from mitmproxy import flow from mitmproxy import http @@ -21,7 +22,7 @@ "light", "dark", "solarized_light", - "solarized_dark" + "solarized_dark", ] view_orders = [ "time", @@ -35,17 +36,13 @@ "horizontal", ] -console_flowlist_layout = [ - "default", - "table", - "list" -] +console_flowlist_layout = ["default", "table", "list"] class ConsoleAddon: """ - An addon that exposes console-specific commands, and hooks into required - events. + An addon that exposes console-specific commands, and hooks into required + events. """ def __init__(self, master): @@ -54,81 +51,90 @@ def __init__(self, master): def load(self, loader): loader.add_option( - "console_default_contentview", str, "auto", + "console_default_contentview", + str, + "auto", "The default content view mode.", - choices=[i.name.lower() for i in contentviews.views] + choices=[i.name.lower() for i in contentviews.views], ) loader.add_option( - "console_eventlog_verbosity", str, 'info', + "console_eventlog_verbosity", + str, + "info", "EventLog verbosity.", - choices=log.LogTierOrder + choices=log.LogTierOrder, ) loader.add_option( - "console_layout", str, "single", + "console_layout", + str, + "single", "Console layout.", choices=sorted(console_layouts), ) loader.add_option( - "console_layout_headers", bool, True, + "console_layout_headers", + bool, + True, "Show layout component headers", ) loader.add_option( - "console_focus_follow", bool, False, - "Focus follows new flows." + "console_focus_follow", bool, False, "Focus follows new flows." ) loader.add_option( - "console_palette", str, "solarized_dark", + "console_palette", + str, + "solarized_dark", "Color palette.", choices=sorted(console_palettes), ) loader.add_option( - "console_palette_transparent", bool, True, - "Set transparent background for palette." - ) - loader.add_option( - "console_mouse", bool, True, - "Console mouse interaction." + "console_palette_transparent", + bool, + True, + "Set transparent background for palette.", ) + loader.add_option("console_mouse", bool, True, "Console mouse interaction.") loader.add_option( "console_flowlist_layout", - str, "default", + str, + "default", "Set the flowlist layout", - choices=sorted(console_flowlist_layout) + choices=sorted(console_flowlist_layout), ) loader.add_option( - "console_strip_trailing_newlines", bool, False, - "Strip trailing newlines from edited request/response bodies." + "console_strip_trailing_newlines", + bool, + False, + "Strip trailing newlines from edited request/response bodies.", ) @command.command("console.layout.options") - def layout_options(self) -> typing.Sequence[str]: + def layout_options(self) -> Sequence[str]: """ - Returns the available options for the console_layout option. + Returns the available options for the console_layout option. """ return ["single", "vertical", "horizontal"] @command.command("console.layout.cycle") def layout_cycle(self) -> None: """ - Cycle through the console layout options. + Cycle through the console layout options. """ opts = self.layout_options() off = self.layout_options().index(ctx.options.console_layout) - ctx.options.update( - console_layout=opts[(off + 1) % len(opts)] - ) + ctx.options.update(console_layout=opts[(off + 1) % len(opts)]) @command.command("console.panes.next") def panes_next(self) -> None: """ - Go to the next layout pane. + Go to the next layout pane. """ self.master.window.switch() @command.command("console.options.reset.focus") def options_reset_current(self) -> None: """ - Reset the current option in the options editor. + Reset the current option in the options editor. """ fv = self.master.window.current("options") if not fv: @@ -138,70 +144,70 @@ def options_reset_current(self) -> None: @command.command("console.nav.start") def nav_start(self) -> None: """ - Go to the start of a list or scrollable. + Go to the start of a list or scrollable. """ self.master.inject_key("m_start") @command.command("console.nav.end") def nav_end(self) -> None: """ - Go to the end of a list or scrollable. + Go to the end of a list or scrollable. """ self.master.inject_key("m_end") @command.command("console.nav.next") def nav_next(self) -> None: """ - Go to the next navigatable item. + Go to the next navigatable item. """ self.master.inject_key("m_next") @command.command("console.nav.select") def nav_select(self) -> None: """ - Select a navigable item for viewing or editing. + Select a navigable item for viewing or editing. """ self.master.inject_key("m_select") @command.command("console.nav.up") def nav_up(self) -> None: """ - Go up. + Go up. """ self.master.inject_key("up") @command.command("console.nav.down") def nav_down(self) -> None: """ - Go down. + Go down. """ self.master.inject_key("down") @command.command("console.nav.pageup") def nav_pageup(self) -> None: """ - Go up. + Go up. """ self.master.inject_key("page up") @command.command("console.nav.pagedown") def nav_pagedown(self) -> None: """ - Go down. + Go down. """ self.master.inject_key("page down") @command.command("console.nav.left") def nav_left(self) -> None: """ - Go left. + Go left. """ self.master.inject_key("left") @command.command("console.nav.right") def nav_right(self) -> None: """ - Go right. + Go right. """ self.master.inject_key("right") @@ -209,14 +215,14 @@ def nav_right(self) -> None: def console_choose( self, prompt: str, - choices: typing.Sequence[str], + choices: Sequence[str], cmd: mitmproxy.types.Cmd, - *args: mitmproxy.types.CmdArgs + *args: mitmproxy.types.CmdArgs, ) -> None: """ - Prompt the user to choose from a specified list of strings, then - invoke another command with all occurrences of {choice} replaced by - the choice the user made. + Prompt the user to choose from a specified list of strings, then + invoke another command with all occurrences of {choice} replaced by + the choice the user made. """ def callback(opt): @@ -227,9 +233,7 @@ def callback(opt): except exceptions.CommandError as e: ctx.log.error(str(e)) - self.master.overlay( - overlay.Chooser(self.master, prompt, choices, "", callback) - ) + self.master.overlay(overlay.Chooser(self.master, prompt, choices, "", callback)) @command.command("console.choose.cmd") def console_choose_cmd( @@ -237,12 +241,12 @@ def console_choose_cmd( prompt: str, choicecmd: mitmproxy.types.Cmd, subcmd: mitmproxy.types.Cmd, - *args: mitmproxy.types.CmdArgs + *args: mitmproxy.types.CmdArgs, ) -> None: """ - Prompt the user to choose from a list of strings returned by a - command, then invoke another command with all occurrences of {choice} - replaced by the choice the user made. + Prompt the user to choose from a list of strings returned by a + command, then invoke another command with all occurrences of {choice} + replaced by the choice the user made. """ choices = ctx.master.commands.execute(choicecmd) @@ -254,9 +258,7 @@ def callback(opt): except exceptions.CommandError as e: ctx.log.error(str(e)) - self.master.overlay( - overlay.Chooser(self.master, prompt, choices, "", callback) - ) + self.master.overlay(overlay.Chooser(self.master, prompt, choices, "", callback)) @command.command("console.command") def console_command(self, *command_str: str) -> None: @@ -276,10 +278,7 @@ def console_command_set(self, option_name: str) -> None: option_value = getattr(self.master.options, option_name, None) or "" set_command = f"set {option_name} {option_value!r}" cursor = len(set_command) - 1 - signals.status_prompt_command.send( - partial=set_command, - cursor=cursor - ) + signals.status_prompt_command.send(partial=set_command, cursor=cursor) @command.command("console.view.keybindings") def view_keybindings(self) -> None: @@ -309,7 +308,7 @@ def view_help(self) -> None: @command.command("console.view.flow") def view_flow(self, flow: flow.Flow) -> None: """View a flow.""" - if isinstance(flow, (http.HTTPFlow, tcp.TCPFlow)): + if isinstance(flow, (http.HTTPFlow, tcp.TCPFlow, dns.DNSFlow)): self.master.switch_view("flowview") else: ctx.log.warn(f"No detail view for {type(flow).__name__}.") @@ -322,8 +321,8 @@ def exit(self) -> None: @command.command("console.view.pop") def view_pop(self) -> None: """ - Pop a view off the console stack. At the top level, this prompts the - user to exit mitmproxy. + Pop a view off the console stack. At the top level, this prompts the + user to exit mitmproxy. """ signals.pop_view_state.send(self) @@ -331,14 +330,16 @@ def view_pop(self) -> None: @command.argument("part", type=mitmproxy.types.Choice("console.bodyview.options")) def bodyview(self, flow: flow.Flow, part: str) -> None: """ - Spawn an external viewer for a flow request or response body based - on the detected MIME type. We use the mailcap system to find the - correct viewer, and fall back to the programs in $PAGER or $EDITOR - if necessary. + Spawn an external viewer for a flow request or response body based + on the detected MIME type. We use the mailcap system to find the + correct viewer, and fall back to the programs in $PAGER or $EDITOR + if necessary. """ fpart = getattr(flow, part, None) if not fpart: - raise exceptions.CommandError("Part must be either request or response, not %s." % part) + raise exceptions.CommandError( + "Part must be either request or response, not %s." % part + ) t = fpart.headers.get("content-type") content = fpart.get_content(strict=False) if not content: @@ -346,23 +347,23 @@ def bodyview(self, flow: flow.Flow, part: str) -> None: self.master.spawn_external_viewer(content, t) @command.command("console.bodyview.options") - def bodyview_options(self) -> typing.Sequence[str]: + def bodyview_options(self) -> Sequence[str]: """ - Possible parts for console.bodyview. + Possible parts for console.bodyview. """ return ["request", "response"] @command.command("console.edit.focus.options") - def edit_focus_options(self) -> typing.Sequence[str]: + def edit_focus_options(self) -> Sequence[str]: """ - Possible components for console.edit.focus. + Possible components for console.edit.focus. """ flow = self.master.view.focus.flow focus_options = [] - if type(flow) == tcp.TCPFlow: + if isinstance(flow, tcp.TCPFlow): focus_options = ["tcp-message"] - elif type(flow) == http.HTTPFlow: + elif isinstance(flow, http.HTTPFlow): focus_options = [ "cookies", "urlencoded form", @@ -379,14 +380,20 @@ def edit_focus_options(self) -> typing.Sequence[str]: "set-cookies", "url", ] + elif isinstance(flow, dns.DNSFlow): + raise exceptions.CommandError( + "Cannot edit DNS flows yet, please submit a patch." + ) return focus_options @command.command("console.edit.focus") - @command.argument("flow_part", type=mitmproxy.types.Choice("console.edit.focus.options")) + @command.argument( + "flow_part", type=mitmproxy.types.Choice("console.edit.focus.options") + ) def edit_focus(self, flow_part: str) -> None: """ - Edit a component of the currently focused flow. + Edit a component of the currently focused flow. """ flow = self.master.view.focus.flow # This shouldn't be necessary once this command is "console.edit @focus", @@ -396,8 +403,8 @@ def edit_focus(self, flow_part: str) -> None: flow.backup() require_dummy_response = ( - flow_part in ("response-headers", "response-body", "set-cookies") and - flow.response is None + flow_part in ("response-headers", "response-body", "set-cookies") + and flow.response is None ) if require_dummy_response: flow.response = http.Response.make() @@ -438,8 +445,7 @@ def edit_focus(self, flow_part: str) -> None: flow.request.url = url.decode() elif flow_part in ["method", "status_code", "reason"]: self.master.commands.call_strings( - "console.command", - ["flow.set", "@focus", flow_part] + "console.command", ["flow.set", "@focus", flow_part] ) elif flow_part == "tcp-message": message = flow.messages[-1] @@ -455,47 +461,47 @@ def _grideditor(self): @command.command("console.grideditor.add") def grideditor_add(self) -> None: """ - Add a row after the cursor. + Add a row after the cursor. """ self._grideditor().cmd_add() @command.command("console.grideditor.insert") def grideditor_insert(self) -> None: """ - Insert a row before the cursor. + Insert a row before the cursor. """ self._grideditor().cmd_insert() @command.command("console.grideditor.delete") def grideditor_delete(self) -> None: """ - Delete row + Delete row """ self._grideditor().cmd_delete() @command.command("console.grideditor.load") def grideditor_load(self, path: mitmproxy.types.Path) -> None: """ - Read a file into the currrent cell. + Read a file into the currrent cell. """ self._grideditor().cmd_read_file(path) @command.command("console.grideditor.load_escaped") def grideditor_load_escaped(self, path: mitmproxy.types.Path) -> None: """ - Read a file containing a Python-style escaped string into the - currrent cell. + Read a file containing a Python-style escaped string into the + currrent cell. """ self._grideditor().cmd_read_file_escaped(path) @command.command("console.grideditor.save") def grideditor_save(self, path: mitmproxy.types.Path) -> None: """ - Save data to file as a CSV. + Save data to file as a CSV. """ rows = self._grideditor().value try: - with open(path, "w", newline='', encoding="utf8") as fp: + with open(path, "w", newline="", encoding="utf8") as fp: writer = csv.writer(fp) for row in rows: writer.writerow( @@ -508,15 +514,17 @@ def grideditor_save(self, path: mitmproxy.types.Path) -> None: @command.command("console.grideditor.editor") def grideditor_editor(self) -> None: """ - Spawn an external editor on the current cell. + Spawn an external editor on the current cell. """ self._grideditor().cmd_spawn_editor() @command.command("console.flowview.mode.set") - @command.argument("mode", type=mitmproxy.types.Choice("console.flowview.mode.options")) + @command.argument( + "mode", type=mitmproxy.types.Choice("console.flowview.mode.options") + ) def flowview_mode_set(self, mode: str) -> None: """ - Set the display mode for the current flow view. + Set the display mode for the current flow view. """ fv = self.master.window.current_window("flowview") if not fv: @@ -528,23 +536,22 @@ def flowview_mode_set(self, mode: str) -> None: try: self.master.commands.call_strings( - "view.settings.setval", - ["@focus", f"flowview_mode_{idx}", mode] + "view.settings.setval", ["@focus", f"flowview_mode_{idx}", mode] ) except exceptions.CommandError as e: ctx.log.error(str(e)) @command.command("console.flowview.mode.options") - def flowview_mode_options(self) -> typing.Sequence[str]: + def flowview_mode_options(self) -> Sequence[str]: """ - Returns the valid options for the flowview mode. + Returns the valid options for the flowview mode. """ return [i.name.lower() for i in contentviews.views] @command.command("console.flowview.mode") def flowview_mode(self) -> str: """ - Get the display mode for the current flow view. + Get the display mode for the current flow view. """ fv = self.master.window.current_window("flowview") if not fv: @@ -553,41 +560,40 @@ def flowview_mode(self) -> str: return self.master.commands.call_strings( "view.settings.getval", - ["@focus", f"flowview_mode_{idx}", self.master.options.console_default_contentview] + [ + "@focus", + f"flowview_mode_{idx}", + self.master.options.console_default_contentview, + ], ) @command.command("console.key.contexts") - def key_contexts(self) -> typing.Sequence[str]: + def key_contexts(self) -> Sequence[str]: """ - The available contexts for key binding. + The available contexts for key binding. """ return list(sorted(keymap.Contexts)) @command.command("console.key.bind") def key_bind( self, - contexts: typing.Sequence[str], + contexts: Sequence[str], key: str, cmd: mitmproxy.types.Cmd, - *args: mitmproxy.types.CmdArgs + *args: mitmproxy.types.CmdArgs, ) -> None: """ - Bind a shortcut key. + Bind a shortcut key. """ try: - self.master.keymap.add( - key, - cmd + " " + " ".join(args), - contexts, - "" - ) + self.master.keymap.add(key, cmd + " " + " ".join(args), contexts, "") except ValueError as v: raise exceptions.CommandError(v) @command.command("console.key.unbind") - def key_unbind(self, contexts: typing.Sequence[str], key: str) -> None: + def key_unbind(self, contexts: Sequence[str], key: str) -> None: """ - Un-bind a shortcut key. + Un-bind a shortcut key. """ try: self.master.keymap.remove(key, contexts) @@ -606,7 +612,7 @@ def _keyfocus(self): @command.command("console.key.unbind.focus") def key_unbind_focus(self) -> None: """ - Un-bind the shortcut key currently focused in the key binding viewer. + Un-bind the shortcut key currently focused in the key binding viewer. """ b = self._keyfocus() try: @@ -617,7 +623,7 @@ def key_unbind_focus(self) -> None: @command.command("console.key.execute.focus") def key_execute_focus(self) -> None: """ - Execute the currently focused key binding. + Execute the currently focused key binding. """ b = self._keyfocus() self.console_command(b.command) @@ -625,7 +631,7 @@ def key_execute_focus(self) -> None: @command.command("console.key.edit.focus") def key_edit_focus(self) -> None: """ - Execute the currently focused key binding. + Execute the currently focused key binding. """ b = self._keyfocus() self.console_command( diff --git a/mitmproxy/tools/console/defaultkeys.py b/mitmproxy/tools/console/defaultkeys.py index 6fcb5f5de4..e20a967605 100644 --- a/mitmproxy/tools/console/defaultkeys.py +++ b/mitmproxy/tools/console/defaultkeys.py @@ -1,6 +1,11 @@ def map(km): km.add(":", "console.command ", ["commonkey", "global"], "Command prompt") - km.add(";", "console.command flow.comment @focus ''", ["flowlist", "flowview"], "Add comment to flow") + km.add( + ";", + "console.command flow.comment @focus ''", + ["flowlist", "flowview"], + "Add comment to flow", + ) km.add("?", "console.view.help", ["global"], "View help") km.add("B", "browser.start", ["global"], "Start an attached browser") km.add("C", "console.view.commands", ["global"], "View commands") @@ -28,18 +33,41 @@ def map(km): km.add("ctrl f", "console.nav.pagedown", ["global"], "Page down") km.add("ctrl b", "console.nav.pageup", ["global"], "Page up") - km.add("I", "set intercept_active toggle", ["global"], "Toggle whether the filtering via the intercept option is enabled") + km.add( + "I", + "set intercept_active toggle", + ["global"], + "Toggle whether the filtering via the intercept option is enabled", + ) km.add("i", "console.command.set intercept", ["global"], "Set intercept") km.add("W", "console.command.set save_stream_file", ["global"], "Stream to file") - km.add("A", "flow.resume @all", ["flowlist", "flowview"], "Resume all intercepted flows") - km.add("a", "flow.resume @focus", ["flowlist", "flowview"], "Resume this intercepted flow") km.add( - "b", "console.command cut.save @focus response.content ", + "A", + "flow.resume @all", + ["flowlist", "flowview"], + "Resume all intercepted flows", + ) + km.add( + "a", + "flow.resume @focus", + ["flowlist", "flowview"], + "Resume this intercepted flow", + ) + km.add( + "b", + "console.command cut.save @focus response.content ", ["flowlist", "flowview"], - "Save response body to file" + "Save response body to file", + ) + km.add( + "d", + "view.flows.remove @focus", + ["flowlist", "flowview"], + "Delete flow from view", + ) + km.add( + "D", "view.flows.duplicate @focus", ["flowlist", "flowview"], "Duplicate flow" ) - km.add("d", "view.flows.remove @focus", ["flowlist", "flowview"], "Delete flow from view") - km.add("D", "view.flows.duplicate @focus", ["flowlist", "flowview"], "Duplicate flow") km.add( "e", """ @@ -47,7 +75,7 @@ def map(km): console.command export.file {choice} @focus """, ["flowlist", "flowview"], - "Export this flow to file" + "Export this flow to file", ) km.add("f", "console.command.set view_filter", ["flowlist"], "Set view filter") km.add("F", "set console_focus_follow toggle", ["flowlist"], "Set focus follow") @@ -55,16 +83,23 @@ def map(km): "ctrl l", "console.command cut.clip ", ["flowlist", "flowview"], - "Send cuts to clipboard" + "Send cuts to clipboard", + ) + km.add( + "L", "console.command view.flows.load ", ["flowlist"], "Load flows from file" ) - km.add("L", "console.command view.flows.load ", ["flowlist"], "Load flows from file") km.add("m", "flow.mark.toggle @focus", ["flowlist"], "Toggle mark on this flow") - km.add("M", "view.properties.marked.toggle", ["flowlist"], "Toggle viewing marked flows") + km.add( + "M", + "view.properties.marked.toggle", + ["flowlist"], + "Toggle viewing marked flows", + ) km.add( "n", "console.command view.flows.create get https://example.com/", ["flowlist"], - "Create a new flow" + "Create a new flow", ) km.add( "o", @@ -73,22 +108,36 @@ def map(km): set view_order {choice} """, ["flowlist"], - "Set flow list order" + "Set flow list order", ) km.add("r", "replay.client @focus", ["flowlist", "flowview"], "Replay this flow") km.add("S", "console.command replay.server ", ["flowlist"], "Start server replay") - km.add("v", "set view_order_reversed toggle", ["flowlist"], "Reverse flow list order") + km.add( + "v", "set view_order_reversed toggle", ["flowlist"], "Reverse flow list order" + ) km.add("U", "flow.mark @all false", ["flowlist"], "Un-set all marks") - km.add("w", "console.command save.file @shown ", ["flowlist"], "Save listed flows to file") - km.add("V", "flow.revert @focus", ["flowlist", "flowview"], "Revert changes to this flow") + km.add( + "w", + "console.command save.file @shown ", + ["flowlist"], + "Save listed flows to file", + ) + km.add( + "V", + "flow.revert @focus", + ["flowlist", "flowview"], + "Revert changes to this flow", + ) km.add("X", "flow.kill @focus", ["flowlist"], "Kill this flow") km.add("z", "view.flows.remove @all", ["flowlist"], "Clear flow list") - km.add("Z", "view.flows.remove @hidden", ["flowlist"], "Purge all flows not showing") + km.add( + "Z", "view.flows.remove @hidden", ["flowlist"], "Purge all flows not showing" + ) km.add( "|", "console.command script.run @focus ", ["flowlist", "flowview"], - "Run a script on this flow" + "Run a script on this flow", ) km.add( @@ -98,7 +147,7 @@ def map(km): console.edit.focus {choice} """, ["flowview"], - "Edit a flow component" + "Edit a flow component", ) km.add( "f", @@ -116,7 +165,7 @@ def map(km): console.bodyview @focus {choice} """, ["flowview"], - "View flow body in an external viewer" + "View flow body in an external viewer", ) km.add("p", "view.focus.prev", ["flowview"], "Go to previous flow") km.add( @@ -126,7 +175,7 @@ def map(km): console.flowview.mode.set {choice} """, ["flowview"], - "Set flow view mode" + "Set flow view mode", ) km.add( "z", @@ -135,7 +184,7 @@ def map(km): flow.encode.toggle @focus {choice} """, ["flowview"], - "Encode/decode flow body" + "Encode/decode flow body", ) km.add("L", "console.command options.load ", ["options"], "Load from file") @@ -144,26 +193,28 @@ def map(km): km.add("d", "console.options.reset.focus", ["options"], "Reset this option") km.add("a", "console.grideditor.add", ["grideditor"], "Add a row after cursor") - km.add("A", "console.grideditor.insert", ["grideditor"], "Insert a row before cursor") + km.add( + "A", "console.grideditor.insert", ["grideditor"], "Insert a row before cursor" + ) km.add("d", "console.grideditor.delete", ["grideditor"], "Delete this row") km.add( "r", "console.command console.grideditor.load", ["grideditor"], - "Read unescaped data into the current cell from file" + "Read unescaped data into the current cell from file", ) km.add( "R", "console.command console.grideditor.load_escaped", ["grideditor"], - "Load a Python-style escaped string into the current cell from file" + "Load a Python-style escaped string into the current cell from file", ) km.add("e", "console.grideditor.editor", ["grideditor"], "Edit in external editor") km.add( "w", "console.command console.grideditor.save ", ["grideditor"], - "Save data to file as CSV" + "Save data to file as CSV", ) km.add("z", "eventstore.clear", ["eventlog"], "Clear") @@ -175,23 +226,23 @@ def map(km): console.command console.key.bind {choice} """, ["keybindings"], - "Add a key binding" + "Add a key binding", ) km.add( "d", "console.key.unbind.focus", ["keybindings"], - "Unbind the currently focused key binding" + "Unbind the currently focused key binding", ) km.add( "x", "console.key.execute.focus", ["keybindings"], - "Execute the currently focused key binding" + "Execute the currently focused key binding", ) km.add( "enter", "console.key.edit.focus", ["keybindings"], - "Edit the currently focused key binding" + "Edit the currently focused key binding", ) diff --git a/mitmproxy/tools/console/eventlog.py b/mitmproxy/tools/console/eventlog.py index 9063066aeb..246de37c88 100644 --- a/mitmproxy/tools/console/eventlog.py +++ b/mitmproxy/tools/console/eventlog.py @@ -15,21 +15,20 @@ class EventLog(urwid.ListBox, layoutwidget.LayoutWidget): def __init__(self, master): self.master = master - self.walker = LogBufferWalker( - collections.deque(maxlen=self.master.events.size) - ) + self.walker = LogBufferWalker(collections.deque(maxlen=self.master.events.size)) master.events.sig_add.connect(self.add_event) master.events.sig_refresh.connect(self.refresh_events) - self.master.options.subscribe(self.refresh_events, ["console_eventlog_verbosity"]) + self.master.options.subscribe( + self.refresh_events, ["console_eventlog_verbosity"] + ) self.refresh_events() super().__init__(self.walker) def load(self, loader): loader.add_option( - "console_focus_follow", bool, False, - "Focus follows new flows." + "console_focus_follow", bool, False, "Focus follows new flows." ) def set_focus(self, index): @@ -44,9 +43,11 @@ def keypress(self, size, key): return super().keypress(size, key) def add_event(self, event_store, entry: log.LogEntry): - if log.log_tier(self.master.options.console_eventlog_verbosity) < log.log_tier(entry.level): + if log.log_tier(self.master.options.console_eventlog_verbosity) < log.log_tier( + entry.level + ): return - txt = "{}: {}".format(entry.level, str(entry.msg)) + txt = f"{entry.level}: {str(entry.msg)}" if entry.level in ("error", "warn", "alert"): e = urwid.Text((entry.level, txt)) else: diff --git a/mitmproxy/tools/console/flowdetailview.py b/mitmproxy/tools/console/flowdetailview.py index d6990fe50f..56161d50ce 100644 --- a/mitmproxy/tools/console/flowdetailview.py +++ b/mitmproxy/tools/console/flowdetailview.py @@ -1,4 +1,5 @@ -import typing +from typing import Optional + import urwid import mitmproxy.flow @@ -24,8 +25,8 @@ def flowdetails(state, flow: mitmproxy.flow.Flow): sc = flow.server_conn cc = flow.client_conn - req: typing.Optional[http.Request] - resp: typing.Optional[http.Response] + req: Optional[http.Request] + resp: Optional[http.Response] if isinstance(flow, http.HTTPFlow): req = flow.request resp = flow.response @@ -55,28 +56,32 @@ def flowdetails(state, flow: mitmproxy.flow.Flow): if sc.alpn: parts.append(("ALPN", strutils.bytes_to_escaped_str(sc.alpn))) - text.extend( - common.format_keyvals(parts, indent=4) - ) + text.extend(common.format_keyvals(parts, indent=4)) if sc.certificate_list: c = sc.certificate_list[0] text.append(urwid.Text([("head", "Server Certificate:")])) parts = [ ("Type", "%s, %s bits" % c.keyinfo), - ("SHA256 digest", c.fingerprint().hex(' ')), + ("SHA256 digest", c.fingerprint().hex(" ")), ("Valid from", str(c.notbefore)), ("Valid to", str(c.notafter)), ("Serial", str(c.serial)), - ("Subject", urwid.Pile(common.format_keyvals(c.subject, key_format="highlight"))), - ("Issuer", urwid.Pile(common.format_keyvals(c.issuer, key_format="highlight"))) + ( + "Subject", + urwid.Pile( + common.format_keyvals(c.subject, key_format="highlight") + ), + ), + ( + "Issuer", + urwid.Pile(common.format_keyvals(c.issuer, key_format="highlight")), + ), ] if c.altnames: parts.append(("Alt names", ", ".join(c.altnames))) - text.extend( - common.format_keyvals(parts, indent=4) - ) + text.extend(common.format_keyvals(parts, indent=4)) if cc is not None: text.append(urwid.Text([("head", "Client Connection:")])) @@ -95,87 +100,44 @@ def flowdetails(state, flow: mitmproxy.flow.Flow): if cc.alpn: parts.append(("ALPN", strutils.bytes_to_escaped_str(cc.alpn))) - text.extend( - common.format_keyvals(parts, indent=4) - ) + text.extend(common.format_keyvals(parts, indent=4)) parts = [] if cc is not None and cc.timestamp_start: parts.append( - ( - "Client conn. established", - maybe_timestamp(cc, "timestamp_start") - ) + ("Client conn. established", maybe_timestamp(cc, "timestamp_start")) ) if cc.tls_established: parts.append( ( "Client conn. TLS handshake", - maybe_timestamp(cc, "timestamp_tls_setup") + maybe_timestamp(cc, "timestamp_tls_setup"), ) ) - parts.append( - ( - "Client conn. closed", - maybe_timestamp(cc, "timestamp_end") - ) - ) + parts.append(("Client conn. closed", maybe_timestamp(cc, "timestamp_end"))) if sc is not None and sc.timestamp_start: + parts.append(("Server conn. initiated", maybe_timestamp(sc, "timestamp_start"))) parts.append( - ( - "Server conn. initiated", - maybe_timestamp(sc, "timestamp_start") - ) - ) - parts.append( - ( - "Server conn. TCP handshake", - maybe_timestamp(sc, "timestamp_tcp_setup") - ) + ("Server conn. TCP handshake", maybe_timestamp(sc, "timestamp_tcp_setup")) ) if sc.tls_established: parts.append( ( "Server conn. TLS handshake", - maybe_timestamp(sc, "timestamp_tls_setup") + maybe_timestamp(sc, "timestamp_tls_setup"), ) ) - parts.append( - ( - "Server conn. closed", - maybe_timestamp(sc, "timestamp_end") - ) - ) + parts.append(("Server conn. closed", maybe_timestamp(sc, "timestamp_end"))) if req is not None and req.timestamp_start: - parts.append( - ( - "First request byte", - maybe_timestamp(req, "timestamp_start") - ) - ) - parts.append( - ( - "Request complete", - maybe_timestamp(req, "timestamp_end") - ) - ) + parts.append(("First request byte", maybe_timestamp(req, "timestamp_start"))) + parts.append(("Request complete", maybe_timestamp(req, "timestamp_end"))) if resp is not None and resp.timestamp_start: - parts.append( - ( - "First response byte", - maybe_timestamp(resp, "timestamp_start") - ) - ) - parts.append( - ( - "Response complete", - maybe_timestamp(resp, "timestamp_end") - ) - ) + parts.append(("First response byte", maybe_timestamp(resp, "timestamp_start"))) + parts.append(("Response complete", maybe_timestamp(resp, "timestamp_end"))) if parts: # sort operations by timestamp diff --git a/mitmproxy/tools/console/flowlist.py b/mitmproxy/tools/console/flowlist.py index b21a16b3b8..8c72a0a3bc 100644 --- a/mitmproxy/tools/console/flowlist.py +++ b/mitmproxy/tools/console/flowlist.py @@ -1,12 +1,14 @@ +from functools import lru_cache +from typing import Optional + import urwid +import mitmproxy.tools.console.master from mitmproxy.tools.console import common from mitmproxy.tools.console import layoutwidget -import mitmproxy.tools.console.master # noqa class FlowItem(urwid.WidgetWrap): - def __init__(self, master, flow): self.master, self.flow = master, flow w = self.get_text() @@ -15,7 +17,7 @@ def __init__(self, master, flow): def get_text(self): cols, _ = self.master.ui.get_cols_rows() layout = self.master.options.console_flowlist_layout - if layout == "list" or (layout == 'default' and cols < 100): + if layout == "list" or (layout == "default" and cols < 100): render_mode = common.RenderMode.LIST else: render_mode = common.RenderMode.TABLE @@ -40,6 +42,7 @@ def keypress(self, size, key): class FlowListWalker(urwid.ListWalker): + master: "mitmproxy.tools.console.master.ConsoleMaster" def __init__(self, master): self.master = master @@ -47,13 +50,14 @@ def __init__(self, master): def positions(self, reverse=False): # The stub implementation of positions can go once this issue is resolved: # https://github.com/urwid/urwid/issues/294 - ret = range(self.master.commands.execute("view.properties.length")) + ret = range(self.master.view.get_length()) if reverse: return reversed(ret) return ret def view_changed(self): self._modified() + self._get.cache_clear() def get_focus(self): if not self.master.view.focus.flow: @@ -65,33 +69,28 @@ def set_focus(self, index): if self.master.commands.execute("view.properties.inbounds %d" % index): self.master.view.focus.index = index - def get_next(self, pos): - pos = pos + 1 - if not self.master.commands.execute("view.properties.inbounds %d" % pos): + @lru_cache(maxsize=None) + def _get(self, pos: int) -> tuple[Optional[FlowItem], Optional[int]]: + if not self.master.view.inbounds(pos): return None, None - f = FlowItem(self.master, self.master.view[pos]) - return f, pos + return FlowItem(self.master, self.master.view[pos]), pos + + def get_next(self, pos): + return self._get(pos + 1) def get_prev(self, pos): - pos = pos - 1 - if not self.master.commands.execute("view.properties.inbounds %d" % pos): - return None, None - f = FlowItem(self.master, self.master.view[pos]) - return f, pos + return self._get(pos - 1) class FlowListBox(urwid.ListBox, layoutwidget.LayoutWidget): title = "Flows" keyctx = "flowlist" - def __init__( - self, master: "mitmproxy.tools.console.master.ConsoleMaster" - ) -> None: + def __init__(self, master: "mitmproxy.tools.console.master.ConsoleMaster") -> None: self.master: "mitmproxy.tools.console.master.ConsoleMaster" = master super().__init__(FlowListWalker(master)) self.master.options.subscribe( - self.set_flowlist_layout, - ["console_flowlist_layout"] + self.set_flowlist_layout, ["console_flowlist_layout"] ) def keypress(self, size, key): diff --git a/mitmproxy/tools/console/flowview.py b/mitmproxy/tools/console/flowview.py index 93c9703a8e..96701f5026 100644 --- a/mitmproxy/tools/console/flowview.py +++ b/mitmproxy/tools/console/flowview.py @@ -1,13 +1,15 @@ import math import sys from functools import lru_cache -from typing import Optional, Union # noqa +from typing import Optional -import mitmproxy.flow -import mitmproxy.tools.console.master # noqa import urwid + +import mitmproxy.flow +import mitmproxy.tools.console.master from mitmproxy import contentviews from mitmproxy import ctx +from mitmproxy import dns from mitmproxy import http from mitmproxy import tcp from mitmproxy.tools.console import common @@ -23,7 +25,6 @@ class SearchError(Exception): class FlowViewHeader(urwid.WidgetWrap): - def __init__( self, master: "mitmproxy.tools.console.master.ConsoleMaster", @@ -49,7 +50,8 @@ def __init__(self, master): super().__init__([]) self.show() self.last_displayed_body = None - contentviews.on_add.connect(self.contentview_added) + contentviews.on_add.connect(self.contentview_changed) + contentviews.on_remove.connect(self.contentview_changed) @property def view(self): @@ -59,11 +61,12 @@ def view(self): def flow(self) -> mitmproxy.flow.Flow: return self.master.view.focus.flow - def contentview_added(self, view): + def contentview_changed(self, view): # this is called when a contentview addon is live-reloaded. # we clear our cache and then rerender self._get_content_view.cache_clear() - self.show() + if self.master.window.current_window("flowview"): + self.show() def focus_changed(self): f = self.flow @@ -87,6 +90,12 @@ def focus_changed(self): (self.tab_tcp_stream, self.view_tcp_stream), (self.tab_details, self.view_details), ] + elif isinstance(f, dns.DNSFlow): + self.tabs = [ + (self.tab_dns_request, self.view_dns_request), + (self.tab_dns_response, self.view_dns_response), + (self.tab_details, self.view_details), + ] self.show() else: self.master.window.pop() @@ -107,6 +116,22 @@ def tab_http_response(self): else: return "Response" + def tab_dns_request(self) -> str: + flow = self.flow + assert isinstance(flow, dns.DNSFlow) + if self.flow.intercepted and not flow.response: + return "Request intercepted" + else: + return "Request" + + def tab_dns_response(self) -> str: + flow = self.flow + assert isinstance(flow, dns.DNSFlow) + if self.flow.intercepted and flow.response: + return "Response intercepted" + else: + return "Response" + def tab_tcp_stream(self): return "TCP Stream" @@ -126,6 +151,16 @@ def view_response(self): assert isinstance(flow, http.HTTPFlow) return self.conn_text(flow.response) + def view_dns_request(self): + flow = self.flow + assert isinstance(flow, dns.DNSFlow) + return self.dns_message_text("request", flow.request) + + def view_dns_response(self): + flow = self.flow + assert isinstance(flow, dns.DNSFlow) + return self.dns_message_text("response", flow.response) + def _contentview_status_bar(self, description: str, viewmode: str): cols = [ urwid.Text( @@ -136,12 +171,12 @@ def _contentview_status_bar(self, description: str, viewmode: str): urwid.Text( [ " ", - ('heading', "["), - ('heading_key', "m"), - ('heading', (":%s]" % viewmode)), + ("heading", "["), + ("heading_key", "m"), + ("heading", (":%s]" % viewmode)), ], - align="right" - ) + align="right", + ), ] contentview_status_bar = urwid.AttrWrap(urwid.Columns(cols), "heading") return contentview_status_bar @@ -172,17 +207,31 @@ def view_websocket_messages(self): widget_lines.append(urwid.Text(line)) if flow.websocket.closed_by_client is not None: - widget_lines.append(urwid.Text([ - (self.FROM_CLIENT_MARKER if flow.websocket.closed_by_client else self.TO_CLIENT_MARKER), - ("alert" if flow.websocket.close_code in (1000, 1001, 1005) else "error", - f"Connection closed: {flow.websocket.close_code} {flow.websocket.close_reason}") - ])) + widget_lines.append( + urwid.Text( + [ + ( + self.FROM_CLIENT_MARKER + if flow.websocket.closed_by_client + else self.TO_CLIENT_MARKER + ), + ( + "alert" + if flow.websocket.close_code in (1000, 1001, 1005) + else "error", + f"Connection closed: {flow.websocket.close_code} {flow.websocket.close_reason}", + ), + ] + ) + ) if flow.intercepted: markup = widget_lines[-1].get_text()[0] widget_lines[-1].set_text(("intercept", markup)) - widget_lines.insert(0, self._contentview_status_bar(viewmode.capitalize(), viewmode)) + widget_lines.insert( + 0, self._contentview_status_bar(viewmode.capitalize(), viewmode) + ) return searchable.Searchable(widget_lines) @@ -226,7 +275,9 @@ def view_tcp_stream(self) -> urwid.Widget: markup = widget_lines[-1].get_text()[0] widget_lines[-1].set_text(("intercept", markup)) - widget_lines.insert(0, self._contentview_status_bar(viewmode.capitalize(), viewmode)) + widget_lines.insert( + 0, self._contentview_status_bar(viewmode.capitalize(), viewmode) + ) return searchable.Searchable(widget_lines) @@ -238,20 +289,26 @@ def content_view(self, viewmode, message): msg, body = "", [urwid.Text([("error", "[content missing]")])] return msg, body else: - full = self.master.commands.execute("view.settings.getval @focus fullcontents false") + full = self.master.commands.execute( + "view.settings.getval @focus fullcontents false" + ) if full == "true": limit = sys.maxsize else: limit = ctx.options.content_view_lines_cutoff - flow_modify_cache_invalidation = hash(( - message.raw_content, - message.headers.fields, - getattr(message, "path", None), - )) + flow_modify_cache_invalidation = hash( + ( + message.raw_content, + message.headers.fields, + getattr(message, "path", None), + ) + ) # we need to pass the message off-band because it's not hashable self._get_content_view_message = message - return self._get_content_view(viewmode, limit, flow_modify_cache_invalidation) + return self._get_content_view( + viewmode, limit, flow_modify_cache_invalidation + ) @lru_cache(maxsize=200) def _get_content_view(self, viewmode, max_lines, _): @@ -275,7 +332,7 @@ def _get_content_view(self, viewmode, max_lines, _): txt = [] for (style, text) in line: if total_chars + len(text) > max_chars: - text = text[:max_chars - total_chars] + text = text[: max_chars - total_chars] txt.append((style, text)) total_chars += len(text) if total_chars == max_chars: @@ -286,11 +343,19 @@ def _get_content_view(self, viewmode, max_lines, _): text_objects.append(urwid.Text(txt)) if total_chars == max_chars: - text_objects.append(urwid.Text([ - ("highlight", "Stopped displaying data after %d lines. Press " % max_lines), - ("key", "f"), - ("highlight", " to load all data.") - ])) + text_objects.append( + urwid.Text( + [ + ( + "highlight", + "Stopped displaying data after %d lines. Press " + % max_lines, + ), + ("key", "f"), + ("highlight", " to load all data."), + ] + ) + ) break return description, text_objects @@ -319,10 +384,7 @@ def conn_text(self, conn): k = strutils.bytes_to_escaped_str(k) + ":" v = strutils.bytes_to_escaped_str(v) hdrs.append((k, v)) - txt = common.format_keyvals( - hdrs, - key_format="header" - ) + txt = common.format_keyvals(hdrs, key_format="header") viewmode = self.master.commands.call("console.flowview.mode") msg, body = self.content_view(viewmode, conn) @@ -335,12 +397,12 @@ def conn_text(self, conn): urwid.Text( [ " ", - ('heading', "["), - ('heading_key', "m"), - ('heading', (":%s]" % viewmode)), + ("heading", "["), + ("heading_key", "m"), + ("heading", (":%s]" % viewmode)), ], - align="right" - ) + align="right", + ), ] title = urwid.AttrWrap(urwid.Columns(cols), "heading") @@ -355,10 +417,57 @@ def conn_text(self, conn): ("key", "e"), ("highlight", " and edit any aspect to add one."), ] - ) + ), ] return searchable.Searchable(txt) + def dns_message_text( + self, type: str, message: Optional[dns.Message] + ) -> searchable.Searchable: + # Keep in sync with web/src/js/components/FlowView/DnsMessages.tsx + if message: + + def rr_text(rr: dns.ResourceRecord): + return urwid.Text( + f" {rr.name} {dns.types.to_str(rr.type)} {dns.classes.to_str(rr.class_)} {rr.ttl} {str(rr)}" + ) + + txt = [] + txt.append( + urwid.Text( + "{recursive}Question".format( + recursive="Recursive " if message.recursion_desired else "", + ) + ) + ) + txt.extend( + urwid.Text( + f" {q.name} {dns.types.to_str(q.type)} {dns.classes.to_str(q.class_)}" + ) + for q in message.questions + ) + txt.append(urwid.Text("")) + txt.append( + urwid.Text( + "{authoritative}{recursive}Answer".format( + authoritative="Authoritative " + if message.authoritative_answer + else "", + recursive="Recursive " if message.recursion_available else "", + ) + ) + ) + txt.extend(map(rr_text, message.answers)) + txt.append(urwid.Text("")) + txt.append(urwid.Text("Authority")) + txt.extend(map(rr_text, message.authorities)) + txt.append(urwid.Text("")) + txt.append(urwid.Text("Addition")) + txt.extend(map(rr_text, message.additionals)) + return searchable.Searchable(txt) + else: + return searchable.Searchable([urwid.Text(("highlight", f"No {type}."))]) + class FlowView(urwid.Frame, layoutwidget.LayoutWidget): keyctx = "flowview" diff --git a/mitmproxy/tools/console/grideditor/__init__.py b/mitmproxy/tools/console/grideditor/__init__.py index 894f3d22c1..c13ea70e37 100644 --- a/mitmproxy/tools/console/grideditor/__init__.py +++ b/mitmproxy/tools/console/grideditor/__init__.py @@ -1,2 +1,29 @@ -from .editors import * # noqa -from . import base # noqa +from . import base +from .editors import ( + CookieAttributeEditor, + CookieEditor, + DataViewer, + OptionsEditor, + PathEditor, + QueryEditor, + RequestHeaderEditor, + RequestMultipartEditor, + RequestUrlEncodedEditor, + ResponseHeaderEditor, + SetCookieEditor, +) + +__all__ = [ + "base", + "QueryEditor", + "RequestHeaderEditor", + "ResponseHeaderEditor", + "RequestMultipartEditor", + "RequestUrlEncodedEditor", + "PathEditor", + "CookieEditor", + "CookieAttributeEditor", + "SetCookieEditor", + "OptionsEditor", + "DataViewer", +] diff --git a/mitmproxy/tools/console/grideditor/base.py b/mitmproxy/tools/console/grideditor/base.py index d6991877a8..c4fdb5a617 100644 --- a/mitmproxy/tools/console/grideditor/base.py +++ b/mitmproxy/tools/console/grideditor/base.py @@ -1,17 +1,19 @@ import abc import copy import os -import typing +from collections.abc import Callable, Container, Iterable, Sequence +from typing import Any, AnyStr, Optional + import urwid from mitmproxy.utils import strutils from mitmproxy import exceptions from mitmproxy.tools.console import signals from mitmproxy.tools.console import layoutwidget -import mitmproxy.tools.console.master # noqa +import mitmproxy.tools.console.master -def read_file(filename: str, escaped: bool) -> typing.AnyStr: +def read_file(filename: str, escaped: bool) -> AnyStr: filename = os.path.expanduser(filename) try: with open(filename, "r" if escaped else "rb") as f: @@ -53,28 +55,27 @@ def Edit(self, data) -> Cell: pass @abc.abstractmethod - def blank(self) -> typing.Any: + def blank(self) -> Any: pass - def keypress(self, key: str, editor: "GridEditor") -> typing.Optional[str]: + def keypress(self, key: str, editor: "GridEditor") -> Optional[str]: return key class GridRow(urwid.WidgetWrap): - def __init__( - self, - focused: typing.Optional[int], - editing: bool, - editor: "GridEditor", - values: typing.Tuple[typing.Iterable[bytes], typing.Container[int]] + self, + focused: Optional[int], + editing: bool, + editor: "GridEditor", + values: tuple[Iterable[bytes], Container[int]], ) -> None: self.focused = focused self.editor = editor - self.edit_col: typing.Optional[Cell] = None + self.edit_col: Optional[Cell] = None errors = values[1] - self.fields: typing.Sequence[typing.Any] = [] + self.fields: Sequence[Any] = [] for i, v in enumerate(values[0]): if focused == i and editing: self.edit_col = self.editor.columns[i].Edit(v) @@ -93,10 +94,7 @@ def __init__( fspecs = self.fields[:] if len(self.fields) > 1: fspecs[0] = ("fixed", self.editor.first_width + 2, fspecs[0]) - w = urwid.Columns( - fspecs, - dividechars=2 - ) + w = urwid.Columns(fspecs, dividechars=2) if focused is not None: w.set_focus_column(focused) super().__init__(w) @@ -113,30 +111,24 @@ def selectable(self): class GridWalker(urwid.ListWalker): """ - Stores rows as a list of (rows, errors) tuples, where rows is a list - and errors is a set with an entry of each offset in rows that is an - error. + Stores rows as a list of (rows, errors) tuples, where rows is a list + and errors is a set with an entry of each offset in rows that is an + error. """ - def __init__( - self, - lst: typing.Iterable[list], - editor: "GridEditor" - ) -> None: - self.lst: typing.Sequence[typing.Tuple[typing.Any, typing.Set]] = [(i, set()) for i in lst] + def __init__(self, lst: Iterable[list], editor: "GridEditor") -> None: + self.lst: Sequence[tuple[Any, set]] = [(i, set()) for i in lst] self.editor = editor self.focus = 0 self.focus_col = 0 - self.edit_row: typing.Optional[GridRow] = None + self.edit_row: Optional[GridRow] = None def _modified(self): self.editor.show_empty_msg() return super()._modified() def add_value(self, lst): - self.lst.append( - (lst[:], set()) - ) + self.lst.append((lst[:], set())) self._modified() def get_current_value(self): @@ -169,10 +161,7 @@ def delete_focus(self): def _insert(self, pos): self.focus = pos - self.lst.insert( - self.focus, - ([c.blank() for c in self.editor.columns], set()) - ) + self.lst.insert(self.focus, ([c.blank() for c in self.editor.columns], set())) self.focus_col = 0 self.start_edit() @@ -220,12 +209,10 @@ def get_focus(self): if self.edit_row: return self.edit_row, self.focus elif self.lst: - return GridRow( - self.focus_col, - False, - self.editor, - self.lst[self.focus] - ), self.focus + return ( + GridRow(self.focus_col, False, self.editor, self.lst[self.focus]), + self.focus, + ) else: return None, None @@ -258,14 +245,14 @@ class BaseGridEditor(urwid.WidgetWrap): keyctx = "grideditor" def __init__( - self, - master: "mitmproxy.tools.console.master.ConsoleMaster", - title, - columns, - value: typing.Any, - callback: typing.Callable[..., None], - *cb_args, - **cb_kwargs + self, + master: "mitmproxy.tools.console.master.ConsoleMaster", + title, + columns, + value: Any, + callback: Callable[..., None], + *cb_args, + **cb_kwargs ) -> None: value = self.data_in(copy.deepcopy(value)) self.master = master @@ -292,10 +279,7 @@ def __init__( headings.append(("fixed", first_width + 2, c)) else: headings.append(c) - h = urwid.Columns( - headings, - dividechars=2 - ) + h = urwid.Columns(headings, dividechars=2) h = urwid.AttrWrap(h, "heading") self.walker = GridWalker(self.value, self) @@ -356,22 +340,22 @@ def keypress(self, size, key): elif column.keypress(key, self) and not self.handle_key(key): return self._w.keypress(size, key) - def data_out(self, data: typing.Sequence[list]) -> typing.Any: + def data_out(self, data: Sequence[list]) -> Any: """ - Called on raw list data, before data is returned through the - callback. + Called on raw list data, before data is returned through the + callback. """ return data - def data_in(self, data: typing.Any) -> typing.Iterable[list]: + def data_in(self, data: Any) -> Iterable[list]: """ - Called to prepare provided data. + Called to prepare provided data. """ return data - def is_error(self, col: int, val: typing.Any) -> typing.Optional[str]: + def is_error(self, col: int, val: Any) -> Optional[str]: """ - Return None, or a string error message. + Return None, or a string error message. """ return None @@ -403,32 +387,27 @@ def cmd_spawn_editor(self): class GridEditor(BaseGridEditor): title = "" - columns: typing.Sequence[Column] = () + columns: Sequence[Column] = () keyctx = "grideditor" def __init__( - self, - master: "mitmproxy.tools.console.master.ConsoleMaster", - value: typing.Any, - callback: typing.Callable[..., None], - *cb_args, - **cb_kwargs + self, + master: "mitmproxy.tools.console.master.ConsoleMaster", + value: Any, + callback: Callable[..., None], + *cb_args, + **cb_kwargs ) -> None: super().__init__( - master, - self.title, - self.columns, - value, - callback, - *cb_args, - **cb_kwargs + master, self.title, self.columns, value, callback, *cb_args, **cb_kwargs ) class FocusEditor(urwid.WidgetWrap, layoutwidget.LayoutWidget): """ - A specialised GridEditor that edits the current focused flow. + A specialised GridEditor that edits the current focused flow. """ + keyctx = "grideditor" def __init__(self, master): @@ -441,19 +420,19 @@ def call(self, v, name, *args, **kwargs): def get_data(self, flow): """ - Retrieve the data to edit from the current flow. + Retrieve the data to edit from the current flow. """ raise NotImplementedError def set_data(self, vals, flow): """ - Set the current data on the flow. + Set the current data on the flow. """ raise NotImplementedError def set_data_update(self, vals, flow): self.set_data(vals, flow) - signals.flow_change.send(self, flow = flow) + signals.flow_change.send(self, flow=flow) def key_responder(self): return self._w diff --git a/mitmproxy/tools/console/grideditor/col_bytes.py b/mitmproxy/tools/console/grideditor/col_bytes.py index e27a4474a8..02f1f955b0 100644 --- a/mitmproxy/tools/console/grideditor/col_bytes.py +++ b/mitmproxy/tools/console/grideditor/col_bytes.py @@ -44,9 +44,5 @@ def get_data(self) -> bytes: try: return strutils.escaped_str_to_bytes(txt) except ValueError: - signals.status_message.send( - self, - message="Invalid data.", - expire=1000 - ) + signals.status_message.send(self, message="Invalid data.", expire=1000) raise diff --git a/mitmproxy/tools/console/grideditor/col_subgrid.py b/mitmproxy/tools/console/grideditor/col_subgrid.py index c9cbf66dd1..02465647cd 100644 --- a/mitmproxy/tools/console/grideditor/col_subgrid.py +++ b/mitmproxy/tools/console/grideditor/col_subgrid.py @@ -21,9 +21,7 @@ def blank(self): def keypress(self, key, editor): if key in "rRe": signals.status_message.send( - self, - message="Press enter to edit this field.", - expire=1000 + self, message="Press enter to edit this field.", expire=1000 ) return elif key == "m_select": diff --git a/mitmproxy/tools/console/grideditor/col_text.py b/mitmproxy/tools/console/grideditor/col_text.py index 576c33acce..f4ff5ee2ad 100644 --- a/mitmproxy/tools/console/grideditor/col_text.py +++ b/mitmproxy/tools/console/grideditor/col_text.py @@ -35,11 +35,7 @@ def get_data(self): try: return data.decode(*self.encoding_args) except ValueError: - signals.status_message.send( - self, - message="Invalid encoding.", - expire=1000 - ) + signals.status_message.send(self, message="Invalid encoding.", expire=1000) raise diff --git a/mitmproxy/tools/console/grideditor/col_viewany.py b/mitmproxy/tools/console/grideditor/col_viewany.py index f5d35eeed3..2801587c04 100644 --- a/mitmproxy/tools/console/grideditor/col_viewany.py +++ b/mitmproxy/tools/console/grideditor/col_viewany.py @@ -1,8 +1,7 @@ """ A display-only column that displays any data type. """ - -import typing +from typing import Any import urwid from mitmproxy.tools.console.grideditor import base @@ -20,7 +19,7 @@ def blank(self): class Display(base.Cell): - def __init__(self, data: typing.Any) -> None: + def __init__(self, data: Any) -> None: self.data = data if isinstance(data, bytes): data = strutils.bytes_to_escaped_str(data) @@ -29,5 +28,5 @@ def __init__(self, data: typing.Any) -> None: w = urwid.Text(data, wrap="any") super().__init__(w) - def get_data(self) -> typing.Any: + def get_data(self) -> Any: return self.data diff --git a/mitmproxy/tools/console/grideditor/editors.py b/mitmproxy/tools/console/grideditor/editors.py index cf3aa05a67..580f1de8f7 100644 --- a/mitmproxy/tools/console/grideditor/editors.py +++ b/mitmproxy/tools/console/grideditor/editors.py @@ -1,5 +1,6 @@ +from typing import Any, Union + import urwid -import typing from mitmproxy import exceptions from mitmproxy.http import Headers @@ -14,10 +15,7 @@ class QueryEditor(base.FocusEditor): title = "Edit Query" - columns = [ - col_text.Column("Key"), - col_text.Column("Value") - ] + columns = [col_text.Column("Key"), col_text.Column("Value")] def get_data(self, flow): return flow.request.query.items(multi=True) @@ -27,10 +25,7 @@ def set_data(self, vals, flow): class HeaderEditor(base.FocusEditor): - columns = [ - col_bytes.Column("Key"), - col_bytes.Column("Value") - ] + columns = [col_bytes.Column("Key"), col_bytes.Column("Value")] class RequestHeaderEditor(HeaderEditor): @@ -55,10 +50,7 @@ def set_data(self, vals, flow): class RequestMultipartEditor(base.FocusEditor): title = "Edit Multipart Form" - columns = [ - col_text.Column("Key"), - col_text.Column("Value") - ] + columns = [col_text.Column("Key"), col_text.Column("Value")] def get_data(self, flow): @@ -70,10 +62,7 @@ def set_data(self, vals, flow): class RequestUrlEncodedEditor(base.FocusEditor): title = "Edit UrlEncoded Form" - columns = [ - col_text.Column("Key"), - col_text.Column("Value") - ] + columns = [col_text.Column("Key"), col_text.Column("Value")] def get_data(self, flow): @@ -146,7 +135,7 @@ def layout_pushed(self, prev): self.grideditor.walker.get_current_value(), self.grideditor.set_subeditor_value, self.grideditor.walker.focus, - self.grideditor.walker.focus_col + self.grideditor.walker.focus_col, ) else: self._w = urwid.Pile([]) @@ -169,12 +158,7 @@ def data_in(self, data): def data_out(self, data): vals = [] for key, value, attrs in data: - vals.append( - [ - key, - (value, attrs) - ] - ) + vals.append([key, (value, attrs)]) return vals def get_data(self, flow): @@ -186,9 +170,7 @@ def set_data(self, vals, flow): class OptionsEditor(base.GridEditor, layoutwidget.LayoutWidget): title = "" - columns = [ - col_text.Column("") - ] + columns = [col_text.Column("")] def __init__(self, master, name, vals): self.name = name @@ -208,16 +190,17 @@ class DataViewer(base.GridEditor, layoutwidget.LayoutWidget): title = "" def __init__( - self, - master, - vals: typing.Union[ - typing.List[typing.List[typing.Any]], - typing.List[typing.Any], - str, - ]) -> None: - if vals: + self, + master, + vals: Union[ + list[list[Any]], + list[Any], + Any, + ], + ) -> None: + if vals is not None: # Whatever vals is, make it a list of rows containing lists of column values. - if isinstance(vals, str): + if not isinstance(vals, list): vals = [vals] if not isinstance(vals[0], list): vals = [[i] for i in vals] diff --git a/mitmproxy/tools/console/help.py b/mitmproxy/tools/console/help.py index 5a7bbb9a99..55e7bf36d6 100644 --- a/mitmproxy/tools/console/help.py +++ b/mitmproxy/tools/console/help.py @@ -48,25 +48,11 @@ def format_keys(self, binds): return common.format_keyvals(kvs) def keybindings(self): - text = [ - urwid.Text( - [ - ("title", "Common Keybindings") - ] - ) - - ] + text = [urwid.Text([("title", "Common Keybindings")])] text.extend(self.format_keys(self.master.keymap.list("commonkey"))) - text.append( - urwid.Text( - [ - "\n", - ("title", "Keybindings for this view") - ] - ) - ) + text.append(urwid.Text(["\n", ("title", "Keybindings for this view")])) if self.helpctx: text.extend(self.format_keys(self.master.keymap.list(self.helpctx))) @@ -95,8 +81,14 @@ def filtexp(self): "\n", ("text", " Regexes are Python-style.\n"), ("text", " Regexes can be specified as quoted strings.\n"), - ("text", " Header matching (~h, ~hq, ~hs) is against a string of the form \"name: value\".\n"), - ("text", " Expressions with no operators are regex matches against URL.\n"), + ( + "text", + ' Header matching (~h, ~hq, ~hs) is against a string of the form "name: value".\n', + ), + ( + "text", + " Expressions with no operators are regex matches against URL.\n", + ), ("text", " Default binary operator is &.\n"), ("head", "\n Examples:\n"), ] @@ -105,16 +97,17 @@ def filtexp(self): examples = [ (r"google\.com", r"Url containing \"google.com"), ("~q ~b test", r"Requests where body contains \"test\""), - (r"!(~q & ~t \"text/html\")", "Anything but requests with a text/html content type."), + ( + r"!(~q & ~t \"text/html\")", + "Anything but requests with a text/html content type.", + ), ] - text.extend( - common.format_keyvals(examples, indent=4) - ) + text.extend(common.format_keyvals(examples, indent=4)) return CListBox(text) def layout_pushed(self, prev): """ - We are just about to push a window onto the stack. + We are just about to push a window onto the stack. """ self.helpctx = prev.keyctx self.show() diff --git a/mitmproxy/tools/console/keybindings.py b/mitmproxy/tools/console/keybindings.py index bd8f7de420..821e99dda0 100644 --- a/mitmproxy/tools/console/keybindings.py +++ b/mitmproxy/tools/console/keybindings.py @@ -120,9 +120,7 @@ def set_active(self, val): def widget(self, txt): cols, _ = self.master.ui.get_cols_rows() - return urwid.ListBox( - [urwid.Text(i) for i in textwrap.wrap(txt, cols)] - ) + return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)]) def sig_mod(self, txt): self.set_body(self.widget(txt)) @@ -150,9 +148,7 @@ def get_focused_binding(self): def keypress(self, size, key): if key == "m_next": - self.focus_position = ( - self.focus_position + 1 - ) % len(self.widget_list) + self.focus_position = (self.focus_position + 1) % len(self.widget_list) self.widget_list[1].set_active(self.focus_position == 1) key = None @@ -160,7 +156,7 @@ def keypress(self, size, key): # So much for "closed for modification, but open for extension". item_rows = None if len(size) == 2: - item_rows = self.get_item_rows(size, focus = True) + 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) diff --git a/mitmproxy/tools/console/keymap.py b/mitmproxy/tools/console/keymap.py index 1ba7c9bd11..160501064c 100644 --- a/mitmproxy/tools/console/keymap.py +++ b/mitmproxy/tools/console/keymap.py @@ -1,5 +1,6 @@ -import typing import os +from collections.abc import Sequence +from typing import Optional import ruamel.yaml import ruamel.yaml.error @@ -33,9 +34,16 @@ class KeyBindingError(Exception): navkeys = [ - "m_start", "m_end", "m_next", "m_select", - "up", "down", "page_up", "page_down", - "left", "right" + "m_start", + "m_end", + "m_next", + "m_select", + "up", + "down", + "page_up", + "page_down", + "left", + "right", ] @@ -46,8 +54,8 @@ def __init__(self, key, command, contexts, help): def keyspec(self): """ - Translate the key spec from a convenient user specification to one - Urwid understands. + Translate the key spec from a convenient user specification to one + Urwid understands. """ return self.key.replace("space", " ") @@ -70,15 +78,9 @@ def _check_contexts(self, contexts): if c not in Contexts: raise ValueError("Unsupported context: %s" % c) - def add( - self, - key: str, - command: str, - contexts: typing.Sequence[str], - help="" - ) -> None: + def add(self, key: str, command: str, contexts: Sequence[str], help="") -> None: """ - Add a key to the key map. + Add a key to the key map. """ self._check_contexts(contexts) @@ -96,9 +98,9 @@ def add( self.bind(b) signals.keybindings_change.send(self) - def remove(self, key: str, contexts: typing.Sequence[str]) -> None: + def remove(self, key: str, contexts: Sequence[str]) -> None: """ - Remove a key from the key map. + Remove a key from the key map. """ self._check_contexts(contexts) for c in contexts: @@ -117,18 +119,18 @@ def bind(self, binding: Binding) -> None: def unbind(self, binding: Binding) -> None: """ - Unbind also removes the binding from the list. + Unbind also removes the binding from the list. """ for c in binding.contexts: del self.keys[c][binding.keyspec()] self.bindings = [b for b in self.bindings if b != binding] - def get(self, context: str, key: str) -> typing.Optional[Binding]: + def get(self, context: str, key: str) -> Optional[Binding]: if context in self.keys: return self.keys[context].get(key, None) return None - def list(self, context: str) -> typing.Sequence[Binding]: + def list(self, context: str) -> Sequence[Binding]: b = [x for x in self.bindings if context in x.contexts or context == "all"] single = [x for x in b if len(x.key.split()) == 1] multi = [x for x in b if len(x.key.split()) != 1] @@ -136,19 +138,19 @@ def list(self, context: str) -> typing.Sequence[Binding]: multi.sort(key=lambda x: x.sortkey()) return single + multi - def handle(self, context: str, key: str) -> typing.Optional[str]: + def handle(self, context: str, key: str) -> Optional[str]: """ - Returns the key if it has not been handled, or None. + Returns the key if it has not been handled, or None. """ b = self.get(context, key) or self.get("global", key) if b: return self.executor(b.command) return key - def handle_only(self, context: str, key: str) -> typing.Optional[str]: + def handle_only(self, context: str, key: str) -> Optional[str]: """ - Like handle, but ignores global bindings. Returns the key if it has - not been handled, or None. + Like handle, but ignores global bindings. Returns the key if it has + not been handled, or None. """ b = self.get(context, key) if b: @@ -173,9 +175,7 @@ def keymap_load_path(self, path: mitmproxy.types.Path) -> None: try: self.load_path(ctx.master.keymap, path) # type: ignore except (OSError, KeyBindingError) as e: - raise exceptions.CommandError( - "Could not load key bindings - %s" % e - ) from e + raise exceptions.CommandError("Could not load key bindings - %s" % e) from e def running(self): p = os.path.join(os.path.expanduser(ctx.options.confdir), self.defaultFile) @@ -191,40 +191,34 @@ def load_path(self, km, p): try: txt = f.read() except UnicodeDecodeError as e: - raise KeyBindingError( - f"Encoding error - expected UTF8: {p}: {e}" - ) + raise KeyBindingError(f"Encoding error - expected UTF8: {p}: {e}") try: vals = self.parse(txt) except KeyBindingError as e: - raise KeyBindingError( - f"Error reading {p}: {e}" - ) from e + raise KeyBindingError(f"Error reading {p}: {e}") from e for v in vals: user_ctxs = v.get("ctx", ["global"]) try: km._check_contexts(user_ctxs) km.remove(v["key"], user_ctxs) km.add( - key = v["key"], - command = v["cmd"], - contexts = user_ctxs, - help = v.get("help", None), + key=v["key"], + command=v["cmd"], + contexts=user_ctxs, + help=v.get("help", None), ) except ValueError as e: - raise KeyBindingError( - f"Error reading {p}: {e}" - ) from e + raise KeyBindingError(f"Error reading {p}: {e}") from e def parse(self, text): try: - data = ruamel.yaml.YAML(typ='safe', pure=True).load(text) + data = ruamel.yaml.YAML(typ="safe", pure=True).load(text) except ruamel.yaml.error.MarkedYAMLError as v: if hasattr(v, "problem_mark"): snip = v.problem_mark.get_snippet() raise KeyBindingError( - "Key binding config error at line %s:\n%s\n%s" % - (v.problem_mark.line + 1, snip, v.problem) + "Key binding config error at line %s:\n%s\n%s" + % (v.problem_mark.line + 1, snip, v.problem) ) else: raise KeyBindingError("Could not parse key bindings.") diff --git a/mitmproxy/tools/console/layoutwidget.py b/mitmproxy/tools/console/layoutwidget.py index aaf46bb4d1..dd6e910021 100644 --- a/mitmproxy/tools/console/layoutwidget.py +++ b/mitmproxy/tools/console/layoutwidget.py @@ -1,40 +1,37 @@ class LayoutWidget: """ - All top-level layout widgets and all widgets that may be set in an - overlay must comply with this API. + All top-level layout widgets and all widgets that may be set in an + overlay must comply with this API. """ + # Title is only required for windows, not overlay components title = "" keyctx = "" def key_responder(self): """ - Returns the object responding to key input. Usually self, but may be - a wrapped object. + Returns the object responding to key input. Usually self, but may be + a wrapped object. """ return self def focus_changed(self): """ - The view focus has changed. Layout objects should implement the API - rather than directly subscribing to events. + The view focus has changed. Layout objects should implement the API + rather than directly subscribing to events. """ - pass def view_changed(self): """ - The view list has changed. + The view list has changed. """ - pass def layout_popping(self): """ - We are just about to pop a window off the stack, or exit an overlay. + We are just about to pop a window off the stack, or exit an overlay. """ - pass def layout_pushed(self, prev): """ - We have just pushed a window onto the stack. + We have just pushed a window onto the stack. """ - pass diff --git a/mitmproxy/tools/console/master.py b/mitmproxy/tools/console/master.py index ea3668f465..38761ba40a 100644 --- a/mitmproxy/tools/console/master.py +++ b/mitmproxy/tools/console/master.py @@ -1,16 +1,13 @@ import asyncio -import mailcap import mimetypes import os import os.path import shlex import shutil -import signal import stat import subprocess import sys import tempfile -import typing # noqa import contextlib import threading @@ -21,10 +18,11 @@ from mitmproxy import addons from mitmproxy import master from mitmproxy import log -from mitmproxy.addons import intercept +from mitmproxy.addons import errorcheck, intercept from mitmproxy.addons import eventstore from mitmproxy.addons import readfile from mitmproxy.addons import view +from mitmproxy.contrib.tornado import patch_tornado from mitmproxy.tools.console import consoleaddons from mitmproxy.tools.console import defaultkeys from mitmproxy.tools.console import keymap @@ -34,12 +32,9 @@ class ConsoleMaster(master.Master): - def __init__(self, opts): super().__init__(opts) - self.start_err: typing.Optional[log.LogEntry] = None - self.view: view.View = view.View() self.events = eventstore.EventStore() self.events.sig_add.connect(self.sig_add_log) @@ -59,13 +54,9 @@ def __init__(self, opts): readfile.ReadFile(), consoleaddons.ConsoleAddon(self), keymap.KeymapConfig(), + errorcheck.ErrorCheck(log_to_stderr=True), ) - def sigint_handler(*args, **kwargs): - self.prompt_for_exit() - - signal.signal(signal.SIGINT, sigint_handler) - self.window = None def __setattr__(self, name, value): @@ -73,37 +64,37 @@ def __setattr__(self, name, value): signals.update_settings.send(self) def options_error(self, opts, exc): - signals.status_message.send( - message=str(exc), - expire=1 - ) + signals.status_message.send(message=str(exc), expire=1) def prompt_for_exit(self): signals.status_prompt_onekey.send( self, - prompt = "Quit", - keys = ( + prompt="Quit", + keys=( ("yes", "y"), ("no", "n"), ), - callback = self.quit, + callback=self.quit, ) def sig_add_log(self, event_store, entry: log.LogEntry): - if log.log_tier(self.options.console_eventlog_verbosity) < log.log_tier(entry.level): + if log.log_tier(self.options.console_eventlog_verbosity) < log.log_tier( + entry.level + ): return if entry.level in ("error", "warn", "alert"): signals.status_message.send( - message = ( + message=( entry.level, - "{}: {}".format(entry.level.title(), str(entry.msg).lstrip()) + f"{entry.level.title()}: {str(entry.msg).lstrip()}", ), - expire=5 + expire=5, ) def sig_call_in(self, sender, seconds, callback, args=()): def cb(*_): return callback(*args) + self.loop.set_alarm_in(seconds, cb) @contextlib.contextmanager @@ -132,7 +123,7 @@ def get_editor(self) -> str: def spawn_editor(self, data): text = not isinstance(data, bytes) - fd, name = tempfile.mkstemp('', "mitmproxy", text=text) + fd, name = tempfile.mkstemp("", "mitmproxy", text=text) with open(fd, "w" if text else "wb") as f: f.write(data) c = self.get_editor() @@ -142,9 +133,7 @@ def spawn_editor(self, data): try: subprocess.call(cmd) except: - signals.status_message.send( - message="Can't start editor: %s" % c - ) + signals.status_message.send(message="Can't start editor: %s" % c) else: with open(name, "r" if text else "rb") as f: data = f.read() @@ -164,24 +153,20 @@ def spawn_external_viewer(self, data, contenttype): # read-only to remind the user that this is a view function os.chmod(name, stat.S_IREAD) - cmd = None - shell = False + # hm which one should get priority? + c = ( + os.environ.get("MITMPROXY_EDITOR") + or os.environ.get("PAGER") + or os.environ.get("EDITOR") + ) + if not c: + c = "less" + cmd = shlex.split(c) + cmd.append(name) - if contenttype: - c = mailcap.getcaps() - cmd, _ = mailcap.findmatch(c, contenttype, filename=name) - if cmd: - shell = True - if not cmd: - # hm which one should get priority? - c = os.environ.get("MITMPROXY_EDITOR") or os.environ.get("PAGER") or os.environ.get("EDITOR") - if not c: - c = "less" - cmd = shlex.split(c) - cmd.append(name) with self.uistopped(): try: - subprocess.call(cmd, shell=shell) + subprocess.call(cmd, shell=False) except: signals.status_message.send( message="Can't start external viewer: %s" % " ".join(c) @@ -201,41 +186,56 @@ def set_palette(self, opts, updated): def inject_key(self, key): self.loop.process_input([key]) - def run(self): + async def running(self) -> None: if not sys.stdout.isatty(): - print("Error: mitmproxy's console interface requires a tty. " - "Please run mitmproxy in an interactive shell environment.", file=sys.stderr) + print( + "Error: mitmproxy's console interface requires a tty. " + "Please run mitmproxy in an interactive shell environment.", + file=sys.stderr, + ) sys.exit(1) + detected_encoding = urwid.detected_encoding.lower() + if os.name != "nt" and detected_encoding and "utf" not in detected_encoding: + print( + f"mitmproxy expects a UTF-8 console environment, not {urwid.detected_encoding!r}. " + f"Set your LANG environment variable to something like en_US.UTF-8.", + file=sys.stderr, + ) + # Experimental (04/2022): We just don't exit here and see if/how that affects users. + # sys.exit(1) + urwid.set_encoding("utf8") + signals.call_in.connect(self.sig_call_in) self.ui = window.Screen() self.ui.set_terminal_properties(256) self.set_palette(self.options, None) self.options.subscribe( - self.set_palette, - ["console_palette", "console_palette_transparent"] + self.set_palette, ["console_palette", "console_palette_transparent"] ) - loop = asyncio.get_event_loop() + + loop = asyncio.get_running_loop() if isinstance(loop, getattr(asyncio, "ProactorEventLoop", tuple())): + patch_tornado() # fix for https://bugs.python.org/issue37373 - loop = AddThreadSelectorEventLoop(loop) + loop = AddThreadSelectorEventLoop(loop) # type: ignore self.loop = urwid.MainLoop( urwid.SolidFill("x"), event_loop=urwid.AsyncioEventLoop(loop=loop), - screen = self.ui, - handle_mouse = self.options.console_mouse, + screen=self.ui, + handle_mouse=self.options.console_mouse, ) self.window = window.Window(self) self.loop.widget = self.window self.window.refresh() - if self.start_err: - def display_err(*_): - self.sig_add_log(None, self.start_err) - self.start_err = None - self.loop.set_alarm_in(0.01, display_err) + self.loop.start() - super().run_loop(self.loop.run) + await super().running() + + async def done(self): + self.loop.stop() + await super().done() def overlay(self, widget, **kwargs): self.window.set_overlay(widget, **kwargs) diff --git a/mitmproxy/tools/console/options.py b/mitmproxy/tools/console/options.py index 0f7ff0bff0..b922ee32f0 100644 --- a/mitmproxy/tools/console/options.py +++ b/mitmproxy/tools/console/options.py @@ -1,8 +1,10 @@ +from collections.abc import Sequence + import urwid import blinker import textwrap import pprint -from typing import Optional, Sequence +from typing import Optional from mitmproxy import exceptions from mitmproxy import optmanager @@ -22,11 +24,7 @@ def can_edit_inplace(opt): def fcol(s, width, attr): s = str(s) - return ( - "fixed", - width, - urwid.Text((attr, s)) - ) + return ("fixed", width, urwid.Text((attr, s))) option_focus_change = blinker.Signal() @@ -61,22 +59,19 @@ def get_widget(self): valw = urwid.Edit(edit_text=displayval) else: valw = urwid.AttrMap( - urwid.Padding( - urwid.Text([(valstyle, displayval)]) - ), - valstyle + urwid.Padding(urwid.Text([(valstyle, displayval)])), valstyle ) return urwid.Columns( [ ( self.namewidth, - urwid.Text([("title", self.opt.name.ljust(self.namewidth))]) + urwid.Text([("title", self.opt.name.ljust(self.namewidth))]), ), - valw + valw, ], dividechars=2, - focus_column=1 + focus_column=1, ) def get_edit_text(self): @@ -128,9 +123,7 @@ def get_edit_text(self): def _get(self, pos, editing): name = self.opts[pos] opt = self.master.options._options[name] - return OptionItem( - self, opt, pos == self.index, self.maxlen, editing - ) + return OptionItem(self, opt, pos == self.index, self.maxlen, editing) def get_focus(self): return self.focus_obj, self.index @@ -181,9 +174,7 @@ def keypress(self, size, key): foc, idx = self.get_focus() v = self.walker.get_edit_text() try: - current = getattr(self.master.options, foc.opt.name) - d = self.master.options.parse_setval(foc.opt, v, current) - self.master.options.update(**{foc.opt.name: d}) + self.master.options.set(f"{foc.opt.name}={v}") except exceptions.OptionsError as v: signals.status_message.send(message=str(v)) self.walker.stop_editing() @@ -214,7 +205,7 @@ def keypress(self, size, key): foc.opt.name, foc.opt.choices, foc.opt.current(), - self.master.options.setter(foc.opt.name) + self.master.options.setter(foc.opt.name), ) ) elif foc.opt.typespec == Sequence[str]: @@ -223,9 +214,9 @@ def keypress(self, size, key): self.master, foc.opt.name, foc.opt.current(), - HELP_HEIGHT + 5 + HELP_HEIGHT + 5, ), - valign="top" + valign="top", ) else: raise NotImplementedError() @@ -246,9 +237,7 @@ def set_active(self, val): def widget(self, txt): cols, _ = self.master.ui.get_cols_rows() - return urwid.ListBox( - [urwid.Text(i) for i in textwrap.wrap(txt, cols)] - ) + return urwid.ListBox([urwid.Text(i) for i in textwrap.wrap(txt, cols)]) def sig_mod(self, txt): self.set_body(self.widget(txt)) @@ -275,9 +264,7 @@ def current_name(self): def keypress(self, size, key): if key == "m_next": - self.focus_position = ( - self.focus_position + 1 - ) % len(self.widget_list) + self.focus_position = (self.focus_position + 1) % len(self.widget_list) self.widget_list[1].set_active(self.focus_position == 1) key = None @@ -285,7 +272,7 @@ def keypress(self, size, key): # So much for "closed for modification, but open for extension". item_rows = None if len(size) == 2: - item_rows = self.get_item_rows(size, focus = True) + 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) diff --git a/mitmproxy/tools/console/overlay.py b/mitmproxy/tools/console/overlay.py index 8b195703c4..d303e28a7e 100644 --- a/mitmproxy/tools/console/overlay.py +++ b/mitmproxy/tools/console/overlay.py @@ -9,17 +9,11 @@ class SimpleOverlay(urwid.Overlay, layoutwidget.LayoutWidget): - def __init__(self, master, widget, parent, width, valign="middle"): self.widget = widget self.master = master super().__init__( - widget, - parent, - align="center", - width=width, - valign=valign, - height="pack" + widget, parent, align="center", width=width, valign=valign, height="pack" ) @property @@ -74,7 +68,7 @@ def __init__(self, choices, current): def _get(self, idx, focus): c = self.choices[idx] - return Choice(c, focus, c == self.current, self.shortcuts[idx:idx + 1]) + return Choice(c, focus, c == self.current, self.shortcuts[idx : idx + 1]) def set_focus(self, index): self.index = index @@ -96,7 +90,7 @@ def get_prev(self, pos): def choice_by_shortcut(self, shortcut): for i, choice in enumerate(self.choices): - if shortcut == self.shortcuts[i:i + 1]: + if shortcut == self.shortcuts[i : i + 1]: return choice return None @@ -108,20 +102,17 @@ def __init__(self, master, title, choices, current, callback): self.master = master self.choices = choices self.callback = callback - choicewidth = max([len(i) for i in choices]) + choicewidth = max(len(i) for i in choices) self.width = max(choicewidth, len(title)) + 7 self.walker = ChooserListWalker(choices, current) super().__init__( urwid.AttrWrap( urwid.LineBox( - urwid.BoxAdapter( - urwid.ListBox(self.walker), - len(choices) - ), - title=title + urwid.BoxAdapter(urwid.ListBox(self.walker), len(choices)), + title=title, ), - "background" + "background", ) ) @@ -156,17 +147,14 @@ class OptionsOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget): def __init__(self, master, name, vals, vspace): """ - vspace: how much vertical space to keep clear + vspace: how much vertical space to keep clear """ cols, rows = master.ui.get_cols_rows() self.ge = grideditor.OptionsEditor(master, name, vals) super().__init__( urwid.AttrWrap( - urwid.LineBox( - urwid.BoxAdapter(self.ge, rows - vspace), - title=name - ), - "background" + urwid.LineBox(urwid.BoxAdapter(self.ge, rows - vspace), title=name), + "background", ) ) self.width = math.ceil(cols * 0.8) @@ -183,17 +171,14 @@ class DataViewerOverlay(urwid.WidgetWrap, layoutwidget.LayoutWidget): def __init__(self, master, vals): """ - vspace: how much vertical space to keep clear + vspace: how much vertical space to keep clear """ cols, rows = master.ui.get_cols_rows() self.ge = grideditor.DataViewer(master, vals) super().__init__( urwid.AttrWrap( - urwid.LineBox( - urwid.BoxAdapter(self.ge, rows - 5), - title="Data viewer" - ), - "background" + urwid.LineBox(urwid.BoxAdapter(self.ge, rows - 5), title="Data viewer"), + "background", ) ) self.width = math.ceil(cols * 0.8) diff --git a/mitmproxy/tools/console/palettes.py b/mitmproxy/tools/console/palettes.py index 5713588646..85cec35cfb 100644 --- a/mitmproxy/tools/console/palettes.py +++ b/mitmproxy/tools/console/palettes.py @@ -1,53 +1,92 @@ -import typing # noqa # Low-color themes should ONLY use the standard foreground and background # colours listed here: # # http://urwid.org/manual/displayattributes.html # +from collections.abc import Mapping, Sequence +from typing import Optional class Palette: _fields = [ - 'background', - 'title', - + "background", + "title", # Status bar & heading - 'heading', 'heading_key', 'heading_inactive', - + "heading", + "heading_key", + "heading_inactive", # Help - 'key', 'head', 'text', - + "key", + "head", + "text", # Options - 'option_selected', 'option_active', 'option_active_selected', - 'option_selected_key', - + "option_selected", + "option_active", + "option_active_selected", + "option_selected_key", # List and Connections - 'method_get', 'method_post', 'method_delete', 'method_other', 'method_head', 'method_put', 'method_http2_push', - 'scheme_http', 'scheme_https', 'scheme_ws', 'scheme_wss', 'scheme_tcp', 'scheme_other', - 'url_punctuation', 'url_domain', 'url_filename', 'url_extension', 'url_query_key', 'url_query_value', - 'content_none', 'content_text', 'content_script', 'content_media', 'content_data', 'content_raw', 'content_other', - 'focus', - 'code_200', 'code_300', 'code_400', 'code_500', 'code_other', - 'error', "warn", "alert", - 'header', 'highlight', 'intercept', 'replay', 'mark', - + "method_get", + "method_post", + "method_delete", + "method_other", + "method_head", + "method_put", + "method_http2_push", + "scheme_http", + "scheme_https", + "scheme_ws", + "scheme_wss", + "scheme_tcp", + "scheme_dns", + "scheme_other", + "url_punctuation", + "url_domain", + "url_filename", + "url_extension", + "url_query_key", + "url_query_value", + "content_none", + "content_text", + "content_script", + "content_media", + "content_data", + "content_raw", + "content_other", + "focus", + "code_200", + "code_300", + "code_400", + "code_500", + "code_other", + "error", + "warn", + "alert", + "header", + "highlight", + "intercept", + "replay", + "mark", # Hex view - 'offset', - + "offset", # JSON view - 'json_string', 'json_number', 'json_boolean', - + "json_string", + "json_number", + "json_boolean", # TCP flow details - 'from_client', 'to_client', - + "from_client", + "to_client", # Grid Editor - 'focusfield', 'focusfield_error', 'field_error', 'editfield', - + "focusfield", + "focusfield_error", + "field_error", + "editfield", # Commander - 'commander_command', 'commander_invalid', 'commander_hint' + "commander_command", + "commander_invalid", + "commander_hint", ] - _fields.extend(['gradient_%02d' % i for i in range(100)]) - high: typing.Optional[typing.Mapping[str, typing.Sequence[str]]] = None + _fields.extend(["gradient_%02d" % i for i in range(100)]) + high: Optional[Mapping[str, Sequence[str]]] = None def palette(self, transparent): l = [] @@ -81,7 +120,7 @@ def palette(self, transparent): def gen_gradient(palette, cols): for i in range(100): - palette['gradient_%02d' % i] = (cols[i * len(cols) // 100], 'default') + palette["gradient_%02d" % i] = (cols[i * len(cols) // 100], "default") def gen_rgb_gradient(palette, cols): @@ -97,230 +136,205 @@ def gen_rgb_gradient(palette, cols): round(t0[1] + (t1[1] - t0[1]) * pp), round(t0[2] + (t1[2] - t0[2]) * pp), ) - palette['gradient_%02d' % i] = ("#%x%x%x" % t, 'default') + palette["gradient_%02d" % i] = ("#%x%x%x" % t, "default") class LowDark(Palette): """ - Low-color dark background + Low-color dark background """ - low = dict( - background = ('white', 'black'), - title = ('white,bold', 'default'), + low = dict( + background=("white", "black"), + title=("white,bold", "default"), # Status bar & heading - heading = ('white', 'dark blue'), - heading_key = ('light cyan', 'dark blue'), - heading_inactive = ('dark gray', 'light gray'), - + heading=("white", "dark blue"), + heading_key=("light cyan", "dark blue"), + heading_inactive=("dark gray", "light gray"), # Help - key = ('light cyan', 'default'), - head = ('white,bold', 'default'), - text = ('light gray', 'default'), - + key=("light cyan", "default"), + head=("white,bold", "default"), + text=("light gray", "default"), # Options - option_selected = ('black', 'light gray'), - option_selected_key = ('light cyan', 'light gray'), - option_active = ('light red', 'default'), - option_active_selected = ('light red', 'light gray'), - + option_selected=("black", "light gray"), + option_selected_key=("light cyan", "light gray"), + option_active=("light red", "default"), + option_active_selected=("light red", "light gray"), # List and Connections - method_get = ('light green', 'default'), - method_post = ('brown', 'default'), - method_delete = ('light red', 'default'), - method_head = ('dark cyan', 'default'), - method_put = ('dark red', 'default'), - method_other = ('dark magenta', 'default'), - method_http2_push = ('dark gray', 'default'), - - scheme_http = ('dark cyan', 'default'), - scheme_https = ('dark green', 'default'), - scheme_ws=('brown', 'default'), - scheme_wss=('dark magenta', 'default'), - scheme_tcp=('dark magenta', 'default'), - scheme_other = ('dark magenta', 'default'), - - url_punctuation = ('light gray', 'default'), - url_domain = ('white', 'default'), - url_filename = ('dark cyan', 'default'), - url_extension = ('light gray', 'default'), - url_query_key = ('white', 'default'), - url_query_value = ('light gray', 'default'), - - content_none = ('dark gray', 'default'), - content_text = ('light gray', 'default'), - content_script = ('dark green', 'default'), - content_media = ('light blue', 'default'), - content_data = ('brown', 'default'), - content_raw = ('dark red', 'default'), - content_other = ('dark magenta', 'default'), - - focus = ('yellow', 'default'), - - code_200 = ('dark green', 'default'), - code_300 = ('light blue', 'default'), - code_400 = ('light red', 'default'), - code_500 = ('light red', 'default'), - code_other = ('dark red', 'default'), - - alert = ('light magenta', 'default'), - warn = ('brown', 'default'), - error = ('light red', 'default'), - - header = ('dark cyan', 'default'), - highlight = ('white,bold', 'default'), - intercept = ('brown', 'default'), - replay = ('light green', 'default'), - mark = ('light red', 'default'), - + method_get=("light green", "default"), + method_post=("brown", "default"), + method_delete=("light red", "default"), + method_head=("dark cyan", "default"), + method_put=("dark red", "default"), + method_other=("dark magenta", "default"), + method_http2_push=("dark gray", "default"), + scheme_http=("dark cyan", "default"), + scheme_https=("dark green", "default"), + scheme_ws=("brown", "default"), + scheme_wss=("dark magenta", "default"), + scheme_tcp=("dark magenta", "default"), + scheme_dns=("dark blue", "default"), + scheme_other=("dark magenta", "default"), + url_punctuation=("light gray", "default"), + url_domain=("white", "default"), + url_filename=("dark cyan", "default"), + url_extension=("light gray", "default"), + url_query_key=("white", "default"), + url_query_value=("light gray", "default"), + content_none=("dark gray", "default"), + content_text=("light gray", "default"), + content_script=("dark green", "default"), + content_media=("light blue", "default"), + content_data=("brown", "default"), + content_raw=("dark red", "default"), + content_other=("dark magenta", "default"), + focus=("yellow", "default"), + code_200=("dark green", "default"), + code_300=("light blue", "default"), + code_400=("light red", "default"), + code_500=("light red", "default"), + code_other=("dark red", "default"), + alert=("light magenta", "default"), + warn=("brown", "default"), + error=("light red", "default"), + header=("dark cyan", "default"), + highlight=("white,bold", "default"), + intercept=("brown", "default"), + replay=("light green", "default"), + mark=("light red", "default"), # Hex view - offset = ('dark cyan', 'default'), - + offset=("dark cyan", "default"), # JSON view - json_string = ('dark blue', 'default'), - json_number = ('light magenta', 'default'), - json_boolean = ('dark magenta', 'default'), - + json_string=("dark blue", "default"), + json_number=("light magenta", "default"), + json_boolean=("dark magenta", "default"), # TCP flow details - from_client = ('light blue', 'default'), - to_client = ('light red', 'default'), - + from_client=("light blue", "default"), + to_client=("light red", "default"), # Grid Editor - focusfield = ('black', 'light gray'), - focusfield_error = ('dark red', 'light gray'), - field_error = ('dark red', 'default'), - editfield = ('white', 'default'), - - - commander_command = ('white,bold', 'default'), - commander_invalid = ('light red', 'default'), - commander_hint = ('dark gray', 'default'), + focusfield=("black", "light gray"), + focusfield_error=("dark red", "light gray"), + field_error=("dark red", "default"), + editfield=("white", "default"), + commander_command=("white,bold", "default"), + commander_invalid=("light red", "default"), + commander_hint=("dark gray", "default"), + ) + gen_gradient( + low, + ["light red", "yellow", "light green", "dark green", "dark cyan", "dark blue"], ) - gen_gradient(low, ['light red', 'yellow', 'light green', 'dark green', 'dark cyan', 'dark blue']) class Dark(LowDark): high = dict( - heading_inactive = ('g58', 'g11'), - intercept = ('#f60', 'default'), - - option_selected = ('g85', 'g45'), - option_selected_key = ('light cyan', 'g50'), - option_active_selected = ('light red', 'g50'), + heading_inactive=("g58", "g11"), + intercept=("#f60", "default"), + option_selected=("g85", "g45"), + option_selected_key=("light cyan", "g50"), + option_active_selected=("light red", "g50"), ) class LowLight(Palette): """ - Low-color light background + Low-color light background """ - low = dict( - background = ('black', 'white'), - title = ('dark magenta', 'default'), + low = dict( + background=("black", "white"), + title=("dark magenta", "default"), # Status bar & heading - heading = ('white', 'black'), - heading_key = ('dark blue', 'black'), - heading_inactive = ('black', 'light gray'), - + heading=("white", "black"), + heading_key=("dark blue", "black"), + heading_inactive=("black", "light gray"), # Help - key = ('dark blue', 'default'), - head = ('black', 'default'), - text = ('dark gray', 'default'), - + key=("dark blue", "default"), + head=("black", "default"), + text=("dark gray", "default"), # Options - option_selected = ('black', 'light gray'), - option_selected_key = ('dark blue', 'light gray'), - option_active = ('light red', 'default'), - option_active_selected = ('light red', 'light gray'), - + option_selected=("black", "light gray"), + option_selected_key=("dark blue", "light gray"), + option_active=("light red", "default"), + option_active_selected=("light red", "light gray"), # List and Connections - method_get = ('dark green', 'default'), - method_post = ('brown', 'default'), - method_head = ('dark cyan', 'default'), - method_put = ('light red', 'default'), - method_delete = ('dark red', 'default'), - method_other = ('light magenta', 'default'), - method_http2_push = ('light gray', 'default'), - - scheme_http = ('dark cyan', 'default'), - scheme_https = ('light green', 'default'), - scheme_ws=('brown', 'default'), - scheme_wss=('light magenta', 'default'), - scheme_tcp=('light magenta', 'default'), - scheme_other = ('light magenta', 'default'), - - url_punctuation = ('dark gray', 'default'), - url_domain = ('dark gray', 'default'), - url_filename = ('black', 'default'), - url_extension = ('dark gray', 'default'), - url_query_key = ('light blue', 'default'), - url_query_value = ('dark blue', 'default'), - - content_none = ('black', 'default'), - content_text = ('dark gray', 'default'), - content_script = ('light green', 'default'), - content_media = ('light blue', 'default'), - content_data = ('brown', 'default'), - content_raw = ('light red', 'default'), - content_other = ('light magenta', 'default'), - - focus = ('black', 'default'), - - code_200 = ('dark green', 'default'), - code_300 = ('light blue', 'default'), - code_400 = ('dark red', 'default'), - code_500 = ('dark red', 'default'), - code_other = ('light red', 'default'), - - error = ('light red', 'default'), - warn = ('brown', 'default'), - alert = ('light magenta', 'default'), - - header = ('dark blue', 'default'), - highlight = ('black,bold', 'default'), - intercept = ('brown', 'default'), - replay = ('dark green', 'default'), - mark = ('dark red', 'default'), - + method_get=("dark green", "default"), + method_post=("brown", "default"), + method_head=("dark cyan", "default"), + method_put=("light red", "default"), + method_delete=("dark red", "default"), + method_other=("light magenta", "default"), + method_http2_push=("light gray", "default"), + scheme_http=("dark cyan", "default"), + scheme_https=("light green", "default"), + scheme_ws=("brown", "default"), + scheme_wss=("light magenta", "default"), + scheme_tcp=("light magenta", "default"), + scheme_dns=("light blue", "default"), + scheme_other=("light magenta", "default"), + url_punctuation=("dark gray", "default"), + url_domain=("dark gray", "default"), + url_filename=("black", "default"), + url_extension=("dark gray", "default"), + url_query_key=("light blue", "default"), + url_query_value=("dark blue", "default"), + content_none=("black", "default"), + content_text=("dark gray", "default"), + content_script=("light green", "default"), + content_media=("light blue", "default"), + content_data=("brown", "default"), + content_raw=("light red", "default"), + content_other=("light magenta", "default"), + focus=("black", "default"), + code_200=("dark green", "default"), + code_300=("light blue", "default"), + code_400=("dark red", "default"), + code_500=("dark red", "default"), + code_other=("light red", "default"), + error=("light red", "default"), + warn=("brown", "default"), + alert=("light magenta", "default"), + header=("dark blue", "default"), + highlight=("black,bold", "default"), + intercept=("brown", "default"), + replay=("dark green", "default"), + mark=("dark red", "default"), # Hex view - offset = ('dark blue', 'default'), - + offset=("dark blue", "default"), # JSON view - json_string = ('dark blue', 'default'), - json_number = ('light magenta', 'default'), - json_boolean = ('dark magenta', 'default'), - + json_string=("dark blue", "default"), + json_number=("light magenta", "default"), + json_boolean=("dark magenta", "default"), # TCP flow details - from_client = ('dark blue', 'default'), - to_client = ('dark red', 'default'), - + from_client=("dark blue", "default"), + to_client=("dark red", "default"), # Grid Editor - focusfield = ('black', 'light gray'), - focusfield_error = ('dark red', 'light gray'), - field_error = ('dark red', 'black'), - editfield = ('black', 'default'), - - commander_command = ('dark magenta', 'default'), - commander_invalid = ('light red', 'default'), - commander_hint = ('light gray', 'default'), + focusfield=("black", "light gray"), + focusfield_error=("dark red", "light gray"), + field_error=("dark red", "black"), + editfield=("black", "default"), + commander_command=("dark magenta", "default"), + commander_invalid=("light red", "default"), + commander_hint=("light gray", "default"), + ) + gen_gradient( + low, + ["light red", "yellow", "light green", "dark green", "dark cyan", "dark blue"], ) - gen_gradient(low, ['light red', 'yellow', 'light green', 'dark green', 'dark cyan', 'dark blue']) class Light(LowLight): high = dict( - background = ('black', 'g100'), - heading = ('g99', '#08f'), - heading_key = ('#0ff,bold', '#08f'), - heading_inactive = ('g35', 'g85'), - replay = ('#0a0,bold', 'default'), - - option_selected = ('black', 'g85'), - option_selected_key = ('dark blue', 'g85'), - option_active_selected = ('light red', 'g85'), + background=("black", "g100"), + heading=("g99", "#08f"), + heading_key=("#0ff,bold", "#08f"), + heading_inactive=("g35", "g85"), + replay=("#0a0,bold", "default"), + option_selected=("black", "g85"), + option_selected_key=("dark blue", "g85"), + option_active_selected=("light red", "g85"), ) @@ -346,171 +360,167 @@ class Light(LowLight): class SolarizedLight(LowLight): high = dict( - background = (sol_base00, sol_base3), - title = (sol_cyan, 'default'), - text = (sol_base00, 'default'), - + background=(sol_base00, sol_base3), + title=(sol_cyan, "default"), + text=(sol_base00, "default"), # Status bar & heading - heading = (sol_base2, sol_base02), - heading_key = (sol_blue, sol_base03), - heading_inactive = (sol_base03, sol_base1), - + heading=(sol_base2, sol_base02), + heading_key=(sol_blue, sol_base03), + heading_inactive=(sol_base03, sol_base1), # Help - key = (sol_blue, 'default',), - head = (sol_base00, 'default'), - + key=( + sol_blue, + "default", + ), + head=(sol_base00, "default"), # Options - option_selected = (sol_base03, sol_base2), - option_selected_key = (sol_blue, sol_base2), - option_active = (sol_orange, 'default'), - option_active_selected = (sol_orange, sol_base2), - + option_selected=(sol_base03, sol_base2), + option_selected_key=(sol_blue, sol_base2), + option_active=(sol_orange, "default"), + option_active_selected=(sol_orange, sol_base2), # List and Connections - - method_get = (sol_green, 'default'), - method_post = (sol_orange, 'default'), - method_head = (sol_cyan, 'default'), - method_put = (sol_red, 'default'), - method_delete = (sol_red, 'default'), - method_other = (sol_magenta, 'default'), - method_http2_push = ('light gray', 'default'), - - scheme_http = (sol_cyan, 'default'), - scheme_https = ('light green', 'default'), - scheme_ws=(sol_orange, 'default'), - scheme_wss=('light magenta', 'default'), - scheme_tcp=('light magenta', 'default'), - scheme_other = ('light magenta', 'default'), - - url_punctuation = ('dark gray', 'default'), - url_domain = ('dark gray', 'default'), - url_filename = ('black', 'default'), - url_extension = ('dark gray', 'default'), - url_query_key = (sol_blue, 'default'), - url_query_value = ('dark blue', 'default'), - - focus = (sol_base01, 'default'), - - code_200 = (sol_green, 'default'), - code_300 = (sol_blue, 'default'), - code_400 = (sol_orange, 'default',), - code_500 = (sol_red, 'default'), - code_other = (sol_magenta, 'default'), - - error = (sol_red, 'default'), - warn = (sol_orange, 'default'), - alert = (sol_magenta, 'default'), - - header = (sol_blue, 'default'), - highlight = (sol_base01, 'default'), - intercept = (sol_red, 'default',), - replay = (sol_green, 'default',), - + method_get=(sol_green, "default"), + method_post=(sol_orange, "default"), + method_head=(sol_cyan, "default"), + method_put=(sol_red, "default"), + method_delete=(sol_red, "default"), + method_other=(sol_magenta, "default"), + method_http2_push=("light gray", "default"), + scheme_http=(sol_cyan, "default"), + scheme_https=("light green", "default"), + scheme_ws=(sol_orange, "default"), + scheme_wss=("light magenta", "default"), + scheme_tcp=("light magenta", "default"), + scheme_dns=("light blue", "default"), + scheme_other=("light magenta", "default"), + url_punctuation=("dark gray", "default"), + url_domain=("dark gray", "default"), + url_filename=("black", "default"), + url_extension=("dark gray", "default"), + url_query_key=(sol_blue, "default"), + url_query_value=("dark blue", "default"), + focus=(sol_base01, "default"), + code_200=(sol_green, "default"), + code_300=(sol_blue, "default"), + code_400=( + sol_orange, + "default", + ), + code_500=(sol_red, "default"), + code_other=(sol_magenta, "default"), + error=(sol_red, "default"), + warn=(sol_orange, "default"), + alert=(sol_magenta, "default"), + header=(sol_blue, "default"), + highlight=(sol_base01, "default"), + intercept=( + sol_red, + "default", + ), + replay=( + sol_green, + "default", + ), # Hex view - offset = (sol_cyan, 'default'), - + offset=(sol_cyan, "default"), # JSON view - json_string = (sol_cyan, 'default'), - json_number = (sol_blue, 'default'), - json_boolean = (sol_magenta, 'default'), - + json_string=(sol_cyan, "default"), + json_number=(sol_blue, "default"), + json_boolean=(sol_magenta, "default"), # TCP flow details - from_client = (sol_blue, 'default'), - to_client = (sol_red, 'default'), - + from_client=(sol_blue, "default"), + to_client=(sol_red, "default"), # Grid Editor - focusfield = (sol_base00, sol_base2), - focusfield_error = (sol_red, sol_base2), - field_error = (sol_red, 'default'), - editfield = (sol_base01, 'default'), - - commander_command = (sol_cyan, 'default'), - commander_invalid = (sol_orange, 'default'), - commander_hint = (sol_base1, 'default'), + focusfield=(sol_base00, sol_base2), + focusfield_error=(sol_red, sol_base2), + field_error=(sol_red, "default"), + editfield=(sol_base01, "default"), + commander_command=(sol_cyan, "default"), + commander_invalid=(sol_orange, "default"), + commander_hint=(sol_base1, "default"), ) class SolarizedDark(LowDark): high = dict( - background = (sol_base2, sol_base03), - title = (sol_blue, 'default'), - text = (sol_base1, 'default'), - + background=(sol_base2, sol_base03), + title=(sol_blue, "default"), + text=(sol_base1, "default"), # Status bar & heading - heading = (sol_base2, sol_base01), - heading_key = (sol_blue + ",bold", sol_base01), - heading_inactive = (sol_base1, sol_base02), - + heading=(sol_base2, sol_base01), + heading_key=(sol_blue + ",bold", sol_base01), + heading_inactive=(sol_base1, sol_base02), # Help - key = (sol_blue, 'default',), - head = (sol_base2, 'default'), - + key=( + sol_blue, + "default", + ), + head=(sol_base2, "default"), # Options - option_selected = (sol_base03, sol_base00), - option_selected_key = (sol_blue, sol_base00), - option_active = (sol_orange, 'default'), - option_active_selected = (sol_orange, sol_base00), - + option_selected=(sol_base03, sol_base00), + option_selected_key=(sol_blue, sol_base00), + option_active=(sol_orange, "default"), + option_active_selected=(sol_orange, sol_base00), # List and Connections - focus = (sol_base1, 'default'), - - method_get = (sol_green, 'default'), - method_post = (sol_orange, 'default'), - method_delete = (sol_red, 'default'), - method_head = (sol_cyan, 'default'), - method_put = (sol_red, 'default'), - method_other = (sol_magenta, 'default'), - method_http2_push = (sol_base01, 'default'), - - url_punctuation = ('h242', 'default'), - url_domain = ('h252', 'default'), - url_filename = ('h132', 'default'), - url_extension = ('h96', 'default'), - url_query_key = ('h37', 'default'), - url_query_value = ('h30', 'default'), - - content_none = (sol_base01, 'default'), - content_text = (sol_base1, 'default'), - content_media = (sol_blue, 'default'), - - code_200 = (sol_green, 'default'), - code_300 = (sol_blue, 'default'), - code_400 = (sol_orange, 'default',), - code_500 = (sol_red, 'default'), - code_other = (sol_magenta, 'default'), - - error = (sol_red, 'default'), - warn = (sol_orange, 'default'), - alert = (sol_magenta, 'default'), - - header = (sol_blue, 'default'), - highlight = (sol_base01, 'default'), - intercept = (sol_red, 'default',), - replay = (sol_green, 'default',), - + focus=(sol_base1, "default"), + method_get=(sol_green, "default"), + method_post=(sol_orange, "default"), + method_delete=(sol_red, "default"), + method_head=(sol_cyan, "default"), + method_put=(sol_red, "default"), + method_other=(sol_magenta, "default"), + method_http2_push=(sol_base01, "default"), + url_punctuation=("h242", "default"), + url_domain=("h252", "default"), + url_filename=("h132", "default"), + url_extension=("h96", "default"), + url_query_key=("h37", "default"), + url_query_value=("h30", "default"), + content_none=(sol_base01, "default"), + content_text=(sol_base1, "default"), + content_media=(sol_blue, "default"), + code_200=(sol_green, "default"), + code_300=(sol_blue, "default"), + code_400=( + sol_orange, + "default", + ), + code_500=(sol_red, "default"), + code_other=(sol_magenta, "default"), + error=(sol_red, "default"), + warn=(sol_orange, "default"), + alert=(sol_magenta, "default"), + header=(sol_blue, "default"), + highlight=(sol_base01, "default"), + intercept=( + sol_red, + "default", + ), + replay=( + sol_green, + "default", + ), # Hex view - offset = (sol_cyan, 'default'), - + offset=(sol_cyan, "default"), # JSON view - json_string = (sol_cyan, 'default'), - json_number = (sol_blue, 'default'), - json_boolean = (sol_magenta, 'default'), - + json_string=(sol_cyan, "default"), + json_number=(sol_blue, "default"), + json_boolean=(sol_magenta, "default"), # TCP flow details - from_client = (sol_blue, 'default'), - to_client = (sol_red, 'default'), - + from_client=(sol_blue, "default"), + to_client=(sol_red, "default"), # Grid Editor - focusfield = (sol_base0, sol_base02), - focusfield_error = (sol_red, sol_base02), - field_error = (sol_red, 'default'), - editfield = (sol_base1, 'default'), - - commander_command = (sol_blue, 'default'), - commander_invalid = (sol_orange, 'default'), - commander_hint = (sol_base00, 'default'), + focusfield=(sol_base0, sol_base02), + focusfield_error=(sol_red, sol_base02), + field_error=(sol_red, "default"), + editfield=(sol_base1, "default"), + commander_command=(sol_blue, "default"), + commander_invalid=(sol_orange, "default"), + commander_hint=(sol_base00, "default"), + ) + gen_rgb_gradient( + high, [(15, 0, 0), (15, 15, 0), (0, 15, 0), (0, 15, 15), (0, 0, 15)] ) - gen_rgb_gradient(high, [(15, 0, 0), (15, 15, 0), (0, 15, 0), (0, 15, 15), (0, 0, 15)]) DEFAULT = "dark" diff --git a/mitmproxy/tools/console/searchable.py b/mitmproxy/tools/console/searchable.py index 241ac40176..e71fe862a9 100644 --- a/mitmproxy/tools/console/searchable.py +++ b/mitmproxy/tools/console/searchable.py @@ -4,7 +4,6 @@ class Highlight(urwid.AttrMap): - def __init__(self, t): urwid.AttrMap.__init__( self, @@ -15,11 +14,9 @@ def __init__(self, t): class Searchable(urwid.ListBox): - def __init__(self, contents): self.walker = urwid.SimpleFocusListWalker(contents) urwid.ListBox.__init__(self, self.walker) - self.set_focus(len(self.walker) - 1) self.search_offset = 0 self.current_highlight = None self.search_term = None @@ -28,9 +25,7 @@ def __init__(self, contents): def keypress(self, size, key): if key == "/": signals.status_prompt.send( - prompt = "Search for", - text = "", - callback = self.set_search + prompt="Search for", text="", callback=self.set_search ) elif key == "n": self.find_next(False) diff --git a/mitmproxy/tools/console/statusbar.py b/mitmproxy/tools/console/statusbar.py index 57c5914ce7..8ab3a13392 100644 --- a/mitmproxy/tools/console/statusbar.py +++ b/mitmproxy/tools/console/statusbar.py @@ -3,7 +3,7 @@ import urwid -import mitmproxy.tools.console.master # noqa +import mitmproxy.tools.console.master from mitmproxy.tools.console import commandexecutor from mitmproxy.tools.console import common from mitmproxy.tools.console import signals @@ -33,7 +33,6 @@ def __call__(self, txt): class ActionBar(urwid.WidgetWrap): - def __init__(self, master): self.master = master urwid.WidgetWrap.__init__(self, None) @@ -54,6 +53,7 @@ def sig_message(self, sender, message, expire=1): w = urwid.Text(self.shorten_message(message, cols)) self._w = w if expire: + def cb(*args): if w == self._w: self.clear() @@ -63,7 +63,8 @@ def cb(*args): def prep_prompt(self, p): return p.strip() + ": " - def shorten_message(self, msg, max_width): + @staticmethod + def shorten_message(msg, max_width): """ Shorten message so that it fits into a single line in the statusbar. """ @@ -98,7 +99,9 @@ def sig_prompt(self, sender, prompt, text, callback, args=()): self._w = urwid.Edit(self.prep_prompt(prompt), text or "") self.prompting = PromptStub(callback, args) - def sig_prompt_command(self, sender, partial: str = "", cursor: Optional[int] = None): + def sig_prompt_command( + self, sender, partial: str = "", cursor: Optional[int] = None + ): signals.focus.send(self, section="footer") self._w = commander.CommandEdit( self.master, @@ -116,8 +119,8 @@ def execute_command(self, txt): def sig_prompt_onekey(self, sender, prompt, keys, callback, args=()): """ - Keys are a set of (word, key) tuples. The appropriate key in the - word is highlighted. + Keys are a set of (word, key) tuples. The appropriate key in the + word is highlighted. """ signals.focus.send(self, section="footer") prompt = [prompt, " ("] @@ -175,9 +178,7 @@ class StatusBar(urwid.WidgetWrap): REFRESHTIME = 0.5 # Timed refresh time in seconds keyctx = "" - def __init__( - self, master: "mitmproxy.tools.console.master.ConsoleMaster" - ) -> None: + def __init__(self, master: "mitmproxy.tools.console.master.ConsoleMaster") -> None: self.master = master self.ib = urwid.WidgetWrap(urwid.Text("")) self.ab = ActionBar(self.master) @@ -250,8 +251,10 @@ def get_status(self): r.append("[") r.append(("heading_key", "u")) r.append(":%s]" % self.master.options.stickyauth) - if self.master.options.console_default_contentview != 'auto': - r.append("[contentview:%s]" % (self.master.options.console_default_contentview)) + if self.master.options.console_default_contentview != "auto": + r.append( + "[contentview:%s]" % (self.master.options.console_default_contentview) + ) if self.master.options.has_changed("view_order"): r.append("[") r.append(("heading_key", "o")) @@ -305,7 +308,7 @@ def redraw(self): marked = "M" t = [ - ('heading', (f"{arrow} {marked} [{offset}/{fc}]").ljust(11)), + ("heading", (f"{arrow} {marked} [{offset}/{fc}]").ljust(11)), ] if self.master.options.server: @@ -316,10 +319,15 @@ def redraw(self): else: boundaddr = "" t.extend(self.get_status()) - status = urwid.AttrWrap(urwid.Columns([ - urwid.Text(t), - urwid.Text(boundaddr, align="right"), - ]), "heading") + status = urwid.AttrWrap( + urwid.Columns( + [ + urwid.Text(t), + urwid.Text(boundaddr, align="right"), + ] + ), + "heading", + ) self.ib._w = status def selectable(self): diff --git a/mitmproxy/tools/console/tabs.py b/mitmproxy/tools/console/tabs.py index 07b617a1c5..a3ba09a109 100644 --- a/mitmproxy/tools/console/tabs.py +++ b/mitmproxy/tools/console/tabs.py @@ -2,10 +2,9 @@ class Tab(urwid.WidgetWrap): - def __init__(self, offset, content, attr, onclick): """ - onclick is called on click with the tab offset as argument + onclick is called on click with the tab offset as argument """ p = urwid.Text(content, align="center") p = urwid.Padding(p, align="center", width=("relative", 100)) @@ -21,7 +20,6 @@ def mouse_event(self, size, event, button, col, row, focus): class Tabs(urwid.WidgetWrap): - def __init__(self, tabs, tab_offset=0): super().__init__("") self.tab_offset = tab_offset @@ -51,26 +49,11 @@ def show(self): for i in range(len(self.tabs)): txt = self.tabs[i][0]() if i == self.tab_offset % len(self.tabs): - headers.append( - Tab( - i, - txt, - "heading", - self.change_tab - ) - ) + headers.append(Tab(i, txt, "heading", self.change_tab)) else: - headers.append( - Tab( - i, - txt, - "heading_inactive", - self.change_tab - ) - ) + headers.append(Tab(i, txt, "heading_inactive", self.change_tab)) headers = urwid.Columns(headers, dividechars=1) self._w = urwid.Frame( - body = self.tabs[self.tab_offset % len(self.tabs)][1](), - header = headers + body=self.tabs[self.tab_offset % len(self.tabs)][1](), header=headers ) self._w.set_focus("body") diff --git a/mitmproxy/tools/console/window.py b/mitmproxy/tools/console/window.py index 2912abeead..90094e082e 100644 --- a/mitmproxy/tools/console/window.py +++ b/mitmproxy/tools/console/window.py @@ -28,15 +28,11 @@ def __init__(self, window, widget, title, focus): if title: header = urwid.AttrWrap( - urwid.Text(title), - "heading" if focus else "heading_inactive" + urwid.Text(title), "heading" if focus else "heading_inactive" ) else: header = None - super().__init__( - widget, - header=header - ) + super().__init__(widget, header=header) def mouse_event(self, size, event, button, col, row, focus): if event == "mouse press" and button == 1 and not self.is_focused: @@ -48,7 +44,9 @@ def keypress(self, size, key): # Otherwise, in a horizontal layout, urwid's Pile would change the focused widget # if we cannot scroll any further. ret = super().keypress(size, key) - command = self._command_map[ret] # awkward as they don't implement a full dict api + command = self._command_map[ + ret + ] # awkward as they don't implement a full dict api if command and command.startswith("cursor"): return None return ret @@ -65,7 +63,6 @@ def __init__(self, master, base): options=options.Options(master), help=help.HelpView(master), eventlog=eventlog.EventLog(master), - edit_focus_query=grideditor.QueryEditor(master), edit_focus_cookies=grideditor.CookieEditor(master), edit_focus_setcookies=grideditor.SetCookieEditor(master), @@ -81,18 +78,22 @@ def __init__(self, master, base): def set_overlay(self, o, **kwargs): self.overlay = overlay.SimpleOverlay( - self, o, self.top_widget(), o.width, **kwargs, + self, + o, + self.top_widget(), + o.width, + **kwargs, ) def top_window(self): """ - The current top window, ignoring overlays. + The current top window, ignoring overlays. """ return self.windows[self.stack[-1]] def top_widget(self): """ - The current top widget - either a window or the active overlay. + The current top widget - either a window or the active overlay. """ if self.overlay: return self.overlay @@ -107,7 +108,7 @@ def push(self, wname): def pop(self, *args, **kwargs): """ - Pop off the stack, return True if we're already at the top. + Pop off the stack, return True if we're already at the top. """ if not self.overlay and len(self.stack) == 1: return True @@ -119,9 +120,9 @@ def pop(self, *args, **kwargs): def call(self, name, *args, **kwargs): """ - Call a function on both the top window, and the overlay if there is - one. If the widget has a key_responder, we call the function on the - responder instead. + Call a function on both the top window, and the overlay if there is + one. If the widget has a key_responder, we call the function on the + responder instead. """ getattr(self.top_window(), name)(*args, **kwargs) if self.overlay: @@ -132,9 +133,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.AttrWrap(self.statusbar, "background") ) self.master = master self.master.view.sig_view_refresh.connect(self.view_changed) @@ -152,10 +151,7 @@ def __init__(self, master): self.master.options.subscribe(self.configure, ["console_layout"]) self.master.options.subscribe(self.configure, ["console_layout_headers"]) self.pane = 0 - self.stacks = [ - WindowStack(master, "flowlist"), - WindowStack(master, "eventlog") - ] + self.stacks = [WindowStack(master, "flowlist"), WindowStack(master, "eventlog")] def focus_stack(self): return self.stacks[self.pane] @@ -165,7 +161,7 @@ def configure(self, otions, updated): def refresh(self): """ - Redraw the layout. + Redraw the layout. """ c = self.master.options.console_layout if c == "single": @@ -177,28 +173,20 @@ def wrapped(idx): title = self.stacks[idx].top_window().title else: title = None - return StackWidget( - self, - widget, - title, - self.pane == idx - ) + return StackWidget(self, widget, title, self.pane == idx) w = None if c == "single": w = wrapped(0) elif c == "vertical": w = urwid.Pile( - [ - wrapped(i) for i, s in enumerate(self.stacks) - ], - focus_item=self.pane + [wrapped(i) for i, s in enumerate(self.stacks)], focus_item=self.pane ) else: w = urwid.Columns( [wrapped(i) for i, s in enumerate(self.stacks)], dividechars=1, - focus_column=self.pane + focus_column=self.pane, ) self.body = urwid.AttrWrap(w, "background") @@ -210,29 +198,29 @@ def flow_changed(self, sender, flow): def focus_changed(self, *args, **kwargs): """ - Triggered when the focus changes - either when it's modified, or - when it changes to a different flow altogether. + Triggered when the focus changes - either when it's modified, or + when it changes to a different flow altogether. """ for i in self.stacks: i.call("focus_changed") def view_changed(self, *args, **kwargs): """ - Triggered when the view list has changed. + Triggered when the view list has changed. """ for i in self.stacks: i.call("view_changed") def set_overlay(self, o, **kwargs): """ - Set an overlay on the currently focused stack. + Set an overlay on the currently focused stack. """ self.focus_stack().set_overlay(o, **kwargs) self.refresh() def push(self, wname): """ - Push a window onto the currently focused stack. + Push a window onto the currently focused stack. """ self.focus_stack().push(wname) self.refresh() @@ -241,8 +229,8 @@ def push(self, wname): def pop(self, *args, **kwargs): """ - Pop a window from the currently focused stack. If there is only one - window on the stack, this prompts for exit. + Pop a window from the currently focused stack. If there is only one + window on the stack, this prompts for exit. """ if self.focus_stack().pop(): self.master.prompt_for_exit() @@ -287,7 +275,7 @@ def sig_focus(self, sender, section): def switch(self): """ - Switch between the two panes. + Switch between the two panes. """ if self.master.options.console_layout == "single": self.pane = 0 @@ -302,7 +290,7 @@ def mouse_event(self, *args, **kwargs): if args[1] == "mouse drag": signals.status_message.send( message="Hold down fn, shift, alt or ctrl to select text or use the --set console_mouse=false parameter.", - expire=1 + expire=1, ) elif args[1] == "mouse press" and args[2] == 4: self.keypress(args[0], "up") @@ -315,14 +303,10 @@ def mouse_event(self, *args, **kwargs): def keypress(self, size, k): k = super().keypress(size, k) if k: - return self.master.keymap.handle( - self.focus_stack().top_widget().keyctx, - k - ) + return self.master.keymap.handle(self.focus_stack().top_widget().keyctx, k) class Screen(raw_display.Screen): - def write(self, data): if common.IS_WINDOWS_OR_WSL: # replace urwid's SI/SO, which produce artifacts under WSL. diff --git a/mitmproxy/tools/dump.py b/mitmproxy/tools/dump.py index e2fe6f7a70..527a93e8b6 100644 --- a/mitmproxy/tools/dump.py +++ b/mitmproxy/tools/dump.py @@ -1,20 +1,10 @@ from mitmproxy import addons -from mitmproxy import options from mitmproxy import master -from mitmproxy.addons import dumper, termlog, keepserving, readfile - - -class ErrorCheck: - def __init__(self): - self.has_errored = False - - def add_log(self, e): - if e.level == "error": - self.has_errored = True +from mitmproxy import options +from mitmproxy.addons import dumper, errorcheck, keepserving, readfile, termlog class DumpMaster(master.Master): - def __init__( self, options: options.Options, @@ -22,7 +12,6 @@ def __init__( with_dumper=True, ) -> None: super().__init__(options) - self.errorcheck = ErrorCheck() if with_termlog: self.addons.add(termlog.TermLog()) self.addons.add(*addons.default_addons()) @@ -31,5 +20,5 @@ def __init__( self.addons.add( keepserving.KeepServing(), readfile.ReadFileStdin(), - self.errorcheck + errorcheck.ErrorCheck(), ) diff --git a/mitmproxy/tools/main.py b/mitmproxy/tools/main.py index 60923d5b8b..76bd4389be 100644 --- a/mitmproxy/tools/main.py +++ b/mitmproxy/tools/main.py @@ -3,7 +3,8 @@ import os import signal import sys -import typing +from collections.abc import Callable, Sequence +from typing import Any, Optional, TypeVar from mitmproxy import exceptions, master from mitmproxy import options @@ -12,22 +13,6 @@ from mitmproxy.utils import debug, arg_check -def assert_utf8_env(): - spec = "" - for i in ["LANG", "LC_CTYPE", "LC_ALL"]: - spec += os.environ.get(i, "").lower() - if "utf" not in spec: - print( - "Error: mitmproxy requires a UTF console environment.", - file=sys.stderr - ) - print( - "Set your LANG environment variable to something like en_US.UTF-8", - file=sys.stderr - ) - sys.exit(1) - - def process_options(parser, opts, args): if args.version: print(debug.dump_system_info()) @@ -35,10 +20,10 @@ def process_options(parser, opts, args): if args.quiet or args.options or args.commands: # also reduce log verbosity if --options or --commands is passed, # we don't want log messages from regular startup then. - args.termlog_verbosity = 'error' + args.termlog_verbosity = "error" args.flow_detail = 0 if args.verbose: - args.termlog_verbosity = 'debug' + args.termlog_verbosity = "debug" args.flow_detail = 2 adict = {} @@ -48,90 +33,95 @@ def process_options(parser, opts, args): opts.merge(adict) +T = TypeVar("T", bound=master.Master) + + def run( - master_cls: typing.Type[master.Master], - make_parser: typing.Callable[[options.Options], argparse.ArgumentParser], - arguments: typing.Sequence[str], - extra: typing.Callable[[typing.Any], dict] = None -) -> master.Master: # pragma: no cover + master_cls: type[T], + make_parser: Callable[[options.Options], argparse.ArgumentParser], + arguments: Sequence[str], + extra: Callable[[Any], dict] = None, +) -> T: # pragma: no cover """ - extra: Extra argument processing callable which returns a dict of - options. + extra: Extra argument processing callable which returns a dict of + options. """ - debug.register_info_dumpers() - - opts = options.Options() - master = master_cls(opts) - - parser = make_parser(opts) - - # To make migration from 2.x to 3.0 bearable. - if "-R" in sys.argv and sys.argv[sys.argv.index("-R") + 1].startswith("http"): - print("To use mitmproxy in reverse mode please use --mode reverse:SPEC instead") - - try: - args = parser.parse_args(arguments) - except SystemExit: - arg_check.check() - sys.exit(1) - - try: - opts.set(*args.setoptions, defer=True) - optmanager.load_paths( - opts, - os.path.join(opts.confdir, "config.yaml"), - os.path.join(opts.confdir, "config.yml"), - ) - process_options(parser, opts, args) - - if args.options: - optmanager.dump_defaults(opts, sys.stdout) - sys.exit(0) - if args.commands: - master.commands.dump() - sys.exit(0) - if extra: - if args.filter_args: - master.log.info(f"Only processing flows that match \"{' & '.join(args.filter_args)}\"") - opts.update(**extra(args)) - - loop = asyncio.get_event_loop() + + async def main() -> T: + debug.register_info_dumpers() + + opts = options.Options() + master = master_cls(opts) + + parser = make_parser(opts) + + # To make migration from 2.x to 3.0 bearable. + if "-R" in sys.argv and sys.argv[sys.argv.index("-R") + 1].startswith("http"): + print( + "To use mitmproxy in reverse mode please use --mode reverse:SPEC instead" + ) + + try: + args = parser.parse_args(arguments) + except SystemExit: + arg_check.check() + sys.exit(1) + try: - loop.add_signal_handler(signal.SIGINT, getattr(master, "prompt_for_exit", master.shutdown)) - loop.add_signal_handler(signal.SIGTERM, master.shutdown) - except NotImplementedError: - # Not supported on Windows - pass - - # Make sure that we catch KeyboardInterrupts on Windows. - # https://stackoverflow.com/a/36925722/934719 - if os.name == "nt": - async def wakeup(): - while True: - await asyncio.sleep(0.2) - asyncio.ensure_future(wakeup()) - - master.run() - except exceptions.OptionsError as e: - print("{}: {}".format(sys.argv[0], e), file=sys.stderr) - sys.exit(1) - except (KeyboardInterrupt, RuntimeError): - pass - return master - - -def mitmproxy(args=None) -> typing.Optional[int]: # pragma: no cover - if os.name == "nt": - import urwid - urwid.set_encoding("utf8") - else: - assert_utf8_env() + opts.set(*args.setoptions, defer=True) + optmanager.load_paths( + opts, + os.path.join(opts.confdir, "config.yaml"), + os.path.join(opts.confdir, "config.yml"), + ) + process_options(parser, opts, args) + + if args.options: + optmanager.dump_defaults(opts, sys.stdout) + sys.exit(0) + if args.commands: + master.commands.dump() + sys.exit(0) + if extra: + if args.filter_args: + master.log.info( + f"Only processing flows that match \"{' & '.join(args.filter_args)}\"" + ) + opts.update(**extra(args)) + + except exceptions.OptionsError as e: + print(f"{sys.argv[0]}: {e}", file=sys.stderr) + sys.exit(1) + + loop = asyncio.get_running_loop() + + def _sigint(*_): + loop.call_soon_threadsafe( + getattr(master, "prompt_for_exit", master.shutdown) + ) + + def _sigterm(*_): + loop.call_soon_threadsafe(master.shutdown) + + # We can't use loop.add_signal_handler because that's not available on Windows' Proactorloop, + # but signal.signal just works fine for our purposes. + signal.signal(signal.SIGINT, _sigint) + signal.signal(signal.SIGTERM, _sigterm) + + await master.run() + return master + + return asyncio.run(main()) + + +def mitmproxy(args=None) -> Optional[int]: # pragma: no cover from mitmproxy.tools import console + run(console.master.ConsoleMaster, cmdline.mitmproxy, args) return None -def mitmdump(args=None) -> typing.Optional[int]: # pragma: no cover +def mitmdump(args=None) -> Optional[int]: # pragma: no cover from mitmproxy.tools import dump def extra(args): @@ -144,13 +134,12 @@ def extra(args): ) return {} - m = run(dump.DumpMaster, cmdline.mitmdump, args, extra) - if m and m.errorcheck.has_errored: # type: ignore - return 1 + run(dump.DumpMaster, cmdline.mitmdump, args, extra) return None -def mitmweb(args=None) -> typing.Optional[int]: # pragma: no cover +def mitmweb(args=None) -> Optional[int]: # pragma: no cover from mitmproxy.tools import web + run(web.master.WebMaster, cmdline.mitmweb, args) return None diff --git a/mitmproxy/tools/web/__init__.py b/mitmproxy/tools/web/__init__.py index c2252af39e..708a2195c0 100644 --- a/mitmproxy/tools/web/__init__.py +++ b/mitmproxy/tools/web/__init__.py @@ -1,2 +1,3 @@ from mitmproxy.tools.web import master + __all__ = ["master"] diff --git a/mitmproxy/tools/web/app.py b/mitmproxy/tools/web/app.py index 8746c7e8e8..dbb7490789 100644 --- a/mitmproxy/tools/web/app.py +++ b/mitmproxy/tools/web/app.py @@ -4,9 +4,10 @@ import logging import os.path import re +from collections.abc import Sequence from io import BytesIO from itertools import islice -from typing import ClassVar, Optional, Sequence, Union +from typing import ClassVar, Optional, Union import tornado.escape import tornado.web @@ -21,6 +22,7 @@ from mitmproxy import log from mitmproxy import optmanager from mitmproxy import version +from mitmproxy.dns import DNSFlow from mitmproxy.http import HTTPFlow from mitmproxy.tcp import TCPFlow, TCPMessage from mitmproxy.utils.emoji import emoji @@ -59,6 +61,7 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: "modified": flow.modified(), "marked": emoji.get(flow.marked, "🔴") if flow.marked else "", "comment": flow.comment, + "timestamp_created": flow.timestamp_created, } if flow.client_conn: @@ -139,14 +142,20 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: "timestamp_end": flow.response.timestamp_end, } if flow.response.data.trailers: - f["response"]["trailers"] = tuple(flow.response.data.trailers.items(True)) + f["response"]["trailers"] = tuple( + flow.response.data.trailers.items(True) + ) if flow.websocket: f["websocket"] = { "messages_meta": { - "contentLength": sum(len(x.content) for x in flow.websocket.messages), + "contentLength": sum( + len(x.content) for x in flow.websocket.messages + ), "count": len(flow.websocket.messages), - "timestamp_last": flow.websocket.messages[-1].timestamp if flow.websocket.messages else None, + "timestamp_last": flow.websocket.messages[-1].timestamp + if flow.websocket.messages + else None, }, "closed_by_client": flow.websocket.closed_by_client, "close_code": flow.websocket.close_code, @@ -159,6 +168,10 @@ def flow_to_json(flow: mitmproxy.flow.Flow) -> dict: "count": len(flow.messages), "timestamp_last": flow.messages[-1].timestamp if flow.messages else None, } + elif isinstance(flow, DNSFlow): + f["request"] = flow.request.to_json() + if flow.response: + f["response"] = flow.response.to_json() return f @@ -167,7 +180,7 @@ def logentry_to_json(e: log.LogEntry) -> dict: return { "id": id(e), # we just need some kind of id. "message": e.msg, - "level": e.level + "level": e.level, } @@ -196,17 +209,19 @@ def set_default_headers(self): "Content-Security-Policy", "default-src 'self'; " "connect-src 'self' ws:; " - "style-src 'self' 'unsafe-inline'" + "style-src 'self' 'unsafe-inline'", ) @property def json(self): - if not self.request.headers.get("Content-Type", "").startswith("application/json"): + if not self.request.headers.get("Content-Type", "").startswith( + "application/json" + ): raise APIError(400, "Invalid Content-Type, expected application/json.") try: return json.loads(self.request.body.decode()) except Exception as e: - raise APIError(400, "Malformed JSON: {}".format(str(e))) + raise APIError(400, f"Malformed JSON: {str(e)}") @property def filecontents(self): @@ -253,9 +268,7 @@ def get(self): class FilterHelp(RequestHandler): def get(self): - self.write(dict( - commands=flowfilter.help - )) + self.write(dict(commands=flowfilter.help)) class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler): @@ -270,7 +283,9 @@ def on_close(self): @classmethod def broadcast(cls, **kwargs): - message = json.dumps(kwargs, ensure_ascii=False).encode("utf8", "surrogateescape") + message = json.dumps(kwargs, ensure_ascii=False).encode( + "utf8", "surrogateescape" + ) for conn in cls.connections: try: @@ -464,9 +479,11 @@ def message_to_json( viewname: str, message: Union[http.Message, TCPMessage, WebSocketMessage], flow: Union[HTTPFlow, TCPFlow], - max_lines: Optional[int] = None + max_lines: Optional[int] = None, ): - description, lines, error = contentviews.get_message_content_view(viewname, message, flow) + description, lines, error = contentviews.get_message_content_view( + viewname, message, flow + ) if error: self.master.log.error(error) if max_lines: @@ -523,7 +540,9 @@ def get(self) -> None: } for param in cmd.parameters ], - "return_type": command.typename(cmd.return_type) if cmd.return_type else None, + "return_type": command.typename(cmd.return_type) + if cmd.return_type + else None, "signature_help": cmd.signature_help(), } self.write(commands) @@ -533,20 +552,20 @@ class ExecuteCommand(RequestHandler): def post(self, cmd: str): # TODO: We should parse query strings here, this API is painful. try: - args = self.json['arguments'] + args = self.json["arguments"] except APIError: args = [] try: result = self.master.commands.call_strings(cmd, args) except Exception as e: - self.write({ - "error": str(e) - }) + self.write({"error": str(e)}) else: - self.write({ - "value": result, - # "type": command.typename(type(result)) if result is not None else "none" - }) + self.write( + { + "value": result, + # "type": command.typename(type(result)) if result is not None else "none" + } + ) class Events(RequestHandler): @@ -580,7 +599,7 @@ 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)" + "(https://github.com/mitmproxy/mitmproxy/issues/3234)", ) @@ -589,7 +608,7 @@ def get(self): conf = { "static": False, "version": version.VERSION, - "contentViews": [v.name for v in contentviews.views if v.name != "Query"] + "contentViews": [v.name for v in contentviews.views if v.name != "Query"], } self.write(f"MITMWEB_CONF = {json.dumps(conf)};") self.set_header("content-type", "application/javascript") @@ -598,7 +617,9 @@ def get(self): class Application(tornado.web.Application): master: "mitmproxy.tools.web.master.WebMaster" - def __init__(self, master: "mitmproxy.tools.web.master.WebMaster", debug: bool) -> None: + def __init__( + self, master: "mitmproxy.tools.web.master.WebMaster", debug: bool + ) -> None: self.master = master super().__init__( default_host="dns-rebind-protection", @@ -613,7 +634,7 @@ def __init__(self, master: "mitmproxy.tools.web.master.WebMaster", debug: bool) 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.]+|\[[0-9a-fA-F:]+\])$', + r"^(localhost|[0-9.]+|\[[0-9a-fA-F:]+\])$", [ (r"/", IndexHandler), (r"/filter-help(?:\.json)?", FilterHelp), @@ -631,14 +652,18 @@ def __init__(self, master: "mitmproxy.tools.web.master.WebMaster", debug: bool) (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\-]+)/(?Prequest|response|messages)/content.data", FlowContent), + ( + r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response|messages)/content.data", + FlowContent, + ), ( r"/flows/(?P[0-9a-f\-]+)/(?Prequest|response|messages)/" r"content/(?P[0-9a-zA-Z\-\_%]+)(?:\.json)?", - FlowContentView), + FlowContentView, + ), (r"/clear", ClearAll), (r"/options(?:\.json)?", Options), (r"/options/save", SaveOptions), (r"/conf\.js", Conf), - ] + ], ) diff --git a/mitmproxy/tools/web/master.py b/mitmproxy/tools/web/master.py index 704ddd0044..1937fc311a 100644 --- a/mitmproxy/tools/web/master.py +++ b/mitmproxy/tools/web/master.py @@ -1,16 +1,16 @@ import tornado.httpserver import tornado.ioloop -from tornado.platform.asyncio import AsyncIOMainLoop from mitmproxy import addons from mitmproxy import log from mitmproxy import master from mitmproxy import optmanager -from mitmproxy.addons import eventstore +from mitmproxy.addons import errorcheck, eventstore from mitmproxy.addons import intercept from mitmproxy.addons import readfile from mitmproxy.addons import termlog from mitmproxy.addons import view +from mitmproxy.contrib.tornado import patch_tornado from mitmproxy.tools.web import app, webaddons, static_viewer @@ -29,6 +29,8 @@ def __init__(self, options, with_termlog=True): self.options.changed.connect(self._sig_options_update) + if with_termlog: + self.addons.add(termlog.TermLog()) self.addons.add(*addons.default_addons()) self.addons.add( webaddons.WebAddon(), @@ -37,68 +39,51 @@ def __init__(self, options, with_termlog=True): static_viewer.StaticViewer(), self.view, self.events, + errorcheck.ErrorCheck(), ) - if with_termlog: - self.addons.add(termlog.TermLog()) - self.app = app.Application( - self, self.options.web_debug - ) + self.app = app.Application(self, self.options.web_debug) def _sig_view_add(self, view, flow): app.ClientConnection.broadcast( - resource="flows", - cmd="add", - data=app.flow_to_json(flow) + resource="flows", cmd="add", data=app.flow_to_json(flow) ) def _sig_view_update(self, view, flow): app.ClientConnection.broadcast( - resource="flows", - cmd="update", - data=app.flow_to_json(flow) + resource="flows", cmd="update", data=app.flow_to_json(flow) ) def _sig_view_remove(self, view, flow, index): - app.ClientConnection.broadcast( - resource="flows", - cmd="remove", - data=flow.id - ) + app.ClientConnection.broadcast(resource="flows", cmd="remove", data=flow.id) def _sig_view_refresh(self, view): - app.ClientConnection.broadcast( - resource="flows", - cmd="reset" - ) + app.ClientConnection.broadcast(resource="flows", cmd="reset") def _sig_events_add(self, event_store, entry: log.LogEntry): app.ClientConnection.broadcast( - resource="events", - cmd="add", - data=app.logentry_to_json(entry) + resource="events", cmd="add", data=app.logentry_to_json(entry) ) def _sig_events_refresh(self, event_store): - app.ClientConnection.broadcast( - resource="events", - cmd="reset" - ) + app.ClientConnection.broadcast(resource="events", cmd="reset") def _sig_options_update(self, options, updated): options_dict = optmanager.dump_dicts(options, updated) app.ClientConnection.broadcast( - resource="options", - cmd="update", - data=options_dict + resource="options", cmd="update", data=options_dict ) - def run(self): # pragma: no cover - AsyncIOMainLoop().install() - iol = tornado.ioloop.IOLoop.instance() + async def running(self): + patch_tornado() + # Register tornado with the current event loop + tornado.ioloop.IOLoop.current() + + # Add our web app. http_server = tornado.httpserver.HTTPServer(self.app) http_server.listen(self.options.web_port, self.options.web_host) - web_url = f"http://{self.options.web_host}:{self.options.web_port}/" + self.log.info( - f"Web server listening at {web_url}", + f"Web server listening at http://{self.options.web_host}:{self.options.web_port}/", ) - self.run_loop(iol.start) + + return await super().running() diff --git a/mitmproxy/tools/web/static/app.css b/mitmproxy/tools/web/static/app.css index e5822dd36e..f729f67b2d 100644 --- a/mitmproxy/tools/web/static/app.css +++ b/mitmproxy/tools/web/static/app.css @@ -1,2 +1,2 @@ -html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}.resource-icon{width:32px;height:32px}.resource-icon-css{background-image:url(images/chrome-devtools/resourceCSSIcon.png)}.resource-icon-document{background-image:url(images/chrome-devtools/resourceDocumentIcon.png)}.resource-icon-js{background-image:url(images/chrome-devtools/resourceJSIcon.png)}.resource-icon-plain{background-image:url(images/chrome-devtools/resourcePlainIcon.png)}.resource-icon-executable{background-image:url(images/resourceExecutableIcon.png)}.resource-icon-flash{background-image:url(images/resourceFlashIcon.png)}.resource-icon-image{background-image:url(images/resourceImageIcon.png)}.resource-icon-java{background-image:url(images/resourceJavaIcon.png)}.resource-icon-not-modified{background-image:url(images/resourceNotModifiedIcon.png)}.resource-icon-redirect{background-image:url(images/resourceRedirectIcon.png)}.resource-icon-websocket{background-image:url(images/resourceWebSocketIcon.png)}.resource-icon-tcp{background-image:url(images/resourceTcpIcon.png)}#container,#mitmproxy,body,html{height:100%;margin:0;overflow:hidden}#container{display:flex;flex-direction:column;outline:0}#container>.eventlog,#container>footer,#container>header{flex:0 0 auto}.main-view{flex:1 1 auto;height:0;display:flex;flex-direction:row}.main-view.vertical{flex-direction:column}.main-view .flow-detail,.main-view .flow-table{flex:1 1 auto}.splitter{flex:0 0 1px;background-color:#aaa;position:relative}.splitter>div{position:absolute}.splitter.splitter-x{cursor:col-resize}.splitter.splitter-x>div{margin-left:-1px;width:4px;height:100%}.splitter.splitter-y{cursor:row-resize}.splitter.splitter-y>div{margin-top:-1px;height:4px;width:100%}.nav-tabs{border-bottom:solid #a6a6a6 1px}.nav-tabs>a{display:inline-block;border:solid transparent 1px;text-decoration:none}.nav-tabs>a.active{background-color:#fff;border-color:#a6a6a6;border-bottom-color:#fff}.nav-tabs>a.special{color:#fff;background-color:#396cad;border-bottom-color:#396cad}.nav-tabs>a.special:hover{background-color:#5386c6}.nav-tabs-lg>a{padding:3px 14px;margin:0 2px -1px}.nav-tabs-sm>a{padding:0 7px;margin:2px 2px -1px}header{padding-top:6px;background-color:#fff}header>div{display:block;margin:0;padding:0;border-bottom:solid #a6a6a6 1px;height:95px;overflow:visible}.menu-group{margin:0 5px 0 6px;display:inline-block;height:95px}.menu-content{height:79px;display:flow-root}.menu-content>a{display:inline-block}.menu-content>.btn,.menu-content>a>.btn{height:79px;text-align:center;margin:0 1px;padding:12px 5px;border:none;border-radius:0}.menu-content>.btn i,.menu-content>a>.btn i{font-size:20px;display:block;margin:0 auto 5px}.menu-content>.btn.btn-sm{height:26.33333333px;padding:0 5px}.menu-content>.btn.btn-sm i{display:inline-block;font-size:14px;margin:0}.menu-entry{text-align:left;height:26.33333333px;line-height:1;padding:.5rem 1rem}.menu-entry label{font-size:1.2rem;font-weight:400;margin:0}.menu-entry input[type=checkbox]{margin:0 2px;vertical-align:middle}.menu-legend{color:#777;height:16px;text-align:center;font-size:12px;padding:0 5px}.menu-group+.menu-group:before{margin-left:-6px;content:" ";border-left:solid 1px #e6e6e6;margin-top:10px;height:75px;position:absolute}.main-menu{display:flex}.main-menu .menu-group{width:50%}.main-menu .btn-sm{margin-top:6px}.filter-input{margin:4px 0}.filter-input .popover{top:27px;left:43px;display:block;max-width:none;opacity:.9}@media (max-width:767px){.filter-input .popover{top:16px;left:29px;right:2px}}.filter-input .popover .popover-content{max-height:500px;overflow-y:auto}.filter-input .popover .popover-content tr{cursor:pointer}.filter-input .popover .popover-content tr:hover{background-color:hsla(209,52%,84%,.5)!important}.connection-indicator{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em;float:right;margin:5px;opacity:1;transition:all 1s linear}a.connection-indicator:focus,a.connection-indicator:hover{color:#fff;text-decoration:none;cursor:pointer}.connection-indicator:empty{display:none}.btn .connection-indicator{position:relative;top:-1px}.connection-indicator.fetching,.connection-indicator.init{background-color:#5bc0de}.connection-indicator.established{background-color:#5cb85c;opacity:0}.connection-indicator.error{background-color:#d9534f;transition:all .2s linear}.connection-indicator.offline{background-color:#f0ad4e;opacity:1}.flow-table{width:100%;overflow-y:scroll;overflow-x:hidden}.flow-table table{width:100%;table-layout:fixed}.flow-table thead tr{background-color:#f2f2f2;border-bottom:solid #bebebe 1px;line-height:23px}.flow-table th{font-weight:400;position:relative!important;padding-left:1px;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.flow-table th.sort-asc,.flow-table th.sort-desc{background-color:#fafafa}.flow-table th.sort-asc:after,.flow-table th.sort-desc:after{font:normal normal normal 14px/1 FontAwesome;position:absolute;right:3px;top:3px;padding:2px;background-color:rgba(250,250,250,.8)}.flow-table th.sort-asc:after{content:"\f0de"}.flow-table th.sort-desc:after{content:"\f0dd"}.flow-table tr{cursor:pointer;background-color:#fff}.flow-table tr:nth-child(even){background-color:#f2f2f2}.flow-table tr.selected{background-color:#e0ebf5!important}.flow-table tr.highlighted{background-color:#ffeb99}.flow-table tr.highlighted:nth-child(even){background-color:#ffe57f}.flow-table td{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.flow-table tr.intercepted:not(.has-response) .col-method,.flow-table tr.intercepted:not(.has-response) .col-path{color:#ff7f00}.flow-table tr.intercepted.has-response .col-size,.flow-table tr.intercepted.has-response .col-status,.flow-table tr.intercepted.has-response .col-time{color:#ff7f00}.flow-table .fa{line-height:inherit}.flow-table .col-tls{width:10px}.flow-table .col-tls-https{background-color:rgba(0,185,0,.5)}.flow-table .col-icon{width:32px}.flow-table .col-path .fa{margin-left:0;font-size:16px}.flow-table .col-path .fa-repeat{color:green}.flow-table .col-path .fa-pause{color:#ff7f00}.flow-table .col-path .fa-exclamation,.flow-table .col-path .fa-times{color:#8b0000}.flow-table .col-method{width:60px}.flow-table .col-status{width:50px}.flow-table .col-size{width:70px}.flow-table .col-time{width:50px}.flow-table .col-timestamp{width:auto}.flow-table td.col-size,.flow-table td.col-time{text-align:right}.flow-table .col-quickactions{width:0;direction:rtl;overflow:hidden;background-color:inherit;font-size:20px}.flow-table .col-quickactions *{direction:ltr}.flow-table .col-quickactions.hover,.flow-table tr:hover .col-quickactions{overflow:visible}.flow-table .col-quickactions>div{height:32px;background-color:inherit;display:inline-flex;align-items:center}.flow-table .col-quickactions>div>a{margin-right:2px;height:32px;width:32px;border-radius:16px;text-align:center}.flow-table .col-quickactions>div>a:hover{background-color:rgba(0,0,0,.05)}.flow-table .col-quickactions .fa-play{transform:translate(1px,2px)}.flow-table .col-quickactions .fa-repeat{transform:translate(0,2px)}.flow-detail{width:100%;overflow:hidden;display:flex;flex-direction:column}.flow-detail nav{background-color:#f2f2f2}.flow-detail section{overflow-y:scroll;flex:1;padding:5px 12px 10px}.flow-detail section>footer{box-shadow:0 0 3px gray;padding:2px;margin:0;height:23px}.flow-detail .first-line{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;background-color:#428bca;color:#fff;margin:0 -8px 2px;padding:4px 8px;border-radius:5px;word-break:break-all;max-height:100px;overflow-y:auto}.flow-detail .contentview{margin:0 -12px;padding:0 12px}.flow-detail .contentview .controls{display:flex;align-items:center}.flow-detail .contentview .controls h5{flex:1;font-size:12px;font-weight:700;margin:10px 0}.flow-detail .contentview pre button:not(:only-child){margin-top:6px}.flow-detail hr{margin:0}.inline-input{display:inline;margin:0 -3px;padding:0 3px;border:solid transparent 1px}.inline-input:hover{box-shadow:0 0 0 1px rgba(0,0,0,.0125),0 2px 4px rgba(0,0,0,.05),0 2px 6px rgba(0,0,0,.025);background-color:rgba(255,255,255,.1)}.inline-input[placeholder]:empty:not(:focus-visible):before{content:attr(placeholder);color:#d3d3d3;font-style:italic}.inline-input[contenteditable]{outline-width:0;box-shadow:0 0 0 1px rgba(0,0,0,.05),0 2px 4px rgba(0,0,0,.2),0 2px 6px rgba(0,0,0,.1);background-color:rgba(255,255,255,.2)}.inline-input[contenteditable].has-warning{color:#ffb8b8}.flow-detail table{width:100%;table-layout:fixed;word-break:break-all}.flow-detail table td:nth-child(2){font-family:Menlo,Monaco,Consolas,"Courier New",monospace;width:70%}.flow-detail table tr:not(:first-child){border-top:1px solid #f7f7f7}.flow-detail table td{vertical-align:top}.connection-table td:first-child{padding-right:1em}.headers,.trailers{position:relative;min-height:2ex;overflow-wrap:break-word}.headers .kv-row,.trailers .kv-row{margin-bottom:.3em;max-height:12.4ex;overflow-y:auto}.headers .kv-key,.trailers .kv-key{font-weight:700}.headers .kv-value,.trailers .kv-value{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}.headers .inline-input,.trailers .inline-input{background-color:#fff}.headers .kv-add-row,.trailers .kv-add-row{opacity:0;color:#666;position:absolute;bottom:4px;right:4px;transition:all .1s ease-in-out}.headers:hover .kv-add-row,.trailers:hover .kv-add-row{opacity:1}.connection-table td,.timing-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}dl.cert-attributes{display:flex;flex-flow:row;flex-wrap:wrap;margin-bottom:0}dl.cert-attributes dd,dl.cert-attributes dt{text-overflow:ellipsis;overflow:hidden}dl.cert-attributes dt{flex:0 0 2em}dl.cert-attributes dd{flex:0 0 calc(100% - 2em)}.flowview-image{text-align:center;padding:10px 0}.flowview-image img{max-width:100%;max-height:100%}.edit-flow-container{position:fixed;right:20px}.edit-flow{cursor:pointer;position:absolute;right:0;top:5px;height:40px;width:40px;border-radius:20px;z-index:10000;background-color:rgba(255,255,255,.7);border:solid 2px rgba(248,145,59,.7);text-align:center;font-size:22px;line-height:37px;transition:all .1s ease-in-out}.edit-flow:hover{background-color:rgba(239,108,0,.7);color:rgba(0,0,0,.8);border:solid 2px transparent}.eventlog{height:200px;flex:0 0 auto;display:flex;flex-direction:column}.eventlog>div{background-color:#f2f2f2;padding:0 5px;flex:0 0 auto;border-top:1px solid #aaa;cursor:row-resize}.eventlog>pre{flex:1 1 auto;margin:0;border-radius:0;overflow-x:auto;overflow-y:scroll;background-color:#fcfcfc}.eventlog .fa-close{cursor:pointer;float:right;color:grey;padding:3px 0;padding-left:10px}.eventlog .fa-close:hover{color:#000}.eventlog .btn-toggle{margin-top:-2px;margin-left:3px;padding:2px 2px;font-size:10px;line-height:10px;border-radius:2px}.eventlog .label{cursor:pointer;vertical-align:middle;display:inline-block;margin-top:-2px;margin-left:3px}footer{box-shadow:0 -1px 3px #d3d3d3;padding:0 0 4px 3px}footer .label{margin-right:3px}.CodeMirror{border:1px solid #ccc;height:auto!important}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor-mark{background-color:rgba(20,255,20,.5);-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite}.cm-animate-fat-cursor{width:auto;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none;outline:0}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.contentview .header{font-weight:700}.contentview .highlight{font-weight:700}.contentview .offset{color:#00f}.contentview .codeeditor{margin-bottom:12px}.modal-visible{display:block}.modal-dialog{overflow-y:initial!important}.modal-body{max-height:calc(100vh - 200px);overflow-y:auto}.dropdown-menu{margin:0!important}.dropdown-menu>li>a{padding:3px 10px}.command-title{background-color:#f2f2f2;border:1px solid #aaa}.command-result{display:block;margin:0;background-color:#fcfcfc;height:100px;max-height:100px;overflow:auto}.command-suggestion{background-color:#9c9c9c}.argument-suggestion{background-color:hsla(209,52%,84%,.5)!important}.command>.popover{display:block;position:relative;max-width:none}.available-commands{overflow:auto} +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}.resource-icon{width:32px;height:32px}.resource-icon-css{background-image:url(images/chrome-devtools/resourceCSSIcon.png)}.resource-icon-document{background-image:url(images/chrome-devtools/resourceDocumentIcon.png)}.resource-icon-js{background-image:url(images/chrome-devtools/resourceJSIcon.png)}.resource-icon-plain{background-image:url(images/chrome-devtools/resourcePlainIcon.png)}.resource-icon-executable{background-image:url(images/resourceExecutableIcon.png)}.resource-icon-flash{background-image:url(images/resourceFlashIcon.png)}.resource-icon-image{background-image:url(images/resourceImageIcon.png)}.resource-icon-java{background-image:url(images/resourceJavaIcon.png)}.resource-icon-not-modified{background-image:url(images/resourceNotModifiedIcon.png)}.resource-icon-redirect{background-image:url(images/resourceRedirectIcon.png)}.resource-icon-websocket{background-image:url(images/resourceWebSocketIcon.png)}.resource-icon-tcp{background-image:url(images/resourceTcpIcon.png)}.resource-icon-dns{background-image:url(images/resourceDnsIcon.png)}#container,#mitmproxy,body,html{height:100%;margin:0;overflow:hidden}#container{display:flex;flex-direction:column;outline:0}#container>.eventlog,#container>footer,#container>header{flex:0 0 auto}.main-view{flex:1 1 auto;height:0;display:flex;flex-direction:row}.main-view.vertical{flex-direction:column}.main-view .flow-detail,.main-view .flow-table{flex:1 1 auto}.splitter{flex:0 0 1px;background-color:#aaa;position:relative}.splitter>div{position:absolute}.splitter.splitter-x{cursor:col-resize}.splitter.splitter-x>div{margin-left:-1px;width:4px;height:100%}.splitter.splitter-y{cursor:row-resize}.splitter.splitter-y>div{margin-top:-1px;height:4px;width:100%}.nav-tabs{border-bottom:solid #a6a6a6 1px}.nav-tabs>a{display:inline-block;border:solid transparent 1px;text-decoration:none}.nav-tabs>a.active{background-color:#fff;border-color:#a6a6a6;border-bottom-color:#fff}.nav-tabs>a.special{color:#fff;background-color:#396cad;border-bottom-color:#396cad}.nav-tabs>a.special:hover{background-color:#5386c6}.nav-tabs-lg>a{padding:3px 14px;margin:0 2px -1px}.nav-tabs-sm>a{padding:0 7px;margin:2px 2px -1px}header{padding-top:6px;background-color:#fff}header>div{display:block;margin:0;padding:0;border-bottom:solid #a6a6a6 1px;height:95px;overflow:visible}.menu-group{margin:0 5px 0 6px;display:inline-block;height:95px}.menu-content{height:79px;display:flow-root}.menu-content>a{display:inline-block}.menu-content>.btn,.menu-content>a>.btn{height:79px;text-align:center;margin:0 1px;padding:12px 5px;border:none;border-radius:0}.menu-content>.btn i,.menu-content>a>.btn i{font-size:20px;display:block;margin:0 auto 5px}.menu-content>.btn.btn-sm{height:26.33333333px;padding:0 5px}.menu-content>.btn.btn-sm i{display:inline-block;font-size:14px;margin:0}.menu-entry{text-align:left;height:26.33333333px;line-height:1;padding:.5rem 1rem}.menu-entry label{font-size:1.2rem;font-weight:400;margin:0}.menu-entry input[type=checkbox]{margin:0 2px;vertical-align:middle}.menu-legend{color:#777;height:16px;text-align:center;font-size:12px;padding:0 5px}.menu-group+.menu-group:before{margin-left:-6px;content:" ";border-left:solid 1px #e6e6e6;margin-top:10px;height:75px;position:absolute}.main-menu{display:flex}.main-menu .menu-group{width:50%}.main-menu .btn-sm{margin-top:6px}.filter-input{margin:4px 0}.filter-input .popover{top:27px;left:43px;display:block;max-width:none;opacity:.9}@media (max-width:767px){.filter-input .popover{top:16px;left:29px;right:2px}}.filter-input .popover .popover-content{max-height:500px;overflow-y:auto}.filter-input .popover .popover-content tr{cursor:pointer}.filter-input .popover .popover-content tr:hover{background-color:hsla(209,52%,84%,.5)!important}.connection-indicator{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em;float:right;margin:5px;opacity:1;transition:all 1s linear}a.connection-indicator:focus,a.connection-indicator:hover{color:#fff;text-decoration:none;cursor:pointer}.connection-indicator:empty{display:none}.btn .connection-indicator{position:relative;top:-1px}.connection-indicator.fetching,.connection-indicator.init{background-color:#5bc0de}.connection-indicator.established{background-color:#5cb85c;opacity:0}.connection-indicator.error{background-color:#d9534f;transition:all .2s linear}.connection-indicator.offline{background-color:#f0ad4e;opacity:1}.flow-table{width:100%;overflow-y:scroll;overflow-x:hidden}.flow-table table{width:100%;table-layout:fixed}.flow-table thead tr{background-color:#f2f2f2;border-bottom:solid #bebebe 1px;line-height:23px}.flow-table th{font-weight:400;position:relative!important;padding-left:1px;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.flow-table th.sort-asc,.flow-table th.sort-desc{background-color:#fafafa}.flow-table th.sort-asc:after,.flow-table th.sort-desc:after{font:normal normal normal 14px/1 FontAwesome;position:absolute;right:3px;top:3px;padding:2px;background-color:rgba(250,250,250,.8)}.flow-table th.sort-asc:after{content:"\f0de"}.flow-table th.sort-desc:after{content:"\f0dd"}.flow-table tr{cursor:pointer;background-color:#fff}.flow-table tr:nth-child(even){background-color:#f2f2f2}.flow-table tr.selected{background-color:#e0ebf5!important}.flow-table tr.selected.highlighted{background-color:#7bbefc!important}.flow-table tr.highlighted{background-color:#ffeb99}.flow-table tr.highlighted:nth-child(even){background-color:#ffe57f}.flow-table td{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.flow-table tr.intercepted:not(.has-response) .col-method,.flow-table tr.intercepted:not(.has-response) .col-path{color:#ff7f00}.flow-table tr.intercepted.has-response .col-size,.flow-table tr.intercepted.has-response .col-status,.flow-table tr.intercepted.has-response .col-time{color:#ff7f00}.flow-table .fa{line-height:inherit}.flow-table .col-tls{width:10px}.flow-table .col-tls-https{background-color:rgba(0,185,0,.5)}.flow-table .col-icon{width:32px}.flow-table .col-path .fa{margin-left:0;font-size:16px}.flow-table .col-path .fa-repeat{color:green}.flow-table .col-path .fa-pause{color:#ff7f00}.flow-table .col-path .fa-exclamation,.flow-table .col-path .fa-times{color:#8b0000}.flow-table .col-method{width:60px}.flow-table .col-status{width:50px}.flow-table .col-size{width:70px}.flow-table .col-time{width:50px}.flow-table .col-timestamp{width:170px}.flow-table td.col-size,.flow-table td.col-time,.flow-table td.col-timestamp{text-align:right}.flow-table .col-quickactions{width:0;direction:rtl;overflow:hidden;background-color:inherit;font-size:20px}.flow-table .col-quickactions *{direction:ltr}.flow-table .col-quickactions.hover,.flow-table tr:hover .col-quickactions{overflow:visible}.flow-table .col-quickactions>div{height:32px;background-color:inherit;display:inline-flex;align-items:center}.flow-table .col-quickactions>div>a{margin-right:2px;height:32px;width:32px;border-radius:16px;text-align:center}.flow-table .col-quickactions>div>a:hover{background-color:rgba(0,0,0,.05)}.flow-table .col-quickactions .fa-play{transform:translate(1px,2px)}.flow-table .col-quickactions .fa-repeat{transform:translate(0,2px)}.flow-detail{width:100%;overflow:hidden;display:flex;flex-direction:column}.flow-detail nav{background-color:#f2f2f2}.flow-detail section{overflow-y:scroll;flex:1;padding:5px 12px 10px}.flow-detail section>footer{box-shadow:0 0 3px gray;padding:2px;margin:0;height:23px}.flow-detail .first-line{font-family:Menlo,Monaco,Consolas,"Courier New",monospace;background-color:#428bca;color:#fff;margin:0 -8px 2px;padding:4px 8px;border-radius:5px;word-break:break-all;max-height:100px;overflow-y:auto}.flow-detail .contentview{margin:0 -12px;padding:0 12px}.flow-detail .contentview .controls{display:flex;align-items:center}.flow-detail .contentview .controls h5{flex:1;font-size:12px;font-weight:700;margin:10px 0}.flow-detail .contentview pre button:not(:only-child){margin-top:6px}.flow-detail hr{margin:0}.inline-input{display:inline;margin:0 -3px;padding:0 3px;border:solid transparent 1px}.inline-input:hover{box-shadow:0 0 0 1px rgba(0,0,0,.0125),0 2px 4px rgba(0,0,0,.05),0 2px 6px rgba(0,0,0,.025);background-color:rgba(255,255,255,.1)}.inline-input[placeholder]:empty:not(:focus-visible):before{content:attr(placeholder);color:#d3d3d3;font-style:italic}.inline-input[contenteditable]{outline-width:0;box-shadow:0 0 0 1px rgba(0,0,0,.05),0 2px 4px rgba(0,0,0,.2),0 2px 6px rgba(0,0,0,.1);background-color:rgba(255,255,255,.2)}.inline-input[contenteditable].has-warning{color:#ffb8b8}.certificate-table,.connection-table,.timing-table{width:100%;table-layout:fixed;word-break:break-all}.certificate-table td:nth-child(2),.connection-table td:nth-child(2),.timing-table td:nth-child(2){font-family:Menlo,Monaco,Consolas,"Courier New",monospace;width:70%}.certificate-table tr:not(:first-child),.connection-table tr:not(:first-child),.timing-table tr:not(:first-child){border-top:1px solid #f7f7f7}.certificate-table td,.connection-table td,.timing-table td{vertical-align:top}.connection-table td:first-child{padding-right:1em}.headers,.trailers{position:relative;min-height:2ex;overflow-wrap:break-word}.headers .kv-row,.trailers .kv-row{margin-bottom:.3em;max-height:12.4ex;overflow-y:auto}.headers .kv-key,.trailers .kv-key{font-weight:700}.headers .kv-value,.trailers .kv-value{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}.headers .inline-input,.trailers .inline-input{background-color:#fff}.headers .kv-add-row,.trailers .kv-add-row{opacity:0;color:#666;position:absolute;bottom:4px;right:4px;transition:all .1s ease-in-out}.headers:hover .kv-add-row,.trailers:hover .kv-add-row{opacity:1}.connection-table td,.timing-table td{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}dl.cert-attributes{display:flex;flex-flow:row;flex-wrap:wrap;margin-bottom:0}dl.cert-attributes dd,dl.cert-attributes dt{text-overflow:ellipsis;overflow:hidden}dl.cert-attributes dt{flex:0 0 2em}dl.cert-attributes dd{flex:0 0 calc(100% - 2em)}.dns-request table td,.dns-request table th,.dns-response table td,.dns-response table th{padding-right:1rem}.flowview-image{text-align:center;padding:10px 0}.flowview-image img{max-width:100%;max-height:100%}.edit-flow-container{position:fixed;right:20px}.edit-flow{cursor:pointer;position:absolute;right:0;top:5px;height:40px;width:40px;border-radius:20px;z-index:10000;background-color:rgba(255,255,255,.7);border:solid 2px rgba(248,145,59,.7);text-align:center;font-size:22px;line-height:37px;transition:all .1s ease-in-out}.edit-flow:hover{background-color:rgba(239,108,0,.7);color:rgba(0,0,0,.8);border:solid 2px transparent}.eventlog{height:200px;flex:0 0 auto;display:flex;flex-direction:column}.eventlog>div{background-color:#f2f2f2;padding:0 5px;flex:0 0 auto;border-top:1px solid #aaa;cursor:row-resize}.eventlog>pre{flex:1 1 auto;margin:0;border-radius:0;overflow-x:auto;overflow-y:scroll;background-color:#fcfcfc}.eventlog .fa-close{cursor:pointer;float:right;color:grey;padding:3px 0;padding-left:10px}.eventlog .fa-close:hover{color:#000}.eventlog .btn-toggle{margin-top:-2px;margin-left:3px;padding:2px 2px;font-size:10px;line-height:10px;border-radius:2px}.eventlog .label{cursor:pointer;vertical-align:middle;display:inline-block;margin-top:-2px;margin-left:3px}footer{box-shadow:0 -1px 3px #d3d3d3;padding:0 0 4px 3px}footer .label{margin-right:3px}.CodeMirror{border:1px solid #ccc;height:auto!important}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-fat-cursor-mark{background-color:rgba(20,255,20,.5);-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite}.cm-animate-fat-cursor{width:auto;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:0;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta{color:#555}.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-s-default .cm-error{color:red}.cm-invalidchar{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0b0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#a22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-50px;margin-right:-50px;padding-bottom:50px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:50px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none;outline:0}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-50px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre.CodeMirror-line,.CodeMirror pre.CodeMirror-line-like{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre.CodeMirror-line,.CodeMirror-wrap pre.CodeMirror-line-like{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;padding:.1px}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-focused div.CodeMirror-cursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.contentview .header{font-weight:700}.contentview .highlight{font-weight:700}.contentview .offset{color:#00f}.contentview .codeeditor{margin-bottom:12px}.modal-visible{display:block}.modal-dialog{overflow-y:initial!important}.modal-body{max-height:calc(100vh - 200px);overflow-y:auto}.dropdown-menu{margin:0!important}.dropdown-menu>li>a{padding:3px 10px}.command-title{background-color:#f2f2f2;border:1px solid #aaa}.command-result{display:block;margin:0;background-color:#fcfcfc;height:100px;max-height:100px;overflow:auto}.command-suggestion{background-color:#9c9c9c}.argument-suggestion{background-color:hsla(209,52%,84%,.5)!important}.command>.popover{display:block;position:relative;max-width:none}.available-commands{overflow:auto} /*# sourceMappingURL=app.css.map */ diff --git a/mitmproxy/tools/web/static/app.js b/mitmproxy/tools/web/static/app.js index fd773568ce..afb0f93e15 100644 --- a/mitmproxy/tools/web/static/app.js +++ b/mitmproxy/tools/web/static/app.js @@ -1,66 +1,66 @@ -(()=>{var jM=Object.create;var vc=Object.defineProperty,$M=Object.defineProperties,qM=Object.getOwnPropertyDescriptor,VM=Object.getOwnPropertyDescriptors,KM=Object.getOwnPropertyNames,nv=Object.getOwnPropertySymbols,GM=Object.getPrototypeOf,D0=Object.prototype.hasOwnProperty,xC=Object.prototype.propertyIsEnumerable;var R0=(e,t,n)=>t in e?vc(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Le=(e,t)=>{for(var n in t||(t={}))D0.call(t,n)&&R0(e,n,t[n]);if(nv)for(var n of nv(t))xC.call(t,n)&&R0(e,n,t[n]);return e},Ft=(e,t)=>$M(e,VM(t)),SC=e=>vc(e,"__esModule",{value:!0}),o=(e,t)=>vc(e,"name",{value:t,configurable:!0});var Fs=(e,t)=>{var n={};for(var l in e)D0.call(e,l)&&t.indexOf(l)<0&&(n[l]=e[l]);if(e!=null&&nv)for(var l of nv(e))t.indexOf(l)<0&&xC.call(e,l)&&(n[l]=e[l]);return n};var lr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),CC=(e,t)=>{SC(e);for(var n in t)vc(e,n,{get:t[n],enumerable:!0})},YM=(e,t,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of KM(t))!D0.call(e,l)&&l!=="default"&&vc(e,l,{get:()=>t[l],enumerable:!(n=qM(t,l))||n.enumerable});return e},pe=e=>YM(SC(vc(e!=null?jM(GM(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var gc=(e,t,n)=>(R0(e,typeof t!="symbol"?t+"":t,n),n);var Oa=(e,t,n)=>new Promise((l,d)=>{var v=_=>{try{w(n.next(_))}catch(O){d(O)}},p=_=>{try{w(n.throw(_))}catch(O){d(O)}},w=_=>_.done?l(_.value):Promise.resolve(_.value).then(v,p);w((n=n.apply(e,t)).next())});var F0=lr((VI,bC)=>{"use strict";var _C=Object.getOwnPropertySymbols,XM=Object.prototype.hasOwnProperty,QM=Object.prototype.propertyIsEnumerable;function ZM(e){if(e==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}o(ZM,"toObject");function JM(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de",Object.getOwnPropertyNames(e)[0]==="5")return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;var l=Object.getOwnPropertyNames(t).map(function(v){return t[v]});if(l.join("")!=="0123456789")return!1;var d={};return"abcdefghijklmnopqrst".split("").forEach(function(v){d[v]=v}),Object.keys(Object.assign({},d)).join("")==="abcdefghijklmnopqrst"}catch(v){return!1}}o(JM,"shouldUseNative");bC.exports=JM()?Object.assign:function(e,t){for(var n,l=ZM(e),d,v=1;v{"use strict";var I0=F0(),yc=60103,EC=60106;Ct.Fragment=60107;Ct.StrictMode=60108;Ct.Profiler=60114;var TC=60109,kC=60110,OC=60112;Ct.Suspense=60113;var LC=60115,NC=60116;typeof Symbol=="function"&&Symbol.for&&(yo=Symbol.for,yc=yo("react.element"),EC=yo("react.portal"),Ct.Fragment=yo("react.fragment"),Ct.StrictMode=yo("react.strict_mode"),Ct.Profiler=yo("react.profiler"),TC=yo("react.provider"),kC=yo("react.context"),OC=yo("react.forward_ref"),Ct.Suspense=yo("react.suspense"),LC=yo("react.memo"),NC=yo("react.lazy"));var yo,PC=typeof Symbol=="function"&&Symbol.iterator;function eA(e){return e===null||typeof e!="object"?null:(e=PC&&e[PC]||e["@@iterator"],typeof e=="function"?e:null)}o(eA,"y");function yd(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n{"use strict";UC.exports=BC()});var KC=lr(Mt=>{"use strict";var xc,wd,sv,j0;typeof performance=="object"&&typeof performance.now=="function"?(zC=performance,Mt.unstable_now=function(){return zC.now()}):($0=Date,jC=$0.now(),Mt.unstable_now=function(){return $0.now()-jC});var zC,$0,jC;typeof window=="undefined"||typeof MessageChannel!="function"?(Sc=null,q0=null,V0=o(function(){if(Sc!==null)try{var e=Mt.unstable_now();Sc(!0,e),Sc=null}catch(t){throw setTimeout(V0,0),t}},"w"),xc=o(function(e){Sc!==null?setTimeout(xc,0,e):(Sc=e,setTimeout(V0,0))},"f"),wd=o(function(e,t){q0=setTimeout(e,t)},"g"),sv=o(function(){clearTimeout(q0)},"h"),Mt.unstable_shouldYield=function(){return!1},j0=Mt.unstable_forceFrameRate=function(){}):($C=window.setTimeout,qC=window.clearTimeout,typeof console!="undefined"&&(VC=window.cancelAnimationFrame,typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),typeof VC!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")),xd=!1,Sd=null,lv=-1,K0=5,G0=0,Mt.unstable_shouldYield=function(){return Mt.unstable_now()>=G0},j0=o(function(){},"k"),Mt.unstable_forceFrameRate=function(e){0>e||125>>1,d=e[l];if(d!==void 0&&0fv(p,n))_!==void 0&&0>fv(_,p)?(e[l]=_,e[w]=n,l=w):(e[l]=p,e[v]=n,l=v);else if(_!==void 0&&0>fv(_,n))e[l]=_,e[w]=n,l=w;else break e}}return t}return null}o(uv,"K");function fv(e,t){var n=e.sortIndex-t.sortIndex;return n!==0?n:e.id-t.id}o(fv,"I");var Is=[],La=[],oA=1,wo=null,$n=3,cv=!1,$u=!1,Cd=!1;function Q0(e){for(var t=Qo(La);t!==null;){if(t.callback===null)uv(La);else if(t.startTime<=e)uv(La),t.sortIndex=t.expirationTime,X0(Is,t);else break;t=Qo(La)}}o(Q0,"T");function Z0(e){if(Cd=!1,Q0(e),!$u)if(Qo(Is)!==null)$u=!0,xc(J0);else{var t=Qo(La);t!==null&&wd(Z0,t.startTime-e)}}o(Z0,"U");function J0(e,t){$u=!1,Cd&&(Cd=!1,sv()),cv=!0;var n=$n;try{for(Q0(t),wo=Qo(Is);wo!==null&&(!(wo.expirationTime>t)||e&&!Mt.unstable_shouldYield());){var l=wo.callback;if(typeof l=="function"){wo.callback=null,$n=wo.priorityLevel;var d=l(wo.expirationTime<=t);t=Mt.unstable_now(),typeof d=="function"?wo.callback=d:wo===Qo(Is)&&uv(Is),Q0(t)}else uv(Is);wo=Qo(Is)}if(wo!==null)var v=!0;else{var p=Qo(La);p!==null&&wd(Z0,p.startTime-t),v=!1}return v}finally{wo=null,$n=n,cv=!1}}o(J0,"V");var sA=j0;Mt.unstable_IdlePriority=5;Mt.unstable_ImmediatePriority=1;Mt.unstable_LowPriority=4;Mt.unstable_NormalPriority=3;Mt.unstable_Profiling=null;Mt.unstable_UserBlockingPriority=2;Mt.unstable_cancelCallback=function(e){e.callback=null};Mt.unstable_continueExecution=function(){$u||cv||($u=!0,xc(J0))};Mt.unstable_getCurrentPriorityLevel=function(){return $n};Mt.unstable_getFirstCallbackNode=function(){return Qo(Is)};Mt.unstable_next=function(e){switch($n){case 1:case 2:case 3:var t=3;break;default:t=$n}var n=$n;$n=t;try{return e()}finally{$n=n}};Mt.unstable_pauseExecution=function(){};Mt.unstable_requestPaint=sA;Mt.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var n=$n;$n=e;try{return t()}finally{$n=n}};Mt.unstable_scheduleCallback=function(e,t,n){var l=Mt.unstable_now();switch(typeof n=="object"&&n!==null?(n=n.delay,n=typeof n=="number"&&0l?(e.sortIndex=n,X0(La,e),Qo(Is)===null&&e===Qo(La)&&(Cd?sv():Cd=!0,wd(Z0,n-l))):(e.sortIndex=d,X0(Is,e),$u||cv||($u=!0,xc(J0))),e};Mt.unstable_wrapCallback=function(e){var t=$n;return function(){var n=$n;$n=t;try{return e.apply(this,arguments)}finally{$n=n}}}});var YC=lr((XI,GC)=>{"use strict";GC.exports=KC()});var RE=lr(Eo=>{"use strict";var pv=Re(),ar=F0(),yn=YC();function ye(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;nt}return!1}o(fA,"na");function ai(e,t,n,l,d,v,p){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=l,this.attributeNamespace=d,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=v,this.removeEmptyString=p}o(ai,"B");var Ln={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Ln[e]=new ai(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Ln[t]=new ai(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Ln[e]=new ai(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Ln[e]=new ai(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Ln[e]=new ai(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Ln[e]=new ai(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Ln[e]=new ai(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Ln[e]=new ai(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Ln[e]=new ai(e,5,!1,e.toLowerCase(),null,!1,!1)});var ew=/[\-:]([a-z])/g;function tw(e){return e[1].toUpperCase()}o(tw,"pa");"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(ew,tw);Ln[t]=new ai(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(ew,tw);Ln[t]=new ai(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(ew,tw);Ln[t]=new ai(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Ln[e]=new ai(e,1,!1,e.toLowerCase(),null,!1,!1)});Ln.xlinkHref=new ai("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Ln[e]=new ai(e,1,!1,e.toLowerCase(),null,!0,!0)});function rw(e,t,n,l){var d=Ln.hasOwnProperty(t)?Ln[t]:null,v=d!==null?d.type===0:l?!1:!(!(2w||d[p]!==v[w])return` -`+d[p].replace(" at new "," at ");while(1<=p&&0<=w);break}}}finally{pw=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Od(e):""}o(vv,"Pa");function cA(e){switch(e.tag){case 5:return Od(e.type);case 16:return Od("Lazy");case 13:return Od("Suspense");case 19:return Od("SuspenseList");case 0:case 2:case 15:return e=vv(e.type,!1),e;case 11:return e=vv(e.type.render,!1),e;case 22:return e=vv(e.type._render,!1),e;case 1:return e=vv(e.type,!0),e;default:return""}}o(cA,"Qa");function _c(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Na:return"Fragment";case Ku:return"Portal";case Ed:return"Profiler";case nw:return"StrictMode";case Td:return"Suspense";case hv:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case ow:return(e.displayName||"Context")+".Consumer";case iw:return(e._context.displayName||"Context")+".Provider";case dv:var t=e.render;return t=t.displayName||t.name||"",e.displayName||(t!==""?"ForwardRef("+t+")":"ForwardRef");case mv:return _c(e.type);case lw:return _c(e._render);case sw:t=e._payload,e=e._init;try{return _c(e(t))}catch(n){}}return null}o(_c,"Ra");function Pa(e){switch(typeof e){case"boolean":case"number":case"object":case"string":case"undefined":return e;default:return""}}o(Pa,"Sa");function r_(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}o(r_,"Ta");function pA(e){var t=r_(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),l=""+e[t];if(!e.hasOwnProperty(t)&&typeof n!="undefined"&&typeof n.get=="function"&&typeof n.set=="function"){var d=n.get,v=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return d.call(this)},set:function(p){l=""+p,v.call(this,p)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return l},setValue:function(p){l=""+p},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}o(pA,"Ua");function gv(e){e._valueTracker||(e._valueTracker=pA(e))}o(gv,"Va");function n_(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),l="";return e&&(l=r_(e)?e.checked?"true":"false":e.value),e=l,e!==n?(t.setValue(e),!0):!1}o(n_,"Wa");function yv(e){if(e=e||(typeof document!="undefined"?document:void 0),typeof e=="undefined")return null;try{return e.activeElement||e.body}catch(t){return e.body}}o(yv,"Xa");function dw(e,t){var n=t.checked;return ar({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}o(dw,"Ya");function i_(e,t){var n=t.defaultValue==null?"":t.defaultValue,l=t.checked!=null?t.checked:t.defaultChecked;n=Pa(t.value!=null?t.value:n),e._wrapperState={initialChecked:l,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}o(i_,"Za");function o_(e,t){t=t.checked,t!=null&&rw(e,"checked",t,!1)}o(o_,"$a");function hw(e,t){o_(e,t);var n=Pa(t.value),l=t.type;if(n!=null)l==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(l==="submit"||l==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?mw(e,t.type,n):t.hasOwnProperty("defaultValue")&&mw(e,t.type,Pa(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}o(hw,"ab");function s_(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var l=t.type;if(!(l!=="submit"&&l!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}o(s_,"cb");function mw(e,t,n){(t!=="number"||yv(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}o(mw,"bb");function dA(e){var t="";return pv.Children.forEach(e,function(n){n!=null&&(t+=n)}),t}o(dA,"db");function vw(e,t){return e=ar({children:void 0},t),(t=dA(t.children))&&(e.children=t),e}o(vw,"eb");function bc(e,t,n,l){if(e=e.options,t){t={};for(var d=0;d=n.length))throw Error(ye(93));n=n[0]}t=n}t==null&&(t=""),n=t}e._wrapperState={initialValue:Pa(n)}}o(l_,"hb");function a_(e,t){var n=Pa(t.value),l=Pa(t.defaultValue);n!=null&&(n=""+n,n!==e.value&&(e.value=n),t.defaultValue==null&&e.defaultValue!==n&&(e.defaultValue=n)),l!=null&&(e.defaultValue=""+l)}o(a_,"ib");function u_(e){var t=e.textContent;t===e._wrapperState.initialValue&&t!==""&&t!==null&&(e.value=t)}o(u_,"jb");var yw={html:"http://www.w3.org/1999/xhtml",mathml:"http://www.w3.org/1998/Math/MathML",svg:"http://www.w3.org/2000/svg"};function f_(e){switch(e){case"svg":return"http://www.w3.org/2000/svg";case"math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}o(f_,"lb");function ww(e,t){return e==null||e==="http://www.w3.org/1999/xhtml"?f_(t):e==="http://www.w3.org/2000/svg"&&t==="foreignObject"?"http://www.w3.org/1999/xhtml":e}o(ww,"mb");var wv,c_=function(e){return typeof MSApp!="undefined"&&MSApp.execUnsafeLocalFunction?function(t,n,l,d){MSApp.execUnsafeLocalFunction(function(){return e(t,n,l,d)})}:e}(function(e,t){if(e.namespaceURI!==yw.svg||"innerHTML"in e)e.innerHTML=t;else{for(wv=wv||document.createElement("div"),wv.innerHTML=""+t.valueOf().toString()+"",t=wv.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Ld(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}o(Ld,"pb");var Nd={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},hA=["Webkit","ms","Moz","O"];Object.keys(Nd).forEach(function(e){hA.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Nd[t]=Nd[e]})});function p_(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Nd.hasOwnProperty(e)&&Nd[e]?(""+t).trim():t+"px"}o(p_,"sb");function d_(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var l=n.indexOf("--")===0,d=p_(n,t[n],l);n==="float"&&(n="cssFloat"),l?e.setProperty(n,d):e[n]=d}}o(d_,"tb");var mA=ar({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function xw(e,t){if(t){if(mA[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(ye(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(ye(60));if(!(typeof t.dangerouslySetInnerHTML=="object"&&"__html"in t.dangerouslySetInnerHTML))throw Error(ye(61))}if(t.style!=null&&typeof t.style!="object")throw Error(ye(62))}}o(xw,"vb");function Sw(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}o(Sw,"wb");function Cw(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}o(Cw,"xb");var _w=null,Ec=null,Tc=null;function h_(e){if(e=Gd(e)){if(typeof _w!="function")throw Error(ye(280));var t=e.stateNode;t&&(t=Bv(t),_w(e.stateNode,e.type,t))}}o(h_,"Bb");function m_(e){Ec?Tc?Tc.push(e):Tc=[e]:Ec=e}o(m_,"Eb");function v_(){if(Ec){var e=Ec,t=Tc;if(Tc=Ec=null,h_(e),t)for(e=0;el?0:1<n;n++)t.push(e);return t}o(Fw,"Zc");function Tv(e,t,n){e.pendingLanes|=t;var l=t-1;e.suspendedLanes&=l,e.pingedLanes&=l,e=e.eventTimes,t=31-Ra(t),e[t]=n}o(Tv,"$c");var Ra=Math.clz32?Math.clz32:PA,LA=Math.log,NA=Math.LN2;function PA(e){return e===0?32:31-(LA(e)/NA|0)|0}o(PA,"ad");var MA=yn.unstable_UserBlockingPriority,AA=yn.unstable_runWithPriority,kv=!0;function DA(e,t,n,l){Gu||Ew();var d=Iw,v=Gu;Gu=!0;try{g_(d,e,t,n,l)}finally{(Gu=v)||kw()}}o(DA,"gd");function RA(e,t,n,l){AA(MA,Iw.bind(null,e,t,n,l))}o(RA,"id");function Iw(e,t,n,l){if(kv){var d;if((d=(t&4)==0)&&0=Ud),j_=String.fromCharCode(32),$_=!1;function q_(e,t){switch(e){case"keyup":return iD.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}o(q_,"ge");function V_(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}o(V_,"he");var Mc=!1;function sD(e,t){switch(e){case"compositionend":return V_(t);case"keypress":return t.which!==32?null:($_=!0,j_);case"textInput":return e=t.data,e===j_&&$_?null:e;default:return null}}o(sD,"je");function lD(e,t){if(Mc)return e==="compositionend"||!qw&&q_(e,t)?(e=I_(),Ov=Ww=Fa=null,Mc=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=l}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=Z_(n)}}o(J_,"Le");function eb(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?eb(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}o(eb,"Me");function tb(){for(var e=window,t=yv();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch(l){n=!1}if(n)e=t.contentWindow;else break;t=yv(e.document)}return t}o(tb,"Ne");function Kw(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}o(Kw,"Oe");var gD=kl&&"documentMode"in document&&11>=document.documentMode,Ac=null,Gw=null,qd=null,Yw=!1;function rb(e,t,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Yw||Ac==null||Ac!==yv(l)||(l=Ac,"selectionStart"in l&&Kw(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),qd&&$d(qd,l)||(qd=l,l=Fv(Gw,"onSelect"),0Hc||(e.current=r1[Hc],r1[Hc]=null,Hc--)}o(rr,"H");function xr(e,t){Hc++,r1[Hc]=e.current,e.current=t}o(xr,"I");var Wa={},qn=Ha(Wa),Li=Ha(!1),Qu=Wa;function Wc(e,t){var n=e.type.contextTypes;if(!n)return Wa;var l=e.stateNode;if(l&&l.__reactInternalMemoizedUnmaskedChildContext===t)return l.__reactInternalMemoizedMaskedChildContext;var d={},v;for(v in n)d[v]=t[v];return l&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=d),d}o(Wc,"Ef");function Ni(e){return e=e.childContextTypes,e!=null}o(Ni,"Ff");function Uv(){rr(Li),rr(qn)}o(Uv,"Gf");function gb(e,t,n){if(qn.current!==Wa)throw Error(ye(168));xr(qn,t),xr(Li,n)}o(gb,"Hf");function yb(e,t,n){var l=e.stateNode;if(e=t.childContextTypes,typeof l.getChildContext!="function")return n;l=l.getChildContext();for(var d in l)if(!(d in e))throw Error(ye(108,_c(t)||"Unknown",d));return ar({},n,l)}o(yb,"If");function zv(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Wa,Qu=qn.current,xr(qn,e),xr(Li,Li.current),!0}o(zv,"Jf");function wb(e,t,n){var l=e.stateNode;if(!l)throw Error(ye(169));n?(e=yb(e,t,Qu),l.__reactInternalMemoizedMergedChildContext=e,rr(Li),rr(qn),xr(qn,e)):rr(Li),xr(Li,n)}o(wb,"Kf");var n1=null,Zu=null,xD=yn.unstable_runWithPriority,i1=yn.unstable_scheduleCallback,o1=yn.unstable_cancelCallback,SD=yn.unstable_shouldYield,xb=yn.unstable_requestPaint,s1=yn.unstable_now,CD=yn.unstable_getCurrentPriorityLevel,jv=yn.unstable_ImmediatePriority,Sb=yn.unstable_UserBlockingPriority,Cb=yn.unstable_NormalPriority,_b=yn.unstable_LowPriority,bb=yn.unstable_IdlePriority,l1={},_D=xb!==void 0?xb:function(){},Ol=null,$v=null,a1=!1,Eb=s1(),Vn=1e4>Eb?s1:function(){return s1()-Eb};function Bc(){switch(CD()){case jv:return 99;case Sb:return 98;case Cb:return 97;case _b:return 96;case bb:return 95;default:throw Error(ye(332))}}o(Bc,"eg");function Tb(e){switch(e){case 99:return jv;case 98:return Sb;case 97:return Cb;case 96:return _b;case 95:return bb;default:throw Error(ye(332))}}o(Tb,"fg");function Ju(e,t){return e=Tb(e),xD(e,t)}o(Ju,"gg");function Yd(e,t,n){return e=Tb(e),i1(e,t,n)}o(Yd,"hg");function Ws(){if($v!==null){var e=$v;$v=null,o1(e)}kb()}o(Ws,"ig");function kb(){if(!a1&&Ol!==null){a1=!0;var e=0;try{var t=Ol;Ju(99,function(){for(;ede?(ge=ie,ie=null):ge=ie.sibling;var we=W(R,ie,F[de],K);if(we===null){ie===null&&(ie=ge);break}e&&ie&&we.alternate===null&&t(R,ie),P=v(we,P,de),ue===null?V=we:ue.sibling=we,ue=we,ie=ge}if(de===F.length)return n(R,ie),V;if(ie===null){for(;dede?(ge=ie,ie=null):ge=ie.sibling;var qe=W(R,ie,we.value,K);if(qe===null){ie===null&&(ie=ge);break}e&&ie&&qe.alternate===null&&t(R,ie),P=v(qe,P,de),ue===null?V=qe:ue.sibling=qe,ue=qe,ie=ge}if(we.done)return n(R,ie),V;if(ie===null){for(;!we.done;de++,we=F.next())we=Y(R,we.value,K),we!==null&&(P=v(we,P,de),ue===null?V=we:ue.sibling=we,ue=we);return V}for(ie=l(R,ie);!we.done;de++,we=F.next())we=X(ie,R,de,we.value,K),we!==null&&(e&&we.alternate!==null&&ie.delete(we.key===null?de:we.key),P=v(we,P,de),ue===null?V=we:ue.sibling=we,ue=we);return e&&ie.forEach(function(Je){return t(R,Je)}),V}return o(Q,"w"),function(R,P,F,K){var V=typeof F=="object"&&F!==null&&F.type===Na&&F.key===null;V&&(F=F.props.children);var ue=typeof F=="object"&&F!==null;if(ue)switch(F.$$typeof){case bd:e:{for(ue=F.key,V=P;V!==null;){if(V.key===ue){switch(V.tag){case 7:if(F.type===Na){n(R,V.sibling),P=d(V,F.props.children),P.return=R,R=P;break e}break;default:if(V.elementType===F.type){n(R,V.sibling),P=d(V,F.props),P.ref=Qd(R,V,F),P.return=R,R=P;break e}}n(R,V);break}else t(R,V);V=V.sibling}F.type===Na?(P=Xc(F.props.children,R.mode,K,F.key),P.return=R,R=P):(K=mg(F.type,F.key,F.props,null,R.mode,K),K.ref=Qd(R,P,F),K.return=R,R=K)}return p(R);case Ku:e:{for(V=F.key;P!==null;){if(P.key===V)if(P.tag===4&&P.stateNode.containerInfo===F.containerInfo&&P.stateNode.implementation===F.implementation){n(R,P.sibling),P=d(P,F.children||[]),P.return=R,R=P;break e}else{n(R,P);break}else t(R,P);P=P.sibling}P=Y1(F,R.mode,K),P.return=R,R=P}return p(R)}if(typeof F=="string"||typeof F=="number")return F=""+F,P!==null&&P.tag===6?(n(R,P.sibling),P=d(P,F),P.return=R,R=P):(n(R,P),P=G1(F,R.mode,K),P.return=R,R=P),p(R);if(Xv(F))return te(R,P,F,K);if(kd(F))return Q(R,P,F,K);if(ue&&Qv(R,F),typeof F=="undefined"&&!V)switch(R.tag){case 1:case 22:case 0:case 11:case 15:throw Error(ye(152,_c(R.type)||"Component"))}return n(R,P)}}o(Fb,"Sg");var Zv=Fb(!0),Ib=Fb(!1),Zd={},Bs=Ha(Zd),Jd=Ha(Zd),eh=Ha(Zd);function ef(e){if(e===Zd)throw Error(ye(174));return e}o(ef,"dh");function d1(e,t){switch(xr(eh,t),xr(Jd,e),xr(Bs,Zd),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:ww(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=ww(t,e)}rr(Bs),xr(Bs,t)}o(d1,"eh");function jc(){rr(Bs),rr(Jd),rr(eh)}o(jc,"fh");function Hb(e){ef(eh.current);var t=ef(Bs.current),n=ww(t,e.type);t!==n&&(xr(Jd,e),xr(Bs,n))}o(Hb,"gh");function h1(e){Jd.current===e&&(rr(Bs),rr(Jd))}o(h1,"hh");var Sr=Ha(0);function Jv(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if((t.flags&64)!=0)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}o(Jv,"ih");var Ll=null,ja=null,Us=!1;function Wb(e,t){var n=bo(5,null,null,0);n.elementType="DELETED",n.type="DELETED",n.stateNode=t,n.return=e,n.flags=8,e.lastEffect!==null?(e.lastEffect.nextEffect=n,e.lastEffect=n):e.firstEffect=e.lastEffect=n}o(Wb,"mh");function Bb(e,t){switch(e.tag){case 5:var n=e.type;return t=t.nodeType!==1||n.toLowerCase()!==t.nodeName.toLowerCase()?null:t,t!==null?(e.stateNode=t,!0):!1;case 6:return t=e.pendingProps===""||t.nodeType!==3?null:t,t!==null?(e.stateNode=t,!0):!1;case 13:return!1;default:return!1}}o(Bb,"oh");function m1(e){if(Us){var t=ja;if(t){var n=t;if(!Bb(e,t)){if(t=Rc(n.nextSibling),!t||!Bb(e,t)){e.flags=e.flags&-1025|2,Us=!1,Ll=e;return}Wb(Ll,n)}Ll=e,ja=Rc(t.firstChild)}else e.flags=e.flags&-1025|2,Us=!1,Ll=e}}o(m1,"ph");function Ub(e){for(e=e.return;e!==null&&e.tag!==5&&e.tag!==3&&e.tag!==13;)e=e.return;Ll=e}o(Ub,"qh");function eg(e){if(e!==Ll)return!1;if(!Us)return Ub(e),Us=!0,!1;var t=e.type;if(e.tag!==5||t!=="head"&&t!=="body"&&!Jw(t,e.memoizedProps))for(t=ja;t;)Wb(e,t),t=Rc(t.nextSibling);if(Ub(e),e.tag===13){if(e=e.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(ye(317));e:{for(e=e.nextSibling,t=0;e;){if(e.nodeType===8){var n=e.data;if(n==="/$"){if(t===0){ja=Rc(e.nextSibling);break e}t--}else n!=="$"&&n!=="$!"&&n!=="$?"||t++}e=e.nextSibling}ja=null}}else ja=Ll?Rc(e.stateNode.nextSibling):null;return!0}o(eg,"rh");function v1(){ja=Ll=null,Us=!1}o(v1,"sh");var $c=[];function g1(){for(var e=0;e<$c.length;e++)$c[e]._workInProgressVersionPrimary=null;$c.length=0}o(g1,"uh");var th=Vu.ReactCurrentDispatcher,Co=Vu.ReactCurrentBatchConfig,rh=0,Mr=null,Kn=null,Nn=null,tg=!1,nh=!1;function Pi(){throw Error(ye(321))}o(Pi,"Ah");function y1(e,t){if(t===null)return!1;for(var n=0;nv))throw Error(ye(301));v+=1,Nn=Kn=null,t.updateQueue=null,th.current=OD,e=n(l,d)}while(nh)}if(th.current=og,t=Kn!==null&&Kn.next!==null,rh=0,Nn=Kn=Mr=null,tg=!1,t)throw Error(ye(300));return e}o(w1,"Ch");function tf(){var e={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return Nn===null?Mr.memoizedState=Nn=e:Nn=Nn.next=e,Nn}o(tf,"Hh");function rf(){if(Kn===null){var e=Mr.alternate;e=e!==null?e.memoizedState:null}else e=Kn.next;var t=Nn===null?Mr.memoizedState:Nn.next;if(t!==null)Nn=t,Kn=e;else{if(e===null)throw Error(ye(310));Kn=e,e={memoizedState:Kn.memoizedState,baseState:Kn.baseState,baseQueue:Kn.baseQueue,queue:Kn.queue,next:null},Nn===null?Mr.memoizedState=Nn=e:Nn=Nn.next=e}return Nn}o(rf,"Ih");function zs(e,t){return typeof t=="function"?t(e):t}o(zs,"Jh");function ih(e){var t=rf(),n=t.queue;if(n===null)throw Error(ye(311));n.lastRenderedReducer=e;var l=Kn,d=l.baseQueue,v=n.pending;if(v!==null){if(d!==null){var p=d.next;d.next=v.next,v.next=p}l.baseQueue=d=v,n.pending=null}if(d!==null){d=d.next,l=l.baseState;var w=p=v=null,_=d;do{var O=_.lane;if((rh&O)===O)w!==null&&(w=w.next={lane:0,action:_.action,eagerReducer:_.eagerReducer,eagerState:_.eagerState,next:null}),l=_.eagerReducer===e?_.eagerState:e(l,_.action);else{var D={lane:O,action:_.action,eagerReducer:_.eagerReducer,eagerState:_.eagerState,next:null};w===null?(p=w=D,v=l):w=w.next=D,Mr.lanes|=O,ah|=O}_=_.next}while(_!==null&&_!==d);w===null?v=l:w.next=p,xo(l,t.memoizedState)||(Jo=!0),t.memoizedState=l,t.baseState=v,t.baseQueue=w,n.lastRenderedState=l}return[t.memoizedState,n.dispatch]}o(ih,"Kh");function oh(e){var t=rf(),n=t.queue;if(n===null)throw Error(ye(311));n.lastRenderedReducer=e;var l=n.dispatch,d=n.pending,v=t.memoizedState;if(d!==null){n.pending=null;var p=d=d.next;do v=e(v,p.action),p=p.next;while(p!==d);xo(v,t.memoizedState)||(Jo=!0),t.memoizedState=v,t.baseQueue===null&&(t.baseState=v),n.lastRenderedState=v}return[v,l]}o(oh,"Lh");function zb(e,t,n){var l=t._getVersion;l=l(t._source);var d=t._workInProgressVersionPrimary;if(d!==null?e=d===l:(e=e.mutableReadLanes,(e=(rh&e)===e)&&(t._workInProgressVersionPrimary=l,$c.push(t))),e)return n(t._source);throw $c.push(t),Error(ye(350))}o(zb,"Mh");function jb(e,t,n,l){var d=ui;if(d===null)throw Error(ye(349));var v=t._getVersion,p=v(t._source),w=th.current,_=w.useState(function(){return zb(d,t,n)}),O=_[1],D=_[0];_=Nn;var Y=e.memoizedState,W=Y.refs,X=W.getSnapshot,te=Y.source;Y=Y.subscribe;var Q=Mr;return e.memoizedState={refs:W,source:t,subscribe:l},w.useEffect(function(){W.getSnapshot=n,W.setSnapshot=O;var R=v(t._source);if(!xo(p,R)){R=n(t._source),xo(D,R)||(O(R),R=qa(Q),d.mutableReadLanes|=R&d.pendingLanes),R=d.mutableReadLanes,d.entangledLanes|=R;for(var P=d.entanglements,F=R;0n?98:n,function(){e(!0)}),Ju(97<\/script>",e=e.removeChild(e.firstChild)):typeof l.is=="string"?e=p.createElement(n,{is:l.is}):(e=p.createElement(n),n==="select"&&(p=e,l.multiple?p.multiple=!0:l.size&&(p.size=l.size))):e=p.createElementNS(e,n),e[Ia]=t,e[Wv]=l,fE(e,t,!1,!1),t.stateNode=e,p=Sw(n,l),n){case"dialog":tr("cancel",e),tr("close",e),d=l;break;case"iframe":case"object":case"embed":tr("load",e),d=l;break;case"video":case"audio":for(d=0;dW1&&(t.flags|=64,v=!0,lh(l,!1),t.lanes=33554432)}else{if(!v)if(e=Jv(p),e!==null){if(t.flags|=64,v=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),lh(l,!0),l.tail===null&&l.tailMode==="hidden"&&!p.alternate&&!Us)return t=t.lastEffect=l.lastEffect,t!==null&&(t.nextEffect=null),null}else 2*Vn()-l.renderingStartTime>W1&&n!==1073741824&&(t.flags|=64,v=!0,lh(l,!1),t.lanes=33554432);l.isBackwards?(p.sibling=t.child,t.child=p):(n=l.last,n!==null?n.sibling=p:t.child=p,l.last=p)}return l.tail!==null?(n=l.tail,l.rendering=n,l.tail=n.sibling,l.lastEffect=t.lastEffect,l.renderingStartTime=Vn(),n.sibling=null,t=Sr.current,xr(Sr,v?t&1|2:t&1),n):null;case 23:case 24:return q1(),e!==null&&e.memoizedState!==null!=(t.memoizedState!==null)&&l.mode!=="unstable-defer-without-hiding"&&(t.flags|=4),null}throw Error(ye(156,t.tag))}o(ND,"Gi");function PD(e){switch(e.tag){case 1:Ni(e.type)&&Uv();var t=e.flags;return t&4096?(e.flags=t&-4097|64,e):null;case 3:if(jc(),rr(Li),rr(qn),g1(),t=e.flags,(t&64)!=0)throw Error(ye(285));return e.flags=t&-4097|64,e;case 5:return h1(e),null;case 13:return rr(Sr),t=e.flags,t&4096?(e.flags=t&-4097|64,e):null;case 19:return rr(Sr),null;case 4:return jc(),null;case 10:return f1(e),null;case 23:case 24:return q1(),null;default:return null}}o(PD,"Li");function L1(e,t){try{var n="",l=t;do n+=cA(l),l=l.return;while(l);var d=n}catch(v){d=` -Error generating stack: `+v.message+` -`+v.stack}return{value:e,source:t,stack:d}}o(L1,"Mi");function N1(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}o(N1,"Ni");var MD=typeof WeakMap=="function"?WeakMap:Map;function dE(e,t,n){n=Ua(-1,n),n.tag=3,n.payload={element:null};var l=t.value;return n.callback=function(){ug||(ug=!0,B1=l),N1(e,t)},n}o(dE,"Pi");function hE(e,t,n){n=Ua(-1,n),n.tag=3;var l=e.type.getDerivedStateFromError;if(typeof l=="function"){var d=t.value;n.payload=function(){return N1(e,t),l(d)}}var v=e.stateNode;return v!==null&&typeof v.componentDidCatch=="function"&&(n.callback=function(){typeof l!="function"&&(js===null?js=new Set([this]):js.add(this),N1(e,t));var p=t.stack;this.componentDidCatch(t.value,{componentStack:p!==null?p:""})}),n}o(hE,"Si");var AD=typeof WeakSet=="function"?WeakSet:Set;function mE(e){var t=e.ref;if(t!==null)if(typeof t=="function")try{t(null)}catch(n){Ga(e,n)}else t.current=null}o(mE,"Vi");function DD(e,t){switch(t.tag){case 0:case 11:case 15:case 22:return;case 1:if(t.flags&256&&e!==null){var n=e.memoizedProps,l=e.memoizedState;e=t.stateNode,t=e.getSnapshotBeforeUpdate(t.elementType===t.type?n:Zo(t.type,n),l),e.__reactInternalSnapshotBeforeUpdate=t}return;case 3:t.flags&256&&e1(t.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(ye(163))}o(DD,"Xi");function RD(e,t,n){switch(n.tag){case 0:case 11:case 15:case 22:if(t=n.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{if((e.tag&3)==3){var l=e.create;e.destroy=l()}e=e.next}while(e!==t)}if(t=n.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{var d=e;l=d.next,d=d.tag,(d&4)!=0&&(d&1)!=0&&(NE(n,e),jD(n,e)),e=l}while(e!==t)}return;case 1:e=n.stateNode,n.flags&4&&(t===null?e.componentDidMount():(l=n.elementType===n.type?t.memoizedProps:Zo(n.type,t.memoizedProps),e.componentDidUpdate(l,t.memoizedState,e.__reactInternalSnapshotBeforeUpdate))),t=n.updateQueue,t!==null&&Pb(n,t,e);return;case 3:if(t=n.updateQueue,t!==null){if(e=null,n.child!==null)switch(n.child.tag){case 5:e=n.child.stateNode;break;case 1:e=n.child.stateNode}Pb(n,t,e)}return;case 5:e=n.stateNode,t===null&&n.flags&4&&pb(n.type,n.memoizedProps)&&e.focus();return;case 6:return;case 4:return;case 12:return;case 13:n.memoizedState===null&&(n=n.alternate,n!==null&&(n=n.memoizedState,n!==null&&(n=n.dehydrated,n!==null&&L_(n))));return;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(ye(163))}o(RD,"Yi");function vE(e,t){for(var n=e;;){if(n.tag===5){var l=n.stateNode;if(t)l=l.style,typeof l.setProperty=="function"?l.setProperty("display","none","important"):l.display="none";else{l=n.stateNode;var d=n.memoizedProps.style;d=d!=null&&d.hasOwnProperty("display")?d.display:null,l.style.display=p_("display",d)}}else if(n.tag===6)n.stateNode.nodeValue=t?"":n.memoizedProps;else if((n.tag!==23&&n.tag!==24||n.memoizedState===null||n===e)&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===e)break;for(;n.sibling===null;){if(n.return===null||n.return===e)return;n=n.return}n.sibling.return=n.return,n=n.sibling}}o(vE,"aj");function gE(e,t){if(Zu&&typeof Zu.onCommitFiberUnmount=="function")try{Zu.onCommitFiberUnmount(n1,t)}catch(v){}switch(t.tag){case 0:case 11:case 14:case 15:case 22:if(e=t.updateQueue,e!==null&&(e=e.lastEffect,e!==null)){var n=e=e.next;do{var l=n,d=l.destroy;if(l=l.tag,d!==void 0)if((l&4)!=0)NE(t,n);else{l=t;try{d()}catch(v){Ga(l,v)}}n=n.next}while(n!==e)}break;case 1:if(mE(t),e=t.stateNode,typeof e.componentWillUnmount=="function")try{e.props=t.memoizedProps,e.state=t.memoizedState,e.componentWillUnmount()}catch(v){Ga(t,v)}break;case 5:mE(t);break;case 4:SE(e,t)}}o(gE,"bj");function yE(e){e.alternate=null,e.child=null,e.dependencies=null,e.firstEffect=null,e.lastEffect=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.return=null,e.updateQueue=null}o(yE,"dj");function wE(e){return e.tag===5||e.tag===3||e.tag===4}o(wE,"ej");function xE(e){e:{for(var t=e.return;t!==null;){if(wE(t))break e;t=t.return}throw Error(ye(160))}var n=t;switch(t=n.stateNode,n.tag){case 5:var l=!1;break;case 3:t=t.containerInfo,l=!0;break;case 4:t=t.containerInfo,l=!0;break;default:throw Error(ye(161))}n.flags&16&&(Ld(t,""),n.flags&=-17);e:t:for(n=e;;){for(;n.sibling===null;){if(n.return===null||wE(n.return)){n=null;break e}n=n.return}for(n.sibling.return=n.return,n=n.sibling;n.tag!==5&&n.tag!==6&&n.tag!==18;){if(n.flags&2||n.child===null||n.tag===4)continue t;n.child.return=n,n=n.child}if(!(n.flags&2)){n=n.stateNode;break e}}l?P1(e,n,t):M1(e,n,t)}o(xE,"fj");function P1(e,t,n){var l=e.tag,d=l===5||l===6;if(d)e=d?e.stateNode:e.stateNode.instance,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Iv));else if(l!==4&&(e=e.child,e!==null))for(P1(e,t,n),e=e.sibling;e!==null;)P1(e,t,n),e=e.sibling}o(P1,"gj");function M1(e,t,n){var l=e.tag,d=l===5||l===6;if(d)e=d?e.stateNode:e.stateNode.instance,t?n.insertBefore(e,t):n.appendChild(e);else if(l!==4&&(e=e.child,e!==null))for(M1(e,t,n),e=e.sibling;e!==null;)M1(e,t,n),e=e.sibling}o(M1,"hj");function SE(e,t){for(var n=t,l=!1,d,v;;){if(!l){l=n.return;e:for(;;){if(l===null)throw Error(ye(160));switch(d=l.stateNode,l.tag){case 5:v=!1;break e;case 3:d=d.containerInfo,v=!0;break e;case 4:d=d.containerInfo,v=!0;break e}l=l.return}l=!0}if(n.tag===5||n.tag===6){e:for(var p=e,w=n,_=w;;)if(gE(p,_),_.child!==null&&_.tag!==4)_.child.return=_,_=_.child;else{if(_===w)break e;for(;_.sibling===null;){if(_.return===null||_.return===w)break e;_=_.return}_.sibling.return=_.return,_=_.sibling}v?(p=d,w=n.stateNode,p.nodeType===8?p.parentNode.removeChild(w):p.removeChild(w)):d.removeChild(n.stateNode)}else if(n.tag===4){if(n.child!==null){d=n.stateNode.containerInfo,v=!0,n.child.return=n,n=n.child;continue}}else if(gE(e,n),n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return,n.tag===4&&(l=!1)}n.sibling.return=n.return,n=n.sibling}}o(SE,"cj");function A1(e,t){switch(t.tag){case 0:case 11:case 14:case 15:case 22:var n=t.updateQueue;if(n=n!==null?n.lastEffect:null,n!==null){var l=n=n.next;do(l.tag&3)==3&&(e=l.destroy,l.destroy=void 0,e!==void 0&&e()),l=l.next;while(l!==n)}return;case 1:return;case 5:if(n=t.stateNode,n!=null){l=t.memoizedProps;var d=e!==null?e.memoizedProps:l;e=t.type;var v=t.updateQueue;if(t.updateQueue=null,v!==null){for(n[Wv]=l,e==="input"&&l.type==="radio"&&l.name!=null&&o_(n,l),Sw(e,d),t=Sw(e,l),d=0;dd&&(d=p),n&=~v}if(n=d,n=Vn()-n,n=(120>n?120:480>n?480:1080>n?1080:1920>n?1920:3e3>n?3e3:4320>n?4320:1960*ID(n/1960))-n,10{var XM=Object.create;var Ec=Object.defineProperty,QM=Object.defineProperties,ZM=Object.getOwnPropertyDescriptor,JM=Object.getOwnPropertyDescriptors,eA=Object.getOwnPropertyNames,sv=Object.getOwnPropertySymbols,tA=Object.getPrototypeOf,F0=Object.prototype.hasOwnProperty,EC=Object.prototype.propertyIsEnumerable;var I0=(e,t,n)=>t in e?Ec(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,Pe=(e,t)=>{for(var n in t||(t={}))F0.call(t,n)&&I0(e,n,t[n]);if(sv)for(var n of sv(t))EC.call(t,n)&&I0(e,n,t[n]);return e},It=(e,t)=>QM(e,JM(t)),TC=e=>Ec(e,"__esModule",{value:!0}),o=(e,t)=>Ec(e,"name",{value:t,configurable:!0});var Ds=(e,t)=>{var n={};for(var l in e)F0.call(e,l)&&t.indexOf(l)<0&&(n[l]=e[l]);if(e!=null&&sv)for(var l of sv(e))t.indexOf(l)<0&&EC.call(e,l)&&(n[l]=e[l]);return n};var ur=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),kC=(e,t)=>{TC(e);for(var n in t)Ec(e,n,{get:t[n],enumerable:!0})},rA=(e,t,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let l of eA(t))!F0.call(e,l)&&l!=="default"&&Ec(e,l,{get:()=>t[l],enumerable:!(n=ZM(t,l))||n.enumerable});return e},pe=e=>rA(TC(Ec(e!=null?XM(tA(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var Tc=(e,t,n)=>(I0(e,typeof t!="symbol"?t+"":t,n),n);var Na=(e,t,n)=>new Promise((l,d)=>{var m=_=>{try{x(n.next(_))}catch(O){d(O)}},p=_=>{try{x(n.throw(_))}catch(O){d(O)}},x=_=>_.done?l(_.value):Promise.resolve(_.value).then(m,p);x((n=n.apply(e,t)).next())});var H0=ur((t2,LC)=>{"use strict";var OC=Object.getOwnPropertySymbols,nA=Object.prototype.hasOwnProperty,iA=Object.prototype.propertyIsEnumerable;function oA(e){if(e==null)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}o(oA,"toObject");function sA(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de",Object.getOwnPropertyNames(e)[0]==="5")return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;var l=Object.getOwnPropertyNames(t).map(function(m){return t[m]});if(l.join("")!=="0123456789")return!1;var d={};return"abcdefghijklmnopqrst".split("").forEach(function(m){d[m]=m}),Object.keys(Object.assign({},d)).join("")==="abcdefghijklmnopqrst"}catch(m){return!1}}o(sA,"shouldUseNative");LC.exports=sA()?Object.assign:function(e,t){for(var n,l=oA(e),d,m=1;m{"use strict";var W0=H0(),kc=60103,NC=60106;bt.Fragment=60107;bt.StrictMode=60108;bt.Profiler=60114;var PC=60109,MC=60110,AC=60112;bt.Suspense=60113;var DC=60115,RC=60116;typeof Symbol=="function"&&Symbol.for&&(yo=Symbol.for,kc=yo("react.element"),NC=yo("react.portal"),bt.Fragment=yo("react.fragment"),bt.StrictMode=yo("react.strict_mode"),bt.Profiler=yo("react.profiler"),PC=yo("react.provider"),MC=yo("react.context"),AC=yo("react.forward_ref"),bt.Suspense=yo("react.suspense"),DC=yo("react.memo"),RC=yo("react.lazy"));var yo,FC=typeof Symbol=="function"&&Symbol.iterator;function lA(e){return e===null||typeof e!="object"?null:(e=FC&&e[FC]||e["@@iterator"],typeof e=="function"?e:null)}o(lA,"y");function Td(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n{"use strict";VC.exports=qC()});var ZC=ur(Dt=>{"use strict";var Lc,kd,uv,q0;typeof performance=="object"&&typeof performance.now=="function"?(KC=performance,Dt.unstable_now=function(){return KC.now()}):(V0=Date,GC=V0.now(),Dt.unstable_now=function(){return V0.now()-GC});var KC,V0,GC;typeof window=="undefined"||typeof MessageChannel!="function"?(Nc=null,K0=null,G0=o(function(){if(Nc!==null)try{var e=Dt.unstable_now();Nc(!0,e),Nc=null}catch(t){throw setTimeout(G0,0),t}},"w"),Lc=o(function(e){Nc!==null?setTimeout(Lc,0,e):(Nc=e,setTimeout(G0,0))},"f"),kd=o(function(e,t){K0=setTimeout(e,t)},"g"),uv=o(function(){clearTimeout(K0)},"h"),Dt.unstable_shouldYield=function(){return!1},q0=Dt.unstable_forceFrameRate=function(){}):(YC=window.setTimeout,XC=window.clearTimeout,typeof console!="undefined"&&(QC=window.cancelAnimationFrame,typeof window.requestAnimationFrame!="function"&&console.error("This browser doesn't support requestAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills"),typeof QC!="function"&&console.error("This browser doesn't support cancelAnimationFrame. Make sure that you load a polyfill in older browsers. https://reactjs.org/link/react-polyfills")),Od=!1,Ld=null,fv=-1,Y0=5,X0=0,Dt.unstable_shouldYield=function(){return Dt.unstable_now()>=X0},q0=o(function(){},"k"),Dt.unstable_forceFrameRate=function(e){0>e||125>>1,d=e[l];if(d!==void 0&&0dv(p,n))_!==void 0&&0>dv(_,p)?(e[l]=_,e[x]=n,l=x):(e[l]=p,e[m]=n,l=m);else if(_!==void 0&&0>dv(_,n))e[l]=_,e[x]=n,l=x;else break e}}return t}return null}o(pv,"K");function dv(e,t){var n=e.sortIndex-t.sortIndex;return n!==0?n:e.id-t.id}o(dv,"I");var Rs=[],Pa=[],pA=1,wo=null,jn=3,hv=!1,Zu=!1,Nd=!1;function J0(e){for(var t=Qo(Pa);t!==null;){if(t.callback===null)pv(Pa);else if(t.startTime<=e)pv(Pa),t.sortIndex=t.expirationTime,Z0(Rs,t);else break;t=Qo(Pa)}}o(J0,"T");function ew(e){if(Nd=!1,J0(e),!Zu)if(Qo(Rs)!==null)Zu=!0,Lc(tw);else{var t=Qo(Pa);t!==null&&kd(ew,t.startTime-e)}}o(ew,"U");function tw(e,t){Zu=!1,Nd&&(Nd=!1,uv()),hv=!0;var n=jn;try{for(J0(t),wo=Qo(Rs);wo!==null&&(!(wo.expirationTime>t)||e&&!Dt.unstable_shouldYield());){var l=wo.callback;if(typeof l=="function"){wo.callback=null,jn=wo.priorityLevel;var d=l(wo.expirationTime<=t);t=Dt.unstable_now(),typeof d=="function"?wo.callback=d:wo===Qo(Rs)&&pv(Rs),J0(t)}else pv(Rs);wo=Qo(Rs)}if(wo!==null)var m=!0;else{var p=Qo(Pa);p!==null&&kd(ew,p.startTime-t),m=!1}return m}finally{wo=null,jn=n,hv=!1}}o(tw,"V");var dA=q0;Dt.unstable_IdlePriority=5;Dt.unstable_ImmediatePriority=1;Dt.unstable_LowPriority=4;Dt.unstable_NormalPriority=3;Dt.unstable_Profiling=null;Dt.unstable_UserBlockingPriority=2;Dt.unstable_cancelCallback=function(e){e.callback=null};Dt.unstable_continueExecution=function(){Zu||hv||(Zu=!0,Lc(tw))};Dt.unstable_getCurrentPriorityLevel=function(){return jn};Dt.unstable_getFirstCallbackNode=function(){return Qo(Rs)};Dt.unstable_next=function(e){switch(jn){case 1:case 2:case 3:var t=3;break;default:t=jn}var n=jn;jn=t;try{return e()}finally{jn=n}};Dt.unstable_pauseExecution=function(){};Dt.unstable_requestPaint=dA;Dt.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var n=jn;jn=e;try{return t()}finally{jn=n}};Dt.unstable_scheduleCallback=function(e,t,n){var l=Dt.unstable_now();switch(typeof n=="object"&&n!==null?(n=n.delay,n=typeof n=="number"&&0l?(e.sortIndex=n,Z0(Pa,e),Qo(Rs)===null&&e===Qo(Pa)&&(Nd?uv():Nd=!0,kd(ew,n-l))):(e.sortIndex=d,Z0(Rs,e),Zu||hv||(Zu=!0,Lc(tw))),e};Dt.unstable_wrapCallback=function(e){var t=jn;return function(){var n=jn;jn=t;try{return e.apply(this,arguments)}finally{jn=n}}}});var e_=ur((o2,JC)=>{"use strict";JC.exports=ZC()});var BE=ur(Eo=>{"use strict";var mv=De(),fr=H0(),wn=e_();function we(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;nt}return!1}o(gA,"na");function fi(e,t,n,l,d,m,p){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=l,this.attributeNamespace=d,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=m,this.removeEmptyString=p}o(fi,"B");var Pn={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){Pn[e]=new fi(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];Pn[t]=new fi(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){Pn[e]=new fi(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){Pn[e]=new fi(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){Pn[e]=new fi(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){Pn[e]=new fi(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){Pn[e]=new fi(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){Pn[e]=new fi(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){Pn[e]=new fi(e,5,!1,e.toLowerCase(),null,!1,!1)});var rw=/[\-:]([a-z])/g;function nw(e){return e[1].toUpperCase()}o(nw,"pa");"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(rw,nw);Pn[t]=new fi(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(rw,nw);Pn[t]=new fi(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(rw,nw);Pn[t]=new fi(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){Pn[e]=new fi(e,1,!1,e.toLowerCase(),null,!1,!1)});Pn.xlinkHref=new fi("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){Pn[e]=new fi(e,1,!1,e.toLowerCase(),null,!0,!0)});function iw(e,t,n,l){var d=Pn.hasOwnProperty(t)?Pn[t]:null,m=d!==null?d.type===0:l?!1:!(!(2x||d[p]!==m[x])return` +`+d[p].replace(" at new "," at ");while(1<=p&&0<=x);break}}}finally{hw=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Fd(e):""}o(wv,"Pa");function yA(e){switch(e.tag){case 5:return Fd(e.type);case 16:return Fd("Lazy");case 13:return Fd("Suspense");case 19:return Fd("SuspenseList");case 0:case 2:case 15:return e=wv(e.type,!1),e;case 11:return e=wv(e.type.render,!1),e;case 22:return e=wv(e.type._render,!1),e;case 1:return e=wv(e.type,!0),e;default:return""}}o(yA,"Qa");function Mc(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Ma:return"Fragment";case tf:return"Portal";case Ad:return"Profiler";case ow:return"StrictMode";case Dd:return"Suspense";case gv:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case lw:return(e.displayName||"Context")+".Consumer";case sw:return(e._context.displayName||"Context")+".Provider";case vv:var t=e.render;return t=t.displayName||t.name||"",e.displayName||(t!==""?"ForwardRef("+t+")":"ForwardRef");case yv:return Mc(e.type);case uw:return Mc(e._render);case aw:t=e._payload,e=e._init;try{return Mc(e(t))}catch(n){}}return null}o(Mc,"Ra");function Aa(e){switch(typeof e){case"boolean":case"number":case"object":case"string":case"undefined":return e;default:return""}}o(Aa,"Sa");function l_(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}o(l_,"Ta");function wA(e){var t=l_(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),l=""+e[t];if(!e.hasOwnProperty(t)&&typeof n!="undefined"&&typeof n.get=="function"&&typeof n.set=="function"){var d=n.get,m=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return d.call(this)},set:function(p){l=""+p,m.call(this,p)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return l},setValue:function(p){l=""+p},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}o(wA,"Ua");function xv(e){e._valueTracker||(e._valueTracker=wA(e))}o(xv,"Va");function a_(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),l="";return e&&(l=l_(e)?e.checked?"true":"false":e.value),e=l,e!==n?(t.setValue(e),!0):!1}o(a_,"Wa");function Sv(e){if(e=e||(typeof document!="undefined"?document:void 0),typeof e=="undefined")return null;try{return e.activeElement||e.body}catch(t){return e.body}}o(Sv,"Xa");function mw(e,t){var n=t.checked;return fr({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}o(mw,"Ya");function u_(e,t){var n=t.defaultValue==null?"":t.defaultValue,l=t.checked!=null?t.checked:t.defaultChecked;n=Aa(t.value!=null?t.value:n),e._wrapperState={initialChecked:l,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}o(u_,"Za");function f_(e,t){t=t.checked,t!=null&&iw(e,"checked",t,!1)}o(f_,"$a");function vw(e,t){f_(e,t);var n=Aa(t.value),l=t.type;if(n!=null)l==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(l==="submit"||l==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?gw(e,t.type,n):t.hasOwnProperty("defaultValue")&&gw(e,t.type,Aa(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}o(vw,"ab");function c_(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var l=t.type;if(!(l!=="submit"&&l!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}o(c_,"cb");function gw(e,t,n){(t!=="number"||Sv(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}o(gw,"bb");function xA(e){var t="";return mv.Children.forEach(e,function(n){n!=null&&(t+=n)}),t}o(xA,"db");function yw(e,t){return e=fr({children:void 0},t),(t=xA(t.children))&&(e.children=t),e}o(yw,"eb");function Ac(e,t,n,l){if(e=e.options,t){t={};for(var d=0;d=n.length))throw Error(we(93));n=n[0]}t=n}t==null&&(t=""),n=t}e._wrapperState={initialValue:Aa(n)}}o(p_,"hb");function d_(e,t){var n=Aa(t.value),l=Aa(t.defaultValue);n!=null&&(n=""+n,n!==e.value&&(e.value=n),t.defaultValue==null&&e.defaultValue!==n&&(e.defaultValue=n)),l!=null&&(e.defaultValue=""+l)}o(d_,"ib");function h_(e){var t=e.textContent;t===e._wrapperState.initialValue&&t!==""&&t!==null&&(e.value=t)}o(h_,"jb");var xw={html:"http://www.w3.org/1999/xhtml",mathml:"http://www.w3.org/1998/Math/MathML",svg:"http://www.w3.org/2000/svg"};function m_(e){switch(e){case"svg":return"http://www.w3.org/2000/svg";case"math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}o(m_,"lb");function Sw(e,t){return e==null||e==="http://www.w3.org/1999/xhtml"?m_(t):e==="http://www.w3.org/2000/svg"&&t==="foreignObject"?"http://www.w3.org/1999/xhtml":e}o(Sw,"mb");var Cv,v_=function(e){return typeof MSApp!="undefined"&&MSApp.execUnsafeLocalFunction?function(t,n,l,d){MSApp.execUnsafeLocalFunction(function(){return e(t,n,l,d)})}:e}(function(e,t){if(e.namespaceURI!==xw.svg||"innerHTML"in e)e.innerHTML=t;else{for(Cv=Cv||document.createElement("div"),Cv.innerHTML=""+t.valueOf().toString()+"",t=Cv.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}});function Id(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&n.nodeType===3){n.nodeValue=t;return}}e.textContent=t}o(Id,"pb");var Hd={animationIterationCount:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},SA=["Webkit","ms","Moz","O"];Object.keys(Hd).forEach(function(e){SA.forEach(function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),Hd[t]=Hd[e]})});function g_(e,t,n){return t==null||typeof t=="boolean"||t===""?"":n||typeof t!="number"||t===0||Hd.hasOwnProperty(e)&&Hd[e]?(""+t).trim():t+"px"}o(g_,"sb");function y_(e,t){e=e.style;for(var n in t)if(t.hasOwnProperty(n)){var l=n.indexOf("--")===0,d=g_(n,t[n],l);n==="float"&&(n="cssFloat"),l?e.setProperty(n,d):e[n]=d}}o(y_,"tb");var CA=fr({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function Cw(e,t){if(t){if(CA[e]&&(t.children!=null||t.dangerouslySetInnerHTML!=null))throw Error(we(137,e));if(t.dangerouslySetInnerHTML!=null){if(t.children!=null)throw Error(we(60));if(!(typeof t.dangerouslySetInnerHTML=="object"&&"__html"in t.dangerouslySetInnerHTML))throw Error(we(61))}if(t.style!=null&&typeof t.style!="object")throw Error(we(62))}}o(Cw,"vb");function _w(e,t){if(e.indexOf("-")===-1)return typeof t.is=="string";switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}o(_w,"wb");function bw(e){return e=e.target||e.srcElement||window,e.correspondingUseElement&&(e=e.correspondingUseElement),e.nodeType===3?e.parentNode:e}o(bw,"xb");var Ew=null,Dc=null,Rc=null;function w_(e){if(e=rh(e)){if(typeof Ew!="function")throw Error(we(280));var t=e.stateNode;t&&(t=zv(t),Ew(e.stateNode,e.type,t))}}o(w_,"Bb");function x_(e){Dc?Rc?Rc.push(e):Rc=[e]:Dc=e}o(x_,"Eb");function S_(){if(Dc){var e=Dc,t=Rc;if(Rc=Dc=null,w_(e),t)for(e=0;el?0:1<n;n++)t.push(e);return t}o(Hw,"Zc");function Lv(e,t,n){e.pendingLanes|=t;var l=t-1;e.suspendedLanes&=l,e.pingedLanes&=l,e=e.eventTimes,t=31-Ia(t),e[t]=n}o(Lv,"$c");var Ia=Math.clz32?Math.clz32:HA,FA=Math.log,IA=Math.LN2;function HA(e){return e===0?32:31-(FA(e)/IA|0)|0}o(HA,"ad");var WA=wn.unstable_UserBlockingPriority,BA=wn.unstable_runWithPriority,Nv=!0;function UA(e,t,n,l){rf||kw();var d=Ww,m=rf;rf=!0;try{C_(d,e,t,n,l)}finally{(rf=m)||Lw()}}o(UA,"gd");function $A(e,t,n,l){BA(WA,Ww.bind(null,e,t,n,l))}o($A,"id");function Ww(e,t,n,l){if(Nv){var d;if((d=(t&4)==0)&&0=Yd),G_=String.fromCharCode(32),Y_=!1;function X_(e,t){switch(e){case"keyup":return cD.indexOf(t.keyCode)!==-1;case"keydown":return t.keyCode!==229;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}o(X_,"ge");function Q_(e){return e=e.detail,typeof e=="object"&&"data"in e?e.data:null}o(Q_,"he");var Uc=!1;function dD(e,t){switch(e){case"compositionend":return Q_(t);case"keypress":return t.which!==32?null:(Y_=!0,G_);case"textInput":return e=t.data,e===G_&&Y_?null:e;default:return null}}o(dD,"je");function hD(e,t){if(Uc)return e==="compositionend"||!Kw&&X_(e,t)?(e=$_(),Pv=Uw=Ha=null,Uc=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=l}e:{for(;n;){if(n.nextSibling){n=n.nextSibling;break e}n=n.parentNode}n=void 0}n=nb(n)}}o(ib,"Le");function ob(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?ob(e,t.parentNode):"contains"in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}o(ob,"Me");function sb(){for(var e=window,t=Sv();t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href=="string"}catch(l){n=!1}if(n)e=t.contentWindow;else break;t=Sv(e.document)}return t}o(sb,"Ne");function Yw(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t==="input"&&(e.type==="text"||e.type==="search"||e.type==="tel"||e.type==="url"||e.type==="password")||t==="textarea"||e.contentEditable==="true")}o(Yw,"Oe");var bD=Ll&&"documentMode"in document&&11>=document.documentMode,$c=null,Xw=null,Jd=null,Qw=!1;function lb(e,t,n){var l=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Qw||$c==null||$c!==Sv(l)||(l=$c,"selectionStart"in l&&Yw(l)?l={start:l.selectionStart,end:l.selectionEnd}:(l=(l.ownerDocument&&l.ownerDocument.defaultView||window).getSelection(),l={anchorNode:l.anchorNode,anchorOffset:l.anchorOffset,focusNode:l.focusNode,focusOffset:l.focusOffset}),Jd&&Zd(Jd,l)||(Jd=l,l=Wv(Xw,"onSelect"),0Kc||(e.current=i1[Kc],i1[Kc]=null,Kc--)}o(ir,"H");function xr(e,t){Kc++,i1[Kc]=e.current,e.current=t}o(xr,"I");var Ua={},qn=Ba(Ua),Ni=Ba(!1),sf=Ua;function Gc(e,t){var n=e.type.contextTypes;if(!n)return Ua;var l=e.stateNode;if(l&&l.__reactInternalMemoizedUnmaskedChildContext===t)return l.__reactInternalMemoizedMaskedChildContext;var d={},m;for(m in n)d[m]=t[m];return l&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=d),d}o(Gc,"Ef");function Pi(e){return e=e.childContextTypes,e!=null}o(Pi,"Ff");function jv(){ir(Ni),ir(qn)}o(jv,"Gf");function Cb(e,t,n){if(qn.current!==Ua)throw Error(we(168));xr(qn,t),xr(Ni,n)}o(Cb,"Hf");function _b(e,t,n){var l=e.stateNode;if(e=t.childContextTypes,typeof l.getChildContext!="function")return n;l=l.getChildContext();for(var d in l)if(!(d in e))throw Error(we(108,Mc(t)||"Unknown",d));return fr({},n,l)}o(_b,"If");function qv(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||Ua,sf=qn.current,xr(qn,e),xr(Ni,Ni.current),!0}o(qv,"Jf");function bb(e,t,n){var l=e.stateNode;if(!l)throw Error(we(169));n?(e=_b(e,t,sf),l.__reactInternalMemoizedMergedChildContext=e,ir(Ni),ir(qn),xr(qn,e)):ir(Ni),xr(Ni,n)}o(bb,"Kf");var o1=null,lf=null,kD=wn.unstable_runWithPriority,s1=wn.unstable_scheduleCallback,l1=wn.unstable_cancelCallback,OD=wn.unstable_shouldYield,Eb=wn.unstable_requestPaint,a1=wn.unstable_now,LD=wn.unstable_getCurrentPriorityLevel,Vv=wn.unstable_ImmediatePriority,Tb=wn.unstable_UserBlockingPriority,kb=wn.unstable_NormalPriority,Ob=wn.unstable_LowPriority,Lb=wn.unstable_IdlePriority,u1={},ND=Eb!==void 0?Eb:function(){},Nl=null,Kv=null,f1=!1,Nb=a1(),Vn=1e4>Nb?a1:function(){return a1()-Nb};function Yc(){switch(LD()){case Vv:return 99;case Tb:return 98;case kb:return 97;case Ob:return 96;case Lb:return 95;default:throw Error(we(332))}}o(Yc,"eg");function Pb(e){switch(e){case 99:return Vv;case 98:return Tb;case 97:return kb;case 96:return Ob;case 95:return Lb;default:throw Error(we(332))}}o(Pb,"fg");function af(e,t){return e=Pb(e),kD(e,t)}o(af,"gg");function nh(e,t,n){return e=Pb(e),s1(e,t,n)}o(nh,"hg");function Is(){if(Kv!==null){var e=Kv;Kv=null,l1(e)}Mb()}o(Is,"ig");function Mb(){if(!f1&&Nl!==null){f1=!0;var e=0;try{var t=Nl;af(99,function(){for(;ede?(ge=ie,ie=null):ge=ie.sibling;var xe=U(F,ie,R[de],K);if(xe===null){ie===null&&(ie=ge);break}e&&ie&&xe.alternate===null&&t(F,ie),M=m(xe,M,de),ue===null?V=xe:ue.sibling=xe,ue=xe,ie=ge}if(de===R.length)return n(F,ie),V;if(ie===null){for(;dede?(ge=ie,ie=null):ge=ie.sibling;var qe=U(F,ie,xe.value,K);if(qe===null){ie===null&&(ie=ge);break}e&&ie&&qe.alternate===null&&t(F,ie),M=m(qe,M,de),ue===null?V=qe:ue.sibling=qe,ue=qe,ie=ge}if(xe.done)return n(F,ie),V;if(ie===null){for(;!xe.done;de++,xe=R.next())xe=Y(F,xe.value,K),xe!==null&&(M=m(xe,M,de),ue===null?V=xe:ue.sibling=xe,ue=xe);return V}for(ie=l(F,ie);!xe.done;de++,xe=R.next())xe=X(ie,F,de,xe.value,K),xe!==null&&(e&&xe.alternate!==null&&ie.delete(xe.key===null?de:xe.key),M=m(xe,M,de),ue===null?V=xe:ue.sibling=xe,ue=xe);return e&&ie.forEach(function(et){return t(F,et)}),V}return o(Q,"w"),function(F,M,R,K){var V=typeof R=="object"&&R!==null&&R.type===Ma&&R.key===null;V&&(R=R.props.children);var ue=typeof R=="object"&&R!==null;if(ue)switch(R.$$typeof){case Md:e:{for(ue=R.key,V=M;V!==null;){if(V.key===ue){switch(V.tag){case 7:if(R.type===Ma){n(F,V.sibling),M=d(V,R.props.children),M.return=F,F=M;break e}break;default:if(V.elementType===R.type){n(F,V.sibling),M=d(V,R.props),M.ref=oh(F,V,R),M.return=F,F=M;break e}}n(F,V);break}else t(F,V);V=V.sibling}R.type===Ma?(M=op(R.props.children,F.mode,K,R.key),M.return=F,F=M):(K=yg(R.type,R.key,R.props,null,F.mode,K),K.ref=oh(F,M,R),K.return=F,F=K)}return p(F);case tf:e:{for(V=R.key;M!==null;){if(M.key===V)if(M.tag===4&&M.stateNode.containerInfo===R.containerInfo&&M.stateNode.implementation===R.implementation){n(F,M.sibling),M=d(M,R.children||[]),M.return=F,F=M;break e}else{n(F,M);break}else t(F,M);M=M.sibling}M=Q1(R,F.mode,K),M.return=F,F=M}return p(F)}if(typeof R=="string"||typeof R=="number")return R=""+R,M!==null&&M.tag===6?(n(F,M.sibling),M=d(M,R),M.return=F,F=M):(n(F,M),M=X1(R,F.mode,K),M.return=F,F=M),p(F);if(Jv(R))return te(F,M,R,K);if(Rd(R))return Q(F,M,R,K);if(ue&&eg(F,R),typeof R=="undefined"&&!V)switch(F.tag){case 1:case 22:case 0:case 11:case 15:throw Error(we(152,Mc(F.type)||"Component"))}return n(F,M)}}o(Ub,"Sg");var tg=Ub(!0),$b=Ub(!1),sh={},Hs=Ba(sh),lh=Ba(sh),ah=Ba(sh);function uf(e){if(e===sh)throw Error(we(174));return e}o(uf,"dh");function m1(e,t){switch(xr(ah,t),xr(lh,e),xr(Hs,sh),e=t.nodeType,e){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:Sw(null,"");break;default:e=e===8?t.parentNode:t,t=e.namespaceURI||null,e=e.tagName,t=Sw(t,e)}ir(Hs),xr(Hs,t)}o(m1,"eh");function Zc(){ir(Hs),ir(lh),ir(ah)}o(Zc,"fh");function zb(e){uf(ah.current);var t=uf(Hs.current),n=Sw(t,e.type);t!==n&&(xr(lh,e),xr(Hs,n))}o(zb,"gh");function v1(e){lh.current===e&&(ir(Hs),ir(lh))}o(v1,"hh");var Sr=Ba(0);function rg(e){for(var t=e;t!==null;){if(t.tag===13){var n=t.memoizedState;if(n!==null&&(n=n.dehydrated,n===null||n.data==="$?"||n.data==="$!"))return t}else if(t.tag===19&&t.memoizedProps.revealOrder!==void 0){if((t.flags&64)!=0)return t}else if(t.child!==null){t.child.return=t,t=t.child;continue}if(t===e)break;for(;t.sibling===null;){if(t.return===null||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}o(rg,"ih");var Pl=null,qa=null,Ws=!1;function jb(e,t){var n=bo(5,null,null,0);n.elementType="DELETED",n.type="DELETED",n.stateNode=t,n.return=e,n.flags=8,e.lastEffect!==null?(e.lastEffect.nextEffect=n,e.lastEffect=n):e.firstEffect=e.lastEffect=n}o(jb,"mh");function qb(e,t){switch(e.tag){case 5:var n=e.type;return t=t.nodeType!==1||n.toLowerCase()!==t.nodeName.toLowerCase()?null:t,t!==null?(e.stateNode=t,!0):!1;case 6:return t=e.pendingProps===""||t.nodeType!==3?null:t,t!==null?(e.stateNode=t,!0):!1;case 13:return!1;default:return!1}}o(qb,"oh");function g1(e){if(Ws){var t=qa;if(t){var n=t;if(!qb(e,t)){if(t=jc(n.nextSibling),!t||!qb(e,t)){e.flags=e.flags&-1025|2,Ws=!1,Pl=e;return}jb(Pl,n)}Pl=e,qa=jc(t.firstChild)}else e.flags=e.flags&-1025|2,Ws=!1,Pl=e}}o(g1,"ph");function Vb(e){for(e=e.return;e!==null&&e.tag!==5&&e.tag!==3&&e.tag!==13;)e=e.return;Pl=e}o(Vb,"qh");function ng(e){if(e!==Pl)return!1;if(!Ws)return Vb(e),Ws=!0,!1;var t=e.type;if(e.tag!==5||t!=="head"&&t!=="body"&&!t1(t,e.memoizedProps))for(t=qa;t;)jb(e,t),t=jc(t.nextSibling);if(Vb(e),e.tag===13){if(e=e.memoizedState,e=e!==null?e.dehydrated:null,!e)throw Error(we(317));e:{for(e=e.nextSibling,t=0;e;){if(e.nodeType===8){var n=e.data;if(n==="/$"){if(t===0){qa=jc(e.nextSibling);break e}t--}else n!=="$"&&n!=="$!"&&n!=="$?"||t++}e=e.nextSibling}qa=null}}else qa=Pl?jc(e.stateNode.nextSibling):null;return!0}o(ng,"rh");function y1(){qa=Pl=null,Ws=!1}o(y1,"sh");var Jc=[];function w1(){for(var e=0;em))throw Error(we(301));m+=1,Mn=Kn=null,t.updateQueue=null,uh.current=RD,e=n(l,d)}while(ch)}if(uh.current=ag,t=Kn!==null&&Kn.next!==null,fh=0,Mn=Kn=Pr=null,ig=!1,t)throw Error(we(300));return e}o(S1,"Ch");function ff(){var e={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return Mn===null?Pr.memoizedState=Mn=e:Mn=Mn.next=e,Mn}o(ff,"Hh");function cf(){if(Kn===null){var e=Pr.alternate;e=e!==null?e.memoizedState:null}else e=Kn.next;var t=Mn===null?Pr.memoizedState:Mn.next;if(t!==null)Mn=t,Kn=e;else{if(e===null)throw Error(we(310));Kn=e,e={memoizedState:Kn.memoizedState,baseState:Kn.baseState,baseQueue:Kn.baseQueue,queue:Kn.queue,next:null},Mn===null?Pr.memoizedState=Mn=e:Mn=Mn.next=e}return Mn}o(cf,"Ih");function Bs(e,t){return typeof t=="function"?t(e):t}o(Bs,"Jh");function ph(e){var t=cf(),n=t.queue;if(n===null)throw Error(we(311));n.lastRenderedReducer=e;var l=Kn,d=l.baseQueue,m=n.pending;if(m!==null){if(d!==null){var p=d.next;d.next=m.next,m.next=p}l.baseQueue=d=m,n.pending=null}if(d!==null){d=d.next,l=l.baseState;var x=p=m=null,_=d;do{var O=_.lane;if((fh&O)===O)x!==null&&(x=x.next={lane:0,action:_.action,eagerReducer:_.eagerReducer,eagerState:_.eagerState,next:null}),l=_.eagerReducer===e?_.eagerState:e(l,_.action);else{var D={lane:O,action:_.action,eagerReducer:_.eagerReducer,eagerState:_.eagerState,next:null};x===null?(p=x=D,m=l):x=x.next=D,Pr.lanes|=O,vh|=O}_=_.next}while(_!==null&&_!==d);x===null?m=l:x.next=p,xo(l,t.memoizedState)||(Jo=!0),t.memoizedState=l,t.baseState=m,t.baseQueue=x,n.lastRenderedState=l}return[t.memoizedState,n.dispatch]}o(ph,"Kh");function dh(e){var t=cf(),n=t.queue;if(n===null)throw Error(we(311));n.lastRenderedReducer=e;var l=n.dispatch,d=n.pending,m=t.memoizedState;if(d!==null){n.pending=null;var p=d=d.next;do m=e(m,p.action),p=p.next;while(p!==d);xo(m,t.memoizedState)||(Jo=!0),t.memoizedState=m,t.baseQueue===null&&(t.baseState=m),n.lastRenderedState=m}return[m,l]}o(dh,"Lh");function Kb(e,t,n){var l=t._getVersion;l=l(t._source);var d=t._workInProgressVersionPrimary;if(d!==null?e=d===l:(e=e.mutableReadLanes,(e=(fh&e)===e)&&(t._workInProgressVersionPrimary=l,Jc.push(t))),e)return n(t._source);throw Jc.push(t),Error(we(350))}o(Kb,"Mh");function Gb(e,t,n,l){var d=ci;if(d===null)throw Error(we(349));var m=t._getVersion,p=m(t._source),x=uh.current,_=x.useState(function(){return Kb(d,t,n)}),O=_[1],D=_[0];_=Mn;var Y=e.memoizedState,U=Y.refs,X=U.getSnapshot,te=Y.source;Y=Y.subscribe;var Q=Pr;return e.memoizedState={refs:U,source:t,subscribe:l},x.useEffect(function(){U.getSnapshot=n,U.setSnapshot=O;var F=m(t._source);if(!xo(p,F)){F=n(t._source),xo(D,F)||(O(F),F=Ka(Q),d.mutableReadLanes|=F&d.pendingLanes),F=d.mutableReadLanes,d.entangledLanes|=F;for(var M=d.entanglements,R=F;0n?98:n,function(){e(!0)}),af(97<\/script>",e=e.removeChild(e.firstChild)):typeof l.is=="string"?e=p.createElement(n,{is:l.is}):(e=p.createElement(n),n==="select"&&(p=e,l.multiple?p.multiple=!0:l.size&&(p.size=l.size))):e=p.createElementNS(e,n),e[Wa]=t,e[$v]=l,mE(e,t,!1,!1),t.stateNode=e,p=_w(n,l),n){case"dialog":nr("cancel",e),nr("close",e),d=l;break;case"iframe":case"object":case"embed":nr("load",e),d=l;break;case"video":case"audio":for(d=0;dU1&&(t.flags|=64,m=!0,mh(l,!1),t.lanes=33554432)}else{if(!m)if(e=rg(p),e!==null){if(t.flags|=64,m=!0,n=e.updateQueue,n!==null&&(t.updateQueue=n,t.flags|=4),mh(l,!0),l.tail===null&&l.tailMode==="hidden"&&!p.alternate&&!Ws)return t=t.lastEffect=l.lastEffect,t!==null&&(t.nextEffect=null),null}else 2*Vn()-l.renderingStartTime>U1&&n!==1073741824&&(t.flags|=64,m=!0,mh(l,!1),t.lanes=33554432);l.isBackwards?(p.sibling=t.child,t.child=p):(n=l.last,n!==null?n.sibling=p:t.child=p,l.last=p)}return l.tail!==null?(n=l.tail,l.rendering=n,l.tail=n.sibling,l.lastEffect=t.lastEffect,l.renderingStartTime=Vn(),n.sibling=null,t=Sr.current,xr(Sr,m?t&1|2:t&1),n):null;case 23:case 24:return K1(),e!==null&&e.memoizedState!==null!=(t.memoizedState!==null)&&l.mode!=="unstable-defer-without-hiding"&&(t.flags|=4),null}throw Error(we(156,t.tag))}o(ID,"Gi");function HD(e){switch(e.tag){case 1:Pi(e.type)&&jv();var t=e.flags;return t&4096?(e.flags=t&-4097|64,e):null;case 3:if(Zc(),ir(Ni),ir(qn),w1(),t=e.flags,(t&64)!=0)throw Error(we(285));return e.flags=t&-4097|64,e;case 5:return v1(e),null;case 13:return ir(Sr),t=e.flags,t&4096?(e.flags=t&-4097|64,e):null;case 19:return ir(Sr),null;case 4:return Zc(),null;case 10:return p1(e),null;case 23:case 24:return K1(),null;default:return null}}o(HD,"Li");function P1(e,t){try{var n="",l=t;do n+=yA(l),l=l.return;while(l);var d=n}catch(m){d=` +Error generating stack: `+m.message+` +`+m.stack}return{value:e,source:t,stack:d}}o(P1,"Mi");function M1(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}o(M1,"Ni");var WD=typeof WeakMap=="function"?WeakMap:Map;function yE(e,t,n){n=za(-1,n),n.tag=3,n.payload={element:null};var l=t.value;return n.callback=function(){pg||(pg=!0,$1=l),M1(e,t)},n}o(yE,"Pi");function wE(e,t,n){n=za(-1,n),n.tag=3;var l=e.type.getDerivedStateFromError;if(typeof l=="function"){var d=t.value;n.payload=function(){return M1(e,t),l(d)}}var m=e.stateNode;return m!==null&&typeof m.componentDidCatch=="function"&&(n.callback=function(){typeof l!="function"&&(Us===null?Us=new Set([this]):Us.add(this),M1(e,t));var p=t.stack;this.componentDidCatch(t.value,{componentStack:p!==null?p:""})}),n}o(wE,"Si");var BD=typeof WeakSet=="function"?WeakSet:Set;function xE(e){var t=e.ref;if(t!==null)if(typeof t=="function")try{t(null)}catch(n){Xa(e,n)}else t.current=null}o(xE,"Vi");function UD(e,t){switch(t.tag){case 0:case 11:case 15:case 22:return;case 1:if(t.flags&256&&e!==null){var n=e.memoizedProps,l=e.memoizedState;e=t.stateNode,t=e.getSnapshotBeforeUpdate(t.elementType===t.type?n:Zo(t.type,n),l),e.__reactInternalSnapshotBeforeUpdate=t}return;case 3:t.flags&256&&r1(t.stateNode.containerInfo);return;case 5:case 6:case 4:case 17:return}throw Error(we(163))}o(UD,"Xi");function $D(e,t,n){switch(n.tag){case 0:case 11:case 15:case 22:if(t=n.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{if((e.tag&3)==3){var l=e.create;e.destroy=l()}e=e.next}while(e!==t)}if(t=n.updateQueue,t=t!==null?t.lastEffect:null,t!==null){e=t=t.next;do{var d=e;l=d.next,d=d.tag,(d&4)!=0&&(d&1)!=0&&(RE(n,e),XD(n,e)),e=l}while(e!==t)}return;case 1:e=n.stateNode,n.flags&4&&(t===null?e.componentDidMount():(l=n.elementType===n.type?t.memoizedProps:Zo(n.type,t.memoizedProps),e.componentDidUpdate(l,t.memoizedState,e.__reactInternalSnapshotBeforeUpdate))),t=n.updateQueue,t!==null&&Fb(n,t,e);return;case 3:if(t=n.updateQueue,t!==null){if(e=null,n.child!==null)switch(n.child.tag){case 5:e=n.child.stateNode;break;case 1:e=n.child.stateNode}Fb(n,t,e)}return;case 5:e=n.stateNode,t===null&&n.flags&4&&gb(n.type,n.memoizedProps)&&e.focus();return;case 6:return;case 4:return;case 12:return;case 13:n.memoizedState===null&&(n=n.alternate,n!==null&&(n=n.memoizedState,n!==null&&(n=n.dehydrated,n!==null&&D_(n))));return;case 19:case 17:case 20:case 21:case 23:case 24:return}throw Error(we(163))}o($D,"Yi");function SE(e,t){for(var n=e;;){if(n.tag===5){var l=n.stateNode;if(t)l=l.style,typeof l.setProperty=="function"?l.setProperty("display","none","important"):l.display="none";else{l=n.stateNode;var d=n.memoizedProps.style;d=d!=null&&d.hasOwnProperty("display")?d.display:null,l.style.display=g_("display",d)}}else if(n.tag===6)n.stateNode.nodeValue=t?"":n.memoizedProps;else if((n.tag!==23&&n.tag!==24||n.memoizedState===null||n===e)&&n.child!==null){n.child.return=n,n=n.child;continue}if(n===e)break;for(;n.sibling===null;){if(n.return===null||n.return===e)return;n=n.return}n.sibling.return=n.return,n=n.sibling}}o(SE,"aj");function CE(e,t){if(lf&&typeof lf.onCommitFiberUnmount=="function")try{lf.onCommitFiberUnmount(o1,t)}catch(m){}switch(t.tag){case 0:case 11:case 14:case 15:case 22:if(e=t.updateQueue,e!==null&&(e=e.lastEffect,e!==null)){var n=e=e.next;do{var l=n,d=l.destroy;if(l=l.tag,d!==void 0)if((l&4)!=0)RE(t,n);else{l=t;try{d()}catch(m){Xa(l,m)}}n=n.next}while(n!==e)}break;case 1:if(xE(t),e=t.stateNode,typeof e.componentWillUnmount=="function")try{e.props=t.memoizedProps,e.state=t.memoizedState,e.componentWillUnmount()}catch(m){Xa(t,m)}break;case 5:xE(t);break;case 4:TE(e,t)}}o(CE,"bj");function _E(e){e.alternate=null,e.child=null,e.dependencies=null,e.firstEffect=null,e.lastEffect=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.return=null,e.updateQueue=null}o(_E,"dj");function bE(e){return e.tag===5||e.tag===3||e.tag===4}o(bE,"ej");function EE(e){e:{for(var t=e.return;t!==null;){if(bE(t))break e;t=t.return}throw Error(we(160))}var n=t;switch(t=n.stateNode,n.tag){case 5:var l=!1;break;case 3:t=t.containerInfo,l=!0;break;case 4:t=t.containerInfo,l=!0;break;default:throw Error(we(161))}n.flags&16&&(Id(t,""),n.flags&=-17);e:t:for(n=e;;){for(;n.sibling===null;){if(n.return===null||bE(n.return)){n=null;break e}n=n.return}for(n.sibling.return=n.return,n=n.sibling;n.tag!==5&&n.tag!==6&&n.tag!==18;){if(n.flags&2||n.child===null||n.tag===4)continue t;n.child.return=n,n=n.child}if(!(n.flags&2)){n=n.stateNode;break e}}l?A1(e,n,t):D1(e,n,t)}o(EE,"fj");function A1(e,t,n){var l=e.tag,d=l===5||l===6;if(d)e=d?e.stateNode:e.stateNode.instance,t?n.nodeType===8?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(n.nodeType===8?(t=n.parentNode,t.insertBefore(e,n)):(t=n,t.appendChild(e)),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=Bv));else if(l!==4&&(e=e.child,e!==null))for(A1(e,t,n),e=e.sibling;e!==null;)A1(e,t,n),e=e.sibling}o(A1,"gj");function D1(e,t,n){var l=e.tag,d=l===5||l===6;if(d)e=d?e.stateNode:e.stateNode.instance,t?n.insertBefore(e,t):n.appendChild(e);else if(l!==4&&(e=e.child,e!==null))for(D1(e,t,n),e=e.sibling;e!==null;)D1(e,t,n),e=e.sibling}o(D1,"hj");function TE(e,t){for(var n=t,l=!1,d,m;;){if(!l){l=n.return;e:for(;;){if(l===null)throw Error(we(160));switch(d=l.stateNode,l.tag){case 5:m=!1;break e;case 3:d=d.containerInfo,m=!0;break e;case 4:d=d.containerInfo,m=!0;break e}l=l.return}l=!0}if(n.tag===5||n.tag===6){e:for(var p=e,x=n,_=x;;)if(CE(p,_),_.child!==null&&_.tag!==4)_.child.return=_,_=_.child;else{if(_===x)break e;for(;_.sibling===null;){if(_.return===null||_.return===x)break e;_=_.return}_.sibling.return=_.return,_=_.sibling}m?(p=d,x=n.stateNode,p.nodeType===8?p.parentNode.removeChild(x):p.removeChild(x)):d.removeChild(n.stateNode)}else if(n.tag===4){if(n.child!==null){d=n.stateNode.containerInfo,m=!0,n.child.return=n,n=n.child;continue}}else if(CE(e,n),n.child!==null){n.child.return=n,n=n.child;continue}if(n===t)break;for(;n.sibling===null;){if(n.return===null||n.return===t)return;n=n.return,n.tag===4&&(l=!1)}n.sibling.return=n.return,n=n.sibling}}o(TE,"cj");function R1(e,t){switch(t.tag){case 0:case 11:case 14:case 15:case 22:var n=t.updateQueue;if(n=n!==null?n.lastEffect:null,n!==null){var l=n=n.next;do(l.tag&3)==3&&(e=l.destroy,l.destroy=void 0,e!==void 0&&e()),l=l.next;while(l!==n)}return;case 1:return;case 5:if(n=t.stateNode,n!=null){l=t.memoizedProps;var d=e!==null?e.memoizedProps:l;e=t.type;var m=t.updateQueue;if(t.updateQueue=null,m!==null){for(n[$v]=l,e==="input"&&l.type==="radio"&&l.name!=null&&f_(n,l),_w(e,d),t=_w(e,l),d=0;dd&&(d=p),n&=~m}if(n=d,n=Vn()-n,n=(120>n?120:480>n?480:1080>n?1080:1920>n?1920:3e3>n?3e3:4320>n?4320:1960*jD(n/1960))-n,10 component higher in the tree to provide a loading indicator or placeholder to display.`)}Pn!==5&&(Pn=2),_=L1(_,w),W=p;do{switch(W.tag){case 3:v=_,W.flags|=4096,t&=-t,W.lanes|=t;var ue=dE(W,v,t);Nb(W,ue);break e;case 1:v=_;var ie=W.type,de=W.stateNode;if((W.flags&64)==0&&(typeof ie.getDerivedStateFromError=="function"||de!==null&&typeof de.componentDidCatch=="function"&&(js===null||!js.has(de)))){W.flags|=4096,t&=-t,W.lanes|=t;var ge=hE(W,v,t);Nb(W,ge);break e}}W=W.return}while(W!==null)}LE(n)}catch(we){t=we,en===n&&n!==null&&(en=n=n.return);continue}break}while(1)}o(TE,"Sj");function kE(){var e=lg.current;return lg.current=og,e===null?og:e}o(kE,"Pj");function dh(e,t){var n=Xe;Xe|=16;var l=kE();ui===e&&Gn===t||Yc(e,t);do try{WD();break}catch(d){TE(e,d)}while(1);if(u1(),Xe=n,lg.current=l,en!==null)throw Error(ye(261));return ui=null,Gn=0,Pn}o(dh,"Tj");function WD(){for(;en!==null;)OE(en)}o(WD,"ak");function BD(){for(;en!==null&&!SD();)OE(en)}o(BD,"Rj");function OE(e){var t=ME(e.alternate,e,nf);e.memoizedProps=e.pendingProps,t===null?LE(e):en=t,D1.current=null}o(OE,"bk");function LE(e){var t=e;do{var n=t.alternate;if(e=t.return,(t.flags&2048)==0){if(n=ND(n,t,nf),n!==null){en=n;return}if(n=t,n.tag!==24&&n.tag!==23||n.memoizedState===null||(nf&1073741824)!=0||(n.mode&4)==0){for(var l=0,d=n.child;d!==null;)l|=d.lanes|d.childLanes,d=d.sibling;n.childLanes=l}e!==null&&(e.flags&2048)==0&&(e.firstEffect===null&&(e.firstEffect=t.firstEffect),t.lastEffect!==null&&(e.lastEffect!==null&&(e.lastEffect.nextEffect=t.firstEffect),e.lastEffect=t.lastEffect),1p&&(w=p,p=ue,ue=w),w=J_(F,ue),v=J_(F,p),w&&v&&(V.rangeCount!==1||V.anchorNode!==w.node||V.anchorOffset!==w.offset||V.focusNode!==v.node||V.focusOffset!==v.offset)&&(K=K.createRange(),K.setStart(w.node,w.offset),V.removeAllRanges(),ue>p?(V.addRange(K),V.extend(v.node,v.offset)):(K.setEnd(v.node,v.offset),V.addRange(K)))))),K=[],V=F;V=V.parentNode;)V.nodeType===1&&K.push({element:V,left:V.scrollLeft,top:V.scrollTop});for(typeof F.focus=="function"&&F.focus(),F=0;FVn()-H1?Yc(e,0):F1|=n),_o(e,t)}o(qD,"Yj");function VD(e,t){var n=e.stateNode;n!==null&&n.delete(t),t=0,t===0&&(t=e.mode,(t&2)==0?t=1:(t&4)==0?t=Bc()===99?1:2:(Ml===0&&(Ml=qc),t=Nc(62914560&~Ml),t===0&&(t=4194304))),n=Ki(),e=dg(e,t),e!==null&&(Tv(e,t,n),_o(e,n))}o(VD,"lj");var ME;ME=o(function(e,t,n){var l=t.lanes;if(e!==null)if(e.memoizedProps!==t.pendingProps||Li.current)Jo=!0;else if((n&l)!=0)Jo=(e.flags&16384)!=0;else{switch(Jo=!1,t.tag){case 3:nE(t),v1();break;case 5:Hb(t);break;case 1:Ni(t.type)&&zv(t);break;case 4:d1(t,t.stateNode.containerInfo);break;case 10:l=t.memoizedProps.value;var d=t.type._context;xr(qv,d._currentValue),d._currentValue=l;break;case 13:if(t.memoizedState!==null)return(n&t.child.childLanes)!=0?iE(e,t,n):(xr(Sr,Sr.current&1),t=Nl(e,t,n),t!==null?t.sibling:null);xr(Sr,Sr.current&1);break;case 19:if(l=(n&t.childLanes)!=0,(e.flags&64)!=0){if(l)return uE(e,t,n);t.flags|=64}if(d=t.memoizedState,d!==null&&(d.rendering=null,d.tail=null,d.lastEffect=null),xr(Sr,Sr.current),l)break;return null;case 23:case 24:return t.lanes=0,b1(e,t,n)}return Nl(e,t,n)}else Jo=!1;switch(t.lanes=0,t.tag){case 2:if(l=t.type,e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2),e=t.pendingProps,d=Wc(t,qn.current),zc(t,n),d=w1(null,t,l,e,d,n),t.flags|=1,typeof d=="object"&&d!==null&&typeof d.render=="function"&&d.$$typeof===void 0){if(t.tag=1,t.memoizedState=null,t.updateQueue=null,Ni(l)){var v=!0;zv(t)}else v=!1;t.memoizedState=d.state!==null&&d.state!==void 0?d.state:null,c1(t);var p=l.getDerivedStateFromProps;typeof p=="function"&&Gv(t,l,p,e),d.updater=Yv,t.stateNode=d,d._reactInternals=t,p1(t,l,e,n),t=T1(null,t,l,!0,v,n)}else t.tag=0,Mi(null,t,d,n),t=t.child;return t;case 16:d=t.elementType;e:{switch(e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2),e=t.pendingProps,v=d._init,d=v(d._payload),t.type=d,v=t.tag=GD(d),e=Zo(d,e),v){case 0:t=E1(null,t,d,e,n);break e;case 1:t=rE(null,t,d,e,n);break e;case 11:t=Zb(null,t,d,e,n);break e;case 14:t=Jb(null,t,d,Zo(d.type,e),l,n);break e}throw Error(ye(306,d,""))}return t;case 0:return l=t.type,d=t.pendingProps,d=t.elementType===l?d:Zo(l,d),E1(e,t,l,d,n);case 1:return l=t.type,d=t.pendingProps,d=t.elementType===l?d:Zo(l,d),rE(e,t,l,d,n);case 3:if(nE(t),l=t.updateQueue,e===null||l===null)throw Error(ye(282));if(l=t.pendingProps,d=t.memoizedState,d=d!==null?d.element:null,Lb(e,t),Xd(t,l,null,n),l=t.memoizedState.element,l===d)v1(),t=Nl(e,t,n);else{if(d=t.stateNode,(v=d.hydrate)&&(ja=Rc(t.stateNode.containerInfo.firstChild),Ll=t,v=Us=!0),v){if(e=d.mutableSourceEagerHydrationData,e!=null)for(d=0;d{"use strict";function FE(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__=="undefined"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(FE)}catch(e){console.error(e)}}o(FE,"checkDCE");FE(),IE.exports=RE()});var WE=lr((JI,HE)=>{"use strict";var tR="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";HE.exports=tR});var jE=lr((e2,zE)=>{"use strict";var rR=WE();function BE(){}o(BE,"emptyFunction");function UE(){}o(UE,"emptyFunctionWithReset");UE.resetWarningCache=BE;zE.exports=function(){function e(l,d,v,p,w,_){if(_!==rR){var O=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw O.name="Invariant Violation",O}}o(e,"shim"),e.isRequired=e;function t(){return e}o(t,"getShim");var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:UE,resetWarningCache:BE};return n.PropTypes=n,n}});var qE=lr((n2,$E)=>{$E.exports=jE()();var t2,r2});var ZE=lr(It=>{"use strict";var wn=typeof Symbol=="function"&&Symbol.for,ex=wn?Symbol.for("react.element"):60103,tx=wn?Symbol.for("react.portal"):60106,wg=wn?Symbol.for("react.fragment"):60107,xg=wn?Symbol.for("react.strict_mode"):60108,Sg=wn?Symbol.for("react.profiler"):60114,Cg=wn?Symbol.for("react.provider"):60109,_g=wn?Symbol.for("react.context"):60110,rx=wn?Symbol.for("react.async_mode"):60111,bg=wn?Symbol.for("react.concurrent_mode"):60111,Eg=wn?Symbol.for("react.forward_ref"):60112,Tg=wn?Symbol.for("react.suspense"):60113,sR=wn?Symbol.for("react.suspense_list"):60120,kg=wn?Symbol.for("react.memo"):60115,Og=wn?Symbol.for("react.lazy"):60116,lR=wn?Symbol.for("react.block"):60121,aR=wn?Symbol.for("react.fundamental"):60117,uR=wn?Symbol.for("react.responder"):60118,fR=wn?Symbol.for("react.scope"):60119;function Gi(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case ex:switch(e=e.type,e){case rx:case bg:case wg:case Sg:case xg:case Tg:return e;default:switch(e=e&&e.$$typeof,e){case _g:case Eg:case Og:case kg:case Cg:return e;default:return t}}case tx:return t}}}o(Gi,"z");function QE(e){return Gi(e)===bg}o(QE,"A");It.AsyncMode=rx;It.ConcurrentMode=bg;It.ContextConsumer=_g;It.ContextProvider=Cg;It.Element=ex;It.ForwardRef=Eg;It.Fragment=wg;It.Lazy=Og;It.Memo=kg;It.Portal=tx;It.Profiler=Sg;It.StrictMode=xg;It.Suspense=Tg;It.isAsyncMode=function(e){return QE(e)||Gi(e)===rx};It.isConcurrentMode=QE;It.isContextConsumer=function(e){return Gi(e)===_g};It.isContextProvider=function(e){return Gi(e)===Cg};It.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===ex};It.isForwardRef=function(e){return Gi(e)===Eg};It.isFragment=function(e){return Gi(e)===wg};It.isLazy=function(e){return Gi(e)===Og};It.isMemo=function(e){return Gi(e)===kg};It.isPortal=function(e){return Gi(e)===tx};It.isProfiler=function(e){return Gi(e)===Sg};It.isStrictMode=function(e){return Gi(e)===xg};It.isSuspense=function(e){return Gi(e)===Tg};It.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===wg||e===bg||e===Sg||e===xg||e===Tg||e===sR||typeof e=="object"&&e!==null&&(e.$$typeof===Og||e.$$typeof===kg||e.$$typeof===Cg||e.$$typeof===_g||e.$$typeof===Eg||e.$$typeof===aR||e.$$typeof===uR||e.$$typeof===fR||e.$$typeof===lR)};It.typeOf=Gi});var eT=lr((y2,JE)=>{"use strict";JE.exports=ZE()});var lT=lr((w2,sT)=>{"use strict";var nx=eT(),cR={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},pR={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},dR={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},tT={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},ix={};ix[nx.ForwardRef]=dR;ix[nx.Memo]=tT;function rT(e){return nx.isMemo(e)?tT:ix[e.$$typeof]||cR}o(rT,"getStatics");var hR=Object.defineProperty,mR=Object.getOwnPropertyNames,nT=Object.getOwnPropertySymbols,vR=Object.getOwnPropertyDescriptor,gR=Object.getPrototypeOf,iT=Object.prototype;function oT(e,t,n){if(typeof t!="string"){if(iT){var l=gR(t);l&&l!==iT&&oT(e,l,n)}var d=mR(t);nT&&(d=d.concat(nT(t)));for(var v=rT(e),p=rT(t),w=0;w{"use strict";var xn=typeof Symbol=="function"&&Symbol.for,ox=xn?Symbol.for("react.element"):60103,sx=xn?Symbol.for("react.portal"):60106,Lg=xn?Symbol.for("react.fragment"):60107,Ng=xn?Symbol.for("react.strict_mode"):60108,Pg=xn?Symbol.for("react.profiler"):60114,Mg=xn?Symbol.for("react.provider"):60109,Ag=xn?Symbol.for("react.context"):60110,lx=xn?Symbol.for("react.async_mode"):60111,Dg=xn?Symbol.for("react.concurrent_mode"):60111,Rg=xn?Symbol.for("react.forward_ref"):60112,Fg=xn?Symbol.for("react.suspense"):60113,yR=xn?Symbol.for("react.suspense_list"):60120,Ig=xn?Symbol.for("react.memo"):60115,Hg=xn?Symbol.for("react.lazy"):60116,wR=xn?Symbol.for("react.block"):60121,xR=xn?Symbol.for("react.fundamental"):60117,SR=xn?Symbol.for("react.responder"):60118,CR=xn?Symbol.for("react.scope"):60119;function Yi(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case ox:switch(e=e.type,e){case lx:case Dg:case Lg:case Pg:case Ng:case Fg:return e;default:switch(e=e&&e.$$typeof,e){case Ag:case Rg:case Hg:case Ig:case Mg:return e;default:return t}}case sx:return t}}}o(Yi,"z");function aT(e){return Yi(e)===Dg}o(aT,"A");Ht.AsyncMode=lx;Ht.ConcurrentMode=Dg;Ht.ContextConsumer=Ag;Ht.ContextProvider=Mg;Ht.Element=ox;Ht.ForwardRef=Rg;Ht.Fragment=Lg;Ht.Lazy=Hg;Ht.Memo=Ig;Ht.Portal=sx;Ht.Profiler=Pg;Ht.StrictMode=Ng;Ht.Suspense=Fg;Ht.isAsyncMode=function(e){return aT(e)||Yi(e)===lx};Ht.isConcurrentMode=aT;Ht.isContextConsumer=function(e){return Yi(e)===Ag};Ht.isContextProvider=function(e){return Yi(e)===Mg};Ht.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===ox};Ht.isForwardRef=function(e){return Yi(e)===Rg};Ht.isFragment=function(e){return Yi(e)===Lg};Ht.isLazy=function(e){return Yi(e)===Hg};Ht.isMemo=function(e){return Yi(e)===Ig};Ht.isPortal=function(e){return Yi(e)===sx};Ht.isProfiler=function(e){return Yi(e)===Pg};Ht.isStrictMode=function(e){return Yi(e)===Ng};Ht.isSuspense=function(e){return Yi(e)===Fg};Ht.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===Lg||e===Dg||e===Pg||e===Ng||e===Fg||e===yR||typeof e=="object"&&e!==null&&(e.$$typeof===Hg||e.$$typeof===Ig||e.$$typeof===Mg||e.$$typeof===Ag||e.$$typeof===Rg||e.$$typeof===xR||e.$$typeof===SR||e.$$typeof===CR||e.$$typeof===wR)};Ht.typeOf=Yi});var cT=lr((S2,fT)=>{"use strict";fT.exports=uT()});var xh=lr((Jc,wh)=>{(function(){var e,t="4.17.21",n=200,l="Unsupported core-js use. Try https://npms.io/search?q=ponyfill.",d="Expected a function",v="Invalid `variable` option passed into `_.template`",p="__lodash_hash_undefined__",w=500,_="__lodash_placeholder__",O=1,D=2,Y=4,W=1,X=2,te=1,Q=2,R=4,P=8,F=16,K=32,V=64,ue=128,ie=256,de=512,ge=30,we="...",qe=800,Je=16,be=1,yt=2,Be=3,Ve=1/0,Ke=9007199254740991,Ge=17976931348623157e292,Yt=0/0,ut=4294967295,Dr=ut-1,qt=ut>>>1,_t=[["ary",ue],["bind",te],["bindKey",Q],["curry",P],["curryRight",F],["flip",de],["partial",K],["partialRight",V],["rearg",ie]],wt="[object Arguments]",st="[object Array]",_r="[object AsyncFunction]",Bt="[object Boolean]",Ut="[object Date]",ne="[object DOMException]",et="[object Error]",br="[object Function]",zt="[object GeneratorFunction]",jt="[object Map]",xe="[object Number]",Er="[object Null]",on="[object Object]",Dn="[object Promise]",ei="[object Proxy]",sn="[object RegExp]",Vt="[object Set]",cr="[object String]",ft="[object Symbol]",Do="[object Undefined]",vr="[object WeakMap]",Ri="[object WeakSet]",ln="[object ArrayBuffer]",Rn="[object DataView]",hi="[object Float32Array]",mi="[object Float64Array]",Fn="[object Int8Array]",bn="[object Int16Array]",Ys="[object Int32Array]",H="[object Uint8Array]",J="[object Uint8ClampedArray]",he="[object Uint16Array]",Ee="[object Uint32Array]",Xt=/\b__p \+= '';/g,Hl=/\b(__p \+=) '' \+/g,At=/(__e\(.*?\)|\b__t\)) \+\n'';/g,Rr=/&(?:amp|lt|gt|quot|#39);/g,Qt=/[&<>"']/g,ti=RegExp(Rr.source),os=RegExp(Qt.source),to=/<%-([\s\S]+?)%>/g,vi=/<%([\s\S]+?)%>/g,Xs=/<%=([\s\S]+?)%>/g,ss=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,an=/^\w*$/,bp=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Qs=/[\\^$.*+?()[\]{}|]/g,Cf=RegExp(Qs.source),Zs=/^\s+/,Ep=/\s/,_f=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,lu=/\{\n\/\* \[wrapped with (.+)\] \*/,Tp=/,? & /,Wl=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,ls=/[()=,{}\[\]\/\s]/,kp=/\\(\\)?/g,bf=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Js=/\w*$/,au=/^[-+]0x[0-9a-f]+$/i,Ro=/^0b[01]+$/i,Op=/^\[object .+?Constructor\]$/,Fo=/^0o[0-7]+$/i,Bl=/^(?:0|[1-9]\d*)$/,Ef=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,Dt=/($^)/,Ne=/['\n\r\u2028\u2029\\]/g,gi="\\ud800-\\udfff",uu="\\u0300-\\u036f",yi="\\ufe20-\\ufe2f",dt="\\u20d0-\\u20ff",Fi=uu+yi+dt,Io="\\u2700-\\u27bf",el="a-z\\xdf-\\xf6\\xf8-\\xff",ae="\\xac\\xb1\\xd7\\xf7",ze="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",Ul="\\u2000-\\u206f",zl=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",Ho="A-Z\\xc0-\\xd6\\xd8-\\xde",as="\\ufe0e\\ufe0f",jl=ae+ze+Ul+zl,Ue="['\u2019]",Lp="["+gi+"]",tl="["+jl+"]",ri="["+Fi+"]",In="\\d+",fu="["+Io+"]",cu="["+el+"]",us="[^"+gi+jl+In+Io+el+Ho+"]",rl="\\ud83c[\\udffb-\\udfff]",Tf="(?:"+ri+"|"+rl+")",$l="[^"+gi+"]",ql="(?:\\ud83c[\\udde6-\\uddff]){2}",Vl="[\\ud800-\\udbff][\\udc00-\\udfff]",Wo="["+Ho+"]",pu="\\u200d",kf="(?:"+cu+"|"+us+")",Np="(?:"+Wo+"|"+us+")",du="(?:"+Ue+"(?:d|ll|m|re|s|t|ve))?",wi="(?:"+Ue+"(?:D|LL|M|RE|S|T|VE))?",M=Tf+"?",We="["+as+"]?",Bo="(?:"+pu+"(?:"+[$l,ql,Vl].join("|")+")"+We+M+")*",un="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",hu="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",Se=We+M+Bo,Kl="(?:"+[fu,ql,Vl].join("|")+")"+Se,Jh="(?:"+[$l+ri+"?",ri,ql,Vl,Lp].join("|")+")",Of=RegExp(Ue,"g"),Pp=RegExp(ri,"g"),Lf=RegExp(rl+"(?="+rl+")|"+Jh+Se,"g"),mu=RegExp([Wo+"?"+cu+"+"+du+"(?="+[tl,Wo,"$"].join("|")+")",Np+"+"+wi+"(?="+[tl,Wo+kf,"$"].join("|")+")",Wo+"?"+kf+"+"+du,Wo+"+"+wi,hu,un,In,Kl].join("|"),"g"),fs=RegExp("["+pu+gi+Fi+as+"]"),Pe=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,cs=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],Gl=-1,Ae={};Ae[hi]=Ae[mi]=Ae[Fn]=Ae[bn]=Ae[Ys]=Ae[H]=Ae[J]=Ae[he]=Ae[Ee]=!0,Ae[wt]=Ae[st]=Ae[ln]=Ae[Bt]=Ae[Rn]=Ae[Ut]=Ae[et]=Ae[br]=Ae[jt]=Ae[xe]=Ae[on]=Ae[sn]=Ae[Vt]=Ae[cr]=Ae[vr]=!1;var Tt={};Tt[wt]=Tt[st]=Tt[ln]=Tt[Rn]=Tt[Bt]=Tt[Ut]=Tt[hi]=Tt[mi]=Tt[Fn]=Tt[bn]=Tt[Ys]=Tt[jt]=Tt[xe]=Tt[on]=Tt[sn]=Tt[Vt]=Tt[cr]=Tt[ft]=Tt[H]=Tt[J]=Tt[he]=Tt[Ee]=!0,Tt[et]=Tt[br]=Tt[vr]=!1;var Hn={\u00C0:"A",\u00C1:"A",\u00C2:"A",\u00C3:"A",\u00C4:"A",\u00C5:"A",\u00E0:"a",\u00E1:"a",\u00E2:"a",\u00E3:"a",\u00E4:"a",\u00E5:"a",\u00C7:"C",\u00E7:"c",\u00D0:"D",\u00F0:"d",\u00C8:"E",\u00C9:"E",\u00CA:"E",\u00CB:"E",\u00E8:"e",\u00E9:"e",\u00EA:"e",\u00EB:"e",\u00CC:"I",\u00CD:"I",\u00CE:"I",\u00CF:"I",\u00EC:"i",\u00ED:"i",\u00EE:"i",\u00EF:"i",\u00D1:"N",\u00F1:"n",\u00D2:"O",\u00D3:"O",\u00D4:"O",\u00D5:"O",\u00D6:"O",\u00D8:"O",\u00F2:"o",\u00F3:"o",\u00F4:"o",\u00F5:"o",\u00F6:"o",\u00F8:"o",\u00D9:"U",\u00DA:"U",\u00DB:"U",\u00DC:"U",\u00F9:"u",\u00FA:"u",\u00FB:"u",\u00FC:"u",\u00DD:"Y",\u00FD:"y",\u00FF:"y",\u00C6:"Ae",\u00E6:"ae",\u00DE:"Th",\u00FE:"th",\u00DF:"ss",\u0100:"A",\u0102:"A",\u0104:"A",\u0101:"a",\u0103:"a",\u0105:"a",\u0106:"C",\u0108:"C",\u010A:"C",\u010C:"C",\u0107:"c",\u0109:"c",\u010B:"c",\u010D:"c",\u010E:"D",\u0110:"D",\u010F:"d",\u0111:"d",\u0112:"E",\u0114:"E",\u0116:"E",\u0118:"E",\u011A:"E",\u0113:"e",\u0115:"e",\u0117:"e",\u0119:"e",\u011B:"e",\u011C:"G",\u011E:"G",\u0120:"G",\u0122:"G",\u011D:"g",\u011F:"g",\u0121:"g",\u0123:"g",\u0124:"H",\u0126:"H",\u0125:"h",\u0127:"h",\u0128:"I",\u012A:"I",\u012C:"I",\u012E:"I",\u0130:"I",\u0129:"i",\u012B:"i",\u012D:"i",\u012F:"i",\u0131:"i",\u0134:"J",\u0135:"j",\u0136:"K",\u0137:"k",\u0138:"k",\u0139:"L",\u013B:"L",\u013D:"L",\u013F:"L",\u0141:"L",\u013A:"l",\u013C:"l",\u013E:"l",\u0140:"l",\u0142:"l",\u0143:"N",\u0145:"N",\u0147:"N",\u014A:"N",\u0144:"n",\u0146:"n",\u0148:"n",\u014B:"n",\u014C:"O",\u014E:"O",\u0150:"O",\u014D:"o",\u014F:"o",\u0151:"o",\u0154:"R",\u0156:"R",\u0158:"R",\u0155:"r",\u0157:"r",\u0159:"r",\u015A:"S",\u015C:"S",\u015E:"S",\u0160:"S",\u015B:"s",\u015D:"s",\u015F:"s",\u0161:"s",\u0162:"T",\u0164:"T",\u0166:"T",\u0163:"t",\u0165:"t",\u0167:"t",\u0168:"U",\u016A:"U",\u016C:"U",\u016E:"U",\u0170:"U",\u0172:"U",\u0169:"u",\u016B:"u",\u016D:"u",\u016F:"u",\u0171:"u",\u0173:"u",\u0174:"W",\u0175:"w",\u0176:"Y",\u0177:"y",\u0178:"Y",\u0179:"Z",\u017B:"Z",\u017D:"Z",\u017A:"z",\u017C:"z",\u017E:"z",\u0132:"IJ",\u0133:"ij",\u0152:"Oe",\u0153:"oe",\u0149:"'n",\u017F:"s"},vu={"&":"&","<":"<",">":">",'"':""","'":"'"},nl={"&":"&","<":"<",">":">",""":'"',"'":"'"},En={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},Mp=parseFloat,Ap=parseInt,Yl=typeof global=="object"&&global&&global.Object===Object&&global,Fr=typeof self=="object"&&self&&self.Object===Object&&self,Ot=Yl||Fr||Function("return this")(),ps=typeof Jc=="object"&&Jc&&!Jc.nodeType&&Jc,Ir=ps&&typeof wh=="object"&&wh&&!wh.nodeType&&wh,ds=Ir&&Ir.exports===ps,il=ds&&Yl.process,Tr=function(){try{var q=Ir&&Ir.require&&Ir.require("util").types;return q||il&&il.binding&&il.binding("util")}catch(re){}}(),Nf=Tr&&Tr.isArrayBuffer,Pf=Tr&&Tr.isDate,gu=Tr&&Tr.isMap,yu=Tr&&Tr.isRegExp,Xl=Tr&&Tr.isSet,Ql=Tr&&Tr.isTypedArray;function Tn(q,re,Z){switch(Z.length){case 0:return q.call(re);case 1:return q.call(re,Z[0]);case 2:return q.call(re,Z[0],Z[1]);case 3:return q.call(re,Z[0],Z[1],Z[2])}return q.apply(re,Z)}o(Tn,"apply");function Dp(q,re,Z,Oe){for(var Ye=-1,St=q==null?0:q.length;++Ye-1}o(hs,"arrayIncludes");function ms(q,re,Z){for(var Oe=-1,Ye=q==null?0:q.length;++Oe-1;);return Z}o(Ii,"charsStartIndex");function oo(q,re){for(var Z=q.length;Z--&&Uo(re,q[Z],0)>-1;);return Z}o(oo,"charsEndIndex");function Hp(q,re){for(var Z=q.length,Oe=0;Z--;)q[Z]===re&&++Oe;return Oe}o(Hp,"countHolders");var sl=k(Hn),Hi=k(vu);function em(q){return"\\"+En[q]}o(em,"escapeStringChar");function Wp(q,re){return q==null?e:q[re]}o(Wp,"getValue");function Wi(q){return fs.test(q)}o(Wi,"hasUnicode");function so(q){return Pe.test(q)}o(so,"hasUnicodeWord");function tm(q){for(var re,Z=[];!(re=q.next()).done;)Z.push(re.value);return Z}o(tm,"iteratorToArray");function Df(q){var re=-1,Z=Array(q.size);return q.forEach(function(Oe,Ye){Z[++re]=[Ye,Oe]}),Z}o(Df,"mapToArray");function rm(q,re){return function(Z){return q(re(Z))}}o(rm,"overArg");function gs(q,re){for(var Z=-1,Oe=q.length,Ye=0,St=[];++Z-1}o(s0,"listCacheHas");function jf(s,f){var h=this.__data__,y=hl(h,s);return y<0?(++this.size,h.push([s,f])):h[y][1]=f,this}o(jf,"listCacheSet"),po.prototype.clear=qp,po.prototype.delete=dm,po.prototype.get=ku,po.prototype.has=s0,po.prototype.set=jf;function hr(s){var f=-1,h=s==null?0:s.length;for(this.clear();++f=f?s:f)),s}o(ml,"baseClamp");function On(s,f,h,y,E,L){var I,B=f&O,G=f&D,se=f&Y;if(h&&(I=E?h(s,y,E,L):h(s)),I!==e)return I;if(!mr(s))return s;var le=rt(s);if(le){if(I=u(s),!B)return Br(s,I)}else{var fe=pn(s),ke=fe==br||fe==zt;if(ka(s))return ad(s,B);if(fe==on||fe==wt||ke&&!E){if(I=G||ke?{}:a(s),!B)return G?Im(s,u0(I,s)):g0(s,Xp(I,s))}else{if(!Tt[fe])return E?s:{};I=c(s,fe,B)}}L||(L=new Ci);var Ie=L.get(s);if(Ie)return Ie;L.set(s,I),oC(s)?s.forEach(function($e){I.add(On($e,f,h,$e,s,L))}):nC(s)&&s.forEach(function($e,pt){I.set(pt,On($e,f,h,pt,s,L))});var je=se?G?fc:Uu:G?ki:gn,lt=le?e:je(s);return Wn(lt||s,function($e,pt){lt&&(pt=$e,$e=s[pt]),Lu(I,pt,On($e,f,h,pt,s,L))}),I}o(On,"baseClone");function ym(s){var f=gn(s);return function(h){return wm(h,s,f)}}o(ym,"baseConforms");function wm(s,f,h){var y=h.length;if(s==null)return!y;for(s=ht(s);y--;){var E=h[y],L=f[E],I=s[E];if(I===e&&!(E in s)||!L(I))return!1}return!0}o(wm,"baseConformsTo");function xm(s,f,h){if(typeof s!="function")throw new zn(d);return Nt(function(){s.apply(e,h)},f)}o(xm,"baseDelay");function ua(s,f,h,y){var E=-1,L=hs,I=!0,B=s.length,G=[],se=f.length;if(!B)return G;h&&(f=xt(f,gr(h))),y?(L=ms,I=!1):f.length>=n&&(L=Vr,I=!1,f=new Xr(f));e:for(;++EE?0:E+h),y=y===e||y>E?E:it(y),y<0&&(y+=E),y=h>y?0:lC(y);h0&&h(B)?f>1?Qr(B,f-1,h,y,E):no(E,B):y||(E[E.length]=B)}return E}o(Qr,"baseFlatten");var Gf=Wm(),Hr=Wm(!0);function oi(s,f){return s&&Gf(s,f,gn)}o(oi,"baseForOwn");function Yf(s,f){return s&&Hr(s,f,gn)}o(Yf,"baseForOwnRight");function Pu(s,f){return ro(f,function(h){return _l(s[h])})}o(Pu,"baseFunctions");function Ns(s,f){f=zi(f,s);for(var h=0,y=f.length;s!=null&&hf}o(Xf,"baseGt");function Sm(s,f){return s!=null&&Ze.call(s,f)}o(Sm,"baseHas");function Cm(s,f){return s!=null&&f in ht(s)}o(Cm,"baseHasIn");function fa(s,f,h){return s>=Kr(f,h)&&s=120&&le.length>=120)?new Xr(I&&le):e}le=s[0];var fe=-1,ke=B[0];e:for(;++fe-1;)B!==s&&ia.call(B,G,1),ia.call(s,G,1);return s}o(td,"basePullAll");function rd(s,f){for(var h=s?f.length:0,y=h-1;h--;){var E=f[h];if(h==y||E!==L){var L=E;S(E)?ia.call(s,E,1):Fu(s,E)}}return s}o(rd,"basePullAt");function tc(s,f){return s+sa(bs()*(f-s+1))}o(tc,"baseRandom");function Pm(s,f,h,y){for(var E=-1,L=Zt(oa((f-s)/(h||1)),0),I=Z(L);L--;)I[y?L:++E]=s,s+=h;return I}o(Pm,"baseRange");function nd(s,f){var h="";if(!s||f<1||f>Ke)return h;do f%2&&(h+=s),f=sa(f/2),f&&(s+=s);while(f);return h}o(nd,"baseRepeat");function nt(s,f){return dn(Fe(s,f,Oi),s+"")}o(nt,"baseRest");function d0(s){return $f(mc(s))}o(d0,"baseSample");function As(s,f){var h=mc(s);return Ur(h,ml(f,0,h.length))}o(As,"baseSampleSize");function $o(s,f,h,y){if(!mr(s))return s;f=zi(f,s);for(var E=-1,L=f.length,I=L-1,B=s;B!=null&&++EE?0:E+f),h=h>E?E:h,h<0&&(h+=E),E=f>h?0:h-f>>>0,f>>>=0;for(var L=Z(E);++y>>1,I=s[L];I!==null&&!ji(I)&&(h?I<=f:I=n){var se=f?null:Bu(s);if(se)return Rf(se);I=!1,E=Vr,G=new Xr}else G=f?[]:B;e:for(;++y=y?s:_i(s,f,h)}o(Ds,"castSlice");var ya=Yy||function(s){return Ot.clearTimeout(s)};function ad(s,f){if(f)return s.slice();var h=s.length,y=um?um(h):new s.constructor(h);return s.copy(y),y}o(ad,"cloneBuffer");function oc(s){var f=new s.constructor(s.byteLength);return new ul(f).set(new ul(s)),f}o(oc,"cloneArrayBuffer");function v0(s,f){var h=f?oc(s.buffer):s.buffer;return new s.constructor(h,s.byteOffset,s.byteLength)}o(v0,"cloneDataView");function ud(s){var f=new s.constructor(s.source,Js.exec(s));return f.lastIndex=s.lastIndex,f}o(ud,"cloneRegExp");function Am(s){return Jt?ht(Jt.call(s)):{}}o(Am,"cloneSymbol");function Dm(s,f){var h=f?oc(s.buffer):s.buffer;return new s.constructor(h,s.byteOffset,s.length)}o(Dm,"cloneTypedArray");function fd(s,f){if(s!==f){var h=s!==e,y=s===null,E=s===s,L=ji(s),I=f!==e,B=f===null,G=f===f,se=ji(f);if(!B&&!se&&!L&&s>f||L&&I&&G&&!B&&!se||y&&I&&G||!h&&G||!E)return 1;if(!y&&!L&&!se&&s=B)return G;var se=h[y];return G*(se=="desc"?-1:1)}}return s.index-f.index}o(Rm,"compareMultiple");function Fm(s,f,h,y){for(var E=-1,L=s.length,I=h.length,B=-1,G=f.length,se=Zt(L-I,0),le=Z(G+se),fe=!y;++B1?h[E-1]:e,I=E>2?h[2]:e;for(L=s.length>3&&typeof L=="function"?(E--,L):e,I&&C(h[0],h[1],I)&&(L=E<3?e:L,E=1),f=ht(f);++y-1?E[L?f[I]:I]:e}}o(pd,"createFind");function zm(s){return Ko(function(f){var h=f.length,y=h,E=kn.prototype.thru;for(s&&f.reverse();y--;){var L=f[y];if(typeof L!="function")throw new zn(d);if(E&&!I&&zu(L)=="wrapper")var I=new kn([],!0)}for(y=I?y:h;++y1&&mt.reverse(),le&&GB))return!1;var se=L.get(s),le=L.get(f);if(se&&le)return se==f&&le==s;var fe=-1,ke=!0,Ie=h&X?new Xr:e;for(L.set(s,f),L.set(f,s);++fe1?"& ":"")+f[y],f=f.join(h>2?", ":" "),s.replace(_f,`{ +Add a component higher in the tree to provide a loading indicator or placeholder to display.`)}An!==5&&(An=2),_=P1(_,x),U=p;do{switch(U.tag){case 3:m=_,U.flags|=4096,t&=-t,U.lanes|=t;var ue=yE(U,m,t);Rb(U,ue);break e;case 1:m=_;var ie=U.type,de=U.stateNode;if((U.flags&64)==0&&(typeof ie.getDerivedStateFromError=="function"||de!==null&&typeof de.componentDidCatch=="function"&&(Us===null||!Us.has(de)))){U.flags|=4096,t&=-t,U.lanes|=t;var ge=wE(U,m,t);Rb(U,ge);break e}}U=U.return}while(U!==null)}DE(n)}catch(xe){t=xe,en===n&&n!==null&&(en=n=n.return);continue}break}while(1)}o(PE,"Sj");function ME(){var e=fg.current;return fg.current=ag,e===null?ag:e}o(ME,"Pj");function Sh(e,t){var n=Qe;Qe|=16;var l=ME();ci===e&&Gn===t||ip(e,t);do try{VD();break}catch(d){PE(e,d)}while(1);if(c1(),Qe=n,fg.current=l,en!==null)throw Error(we(261));return ci=null,Gn=0,An}o(Sh,"Tj");function VD(){for(;en!==null;)AE(en)}o(VD,"ak");function KD(){for(;en!==null&&!OD();)AE(en)}o(KD,"Rj");function AE(e){var t=IE(e.alternate,e,pf);e.memoizedProps=e.pendingProps,t===null?DE(e):en=t,F1.current=null}o(AE,"bk");function DE(e){var t=e;do{var n=t.alternate;if(e=t.return,(t.flags&2048)==0){if(n=ID(n,t,pf),n!==null){en=n;return}if(n=t,n.tag!==24&&n.tag!==23||n.memoizedState===null||(pf&1073741824)!=0||(n.mode&4)==0){for(var l=0,d=n.child;d!==null;)l|=d.lanes|d.childLanes,d=d.sibling;n.childLanes=l}e!==null&&(e.flags&2048)==0&&(e.firstEffect===null&&(e.firstEffect=t.firstEffect),t.lastEffect!==null&&(e.lastEffect!==null&&(e.lastEffect.nextEffect=t.firstEffect),e.lastEffect=t.lastEffect),1p&&(x=p,p=ue,ue=x),x=ib(R,ue),m=ib(R,p),x&&m&&(V.rangeCount!==1||V.anchorNode!==x.node||V.anchorOffset!==x.offset||V.focusNode!==m.node||V.focusOffset!==m.offset)&&(K=K.createRange(),K.setStart(x.node,x.offset),V.removeAllRanges(),ue>p?(V.addRange(K),V.extend(m.node,m.offset)):(K.setEnd(m.node,m.offset),V.addRange(K)))))),K=[],V=R;V=V.parentNode;)V.nodeType===1&&K.push({element:V,left:V.scrollLeft,top:V.scrollTop});for(typeof R.focus=="function"&&R.focus(),R=0;RVn()-B1?ip(e,0):H1|=n),_o(e,t)}o(ZD,"Yj");function JD(e,t){var n=e.stateNode;n!==null&&n.delete(t),t=0,t===0&&(t=e.mode,(t&2)==0?t=1:(t&4)==0?t=Yc()===99?1:2:(Dl===0&&(Dl=ep),t=Wc(62914560&~Dl),t===0&&(t=4194304))),n=Ki(),e=vg(e,t),e!==null&&(Lv(e,t,n),_o(e,n))}o(JD,"lj");var IE;IE=o(function(e,t,n){var l=t.lanes;if(e!==null)if(e.memoizedProps!==t.pendingProps||Ni.current)Jo=!0;else if((n&l)!=0)Jo=(e.flags&16384)!=0;else{switch(Jo=!1,t.tag){case 3:aE(t),y1();break;case 5:zb(t);break;case 1:Pi(t.type)&&qv(t);break;case 4:m1(t,t.stateNode.containerInfo);break;case 10:l=t.memoizedProps.value;var d=t.type._context;xr(Gv,d._currentValue),d._currentValue=l;break;case 13:if(t.memoizedState!==null)return(n&t.child.childLanes)!=0?uE(e,t,n):(xr(Sr,Sr.current&1),t=Ml(e,t,n),t!==null?t.sibling:null);xr(Sr,Sr.current&1);break;case 19:if(l=(n&t.childLanes)!=0,(e.flags&64)!=0){if(l)return hE(e,t,n);t.flags|=64}if(d=t.memoizedState,d!==null&&(d.rendering=null,d.tail=null,d.lastEffect=null),xr(Sr,Sr.current),l)break;return null;case 23:case 24:return t.lanes=0,T1(e,t,n)}return Ml(e,t,n)}else Jo=!1;switch(t.lanes=0,t.tag){case 2:if(l=t.type,e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2),e=t.pendingProps,d=Gc(t,qn.current),Qc(t,n),d=S1(null,t,l,e,d,n),t.flags|=1,typeof d=="object"&&d!==null&&typeof d.render=="function"&&d.$$typeof===void 0){if(t.tag=1,t.memoizedState=null,t.updateQueue=null,Pi(l)){var m=!0;qv(t)}else m=!1;t.memoizedState=d.state!==null&&d.state!==void 0?d.state:null,d1(t);var p=l.getDerivedStateFromProps;typeof p=="function"&&Qv(t,l,p,e),d.updater=Zv,t.stateNode=d,d._reactInternals=t,h1(t,l,e,n),t=O1(null,t,l,!0,m,n)}else t.tag=0,Ai(null,t,d,n),t=t.child;return t;case 16:d=t.elementType;e:{switch(e!==null&&(e.alternate=null,t.alternate=null,t.flags|=2),e=t.pendingProps,m=d._init,d=m(d._payload),t.type=d,m=t.tag=tR(d),e=Zo(d,e),m){case 0:t=k1(null,t,d,e,n);break e;case 1:t=lE(null,t,d,e,n);break e;case 11:t=nE(null,t,d,e,n);break e;case 14:t=iE(null,t,d,Zo(d.type,e),l,n);break e}throw Error(we(306,d,""))}return t;case 0:return l=t.type,d=t.pendingProps,d=t.elementType===l?d:Zo(l,d),k1(e,t,l,d,n);case 1:return l=t.type,d=t.pendingProps,d=t.elementType===l?d:Zo(l,d),lE(e,t,l,d,n);case 3:if(aE(t),l=t.updateQueue,e===null||l===null)throw Error(we(282));if(l=t.pendingProps,d=t.memoizedState,d=d!==null?d.element:null,Db(e,t),ih(t,l,null,n),l=t.memoizedState.element,l===d)y1(),t=Ml(e,t,n);else{if(d=t.stateNode,(m=d.hydrate)&&(qa=jc(t.stateNode.containerInfo.firstChild),Pl=t,m=Ws=!0),m){if(e=d.mutableSourceEagerHydrationData,e!=null)for(d=0;d{"use strict";function UE(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__=="undefined"||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!="function"))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(UE)}catch(e){console.error(e)}}o(UE,"checkDCE");UE(),$E.exports=BE()});var jE=ur((a2,zE)=>{"use strict";var aR="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";zE.exports=aR});var GE=ur((u2,KE)=>{"use strict";var uR=jE();function qE(){}o(qE,"emptyFunction");function VE(){}o(VE,"emptyFunctionWithReset");VE.resetWarningCache=qE;KE.exports=function(){function e(l,d,m,p,x,_){if(_!==uR){var O=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw O.name="Invariant Violation",O}}o(e,"shim"),e.isRequired=e;function t(){return e}o(t,"getShim");var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:VE,resetWarningCache:qE};return n.PropTypes=n,n}});var XE=ur((p2,YE)=>{YE.exports=GE()();var f2,c2});var nT=ur(Ht=>{"use strict";var xn=typeof Symbol=="function"&&Symbol.for,rx=xn?Symbol.for("react.element"):60103,nx=xn?Symbol.for("react.portal"):60106,Cg=xn?Symbol.for("react.fragment"):60107,_g=xn?Symbol.for("react.strict_mode"):60108,bg=xn?Symbol.for("react.profiler"):60114,Eg=xn?Symbol.for("react.provider"):60109,Tg=xn?Symbol.for("react.context"):60110,ix=xn?Symbol.for("react.async_mode"):60111,kg=xn?Symbol.for("react.concurrent_mode"):60111,Og=xn?Symbol.for("react.forward_ref"):60112,Lg=xn?Symbol.for("react.suspense"):60113,dR=xn?Symbol.for("react.suspense_list"):60120,Ng=xn?Symbol.for("react.memo"):60115,Pg=xn?Symbol.for("react.lazy"):60116,hR=xn?Symbol.for("react.block"):60121,mR=xn?Symbol.for("react.fundamental"):60117,vR=xn?Symbol.for("react.responder"):60118,gR=xn?Symbol.for("react.scope"):60119;function Gi(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case rx:switch(e=e.type,e){case ix:case kg:case Cg:case bg:case _g:case Lg:return e;default:switch(e=e&&e.$$typeof,e){case Tg:case Og:case Pg:case Ng:case Eg:return e;default:return t}}case nx:return t}}}o(Gi,"z");function rT(e){return Gi(e)===kg}o(rT,"A");Ht.AsyncMode=ix;Ht.ConcurrentMode=kg;Ht.ContextConsumer=Tg;Ht.ContextProvider=Eg;Ht.Element=rx;Ht.ForwardRef=Og;Ht.Fragment=Cg;Ht.Lazy=Pg;Ht.Memo=Ng;Ht.Portal=nx;Ht.Profiler=bg;Ht.StrictMode=_g;Ht.Suspense=Lg;Ht.isAsyncMode=function(e){return rT(e)||Gi(e)===ix};Ht.isConcurrentMode=rT;Ht.isContextConsumer=function(e){return Gi(e)===Tg};Ht.isContextProvider=function(e){return Gi(e)===Eg};Ht.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===rx};Ht.isForwardRef=function(e){return Gi(e)===Og};Ht.isFragment=function(e){return Gi(e)===Cg};Ht.isLazy=function(e){return Gi(e)===Pg};Ht.isMemo=function(e){return Gi(e)===Ng};Ht.isPortal=function(e){return Gi(e)===nx};Ht.isProfiler=function(e){return Gi(e)===bg};Ht.isStrictMode=function(e){return Gi(e)===_g};Ht.isSuspense=function(e){return Gi(e)===Lg};Ht.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===Cg||e===kg||e===bg||e===_g||e===Lg||e===dR||typeof e=="object"&&e!==null&&(e.$$typeof===Pg||e.$$typeof===Ng||e.$$typeof===Eg||e.$$typeof===Tg||e.$$typeof===Og||e.$$typeof===mR||e.$$typeof===vR||e.$$typeof===gR||e.$$typeof===hR)};Ht.typeOf=Gi});var oT=ur((k2,iT)=>{"use strict";iT.exports=nT()});var pT=ur((O2,cT)=>{"use strict";var ox=oT(),yR={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},wR={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},xR={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},sT={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},sx={};sx[ox.ForwardRef]=xR;sx[ox.Memo]=sT;function lT(e){return ox.isMemo(e)?sT:sx[e.$$typeof]||yR}o(lT,"getStatics");var SR=Object.defineProperty,CR=Object.getOwnPropertyNames,aT=Object.getOwnPropertySymbols,_R=Object.getOwnPropertyDescriptor,bR=Object.getPrototypeOf,uT=Object.prototype;function fT(e,t,n){if(typeof t!="string"){if(uT){var l=bR(t);l&&l!==uT&&fT(e,l,n)}var d=CR(t);aT&&(d=d.concat(aT(t)));for(var m=lT(e),p=lT(t),x=0;x{"use strict";var Sn=typeof Symbol=="function"&&Symbol.for,lx=Sn?Symbol.for("react.element"):60103,ax=Sn?Symbol.for("react.portal"):60106,Mg=Sn?Symbol.for("react.fragment"):60107,Ag=Sn?Symbol.for("react.strict_mode"):60108,Dg=Sn?Symbol.for("react.profiler"):60114,Rg=Sn?Symbol.for("react.provider"):60109,Fg=Sn?Symbol.for("react.context"):60110,ux=Sn?Symbol.for("react.async_mode"):60111,Ig=Sn?Symbol.for("react.concurrent_mode"):60111,Hg=Sn?Symbol.for("react.forward_ref"):60112,Wg=Sn?Symbol.for("react.suspense"):60113,ER=Sn?Symbol.for("react.suspense_list"):60120,Bg=Sn?Symbol.for("react.memo"):60115,Ug=Sn?Symbol.for("react.lazy"):60116,TR=Sn?Symbol.for("react.block"):60121,kR=Sn?Symbol.for("react.fundamental"):60117,OR=Sn?Symbol.for("react.responder"):60118,LR=Sn?Symbol.for("react.scope"):60119;function Yi(e){if(typeof e=="object"&&e!==null){var t=e.$$typeof;switch(t){case lx:switch(e=e.type,e){case ux:case Ig:case Mg:case Dg:case Ag:case Wg:return e;default:switch(e=e&&e.$$typeof,e){case Fg:case Hg:case Ug:case Bg:case Rg:return e;default:return t}}case ax:return t}}}o(Yi,"z");function dT(e){return Yi(e)===Ig}o(dT,"A");Wt.AsyncMode=ux;Wt.ConcurrentMode=Ig;Wt.ContextConsumer=Fg;Wt.ContextProvider=Rg;Wt.Element=lx;Wt.ForwardRef=Hg;Wt.Fragment=Mg;Wt.Lazy=Ug;Wt.Memo=Bg;Wt.Portal=ax;Wt.Profiler=Dg;Wt.StrictMode=Ag;Wt.Suspense=Wg;Wt.isAsyncMode=function(e){return dT(e)||Yi(e)===ux};Wt.isConcurrentMode=dT;Wt.isContextConsumer=function(e){return Yi(e)===Fg};Wt.isContextProvider=function(e){return Yi(e)===Rg};Wt.isElement=function(e){return typeof e=="object"&&e!==null&&e.$$typeof===lx};Wt.isForwardRef=function(e){return Yi(e)===Hg};Wt.isFragment=function(e){return Yi(e)===Mg};Wt.isLazy=function(e){return Yi(e)===Ug};Wt.isMemo=function(e){return Yi(e)===Bg};Wt.isPortal=function(e){return Yi(e)===ax};Wt.isProfiler=function(e){return Yi(e)===Dg};Wt.isStrictMode=function(e){return Yi(e)===Ag};Wt.isSuspense=function(e){return Yi(e)===Wg};Wt.isValidElementType=function(e){return typeof e=="string"||typeof e=="function"||e===Mg||e===Ig||e===Dg||e===Ag||e===Wg||e===ER||typeof e=="object"&&e!==null&&(e.$$typeof===Ug||e.$$typeof===Bg||e.$$typeof===Rg||e.$$typeof===Fg||e.$$typeof===Hg||e.$$typeof===kR||e.$$typeof===OR||e.$$typeof===LR||e.$$typeof===TR)};Wt.typeOf=Yi});var vT=ur((N2,mT)=>{"use strict";mT.exports=hT()});var Oh=ur((ap,kh)=>{(function(){var e,t="4.17.21",n=200,l="Unsupported core-js use. Try https://npms.io/search?q=ponyfill.",d="Expected a function",m="Invalid `variable` option passed into `_.template`",p="__lodash_hash_undefined__",x=500,_="__lodash_placeholder__",O=1,D=2,Y=4,U=1,X=2,te=1,Q=2,F=4,M=8,R=16,K=32,V=64,ue=128,ie=256,de=512,ge=30,xe="...",qe=800,et=16,Te=1,xt=2,Ue=3,Ve=1/0,Ke=9007199254740991,Ye=17976931348623157e292,Qt=0/0,ft=4294967295,Ar=ft-1,Kt=ft>>>1,Et=[["ary",ue],["bind",te],["bindKey",Q],["curry",M],["curryRight",R],["flip",de],["partial",K],["partialRight",V],["rearg",ie]],St="[object Arguments]",at="[object Array]",_r="[object AsyncFunction]",Ut="[object Boolean]",$t="[object Date]",ne="[object DOMException]",tt="[object Error]",br="[object Function]",jt="[object GeneratorFunction]",qt="[object Map]",Se="[object Number]",Er="[object Null]",nn="[object Object]",Fn="[object Promise]",ei="[object Proxy]",on="[object RegExp]",Gt="[object Set]",dr="[object String]",ct="[object Symbol]",Do="[object Undefined]",vr="[object WeakMap]",Fi="[object WeakSet]",sn="[object ArrayBuffer]",In="[object DataView]",vi="[object Float32Array]",gi="[object Float64Array]",Hn="[object Int8Array]",En="[object Int16Array]",Ks="[object Int32Array]",H="[object Uint8Array]",J="[object Uint8ClampedArray]",he="[object Uint16Array]",ke="[object Uint32Array]",Zt=/\b__p \+= '';/g,Bl=/\b(__p \+=) '' \+/g,Rt=/(__e\(.*?\)|\b__t\)) \+\n'';/g,Dr=/&(?:amp|lt|gt|quot|#39);/g,Jt=/[&<>"']/g,ti=RegExp(Dr.source),os=RegExp(Jt.source),to=/<%-([\s\S]+?)%>/g,yi=/<%([\s\S]+?)%>/g,Gs=/<%=([\s\S]+?)%>/g,ss=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,ln=/^\w*$/,Ap=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,Ys=/[\\^$.*+?()[\]{}|]/g,Pf=RegExp(Ys.source),Xs=/^\s+/,Dp=/\s/,Mf=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,uu=/\{\n\/\* \[wrapped with (.+)\] \*/,Rp=/,? & /,Ul=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,ls=/[()=,{}\[\]\/\s]/,Fp=/\\(\\)?/g,Af=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,Qs=/\w*$/,fu=/^[-+]0x[0-9a-f]+$/i,Ro=/^0b[01]+$/i,Ip=/^\[object .+?Constructor\]$/,Fo=/^0o[0-7]+$/i,$l=/^(?:0|[1-9]\d*)$/,Df=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,zt=/($^)/,Me=/['\n\r\u2028\u2029\\]/g,wi="\\ud800-\\udfff",cu="\\u0300-\\u036f",ri="\\ufe20-\\ufe2f",vt="\\u20d0-\\u20ff",ro=cu+ri+vt,Io="\\u2700-\\u27bf",zl="a-z\\xdf-\\xf6\\xf8-\\xff",ae="\\xac\\xb1\\xd7\\xf7",$e="\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf",pu="\\u2000-\\u206f",du=" \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",as="A-Z\\xc0-\\xd6\\xd8-\\xde",Zs="\\ufe0e\\ufe0f",jl=ae+$e+pu+du,Be="['\u2019]",Hp="["+wi+"]",hu="["+jl+"]",Ho="["+ro+"]",Wn="\\d+",mu="["+Io+"]",ql="["+zl+"]",Wo="[^"+wi+jl+Wn+Io+zl+as+"]",Js="\\ud83c[\\udffb-\\udfff]",Rf="(?:"+Ho+"|"+Js+")",el="[^"+wi+"]",tl="(?:\\ud83c[\\udde6-\\uddff]){2}",us="[\\ud800-\\udbff][\\udc00-\\udfff]",no="["+as+"]",Vl="\\u200d",Ff="(?:"+ql+"|"+Wo+")",Wp="(?:"+no+"|"+Wo+")",rl="(?:"+Be+"(?:d|ll|m|re|s|t|ve))?",an="(?:"+Be+"(?:D|LL|M|RE|S|T|VE))?",vu=Rf+"?",gu="["+Zs+"]?",Kl="(?:"+Vl+"(?:"+[el,tl,us].join("|")+")"+gu+vu+")*",nl="\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Bp="\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])",If=gu+vu+Kl,Up="(?:"+[mu,tl,us].join("|")+")"+If,$p="(?:"+[el+Ho+"?",Ho,tl,us,Hp].join("|")+")",yu=RegExp(Be,"g"),Hf=RegExp(Ho,"g"),wu=RegExp(Js+"(?="+Js+")|"+$p+If,"g"),Wf=RegExp([no+"?"+ql+"+"+rl+"(?="+[hu,no,"$"].join("|")+")",Wp+"+"+an+"(?="+[hu,no+Ff,"$"].join("|")+")",no+"?"+Ff+"+"+rl,no+"+"+an,Bp,nl,Wn,Up].join("|"),"g"),Bf=RegExp("["+Vl+wi+ro+Zs+"]"),Gl=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,Yl=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],N=-1,Ce={};Ce[vi]=Ce[gi]=Ce[Hn]=Ce[En]=Ce[Ks]=Ce[H]=Ce[J]=Ce[he]=Ce[ke]=!0,Ce[St]=Ce[at]=Ce[sn]=Ce[Ut]=Ce[In]=Ce[$t]=Ce[tt]=Ce[br]=Ce[qt]=Ce[Se]=Ce[nn]=Ce[on]=Ce[Gt]=Ce[dr]=Ce[vr]=!1;var gt={};gt[St]=gt[at]=gt[sn]=gt[In]=gt[Ut]=gt[$t]=gt[vi]=gt[gi]=gt[Hn]=gt[En]=gt[Ks]=gt[qt]=gt[Se]=gt[nn]=gt[on]=gt[Gt]=gt[dr]=gt[ct]=gt[H]=gt[J]=gt[he]=gt[ke]=!0,gt[tt]=gt[br]=gt[vr]=!1;var Tn={\u00C0:"A",\u00C1:"A",\u00C2:"A",\u00C3:"A",\u00C4:"A",\u00C5:"A",\u00E0:"a",\u00E1:"a",\u00E2:"a",\u00E3:"a",\u00E4:"a",\u00E5:"a",\u00C7:"C",\u00E7:"c",\u00D0:"D",\u00F0:"d",\u00C8:"E",\u00C9:"E",\u00CA:"E",\u00CB:"E",\u00E8:"e",\u00E9:"e",\u00EA:"e",\u00EB:"e",\u00CC:"I",\u00CD:"I",\u00CE:"I",\u00CF:"I",\u00EC:"i",\u00ED:"i",\u00EE:"i",\u00EF:"i",\u00D1:"N",\u00F1:"n",\u00D2:"O",\u00D3:"O",\u00D4:"O",\u00D5:"O",\u00D6:"O",\u00D8:"O",\u00F2:"o",\u00F3:"o",\u00F4:"o",\u00F5:"o",\u00F6:"o",\u00F8:"o",\u00D9:"U",\u00DA:"U",\u00DB:"U",\u00DC:"U",\u00F9:"u",\u00FA:"u",\u00FB:"u",\u00FC:"u",\u00DD:"Y",\u00FD:"y",\u00FF:"y",\u00C6:"Ae",\u00E6:"ae",\u00DE:"Th",\u00FE:"th",\u00DF:"ss",\u0100:"A",\u0102:"A",\u0104:"A",\u0101:"a",\u0103:"a",\u0105:"a",\u0106:"C",\u0108:"C",\u010A:"C",\u010C:"C",\u0107:"c",\u0109:"c",\u010B:"c",\u010D:"c",\u010E:"D",\u0110:"D",\u010F:"d",\u0111:"d",\u0112:"E",\u0114:"E",\u0116:"E",\u0118:"E",\u011A:"E",\u0113:"e",\u0115:"e",\u0117:"e",\u0119:"e",\u011B:"e",\u011C:"G",\u011E:"G",\u0120:"G",\u0122:"G",\u011D:"g",\u011F:"g",\u0121:"g",\u0123:"g",\u0124:"H",\u0126:"H",\u0125:"h",\u0127:"h",\u0128:"I",\u012A:"I",\u012C:"I",\u012E:"I",\u0130:"I",\u0129:"i",\u012B:"i",\u012D:"i",\u012F:"i",\u0131:"i",\u0134:"J",\u0135:"j",\u0136:"K",\u0137:"k",\u0138:"k",\u0139:"L",\u013B:"L",\u013D:"L",\u013F:"L",\u0141:"L",\u013A:"l",\u013C:"l",\u013E:"l",\u0140:"l",\u0142:"l",\u0143:"N",\u0145:"N",\u0147:"N",\u014A:"N",\u0144:"n",\u0146:"n",\u0148:"n",\u014B:"n",\u014C:"O",\u014E:"O",\u0150:"O",\u014D:"o",\u014F:"o",\u0151:"o",\u0154:"R",\u0156:"R",\u0158:"R",\u0155:"r",\u0157:"r",\u0159:"r",\u015A:"S",\u015C:"S",\u015E:"S",\u0160:"S",\u015B:"s",\u015D:"s",\u015F:"s",\u0161:"s",\u0162:"T",\u0164:"T",\u0166:"T",\u0163:"t",\u0165:"t",\u0167:"t",\u0168:"U",\u016A:"U",\u016C:"U",\u016E:"U",\u0170:"U",\u0172:"U",\u0169:"u",\u016B:"u",\u016D:"u",\u016F:"u",\u0171:"u",\u0173:"u",\u0174:"W",\u0175:"w",\u0176:"Y",\u0177:"y",\u0178:"Y",\u0179:"Z",\u017B:"Z",\u017D:"Z",\u017A:"z",\u017C:"z",\u017E:"z",\u0132:"IJ",\u0133:"ij",\u0152:"Oe",\u0153:"oe",\u0149:"'n",\u017F:"s"},xu={"&":"&","<":"<",">":">",'"':""","'":"'"},ye={"&":"&","<":"<",">":">",""":'"',"'":"'"},kn={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},am=parseFloat,um=parseInt,Su=typeof global=="object"&&global&&global.Object===Object&&global,zp=typeof self=="object"&&self&&self.Object===Object&&self,Nt=Su||zp||Function("return this")(),Ii=typeof ap=="object"&&ap&&!ap.nodeType&&ap,_e=Ii&&typeof kh=="object"&&kh&&!kh.nodeType&&kh,Bo=_e&&_e.exports===Ii,fs=Bo&&Su.process,He=function(){try{var q=_e&&_e.require&&_e.require("util").types;return q||fs&&fs.binding&&fs.binding("util")}catch(re){}}(),Uf=He&&He.isArrayBuffer,xi=He&&He.isDate,Xl=He&&He.isMap,il=He&&He.isRegExp,cs=He&&He.isSet,Cu=He&&He.isTypedArray;function On(q,re,Z){switch(Z.length){case 0:return q.call(re);case 1:return q.call(re,Z[0]);case 2:return q.call(re,Z[0],Z[1]);case 3:return q.call(re,Z[0],Z[1],Z[2])}return q.apply(re,Z)}o(On,"apply");function jp(q,re,Z,Ne){for(var Xe=-1,_t=q==null?0:q.length;++Xe<_t;){var Tr=q[Xe];re(Ne,Tr,Z(Tr),q)}return Ne}o(jp,"arrayAggregator");function Tt(q,re){for(var Z=-1,Ne=q==null?0:q.length;++Z-1}o(ps,"arrayIncludes");function ds(q,re,Z){for(var Ne=-1,Xe=q==null?0:q.length;++Ne-1;);return Z}o(Bi,"charsStartIndex");function ta(q,re){for(var Z=q.length;Z--&&Uo(re,q[Z],0)>-1;);return Z}o(ta,"charsEndIndex");function Vf(q,re){for(var Z=q.length,Ne=0;Z--;)q[Z]===re&&++Ne;return Ne}o(Vf,"countHolders");var ku=Eu(Tn),Kp=Eu(xu);function Kf(q){return"\\"+kn[q]}o(Kf,"escapeStringChar");function Ou(q,re){return q==null?e:q[re]}o(Ou,"getValue");function ni(q){return Bf.test(q)}o(ni,"hasUnicode");function ii(q){return Gl.test(q)}o(ii,"hasUnicodeWord");function y(q){for(var re,Z=[];!(re=q.next()).done;)Z.push(re.value);return Z}o(y,"iteratorToArray");function T(q){var re=-1,Z=Array(q.size);return q.forEach(function(Ne,Xe){Z[++re]=[Xe,Ne]}),Z}o(T,"mapToArray");function B(q,re){return function(Z){return q(re(Z))}}o(B,"overArg");function W(q,re){for(var Z=-1,Ne=q.length,Xe=0,_t=[];++Z-1}o(a0,"listCacheHas");function Zf(s,f){var h=this.__data__,w=vl(h,s);return w<0?(++this.size,h.push([s,f])):h[w][1]=f,this}o(Zf,"listCacheSet"),po.prototype.clear=Jp,po.prototype.delete=vm,po.prototype.get=Ru,po.prototype.has=a0,po.prototype.set=Zf;function hr(s){var f=-1,h=s==null?0:s.length;for(this.clear();++f=f?s:f)),s}o(gl,"baseClamp");function Nn(s,f,h,w,E,L){var I,$=f&O,G=f&D,se=f&Y;if(h&&(I=E?h(s,w,E,L):h(s)),I!==e)return I;if(!mr(s))return s;var le=nt(s);if(le){if(I=u(s),!$)return Wr(s,I)}else{var fe=dn(s),Le=fe==br||fe==jt;if(La(s))return vd(s,$);if(fe==nn||fe==St||Le&&!E){if(I=G||Le?{}:a(s),!$)return G?Bm(s,c0(I,s)):w0(s,id(I,s))}else{if(!gt[fe])return E?s:{};I=c(s,fe,$)}}L||(L=new _i);var Ie=L.get(s);if(Ie)return Ie;L.set(s,I),fC(s)?s.forEach(function(je){I.add(Nn(je,f,h,je,s,L))}):aC(s)&&s.forEach(function(je,dt){I.set(dt,Nn(je,f,h,dt,s,L))});var ze=se?G?wc:Yu:G?Oi:yn,ut=le?e:ze(s);return Tt(ut||s,function(je,dt){ut&&(dt=je,je=s[dt]),Iu(I,dt,Nn(je,f,h,dt,s,L))}),I}o(Nn,"baseClone");function Sm(s){var f=yn(s);return function(h){return Cm(h,s,f)}}o(Sm,"baseConforms");function Cm(s,f,h){var w=h.length;if(s==null)return!w;for(s=ht(s);w--;){var E=h[w],L=f[E],I=s[E];if(I===e&&!(E in s)||!L(I))return!1}return!0}o(Cm,"baseConformsTo");function _m(s,f,h){if(typeof s!="function")throw new $n(d);return Mt(function(){s.apply(e,h)},f)}o(_m,"baseDelay");function ca(s,f,h,w){var E=-1,L=ps,I=!0,$=s.length,G=[],se=f.length;if(!$)return G;h&&(f=Ct(f,Rr(h))),w?(L=ds,I=!1):f.length>=n&&(L=Vr,I=!1,f=new Xr(f));e:for(;++E<$;){var le=s[E],fe=h==null?le:h(le);if(le=w||le!==0?le:0,I&&fe===fe){for(var Le=se;Le--;)if(f[Le]===fe)continue e;G.push(le)}else L(f,fe,w)||G.push(le)}return G}o(ca,"baseDifference");var si=Um(li),tc=Um(ic,!0);function rc(s,f){var h=!0;return si(s,function(w,E,L){return h=!!f(w,E,L),h}),h}o(rc,"baseEvery");function Hu(s,f,h){for(var w=-1,E=s.length;++wE?0:E+h),w=w===e||w>E?E:st(w),w<0&&(w+=E),w=h>w?0:pC(w);h0&&h($)?f>1?Qr($,f-1,h,w,E):io(E,$):w||(E[E.length]=$)}return E}o(Qr,"baseFlatten");var nc=$m(),Ir=$m(!0);function li(s,f){return s&&nc(s,f,yn)}o(li,"baseForOwn");function ic(s,f){return s&&Ir(s,f,yn)}o(ic,"baseForOwnRight");function Wu(s,f){return Hi(f,function(h){return El(s[h])})}o(Wu,"baseFunctions");function Os(s,f){f=$i(f,s);for(var h=0,w=f.length;s!=null&&hf}o(oc,"baseGt");function bm(s,f){return s!=null&&Je.call(s,f)}o(bm,"baseHas");function Em(s,f){return s!=null&&f in ht(s)}o(Em,"baseHasIn");function pa(s,f,h){return s>=Kr(f,h)&&s=120&&le.length>=120)?new Xr(I&&le):e}le=s[0];var fe=-1,Le=$[0];e:for(;++fe-1;)$!==s&&sa.call($,G,1),sa.call(s,G,1);return s}o(ud,"basePullAll");function fd(s,f){for(var h=s?f.length:0,w=h-1;h--;){var E=f[h];if(h==w||E!==L){var L=E;S(E)?sa.call(s,E,1):ju(s,E)}}return s}o(fd,"basePullAt");function fc(s,f){return s+aa(Cs()*(f-s+1))}o(fc,"baseRandom");function Dm(s,f,h,w){for(var E=-1,L=er(la((f-s)/(h||1)),0),I=Z(L);L--;)I[w?L:++E]=s,s+=h;return I}o(Dm,"baseRange");function cd(s,f){var h="";if(!s||f<1||f>Ke)return h;do f%2&&(h+=s),f=aa(f/2),f&&(s+=s);while(f);return h}o(cd,"baseRepeat");function ot(s,f){return hn(Fe(s,f,Li),s+"")}o(ot,"baseRest");function m0(s){return Jf(bc(s))}o(m0,"baseSample");function Ps(s,f){var h=bc(s);return Br(h,gl(f,0,h.length))}o(Ps,"baseSampleSize");function jo(s,f,h,w){if(!mr(s))return s;f=$i(f,s);for(var E=-1,L=f.length,I=L-1,$=s;$!=null&&++EE?0:E+f),h=h>E?E:h,h<0&&(h+=E),E=f>h?0:h-f>>>0,f>>>=0;for(var L=Z(E);++w>>1,I=s[L];I!==null&&!zi(I)&&(h?I<=f:I=n){var se=f?null:Gu(s);if(se)return Fr(se);I=!1,E=Vr,G=new Xr}else G=f?[]:$;e:for(;++w=w?s:bi(s,f,h)}o(Ms,"castSlice");var xa=Qy||function(s){return Nt.clearTimeout(s)};function vd(s,f){if(f)return s.slice();var h=s.length,w=pm?pm(h):new s.constructor(h);return s.copy(w),w}o(vd,"cloneBuffer");function hc(s){var f=new s.constructor(s.byteLength);return new cl(f).set(new cl(s)),f}o(hc,"cloneArrayBuffer");function y0(s,f){var h=f?hc(s.buffer):s.buffer;return new s.constructor(h,s.byteOffset,s.byteLength)}o(y0,"cloneDataView");function gd(s){var f=new s.constructor(s.source,Qs.exec(s));return f.lastIndex=s.lastIndex,f}o(gd,"cloneRegExp");function Fm(s){return tr?ht(tr.call(s)):{}}o(Fm,"cloneSymbol");function Im(s,f){var h=f?hc(s.buffer):s.buffer;return new s.constructor(h,s.byteOffset,s.length)}o(Im,"cloneTypedArray");function yd(s,f){if(s!==f){var h=s!==e,w=s===null,E=s===s,L=zi(s),I=f!==e,$=f===null,G=f===f,se=zi(f);if(!$&&!se&&!L&&s>f||L&&I&&G&&!$&&!se||w&&I&&G||!h&&G||!E)return 1;if(!w&&!L&&!se&&s=$)return G;var se=h[w];return G*(se=="desc"?-1:1)}}return s.index-f.index}o(Hm,"compareMultiple");function Wm(s,f,h,w){for(var E=-1,L=s.length,I=h.length,$=-1,G=f.length,se=er(L-I,0),le=Z(G+se),fe=!w;++$1?h[E-1]:e,I=E>2?h[2]:e;for(L=s.length>3&&typeof L=="function"?(E--,L):e,I&&C(h[0],h[1],I)&&(L=E<3?e:L,E=1),f=ht(f);++w-1?E[L?f[I]:I]:e}}o(xd,"createFind");function qm(s){return Ko(function(f){var h=f.length,w=h,E=Ln.prototype.thru;for(s&&f.reverse();w--;){var L=f[w];if(typeof L!="function")throw new $n(d);if(E&&!I&&Xu(L)=="wrapper")var I=new Ln([],!0)}for(w=I?w:h;++w1&&mt.reverse(),le&&G$))return!1;var se=L.get(s),le=L.get(f);if(se&&le)return se==f&&le==s;var fe=-1,Le=!0,Ie=h&X?new Xr:e;for(L.set(s,f),L.set(f,s);++fe<$;){var ze=s[fe],ut=f[fe];if(w)var je=I?w(ut,ze,fe,f,s,L):w(ze,ut,fe,s,f,L);if(je!==e){if(je)continue;Le=!1;break}if(Ie){if(!oo(f,function(dt,mt){if(!Vr(Ie,mt)&&(ze===dt||E(ze,dt,h,w,L)))return Ie.push(mt)})){Le=!1;break}}else if(!(ze===ut||E(ze,ut,h,w,L))){Le=!1;break}}return L.delete(s),L.delete(f),Le}o(Cd,"equalArrays");function Ym(s,f,h,w,E,L,I){switch(h){case In:if(s.byteLength!=f.byteLength||s.byteOffset!=f.byteOffset)return!1;s=s.buffer,f=f.buffer;case sn:return!(s.byteLength!=f.byteLength||!L(new cl(s),new cl(f)));case Ut:case $t:case Se:return Yo(+s,+f);case tt:return s.name==f.name&&s.message==f.message;case on:case dr:return s==f+"";case qt:var $=T;case Gt:var G=w&U;if($||($=Fr),s.size!=f.size&&!G)return!1;var se=I.get(s);if(se)return se==f;w|=X,I.set(s,f);var le=Cd($(s),$(f),w,E,L,I);return I.delete(s),le;case ct:if(tr)return tr.call(s)==tr.call(f)}return!1}o(Ym,"equalByTag");function Xm(s,f,h,w,E,L){var I=h&U,$=Yu(s),G=$.length,se=Yu(f),le=se.length;if(G!=le&&!I)return!1;for(var fe=G;fe--;){var Le=$[fe];if(!(I?Le in f:Je.call(f,Le)))return!1}var Ie=L.get(s),ze=L.get(f);if(Ie&&ze)return Ie==f&&ze==s;var ut=!0;L.set(s,f),L.set(f,s);for(var je=I;++fe1?"& ":"")+f[w],f=f.join(h>2?", ":" "),s.replace(Mf,`{ /* [wrapped with `+f+`] */ -`)}o(m,"insertWrapDetails");function g(s){return rt(s)||ju(s)||!!(fl&&s&&s[fl])}o(g,"isFlattenable");function S(s,f){var h=typeof s;return f=f??Ke,!!f&&(h=="number"||h!="symbol"&&Bl.test(s))&&s>-1&&s%1==0&&s0){if(++f>=qe)return arguments[0]}else f=0;return s.apply(e,arguments)}}o(sr,"shortOut");function Ur(s,f){var h=-1,y=s.length,E=y-1;for(f=f===e?y:f;++h1?s[f-1]:e;return h=typeof h=="function"?(s.pop(),h):e,qS(s,h)});function VS(s){var f=T(s);return f.__chain__=!0,f}o(VS,"chain");function ML(s,f){return f(s),s}o(ML,"tap");function Ym(s,f){return f(s)}o(Ym,"thru");var AL=Ko(function(s){var f=s.length,h=f?s[0]:0,y=this.__wrapped__,E=o(function(L){return Qp(L,s)},"interceptor");return f>1||this.__actions__.length||!(y instanceof ct)||!S(h)?this.thru(E):(y=y.slice(h,+h+(f?1:0)),y.__actions__.push({func:Ym,args:[E],thisArg:e}),new kn(y,this.__chain__).thru(function(L){return f&&!L.length&&L.push(e),L}))});function DL(){return VS(this)}o(DL,"wrapperChain");function RL(){return new kn(this.value(),this.__chain__)}o(RL,"wrapperCommit");function FL(){this.__values__===e&&(this.__values__=sC(this.value()));var s=this.__index__>=this.__values__.length,f=s?e:this.__values__[this.__index__++];return{done:s,value:f}}o(FL,"wrapperNext");function IL(){return this}o(IL,"wrapperToIterator");function HL(s){for(var f,h=this;h instanceof zf;){var y=mn(h);y.__index__=0,y.__values__=e,f?E.__wrapped__=y:f=y;var E=y;h=h.__wrapped__}return E.__wrapped__=s,f}o(HL,"wrapperPlant");function WL(){var s=this.__wrapped__;if(s instanceof ct){var f=s;return this.__actions__.length&&(f=new ct(this)),f=f.reverse(),f.__actions__.push({func:Ym,args:[w0],thisArg:e}),new kn(f,this.__chain__)}return this.thru(w0)}o(WL,"wrapperReverse");function BL(){return Mm(this.__wrapped__,this.__actions__)}o(BL,"wrapperValue");var UL=lc(function(s,f,h){Ze.call(s,h)?++s[h]:ho(s,h,1)});function zL(s,f,h){var y=rt(s)?wu:Kf;return h&&C(s,f,h)&&(f=e),y(s,He(f,3))}o(zL,"every");function jL(s,f){var h=rt(s)?ro:Jp;return h(s,He(f,3))}o(jL,"filter");var $L=pd(dc),qL=pd(US);function VL(s,f){return Qr(Xm(s,f),1)}o(VL,"flatMap");function KL(s,f){return Qr(Xm(s,f),Ve)}o(KL,"flatMapDeep");function GL(s,f,h){return h=h===e?1:it(h),Qr(Xm(s,f),h)}o(GL,"flatMapDepth");function KS(s,f){var h=rt(s)?Wn:ii;return h(s,He(f,3))}o(KS,"forEach");function GS(s,f){var h=rt(s)?Rp:Vf;return h(s,He(f,3))}o(GS,"forEachRight");var YL=lc(function(s,f,h){Ze.call(s,h)?s[h].push(f):ho(s,h,[f])});function XL(s,f,h,y){s=Ti(s)?s:mc(s),h=h&&!y?it(h):0;var E=s.length;return h<0&&(h=Zt(E+h,0)),tv(s)?h<=E&&s.indexOf(f,h)>-1:!!E&&Uo(s,f,h)>-1}o(XL,"includes");var QL=nt(function(s,f,h){var y=-1,E=typeof f=="function",L=Ti(s)?Z(s.length):[];return ii(s,function(I){L[++y]=E?Tn(f,I,h):ca(I,f,h)}),L}),ZL=lc(function(s,f,h){ho(s,h,f)});function Xm(s,f){var h=rt(s)?xt:va;return h(s,He(f,3))}o(Xm,"map");function JL(s,f,h,y){return s==null?[]:(rt(f)||(f=f==null?[]:[f]),h=y?e:h,rt(h)||(h=h==null?[]:[h]),fn(s,f,h))}o(JL,"orderBy");var eN=lc(function(s,f,h){s[h?0:1].push(f)},function(){return[[],[]]});function tN(s,f,h){var y=rt(s)?Zl:U,E=arguments.length<3;return y(s,He(f,4),h,E,ii)}o(tN,"reduce");function rN(s,f,h){var y=rt(s)?Fp:U,E=arguments.length<3;return y(s,He(f,4),h,E,Vf)}o(rN,"reduceRight");function nN(s,f){var h=rt(s)?ro:Jp;return h(s,Jm(He(f,3)))}o(nN,"reject");function iN(s){var f=rt(s)?$f:d0;return f(s)}o(iN,"sample");function oN(s,f,h){(h?C(s,f,h):f===e)?f=1:f=it(f);var y=rt(s)?Ls:As;return y(s,f)}o(oN,"sampleSize");function sN(s){var f=rt(s)?gm:qo;return f(s)}o(sN,"shuffle");function lN(s){if(s==null)return 0;if(Ti(s))return tv(s)?ll(s):s.length;var f=pn(s);return f==jt||f==Vt?s.size:Jf(s).length}o(lN,"size");function aN(s,f,h){var y=rt(s)?io:h0;return h&&C(s,f,h)&&(f=e),y(s,He(f,3))}o(aN,"some");var uN=nt(function(s,f){if(s==null)return[];var h=f.length;return h>1&&C(s,f[0],f[1])?f=[]:h>2&&C(f[0],f[1],f[2])&&(f=[f[0]]),fn(s,Qr(f,1),[])}),Qm=Xy||function(){return Ot.Date.now()};function fN(s,f){if(typeof f!="function")throw new zn(d);return s=it(s),function(){if(--s<1)return f.apply(this,arguments)}}o(fN,"after");function YS(s,f,h){return f=h?e:f,f=s&&f==null?s.length:f,Ei(s,ue,e,e,e,e,f)}o(YS,"ary");function XS(s,f){var h;if(typeof f!="function")throw new zn(d);return s=it(s),function(){return--s>0&&(h=f.apply(this,arguments)),s<=1&&(f=e),h}}o(XS,"before");var S0=nt(function(s,f,h){var y=te;if(h.length){var E=gs(h,_a(S0));y|=K}return Ei(s,y,f,h,E)}),QS=nt(function(s,f,h){var y=te|Q;if(h.length){var E=gs(h,_a(QS));y|=K}return Ei(f,y,s,h,E)});function ZS(s,f,h){f=h?e:f;var y=Ei(s,P,e,e,e,e,e,f);return y.placeholder=ZS.placeholder,y}o(ZS,"curry");function JS(s,f,h){f=h?e:f;var y=Ei(s,F,e,e,e,e,e,f);return y.placeholder=JS.placeholder,y}o(JS,"curryRight");function eC(s,f,h){var y,E,L,I,B,G,se=0,le=!1,fe=!1,ke=!0;if(typeof s!="function")throw new zn(d);f=go(f)||0,mr(h)&&(le=!!h.leading,fe="maxWait"in h,L=fe?Zt(go(h.maxWait)||0,f):L,ke="trailing"in h?!!h.trailing:ke);function Ie(Pr){var Xo=y,El=E;return y=E=e,se=Pr,I=s.apply(El,Xo),I}o(Ie,"invokeFunc");function je(Pr){return se=Pr,B=Nt(pt,f),le?Ie(Pr):I}o(je,"leadingEdge");function lt(Pr){var Xo=Pr-G,El=Pr-se,wC=f-Xo;return fe?Kr(wC,L-El):wC}o(lt,"remainingWait");function $e(Pr){var Xo=Pr-G,El=Pr-se;return G===e||Xo>=f||Xo<0||fe&&El>=L}o($e,"shouldInvoke");function pt(){var Pr=Qm();if($e(Pr))return mt(Pr);B=Nt(pt,lt(Pr))}o(pt,"timerExpired");function mt(Pr){return B=e,ke&&y?Ie(Pr):(y=E=e,I)}o(mt,"trailingEdge");function $i(){B!==e&&ya(B),se=0,y=G=E=B=e}o($i,"cancel");function li(){return B===e?I:mt(Qm())}o(li,"flush");function qi(){var Pr=Qm(),Xo=$e(Pr);if(y=arguments,E=this,G=Pr,Xo){if(B===e)return je(G);if(fe)return ya(B),B=Nt(pt,f),Ie(G)}return B===e&&(B=Nt(pt,f)),I}return o(qi,"debounced"),qi.cancel=$i,qi.flush=li,qi}o(eC,"debounce");var cN=nt(function(s,f){return xm(s,1,f)}),pN=nt(function(s,f,h){return xm(s,go(f)||0,h)});function dN(s){return Ei(s,de)}o(dN,"flip");function Zm(s,f){if(typeof s!="function"||f!=null&&typeof f!="function")throw new zn(d);var h=o(function(){var y=arguments,E=f?f.apply(this,y):y[0],L=h.cache;if(L.has(E))return L.get(E);var I=s.apply(this,y);return h.cache=L.set(E,I)||L,I},"memoized");return h.cache=new(Zm.Cache||hr),h}o(Zm,"memoize"),Zm.Cache=hr;function Jm(s){if(typeof s!="function")throw new zn(d);return function(){var f=arguments;switch(f.length){case 0:return!s.call(this);case 1:return!s.call(this,f[0]);case 2:return!s.call(this,f[0],f[1]);case 3:return!s.call(this,f[0],f[1],f[2])}return!s.apply(this,f)}}o(Jm,"negate");function hN(s){return XS(2,s)}o(hN,"once");var mN=m0(function(s,f){f=f.length==1&&rt(f[0])?xt(f[0],gr(He())):xt(Qr(f,1),gr(He()));var h=f.length;return nt(function(y){for(var E=-1,L=Kr(y.length,h);++E=f}),ju=pa(function(){return arguments}())?pa:function(s){return wr(s)&&Ze.call(s,"callee")&&!Wf.call(s,"callee")},rt=Z.isArray,NN=Nf?gr(Nf):f0;function Ti(s){return s!=null&&ev(s.length)&&!_l(s)}o(Ti,"isArrayLike");function Nr(s){return wr(s)&&Ti(s)}o(Nr,"isArrayLikeObject");function PN(s){return s===!0||s===!1||wr(s)&&Wr(s)==Bt}o(PN,"isBoolean");var ka=_u||A0,MN=Pf?gr(Pf):da;function AN(s){return wr(s)&&s.nodeType===1&&!gd(s)}o(AN,"isElement");function DN(s){if(s==null)return!0;if(Ti(s)&&(rt(s)||typeof s=="string"||typeof s.splice=="function"||ka(s)||hc(s)||ju(s)))return!s.length;var f=pn(s);if(f==jt||f==Vt)return!s.size;if(ee(s))return!Jf(s).length;for(var h in s)if(Ze.call(s,h))return!1;return!0}o(DN,"isEmpty");function RN(s,f){return ha(s,f)}o(RN,"isEqual");function FN(s,f,h){h=typeof h=="function"?h:e;var y=h?h(s,f):e;return y===e?ha(s,f,e,h):!!y}o(FN,"isEqualWith");function _0(s){if(!wr(s))return!1;var f=Wr(s);return f==et||f==ne||typeof s.message=="string"&&typeof s.name=="string"&&!gd(s)}o(_0,"isError");function IN(s){return typeof s=="number"&&fm(s)}o(IN,"isFinite");function _l(s){if(!mr(s))return!1;var f=Wr(s);return f==br||f==zt||f==_r||f==ei}o(_l,"isFunction");function rC(s){return typeof s=="number"&&s==it(s)}o(rC,"isInteger");function ev(s){return typeof s=="number"&&s>-1&&s%1==0&&s<=Ke}o(ev,"isLength");function mr(s){var f=typeof s;return s!=null&&(f=="object"||f=="function")}o(mr,"isObject");function wr(s){return s!=null&&typeof s=="object"}o(wr,"isObjectLike");var nC=gu?gr(gu):bm;function HN(s,f){return s===f||gl(s,f,ba(f))}o(HN,"isMatch");function WN(s,f,h){return h=typeof h=="function"?h:e,gl(s,f,ba(f),h)}o(WN,"isMatchWith");function BN(s){return iC(s)&&s!=+s}o(BN,"isNaN");function UN(s){if(z(s))throw new Ye(l);return ma(s)}o(UN,"isNative");function zN(s){return s===null}o(zN,"isNull");function jN(s){return s==null}o(jN,"isNil");function iC(s){return typeof s=="number"||wr(s)&&Wr(s)==xe}o(iC,"isNumber");function gd(s){if(!wr(s)||Wr(s)!=on)return!1;var f=na(s);if(f===null)return!0;var h=Ze.call(f,"constructor")&&f.constructor;return typeof h=="function"&&h instanceof h&&uo.call(h)==Gy}o(gd,"isPlainObject");var b0=yu?gr(yu):Mu;function $N(s){return rC(s)&&s>=-Ke&&s<=Ke}o($N,"isSafeInteger");var oC=Xl?gr(Xl):Au;function tv(s){return typeof s=="string"||!rt(s)&&wr(s)&&Wr(s)==cr}o(tv,"isString");function ji(s){return typeof s=="symbol"||wr(s)&&Wr(s)==ft}o(ji,"isSymbol");var hc=Ql?gr(Ql):Em;function qN(s){return s===e}o(qN,"isUndefined");function VN(s){return wr(s)&&pn(s)==vr}o(VN,"isWeakMap");function KN(s){return wr(s)&&Wr(s)==Ri}o(KN,"isWeakSet");var GN=Lt(Ms),YN=Lt(function(s,f){return s<=f});function sC(s){if(!s)return[];if(Ti(s))return tv(s)?Si(s):Br(s);if(Ss&&s[Ss])return tm(s[Ss]());var f=pn(s),h=f==jt?Df:f==Vt?Rf:mc;return h(s)}o(sC,"toArray");function bl(s){if(!s)return s===0?s:0;if(s=go(s),s===Ve||s===-Ve){var f=s<0?-1:1;return f*Ge}return s===s?s:0}o(bl,"toFinite");function it(s){var f=bl(s),h=f%1;return f===f?h?f-h:f:0}o(it,"toInteger");function lC(s){return s?ml(it(s),0,ut):0}o(lC,"toLength");function go(s){if(typeof s=="number")return s;if(ji(s))return Yt;if(mr(s)){var f=typeof s.valueOf=="function"?s.valueOf():s;s=mr(f)?f+"":f}if(typeof s!="string")return s===0?s:+s;s=Un(s);var h=Ro.test(s);return h||Fo.test(s)?Ap(s.slice(2),h?2:8):au.test(s)?Yt:+s}o(go,"toNumber");function aC(s){return jn(s,ki(s))}o(aC,"toPlainObject");function XN(s){return s?ml(it(s),-Ke,Ke):s===0?s:0}o(XN,"toSafeInteger");function Pt(s){return s==null?"":cn(s)}o(Pt,"toString");var QN=wa(function(s,f){if(ee(f)||Ti(f)){jn(f,gn(f),s);return}for(var h in f)Ze.call(f,h)&&Lu(s,h,f[h])}),uC=wa(function(s,f){jn(f,ki(f),s)}),rv=wa(function(s,f,h,y){jn(f,ki(f),s,y)}),ZN=wa(function(s,f,h,y){jn(f,gn(f),s,y)}),JN=Ko(Qp);function eP(s,f){var h=co(s);return f==null?h:Xp(h,f)}o(eP,"create");var tP=nt(function(s,f){s=ht(s);var h=-1,y=f.length,E=y>2?f[2]:e;for(E&&C(f[0],f[1],E)&&(y=1);++h1),L}),jn(s,fc(s),h),y&&(h=On(h,O|D|Y,qm));for(var E=f.length;E--;)Fu(h,f[E]);return h});function wP(s,f){return cC(s,Jm(He(f)))}o(wP,"omitBy");var xP=Ko(function(s,f){return s==null?{}:Lm(s,f)});function cC(s,f){if(s==null)return{};var h=xt(fc(s),function(y){return[y]});return f=He(f),Nm(s,h,function(y,E){return f(y,E[0])})}o(cC,"pickBy");function SP(s,f,h){f=zi(f,s);var y=-1,E=f.length;for(E||(E=1,s=e);++yf){var y=s;s=f,f=y}if(h||s%1||f%1){var E=bs();return Kr(s+E*(f-s+Mp("1e-"+((E+"").length-1))),f)}return tc(s,f)}o(PP,"random");var MP=xa(function(s,f,h){return f=f.toLowerCase(),s+(h?hC(f):f)});function hC(s){return k0(Pt(s).toLowerCase())}o(hC,"capitalize");function mC(s){return s=Pt(s),s&&s.replace(Ef,sl).replace(Pp,"")}o(mC,"deburr");function AP(s,f,h){s=Pt(s),f=cn(f);var y=s.length;h=h===e?y:ml(it(h),0,y);var E=h;return h-=f.length,h>=0&&s.slice(h,E)==f}o(AP,"endsWith");function DP(s){return s=Pt(s),s&&os.test(s)?s.replace(Qt,Hi):s}o(DP,"escape");function RP(s){return s=Pt(s),s&&Cf.test(s)?s.replace(Qs,"\\$&"):s}o(RP,"escapeRegExp");var FP=xa(function(s,f,h){return s+(h?"-":"")+f.toLowerCase()}),IP=xa(function(s,f,h){return s+(h?" ":"")+f.toLowerCase()}),HP=Um("toLowerCase");function WP(s,f,h){s=Pt(s),f=it(f);var y=f?ll(s):0;if(!f||y>=f)return s;var E=(f-y)/2;return ac(sa(E),h)+s+ac(oa(E),h)}o(WP,"pad");function BP(s,f,h){s=Pt(s),f=it(f);var y=f?ll(s):0;return f&&y>>0,h?(s=Pt(s),s&&(typeof f=="string"||f!=null&&!b0(f))&&(f=cn(f),!f&&Wi(s))?Ds(Si(s),0,h):s.split(f,h)):[]}o(VP,"split");var KP=xa(function(s,f,h){return s+(h?" ":"")+k0(f)});function GP(s,f,h){return s=Pt(s),h=h==null?0:ml(it(h),0,s.length),f=cn(f),s.slice(h,h+f.length)==f}o(GP,"startsWith");function YP(s,f,h){var y=T.templateSettings;h&&C(s,f,h)&&(f=e),s=Pt(s),f=rv({},f,y,uc);var E=rv({},f.imports,y.imports,uc),L=gn(E),I=xi(E,L),B,G,se=0,le=f.interpolate||Dt,fe="__p += '",ke=ys((f.escape||Dt).source+"|"+le.source+"|"+(le===Xs?bf:Dt).source+"|"+(f.evaluate||Dt).source+"|$","g"),Ie="//# sourceURL="+(Ze.call(f,"sourceURL")?(f.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++Gl+"]")+` -`;s.replace(ke,function($e,pt,mt,$i,li,qi){return mt||(mt=$i),fe+=s.slice(se,qi).replace(Ne,em),pt&&(B=!0,fe+=`' + -__e(`+pt+`) + -'`),li&&(G=!0,fe+=`'; -`+li+`; +`)}o(v,"insertWrapDetails");function g(s){return nt(s)||Qu(s)||!!(pl&&s&&s[pl])}o(g,"isFlattenable");function S(s,f){var h=typeof s;return f=f??Ke,!!f&&(h=="number"||h!="symbol"&&$l.test(s))&&s>-1&&s%1==0&&s0){if(++f>=qe)return arguments[0]}else f=0;return s.apply(e,arguments)}}o(ar,"shortOut");function Br(s,f){var h=-1,w=s.length,E=w-1;for(f=f===e?w:f;++h1?s[f-1]:e;return h=typeof h=="function"?(s.pop(),h):e,XS(s,h)});function QS(s){var f=k(s);return f.__chain__=!0,f}o(QS,"chain");function WL(s,f){return f(s),s}o(WL,"tap");function Zm(s,f){return f(s)}o(Zm,"thru");var BL=Ko(function(s){var f=s.length,h=f?s[0]:0,w=this.__wrapped__,E=o(function(L){return od(L,s)},"interceptor");return f>1||this.__actions__.length||!(w instanceof pt)||!S(h)?this.thru(E):(w=w.slice(h,+h+(f?1:0)),w.__actions__.push({func:Zm,args:[E],thisArg:e}),new Ln(w,this.__chain__).thru(function(L){return f&&!L.length&&L.push(e),L}))});function UL(){return QS(this)}o(UL,"wrapperChain");function $L(){return new Ln(this.value(),this.__chain__)}o($L,"wrapperCommit");function zL(){this.__values__===e&&(this.__values__=cC(this.value()));var s=this.__index__>=this.__values__.length,f=s?e:this.__values__[this.__index__++];return{done:s,value:f}}o(zL,"wrapperNext");function jL(){return this}o(jL,"wrapperToIterator");function qL(s){for(var f,h=this;h instanceof Qf;){var w=vn(h);w.__index__=0,w.__values__=e,f?E.__wrapped__=w:f=w;var E=w;h=h.__wrapped__}return E.__wrapped__=s,f}o(qL,"wrapperPlant");function VL(){var s=this.__wrapped__;if(s instanceof pt){var f=s;return this.__actions__.length&&(f=new pt(this)),f=f.reverse(),f.__actions__.push({func:Zm,args:[S0],thisArg:e}),new Ln(f,this.__chain__)}return this.thru(S0)}o(VL,"wrapperReverse");function KL(){return Rm(this.__wrapped__,this.__actions__)}o(KL,"wrapperValue");var GL=vc(function(s,f,h){Je.call(s,h)?++s[h]:ho(s,h,1)});function YL(s,f,h){var w=nt(s)?Ql:rc;return h&&C(s,f,h)&&(f=e),w(s,We(f,3))}o(YL,"every");function XL(s,f){var h=nt(s)?Hi:ld;return h(s,We(f,3))}o(XL,"filter");var QL=xd(Cc),ZL=xd(VS);function JL(s,f){return Qr(Jm(s,f),1)}o(JL,"flatMap");function eN(s,f){return Qr(Jm(s,f),Ve)}o(eN,"flatMapDeep");function tN(s,f,h){return h=h===e?1:st(h),Qr(Jm(s,f),h)}o(tN,"flatMapDepth");function ZS(s,f){var h=nt(s)?Tt:si;return h(s,We(f,3))}o(ZS,"forEach");function JS(s,f){var h=nt(s)?$f:tc;return h(s,We(f,3))}o(JS,"forEachRight");var rN=vc(function(s,f,h){Je.call(s,h)?s[h].push(f):ho(s,h,[f])});function nN(s,f,h,w){s=ki(s)?s:bc(s),h=h&&!w?st(h):0;var E=s.length;return h<0&&(h=er(E+h,0)),iv(s)?h<=E&&s.indexOf(f,h)>-1:!!E&&Uo(s,f,h)>-1}o(nN,"includes");var iN=ot(function(s,f,h){var w=-1,E=typeof f=="function",L=ki(s)?Z(s.length):[];return si(s,function(I){L[++w]=E?On(f,I,h):da(I,f,h)}),L}),oN=vc(function(s,f,h){ho(s,h,f)});function Jm(s,f){var h=nt(s)?Ct:ya;return h(s,We(f,3))}o(Jm,"map");function sN(s,f,h,w){return s==null?[]:(nt(f)||(f=f==null?[]:[f]),h=w?e:h,nt(h)||(h=h==null?[]:[h]),cn(s,f,h))}o(sN,"orderBy");var lN=vc(function(s,f,h){s[h?0:1].push(f)},function(){return[[],[]]});function aN(s,f,h){var w=nt(s)?_u:Tu,E=arguments.length<3;return w(s,We(f,4),h,E,si)}o(aN,"reduce");function uN(s,f,h){var w=nt(s)?zf:Tu,E=arguments.length<3;return w(s,We(f,4),h,E,tc)}o(uN,"reduceRight");function fN(s,f){var h=nt(s)?Hi:ld;return h(s,rv(We(f,3)))}o(fN,"reject");function cN(s){var f=nt(s)?Jf:m0;return f(s)}o(cN,"sample");function pN(s,f,h){(h?C(s,f,h):f===e)?f=1:f=st(f);var w=nt(s)?ks:Ps;return w(s,f)}o(pN,"sampleSize");function dN(s){var f=nt(s)?xm:qo;return f(s)}o(dN,"shuffle");function hN(s){if(s==null)return 0;if(ki(s))return iv(s)?Si(s):s.length;var f=dn(s);return f==qt||f==Gt?s.size:ac(s).length}o(hN,"size");function mN(s,f,h){var w=nt(s)?oo:v0;return h&&C(s,f,h)&&(f=e),w(s,We(f,3))}o(mN,"some");var vN=ot(function(s,f){if(s==null)return[];var h=f.length;return h>1&&C(s,f[0],f[1])?f=[]:h>2&&C(f[0],f[1],f[2])&&(f=[f[0]]),cn(s,Qr(f,1),[])}),ev=Zy||function(){return Nt.Date.now()};function gN(s,f){if(typeof f!="function")throw new $n(d);return s=st(s),function(){if(--s<1)return f.apply(this,arguments)}}o(gN,"after");function eC(s,f,h){return f=h?e:f,f=s&&f==null?s.length:f,Ti(s,ue,e,e,e,e,f)}o(eC,"ary");function tC(s,f){var h;if(typeof f!="function")throw new $n(d);return s=st(s),function(){return--s>0&&(h=f.apply(this,arguments)),s<=1&&(f=e),h}}o(tC,"before");var _0=ot(function(s,f,h){var w=te;if(h.length){var E=W(h,Ea(_0));w|=K}return Ti(s,w,f,h,E)}),rC=ot(function(s,f,h){var w=te|Q;if(h.length){var E=W(h,Ea(rC));w|=K}return Ti(f,w,s,h,E)});function nC(s,f,h){f=h?e:f;var w=Ti(s,M,e,e,e,e,e,f);return w.placeholder=nC.placeholder,w}o(nC,"curry");function iC(s,f,h){f=h?e:f;var w=Ti(s,R,e,e,e,e,e,f);return w.placeholder=iC.placeholder,w}o(iC,"curryRight");function oC(s,f,h){var w,E,L,I,$,G,se=0,le=!1,fe=!1,Le=!0;if(typeof s!="function")throw new $n(d);f=go(f)||0,mr(h)&&(le=!!h.leading,fe="maxWait"in h,L=fe?er(go(h.maxWait)||0,f):L,Le="trailing"in h?!!h.trailing:Le);function Ie(Nr){var Xo=w,kl=E;return w=E=e,se=Nr,I=s.apply(kl,Xo),I}o(Ie,"invokeFunc");function ze(Nr){return se=Nr,$=Mt(dt,f),le?Ie(Nr):I}o(ze,"leadingEdge");function ut(Nr){var Xo=Nr-G,kl=Nr-se,bC=f-Xo;return fe?Kr(bC,L-kl):bC}o(ut,"remainingWait");function je(Nr){var Xo=Nr-G,kl=Nr-se;return G===e||Xo>=f||Xo<0||fe&&kl>=L}o(je,"shouldInvoke");function dt(){var Nr=ev();if(je(Nr))return mt(Nr);$=Mt(dt,ut(Nr))}o(dt,"timerExpired");function mt(Nr){return $=e,Le&&w?Ie(Nr):(w=E=e,I)}o(mt,"trailingEdge");function ji(){$!==e&&xa($),se=0,w=G=E=$=e}o(ji,"cancel");function ui(){return $===e?I:mt(ev())}o(ui,"flush");function qi(){var Nr=ev(),Xo=je(Nr);if(w=arguments,E=this,G=Nr,Xo){if($===e)return ze(G);if(fe)return xa($),$=Mt(dt,f),Ie(G)}return $===e&&($=Mt(dt,f)),I}return o(qi,"debounced"),qi.cancel=ji,qi.flush=ui,qi}o(oC,"debounce");var yN=ot(function(s,f){return _m(s,1,f)}),wN=ot(function(s,f,h){return _m(s,go(f)||0,h)});function xN(s){return Ti(s,de)}o(xN,"flip");function tv(s,f){if(typeof s!="function"||f!=null&&typeof f!="function")throw new $n(d);var h=o(function(){var w=arguments,E=f?f.apply(this,w):w[0],L=h.cache;if(L.has(E))return L.get(E);var I=s.apply(this,w);return h.cache=L.set(E,I)||L,I},"memoized");return h.cache=new(tv.Cache||hr),h}o(tv,"memoize"),tv.Cache=hr;function rv(s){if(typeof s!="function")throw new $n(d);return function(){var f=arguments;switch(f.length){case 0:return!s.call(this);case 1:return!s.call(this,f[0]);case 2:return!s.call(this,f[0],f[1]);case 3:return!s.call(this,f[0],f[1],f[2])}return!s.apply(this,f)}}o(rv,"negate");function SN(s){return tC(2,s)}o(SN,"once");var CN=g0(function(s,f){f=f.length==1&&nt(f[0])?Ct(f[0],Rr(We())):Ct(Qr(f,1),Rr(We()));var h=f.length;return ot(function(w){for(var E=-1,L=Kr(w.length,h);++E=f}),Qu=ha(function(){return arguments}())?ha:function(s){return wr(s)&&Je.call(s,"callee")&&!Gf.call(s,"callee")},nt=Z.isArray,IN=Uf?Rr(Uf):p0;function ki(s){return s!=null&&nv(s.length)&&!El(s)}o(ki,"isArrayLike");function Lr(s){return wr(s)&&ki(s)}o(Lr,"isArrayLikeObject");function HN(s){return s===!0||s===!1||wr(s)&&Hr(s)==Ut}o(HN,"isBoolean");var La=Pu||R0,WN=xi?Rr(xi):ma;function BN(s){return wr(s)&&s.nodeType===1&&!Ed(s)}o(BN,"isElement");function UN(s){if(s==null)return!0;if(ki(s)&&(nt(s)||typeof s=="string"||typeof s.splice=="function"||La(s)||_c(s)||Qu(s)))return!s.length;var f=dn(s);if(f==qt||f==Gt)return!s.size;if(ee(s))return!ac(s).length;for(var h in s)if(Je.call(s,h))return!1;return!0}o(UN,"isEmpty");function $N(s,f){return va(s,f)}o($N,"isEqual");function zN(s,f,h){h=typeof h=="function"?h:e;var w=h?h(s,f):e;return w===e?va(s,f,e,h):!!w}o(zN,"isEqualWith");function E0(s){if(!wr(s))return!1;var f=Hr(s);return f==tt||f==ne||typeof s.message=="string"&&typeof s.name=="string"&&!Ed(s)}o(E0,"isError");function jN(s){return typeof s=="number"&&dm(s)}o(jN,"isFinite");function El(s){if(!mr(s))return!1;var f=Hr(s);return f==br||f==jt||f==_r||f==ei}o(El,"isFunction");function lC(s){return typeof s=="number"&&s==st(s)}o(lC,"isInteger");function nv(s){return typeof s=="number"&&s>-1&&s%1==0&&s<=Ke}o(nv,"isLength");function mr(s){var f=typeof s;return s!=null&&(f=="object"||f=="function")}o(mr,"isObject");function wr(s){return s!=null&&typeof s=="object"}o(wr,"isObjectLike");var aC=Xl?Rr(Xl):km;function qN(s,f){return s===f||wl(s,f,Ta(f))}o(qN,"isMatch");function VN(s,f,h){return h=typeof h=="function"?h:e,wl(s,f,Ta(f),h)}o(VN,"isMatchWith");function KN(s){return uC(s)&&s!=+s}o(KN,"isNaN");function GN(s){if(z(s))throw new Xe(l);return ga(s)}o(GN,"isNative");function YN(s){return s===null}o(YN,"isNull");function XN(s){return s==null}o(XN,"isNil");function uC(s){return typeof s=="number"||wr(s)&&Hr(s)==Se}o(uC,"isNumber");function Ed(s){if(!wr(s)||Hr(s)!=nn)return!1;var f=oa(s);if(f===null)return!0;var h=Je.call(f,"constructor")&&f.constructor;return typeof h=="function"&&h instanceof h&&uo.call(h)==Xy}o(Ed,"isPlainObject");var T0=il?Rr(il):Bu;function QN(s){return lC(s)&&s>=-Ke&&s<=Ke}o(QN,"isSafeInteger");var fC=cs?Rr(cs):Uu;function iv(s){return typeof s=="string"||!nt(s)&&wr(s)&&Hr(s)==dr}o(iv,"isString");function zi(s){return typeof s=="symbol"||wr(s)&&Hr(s)==ct}o(zi,"isSymbol");var _c=Cu?Rr(Cu):Om;function ZN(s){return s===e}o(ZN,"isUndefined");function JN(s){return wr(s)&&dn(s)==vr}o(JN,"isWeakMap");function eP(s){return wr(s)&&Hr(s)==Fi}o(eP,"isWeakSet");var tP=Pt(Ns),rP=Pt(function(s,f){return s<=f});function cC(s){if(!s)return[];if(ki(s))return iv(s)?gr(s):Wr(s);if(ws&&s[ws])return y(s[ws]());var f=dn(s),h=f==qt?T:f==Gt?Fr:bc;return h(s)}o(cC,"toArray");function Tl(s){if(!s)return s===0?s:0;if(s=go(s),s===Ve||s===-Ve){var f=s<0?-1:1;return f*Ye}return s===s?s:0}o(Tl,"toFinite");function st(s){var f=Tl(s),h=f%1;return f===f?h?f-h:f:0}o(st,"toInteger");function pC(s){return s?gl(st(s),0,ft):0}o(pC,"toLength");function go(s){if(typeof s=="number")return s;if(zi(s))return Qt;if(mr(s)){var f=typeof s.valueOf=="function"?s.valueOf():s;s=mr(f)?f+"":f}if(typeof s!="string")return s===0?s:+s;s=sl(s);var h=Ro.test(s);return h||Fo.test(s)?um(s.slice(2),h?2:8):fu.test(s)?Qt:+s}o(go,"toNumber");function dC(s){return zn(s,Oi(s))}o(dC,"toPlainObject");function nP(s){return s?gl(st(s),-Ke,Ke):s===0?s:0}o(nP,"toSafeInteger");function At(s){return s==null?"":pn(s)}o(At,"toString");var iP=Sa(function(s,f){if(ee(f)||ki(f)){zn(f,yn(f),s);return}for(var h in f)Je.call(f,h)&&Iu(s,h,f[h])}),hC=Sa(function(s,f){zn(f,Oi(f),s)}),ov=Sa(function(s,f,h,w){zn(f,Oi(f),s,w)}),oP=Sa(function(s,f,h,w){zn(f,yn(f),s,w)}),sP=Ko(od);function lP(s,f){var h=co(s);return f==null?h:id(h,f)}o(lP,"create");var aP=ot(function(s,f){s=ht(s);var h=-1,w=f.length,E=w>2?f[2]:e;for(E&&C(f[0],f[1],E)&&(w=1);++h1),L}),zn(s,wc(s),h),w&&(h=Nn(h,O|D|Y,Gm));for(var E=f.length;E--;)ju(h,f[E]);return h});function TP(s,f){return vC(s,rv(We(f)))}o(TP,"omitBy");var kP=Ko(function(s,f){return s==null?{}:Mm(s,f)});function vC(s,f){if(s==null)return{};var h=Ct(wc(s),function(w){return[w]});return f=We(f),Am(s,h,function(w,E){return f(w,E[0])})}o(vC,"pickBy");function OP(s,f,h){f=$i(f,s);var w=-1,E=f.length;for(E||(E=1,s=e);++wf){var w=s;s=f,f=w}if(h||s%1||f%1){var E=Cs();return Kr(s+E*(f-s+am("1e-"+((E+"").length-1))),f)}return fc(s,f)}o(HP,"random");var WP=Ca(function(s,f,h){return f=f.toLowerCase(),s+(h?wC(f):f)});function wC(s){return L0(At(s).toLowerCase())}o(wC,"capitalize");function xC(s){return s=At(s),s&&s.replace(Df,ku).replace(Hf,"")}o(xC,"deburr");function BP(s,f,h){s=At(s),f=pn(f);var w=s.length;h=h===e?w:gl(st(h),0,w);var E=h;return h-=f.length,h>=0&&s.slice(h,E)==f}o(BP,"endsWith");function UP(s){return s=At(s),s&&os.test(s)?s.replace(Jt,Kp):s}o(UP,"escape");function $P(s){return s=At(s),s&&Pf.test(s)?s.replace(Ys,"\\$&"):s}o($P,"escapeRegExp");var zP=Ca(function(s,f,h){return s+(h?"-":"")+f.toLowerCase()}),jP=Ca(function(s,f,h){return s+(h?" ":"")+f.toLowerCase()}),qP=jm("toLowerCase");function VP(s,f,h){s=At(s),f=st(f);var w=f?Si(s):0;if(!f||w>=f)return s;var E=(f-w)/2;return gc(aa(E),h)+s+gc(la(E),h)}o(VP,"pad");function KP(s,f,h){s=At(s),f=st(f);var w=f?Si(s):0;return f&&w>>0,h?(s=At(s),s&&(typeof f=="string"||f!=null&&!T0(f))&&(f=pn(f),!f&&ni(s))?Ms(gr(s),0,h):s.split(f,h)):[]}o(JP,"split");var eM=Ca(function(s,f,h){return s+(h?" ":"")+L0(f)});function tM(s,f,h){return s=At(s),h=h==null?0:gl(st(h),0,s.length),f=pn(f),s.slice(h,h+f.length)==f}o(tM,"startsWith");function rM(s,f,h){var w=k.templateSettings;h&&C(s,f,h)&&(f=e),s=At(s),f=ov({},f,w,yc);var E=ov({},f.imports,w.imports,yc),L=yn(E),I=ll(E,L),$,G,se=0,le=f.interpolate||zt,fe="__p += '",Le=vs((f.escape||zt).source+"|"+le.source+"|"+(le===Gs?Af:zt).source+"|"+(f.evaluate||zt).source+"|$","g"),Ie="//# sourceURL="+(Je.call(f,"sourceURL")?(f.sourceURL+"").replace(/\s/g," "):"lodash.templateSources["+ ++N+"]")+` +`;s.replace(Le,function(je,dt,mt,ji,ui,qi){return mt||(mt=ji),fe+=s.slice(se,qi).replace(Me,Kf),dt&&($=!0,fe+=`' + +__e(`+dt+`) + +'`),ui&&(G=!0,fe+=`'; +`+ui+`; __p += '`),mt&&(fe+=`' + ((__t = (`+mt+`)) == null ? '' : __t) + -'`),se=qi+$e.length,$e}),fe+=`'; -`;var je=Ze.call(f,"variable")&&f.variable;if(!je)fe=`with (obj) { +'`),se=qi+je.length,je}),fe+=`'; +`;var ze=Je.call(f,"variable")&&f.variable;if(!ze)fe=`with (obj) { `+fe+` } -`;else if(ls.test(je))throw new Ye(v);fe=(G?fe.replace(Xt,""):fe).replace(Hl,"$1").replace(At,"$1;"),fe="function("+(je||"obj")+`) { -`+(je?"":`obj || (obj = {}); -`)+"var __t, __p = ''"+(B?", __e = _.escape":"")+(G?`, __j = Array.prototype.join; +`;else if(ls.test(ze))throw new Xe(m);fe=(G?fe.replace(Zt,""):fe).replace(Bl,"$1").replace(Rt,"$1;"),fe="function("+(ze||"obj")+`) { +`+(ze?"":`obj || (obj = {}); +`)+"var __t, __p = ''"+($?", __e = _.escape":"")+(G?`, __j = Array.prototype.join; function print() { __p += __j.call(arguments, '') } `:`; `)+fe+`return __p -}`;var lt=gC(function(){return St(L,Ie+"return "+fe).apply(e,I)});if(lt.source=fe,_0(lt))throw lt;return lt}o(YP,"template");function XP(s){return Pt(s).toLowerCase()}o(XP,"toLower");function QP(s){return Pt(s).toUpperCase()}o(QP,"toUpper");function ZP(s,f,h){if(s=Pt(s),s&&(h||f===e))return Un(s);if(!s||!(f=cn(f)))return s;var y=Si(s),E=Si(f),L=Ii(y,E),I=oo(y,E)+1;return Ds(y,L,I).join("")}o(ZP,"trim");function JP(s,f,h){if(s=Pt(s),s&&(h||f===e))return s.slice(0,Ff(s)+1);if(!s||!(f=cn(f)))return s;var y=Si(s),E=oo(y,Si(f))+1;return Ds(y,0,E).join("")}o(JP,"trimEnd");function eM(s,f,h){if(s=Pt(s),s&&(h||f===e))return s.replace(Zs,"");if(!s||!(f=cn(f)))return s;var y=Si(s),E=Ii(y,Si(f));return Ds(y,E).join("")}o(eM,"trimStart");function tM(s,f){var h=ge,y=we;if(mr(f)){var E="separator"in f?f.separator:E;h="length"in f?it(f.length):h,y="omission"in f?cn(f.omission):y}s=Pt(s);var L=s.length;if(Wi(s)){var I=Si(s);L=I.length}if(h>=L)return s;var B=h-ll(y);if(B<1)return y;var G=I?Ds(I,0,B).join(""):s.slice(0,B);if(E===e)return G+y;if(I&&(B+=G.length-B),b0(E)){if(s.slice(B).search(E)){var se,le=G;for(E.global||(E=ys(E.source,Pt(Js.exec(E))+"g")),E.lastIndex=0;se=E.exec(le);)var fe=se.index;G=G.slice(0,fe===e?B:fe)}}else if(s.indexOf(cn(E),B)!=B){var ke=G.lastIndexOf(E);ke>-1&&(G=G.slice(0,ke))}return G+y}o(tM,"truncate");function rM(s){return s=Pt(s),s&&ti.test(s)?s.replace(Rr,If):s}o(rM,"unescape");var nM=xa(function(s,f,h){return s+(h?" ":"")+f.toUpperCase()}),k0=Um("toUpperCase");function vC(s,f,h){return s=Pt(s),f=h?e:f,f===e?so(s)?Bi(s):Af(s):s.match(f)||[]}o(vC,"words");var gC=nt(function(s,f){try{return Tn(s,e,f)}catch(h){return _0(h)?h:new Ye(h)}}),iM=Ko(function(s,f){return Wn(f,function(h){h=kt(h),ho(s,h,S0(s[h],s))}),s});function oM(s){var f=s==null?0:s.length,h=He();return s=f?xt(s,function(y){if(typeof y[1]!="function")throw new zn(d);return[h(y[0]),y[1]]}):[],nt(function(y){for(var E=-1;++EKe)return[];var h=ut,y=Kr(s,ut);f=He(f),s-=ut;for(var E=pr(y,f);++h0||f<0)?new ct(h):(s<0?h=h.takeRight(-s):s&&(h=h.drop(s)),f!==e&&(f=it(f),h=f<0?h.dropRight(-f):h.take(f-s)),h)},ct.prototype.takeRightWhile=function(s){return this.reverse().takeWhile(s).reverse()},ct.prototype.toArray=function(){return this.take(ut)},oi(ct.prototype,function(s,f){var h=/^(?:filter|find|map|reject)|While$/.test(f),y=/^(?:head|last)$/.test(f),E=T[y?"take"+(f=="last"?"Right":""):f],L=y||/^find/.test(f);!E||(T.prototype[f]=function(){var I=this.__wrapped__,B=y?[1]:arguments,G=I instanceof ct,se=B[0],le=G||rt(I),fe=o(function(pt){var mt=E.apply(T,no([pt],B));return y&&ke?mt[0]:mt},"interceptor");le&&h&&typeof se=="function"&&se.length!=1&&(G=le=!1);var ke=this.__chain__,Ie=!!this.__actions__.length,je=L&&!ke,lt=G&&!Ie;if(!L&&le){I=lt?I:new ct(this);var $e=s.apply(I,B);return $e.__actions__.push({func:Ym,args:[fe],thisArg:e}),new kn($e,ke)}return je&<?s.apply(this,B):($e=this.thru(fe),je?y?$e.value()[0]:$e.value():$e)})}),Wn(["pop","push","shift","sort","splice","unshift"],function(s){var f=ra[s],h=/^(?:push|sort|unshift)$/.test(s)?"tap":"thru",y=/^(?:pop|shift)$/.test(s);T.prototype[s]=function(){var E=arguments;if(y&&!this.__chain__){var L=this.value();return f.apply(rt(L)?L:[],E)}return this[h](function(I){return f.apply(rt(I)?I:[],E)})}}),oi(ct.prototype,function(s,f){var h=T[f];if(h){var y=h.name+"";Ze.call(aa,y)||(aa[y]=[]),aa[y].push({name:f,func:h})}}),aa[Iu(e,Q).name]=[{name:"wrapper",func:e}],ct.prototype.clone=n0,ct.prototype.reverse=i0,ct.prototype.value=Up,T.prototype.at=AL,T.prototype.chain=DL,T.prototype.commit=RL,T.prototype.next=FL,T.prototype.plant=HL,T.prototype.reverse=WL,T.prototype.toJSON=T.prototype.valueOf=T.prototype.value=BL,T.prototype.first=T.prototype.head,Ss&&(T.prototype[Ss]=IL),T},"runInContext"),lo=sm();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Ot._=lo,define(function(){return lo})):Ir?((Ir.exports=lo)._=lo,ps._=lo):Ot._=lo}).call(Jc)});var TT=lr((vx,gx)=>{(function(e,t){typeof vx=="object"&&typeof gx!="undefined"?gx.exports=t():typeof define=="function"&&define.amd?define(t):e.stable=t()})(vx,function(){"use strict";var e=o(function(l,d){return t(l.slice(),d)},"stable");e.inplace=function(l,d){var v=t(l,d);return v!==l&&n(v,null,l.length,l),l};function t(l,d){typeof d!="function"&&(d=o(function(O,D){return String(O).localeCompare(D)},"comp"));var v=l.length;if(v<=1)return l;for(var p=new Array(v),w=1;ww&&(Y=w),W>w&&(W=w),X=D,te=Y;;)if(X{(function(){"use strict";var e={}.hasOwnProperty;function t(){for(var n=[],l=0;l{(function(e,t){typeof Gx=="object"&&typeof Yx!="undefined"?Yx.exports=t():typeof define=="function"&&define.amd?define(t):(e=e||self,e.CodeMirror=t())})(Gx,function(){"use strict";var e=navigator.userAgent,t=navigator.platform,n=/gecko\/\d/i.test(e),l=/MSIE \d/.test(e),d=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(e),v=/Edge\/(\d+)/.exec(e),p=l||d||v,w=p&&(l?document.documentMode||6:+(v||d)[1]),_=!v&&/WebKit\//.test(e),O=_&&/Qt\/\d+\.\d+/.test(e),D=!v&&/Chrome\//.test(e),Y=/Opera\//.test(e),W=/Apple Computer/.test(navigator.vendor),X=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(e),te=/PhantomJS/.test(e),Q=W&&(/Mobile\/\w+/.test(e)||navigator.maxTouchPoints>2),R=/Android/.test(e),P=Q||R||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(e),F=Q||/Mac/.test(t),K=/\bCrOS\b/.test(e),V=/win/i.test(t),ue=Y&&e.match(/Version\/(\d*\.\d*)/);ue&&(ue=Number(ue[1])),ue&&ue>=15&&(Y=!1,_=!0);var ie=F&&(O||Y&&(ue==null||ue<12.11)),de=n||p&&w>=9;function ge(r){return new RegExp("(^|\\s)"+r+"(?:$|\\s)\\s*")}o(ge,"classTest");var we=o(function(r,i){var u=r.className,a=ge(i).exec(u);if(a){var c=u.slice(a.index+a[0].length);r.className=u.slice(0,a.index)+(c?a[1]+c:"")}},"rmClass");function qe(r){for(var i=r.childNodes.length;i>0;--i)r.removeChild(r.firstChild);return r}o(qe,"removeChildren");function Je(r,i){return qe(r).appendChild(i)}o(Je,"removeChildrenAndAdd");function be(r,i,u,a){var c=document.createElement(r);if(u&&(c.className=u),a&&(c.style.cssText=a),typeof i=="string")c.appendChild(document.createTextNode(i));else if(i)for(var m=0;m=i)return g+(i-m);g+=S-m,g+=u-g%u,m=S+1}}o(_t,"countColumn");var wt=o(function(){this.id=null,this.f=null,this.time=0,this.handler=Dr(this.onTimeout,this)},"Delayed");wt.prototype.onTimeout=function(r){r.id=0,r.time<=+new Date?r.f():setTimeout(r.handler,r.time-+new Date)},wt.prototype.set=function(r,i){this.f=i;var u=+new Date+r;(!this.id||u=i)return a+Math.min(g,i-c);if(c+=m-a,c+=u-c%u,a=m+1,c>=i)return a}}o(br,"findColumn");var zt=[""];function jt(r){for(;zt.length<=r;)zt.push(xe(zt)+" ");return zt[r]}o(jt,"spaceStr");function xe(r){return r[r.length-1]}o(xe,"lst");function Er(r,i){for(var u=[],a=0;a"\x80"&&(r.toUpperCase()!=r.toLowerCase()||sn.test(r))}o(Vt,"isWordCharBasic");function cr(r,i){return i?i.source.indexOf("\\w")>-1&&Vt(r)?!0:i.test(r):Vt(r)}o(cr,"isWordChar");function ft(r){for(var i in r)if(r.hasOwnProperty(i)&&r[i])return!1;return!0}o(ft,"isEmpty");var Do=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;function vr(r){return r.charCodeAt(0)>=768&&Do.test(r)}o(vr,"isExtendingChar");function Ri(r,i,u){for(;(u<0?i>0:iu?-1:1;;){if(i==u)return i;var c=(i+u)/2,m=a<0?Math.ceil(c):Math.floor(c);if(m==i)return r(m)?i:u;r(m)?u=m:i=m+a}}o(ln,"findFirst");function Rn(r,i,u,a){if(!r)return a(i,u,"ltr",0);for(var c=!1,m=0;mi||i==u&&g.to==i)&&(a(Math.max(g.from,i),Math.min(g.to,u),g.level==1?"rtl":"ltr",m),c=!0)}c||a(i,u,"ltr")}o(Rn,"iterateBidiSections");var hi=null;function mi(r,i,u){var a;hi=null;for(var c=0;ci)return c;m.to==i&&(m.from!=m.to&&u=="before"?a=c:hi=c),m.from==i&&(m.from!=m.to&&u!="before"?a=c:hi=c)}return a??hi}o(mi,"getBidiPartAt");var Fn=function(){var r="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",i="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111";function u(b){return b<=247?r.charAt(b):1424<=b&&b<=1524?"R":1536<=b&&b<=1785?i.charAt(b-1536):1774<=b&&b<=2220?"r":8192<=b&&b<=8203?"w":b==8204?"b":"L"}o(u,"charType");var a=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,c=/[stwN]/,m=/[LRr]/,g=/[Lb1n]/,S=/[1n]/;function C(b,N,A){this.level=b,this.from=N,this.to=A}return o(C,"BidiSpan"),function(b,N){var A=N=="ltr"?"L":"R";if(b.length==0||N=="ltr"&&!a.test(b))return!1;for(var $=b.length,z=[],ee=0;ee<$;++ee)z.push(u(b.charCodeAt(ee)));for(var oe=0,ce=A;oe<$;++oe){var me=z[oe];me=="m"?z[oe]=ce:ce=me}for(var Ce=0,ve=A;Ce<$;++Ce){var Te=z[Ce];Te=="1"&&ve=="r"?z[Ce]="n":m.test(Te)&&(ve=Te,Te=="r"&&(z[Ce]="R"))}for(var Fe=1,De=z[0];Fe<$-1;++Fe){var tt=z[Fe];tt=="+"&&De=="1"&&z[Fe+1]=="1"?z[Fe]="1":tt==","&&De==z[Fe+1]&&(De=="1"||De=="n")&&(z[Fe]=De),De=tt}for(var bt=0;bt<$;++bt){var yr=z[bt];if(yr==",")z[bt]="N";else if(yr=="%"){var Nt=void 0;for(Nt=bt+1;Nt<$&&z[Nt]=="%";++Nt);for(var dn=bt&&z[bt-1]=="!"||Nt<$&&z[Nt]=="1"?"1":"N",hn=bt;hn-1&&(a[i]=c.slice(0,m).concat(c.slice(m+1)))}}}o(he,"off");function Ee(r,i){var u=J(r,i);if(!!u.length)for(var a=Array.prototype.slice.call(arguments,2),c=0;c0}o(At,"hasHandler");function Rr(r){r.prototype.on=function(i,u){H(this,i,u)},r.prototype.off=function(i,u){he(this,i,u)}}o(Rr,"eventMixin");function Qt(r){r.preventDefault?r.preventDefault():r.returnValue=!1}o(Qt,"e_preventDefault");function ti(r){r.stopPropagation?r.stopPropagation():r.cancelBubble=!0}o(ti,"e_stopPropagation");function os(r){return r.defaultPrevented!=null?r.defaultPrevented:r.returnValue==!1}o(os,"e_defaultPrevented");function to(r){Qt(r),ti(r)}o(to,"e_stop");function vi(r){return r.target||r.srcElement}o(vi,"e_target");function Xs(r){var i=r.which;return i==null&&(r.button&1?i=1:r.button&2?i=3:r.button&4&&(i=2)),F&&r.ctrlKey&&i==1&&(i=3),i}o(Xs,"e_button");var ss=function(){if(p&&w<9)return!1;var r=be("div");return"draggable"in r||"dragDrop"in r}(),an;function bp(r){if(an==null){var i=be("span","\u200B");Je(r,be("span",[i,document.createTextNode("x")])),r.firstChild.offsetHeight!=0&&(an=i.offsetWidth<=1&&i.offsetHeight>2&&!(p&&w<8))}var u=an?be("span","\u200B"):be("span","\xA0",null,"display: inline-block; width: 1px; margin-right: -1px");return u.setAttribute("cm-text",""),u}o(bp,"zeroWidthElement");var Qs;function Cf(r){if(Qs!=null)return Qs;var i=Je(r,document.createTextNode("A\u062EA")),u=Be(i,0,1).getBoundingClientRect(),a=Be(i,1,2).getBoundingClientRect();return qe(r),!u||u.left==u.right?!1:Qs=a.right-u.right<3}o(Cf,"hasBadBidiRects");var Zs=` +}`;var ut=CC(function(){return _t(L,Ie+"return "+fe).apply(e,I)});if(ut.source=fe,E0(ut))throw ut;return ut}o(rM,"template");function nM(s){return At(s).toLowerCase()}o(nM,"toLower");function iM(s){return At(s).toUpperCase()}o(iM,"toUpper");function oM(s,f,h){if(s=At(s),s&&(h||f===e))return sl(s);if(!s||!(f=pn(f)))return s;var w=gr(s),E=gr(f),L=Bi(w,E),I=ta(w,E)+1;return Ms(w,L,I).join("")}o(oM,"trim");function sM(s,f,h){if(s=At(s),s&&(h||f===e))return s.slice(0,al(s)+1);if(!s||!(f=pn(f)))return s;var w=gr(s),E=ta(w,gr(f))+1;return Ms(w,0,E).join("")}o(sM,"trimEnd");function lM(s,f,h){if(s=At(s),s&&(h||f===e))return s.replace(Xs,"");if(!s||!(f=pn(f)))return s;var w=gr(s),E=Bi(w,gr(f));return Ms(w,E).join("")}o(lM,"trimStart");function aM(s,f){var h=ge,w=xe;if(mr(f)){var E="separator"in f?f.separator:E;h="length"in f?st(f.length):h,w="omission"in f?pn(f.omission):w}s=At(s);var L=s.length;if(ni(s)){var I=gr(s);L=I.length}if(h>=L)return s;var $=h-Si(w);if($<1)return w;var G=I?Ms(I,0,$).join(""):s.slice(0,$);if(E===e)return G+w;if(I&&($+=G.length-$),T0(E)){if(s.slice($).search(E)){var se,le=G;for(E.global||(E=vs(E.source,At(Qs.exec(E))+"g")),E.lastIndex=0;se=E.exec(le);)var fe=se.index;G=G.slice(0,fe===e?$:fe)}}else if(s.indexOf(pn(E),$)!=$){var Le=G.lastIndexOf(E);Le>-1&&(G=G.slice(0,Le))}return G+w}o(aM,"truncate");function uM(s){return s=At(s),s&&ti.test(s)?s.replace(Dr,ul):s}o(uM,"unescape");var fM=Ca(function(s,f,h){return s+(h?" ":"")+f.toUpperCase()}),L0=jm("toUpperCase");function SC(s,f,h){return s=At(s),f=h?e:f,f===e?ii(s)?fn(s):jf(s):s.match(f)||[]}o(SC,"words");var CC=ot(function(s,f){try{return On(s,e,f)}catch(h){return E0(h)?h:new Xe(h)}}),cM=Ko(function(s,f){return Tt(f,function(h){h=Lt(h),ho(s,h,_0(s[h],s))}),s});function pM(s){var f=s==null?0:s.length,h=We();return s=f?Ct(s,function(w){if(typeof w[1]!="function")throw new $n(d);return[h(w[0]),w[1]]}):[],ot(function(w){for(var E=-1;++EKe)return[];var h=ft,w=Kr(s,ft);f=We(f),s-=ft;for(var E=so(w,f);++h0||f<0)?new pt(h):(s<0?h=h.takeRight(-s):s&&(h=h.drop(s)),f!==e&&(f=st(f),h=f<0?h.dropRight(-f):h.take(f-s)),h)},pt.prototype.takeRightWhile=function(s){return this.reverse().takeWhile(s).reverse()},pt.prototype.toArray=function(){return this.take(ft)},li(pt.prototype,function(s,f){var h=/^(?:filter|find|map|reject)|While$/.test(f),w=/^(?:head|last)$/.test(f),E=k[w?"take"+(f=="last"?"Right":""):f],L=w||/^find/.test(f);!E||(k.prototype[f]=function(){var I=this.__wrapped__,$=w?[1]:arguments,G=I instanceof pt,se=$[0],le=G||nt(I),fe=o(function(dt){var mt=E.apply(k,io([dt],$));return w&&Le?mt[0]:mt},"interceptor");le&&h&&typeof se=="function"&&se.length!=1&&(G=le=!1);var Le=this.__chain__,Ie=!!this.__actions__.length,ze=L&&!Le,ut=G&&!Ie;if(!L&&le){I=ut?I:new pt(this);var je=s.apply(I,$);return je.__actions__.push({func:Zm,args:[fe],thisArg:e}),new Ln(je,Le)}return ze&&ut?s.apply(this,$):(je=this.thru(fe),ze?w?je.value()[0]:je.value():je)})}),Tt(["pop","push","shift","sort","splice","unshift"],function(s){var f=ia[s],h=/^(?:push|sort|unshift)$/.test(s)?"tap":"thru",w=/^(?:pop|shift)$/.test(s);k.prototype[s]=function(){var E=arguments;if(w&&!this.__chain__){var L=this.value();return f.apply(nt(L)?L:[],E)}return this[h](function(I){return f.apply(nt(I)?I:[],E)})}}),li(pt.prototype,function(s,f){var h=k[f];if(h){var w=h.name+"";Je.call(fa,w)||(fa[w]=[]),fa[w].push({name:f,func:h})}}),fa[qu(e,Q).name]=[{name:"wrapper",func:e}],pt.prototype.clone=o0,pt.prototype.reverse=s0,pt.prototype.value=Yp,k.prototype.at=BL,k.prototype.chain=UL,k.prototype.commit=$L,k.prototype.next=zL,k.prototype.plant=qL,k.prototype.reverse=VL,k.prototype.toJSON=k.prototype.valueOf=k.prototype.value=KL,k.prototype.first=k.prototype.head,ws&&(k.prototype[ws]=jL),k},"runInContext"),lo=Ci();typeof define=="function"&&typeof define.amd=="object"&&define.amd?(Nt._=lo,define(function(){return lo})):_e?((_e.exports=lo)._=lo,Ii._=lo):Nt._=lo}).call(ap)});var PT=ur((yx,wx)=>{(function(e,t){typeof yx=="object"&&typeof wx!="undefined"?wx.exports=t():typeof define=="function"&&define.amd?define(t):e.stable=t()})(yx,function(){"use strict";var e=o(function(l,d){return t(l.slice(),d)},"stable");e.inplace=function(l,d){var m=t(l,d);return m!==l&&n(m,null,l.length,l),l};function t(l,d){typeof d!="function"&&(d=o(function(O,D){return String(O).localeCompare(D)},"comp"));var m=l.length;if(m<=1)return l;for(var p=new Array(m),x=1;xx&&(Y=x),U>x&&(U=x),X=D,te=Y;;)if(X{(function(){"use strict";var e={}.hasOwnProperty;function t(){for(var n=[],l=0;l{(function(e,t){typeof Xx=="object"&&typeof Qx!="undefined"?Qx.exports=t():typeof define=="function"&&define.amd?define(t):(e=e||self,e.CodeMirror=t())})(Xx,function(){"use strict";var e=navigator.userAgent,t=navigator.platform,n=/gecko\/\d/i.test(e),l=/MSIE \d/.test(e),d=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(e),m=/Edge\/(\d+)/.exec(e),p=l||d||m,x=p&&(l?document.documentMode||6:+(m||d)[1]),_=!m&&/WebKit\//.test(e),O=_&&/Qt\/\d+\.\d+/.test(e),D=!m&&/Chrome\//.test(e),Y=/Opera\//.test(e),U=/Apple Computer/.test(navigator.vendor),X=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(e),te=/PhantomJS/.test(e),Q=U&&(/Mobile\/\w+/.test(e)||navigator.maxTouchPoints>2),F=/Android/.test(e),M=Q||F||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(e),R=Q||/Mac/.test(t),K=/\bCrOS\b/.test(e),V=/win/i.test(t),ue=Y&&e.match(/Version\/(\d*\.\d*)/);ue&&(ue=Number(ue[1])),ue&&ue>=15&&(Y=!1,_=!0);var ie=R&&(O||Y&&(ue==null||ue<12.11)),de=n||p&&x>=9;function ge(r){return new RegExp("(^|\\s)"+r+"(?:$|\\s)\\s*")}o(ge,"classTest");var xe=o(function(r,i){var u=r.className,a=ge(i).exec(u);if(a){var c=u.slice(a.index+a[0].length);r.className=u.slice(0,a.index)+(c?a[1]+c:"")}},"rmClass");function qe(r){for(var i=r.childNodes.length;i>0;--i)r.removeChild(r.firstChild);return r}o(qe,"removeChildren");function et(r,i){return qe(r).appendChild(i)}o(et,"removeChildrenAndAdd");function Te(r,i,u,a){var c=document.createElement(r);if(u&&(c.className=u),a&&(c.style.cssText=a),typeof i=="string")c.appendChild(document.createTextNode(i));else if(i)for(var v=0;v=i)return g+(i-v);g+=S-v,g+=u-g%u,v=S+1}}o(Et,"countColumn");var St=o(function(){this.id=null,this.f=null,this.time=0,this.handler=Ar(this.onTimeout,this)},"Delayed");St.prototype.onTimeout=function(r){r.id=0,r.time<=+new Date?r.f():setTimeout(r.handler,r.time-+new Date)},St.prototype.set=function(r,i){this.f=i;var u=+new Date+r;(!this.id||u=i)return a+Math.min(g,i-c);if(c+=v-a,c+=u-c%u,a=v+1,c>=i)return a}}o(br,"findColumn");var jt=[""];function qt(r){for(;jt.length<=r;)jt.push(Se(jt)+" ");return jt[r]}o(qt,"spaceStr");function Se(r){return r[r.length-1]}o(Se,"lst");function Er(r,i){for(var u=[],a=0;a"\x80"&&(r.toUpperCase()!=r.toLowerCase()||on.test(r))}o(Gt,"isWordCharBasic");function dr(r,i){return i?i.source.indexOf("\\w")>-1&&Gt(r)?!0:i.test(r):Gt(r)}o(dr,"isWordChar");function ct(r){for(var i in r)if(r.hasOwnProperty(i)&&r[i])return!1;return!0}o(ct,"isEmpty");var Do=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;function vr(r){return r.charCodeAt(0)>=768&&Do.test(r)}o(vr,"isExtendingChar");function Fi(r,i,u){for(;(u<0?i>0:iu?-1:1;;){if(i==u)return i;var c=(i+u)/2,v=a<0?Math.ceil(c):Math.floor(c);if(v==i)return r(v)?i:u;r(v)?u=v:i=v+a}}o(sn,"findFirst");function In(r,i,u,a){if(!r)return a(i,u,"ltr",0);for(var c=!1,v=0;vi||i==u&&g.to==i)&&(a(Math.max(g.from,i),Math.min(g.to,u),g.level==1?"rtl":"ltr",v),c=!0)}c||a(i,u,"ltr")}o(In,"iterateBidiSections");var vi=null;function gi(r,i,u){var a;vi=null;for(var c=0;ci)return c;v.to==i&&(v.from!=v.to&&u=="before"?a=c:vi=c),v.from==i&&(v.from!=v.to&&u!="before"?a=c:vi=c)}return a??vi}o(gi,"getBidiPartAt");var Hn=function(){var r="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",i="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111";function u(b){return b<=247?r.charAt(b):1424<=b&&b<=1524?"R":1536<=b&&b<=1785?i.charAt(b-1536):1774<=b&&b<=2220?"r":8192<=b&&b<=8203?"w":b==8204?"b":"L"}o(u,"charType");var a=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,c=/[stwN]/,v=/[LRr]/,g=/[Lb1n]/,S=/[1n]/;function C(b,P,A){this.level=b,this.from=P,this.to=A}return o(C,"BidiSpan"),function(b,P){var A=P=="ltr"?"L":"R";if(b.length==0||P=="ltr"&&!a.test(b))return!1;for(var j=b.length,z=[],ee=0;ee-1&&(a[i]=c.slice(0,v).concat(c.slice(v+1)))}}}o(he,"off");function ke(r,i){var u=J(r,i);if(!!u.length)for(var a=Array.prototype.slice.call(arguments,2),c=0;c0}o(Rt,"hasHandler");function Dr(r){r.prototype.on=function(i,u){H(this,i,u)},r.prototype.off=function(i,u){he(this,i,u)}}o(Dr,"eventMixin");function Jt(r){r.preventDefault?r.preventDefault():r.returnValue=!1}o(Jt,"e_preventDefault");function ti(r){r.stopPropagation?r.stopPropagation():r.cancelBubble=!0}o(ti,"e_stopPropagation");function os(r){return r.defaultPrevented!=null?r.defaultPrevented:r.returnValue==!1}o(os,"e_defaultPrevented");function to(r){Jt(r),ti(r)}o(to,"e_stop");function yi(r){return r.target||r.srcElement}o(yi,"e_target");function Gs(r){var i=r.which;return i==null&&(r.button&1?i=1:r.button&2?i=3:r.button&4&&(i=2)),R&&r.ctrlKey&&i==1&&(i=3),i}o(Gs,"e_button");var ss=function(){if(p&&x<9)return!1;var r=Te("div");return"draggable"in r||"dragDrop"in r}(),ln;function Ap(r){if(ln==null){var i=Te("span","\u200B");et(r,Te("span",[i,document.createTextNode("x")])),r.firstChild.offsetHeight!=0&&(ln=i.offsetWidth<=1&&i.offsetHeight>2&&!(p&&x<8))}var u=ln?Te("span","\u200B"):Te("span","\xA0",null,"display: inline-block; width: 1px; margin-right: -1px");return u.setAttribute("cm-text",""),u}o(Ap,"zeroWidthElement");var Ys;function Pf(r){if(Ys!=null)return Ys;var i=et(r,document.createTextNode("A\u062EA")),u=Ue(i,0,1).getBoundingClientRect(),a=Ue(i,1,2).getBoundingClientRect();return qe(r),!u||u.left==u.right?!1:Ys=a.right-u.right<3}o(Pf,"hasBadBidiRects");var Xs=` b`.split(/\n/).length!=3?function(r){for(var i=0,u=[],a=r.length;i<=a;){var c=r.indexOf(` -`,i);c==-1&&(c=r.length);var m=r.slice(i,r.charAt(c-1)=="\r"?c-1:c),g=m.indexOf("\r");g!=-1?(u.push(m.slice(0,g)),i+=g+1):(u.push(m),i=c+1)}return u}:function(r){return r.split(/\r\n?|\n/)},Ep=window.getSelection?function(r){try{return r.selectionStart!=r.selectionEnd}catch(i){return!1}}:function(r){var i;try{i=r.ownerDocument.selection.createRange()}catch(u){}return!i||i.parentElement()!=r?!1:i.compareEndPoints("StartToEnd",i)!=0},_f=function(){var r=be("div");return"oncopy"in r?!0:(r.setAttribute("oncopy","return;"),typeof r.oncopy=="function")}(),lu=null;function Tp(r){if(lu!=null)return lu;var i=Je(r,be("span","x")),u=i.getBoundingClientRect(),a=Be(i,0,1).getBoundingClientRect();return lu=Math.abs(u.left-a.left)>1}o(Tp,"hasBadZoomedRects");var Wl={},ls={};function kp(r,i){arguments.length>2&&(i.dependencies=Array.prototype.slice.call(arguments,2)),Wl[r]=i}o(kp,"defineMode");function bf(r,i){ls[r]=i}o(bf,"defineMIME");function Js(r){if(typeof r=="string"&&ls.hasOwnProperty(r))r=ls[r];else if(r&&typeof r.name=="string"&&ls.hasOwnProperty(r.name)){var i=ls[r.name];typeof i=="string"&&(i={name:i}),r=ei(i,r),r.name=i.name}else{if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+xml$/.test(r))return Js("application/xml");if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+json$/.test(r))return Js("application/json")}return typeof r=="string"?{name:r}:r||{name:"null"}}o(Js,"resolveMode");function au(r,i){i=Js(i);var u=Wl[i.name];if(!u)return au(r,"text/plain");var a=u(r,i);if(Ro.hasOwnProperty(i.name)){var c=Ro[i.name];for(var m in c)!c.hasOwnProperty(m)||(a.hasOwnProperty(m)&&(a["_"+m]=a[m]),a[m]=c[m])}if(a.name=i.name,i.helperType&&(a.helperType=i.helperType),i.modeProps)for(var g in i.modeProps)a[g]=i.modeProps[g];return a}o(au,"getMode");var Ro={};function Op(r,i){var u=Ro.hasOwnProperty(r)?Ro[r]:Ro[r]={};qt(i,u)}o(Op,"extendMode");function Fo(r,i){if(i===!0)return i;if(r.copyState)return r.copyState(i);var u={};for(var a in i){var c=i[a];c instanceof Array&&(c=c.concat([])),u[a]=c}return u}o(Fo,"copyState");function Bl(r,i){for(var u;r.innerMode&&(u=r.innerMode(i),!(!u||u.mode==r));)i=u.state,r=u.mode;return u||{mode:r,state:i}}o(Bl,"innerMode");function Ef(r,i,u){return r.startState?r.startState(i,u):!0}o(Ef,"startState");var Dt=o(function(r,i,u){this.pos=this.start=0,this.string=r,this.tabSize=i||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0,this.lineOracle=u},"StringStream");Dt.prototype.eol=function(){return this.pos>=this.string.length},Dt.prototype.sol=function(){return this.pos==this.lineStart},Dt.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},Dt.prototype.next=function(){if(this.posi},Dt.prototype.eatSpace=function(){for(var r=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>r},Dt.prototype.skipToEnd=function(){this.pos=this.string.length},Dt.prototype.skipTo=function(r){var i=this.string.indexOf(r,this.pos);if(i>-1)return this.pos=i,!0},Dt.prototype.backUp=function(r){this.pos-=r},Dt.prototype.column=function(){return this.lastColumnPos0?null:(m&&i!==!1&&(this.pos+=m[0].length),m)}},Dt.prototype.current=function(){return this.string.slice(this.start,this.pos)},Dt.prototype.hideFirstChars=function(r,i){this.lineStart+=r;try{return i()}finally{this.lineStart-=r}},Dt.prototype.lookAhead=function(r){var i=this.lineOracle;return i&&i.lookAhead(r)},Dt.prototype.baseToken=function(){var r=this.lineOracle;return r&&r.baseToken(this.pos)};function Ne(r,i){if(i-=r.first,i<0||i>=r.size)throw new Error("There is no line "+(i+r.first)+" in the document.");for(var u=r;!u.lines;)for(var a=0;;++a){var c=u.children[a],m=c.chunkSize();if(i=r.first&&iu?ae(u,Ne(r,u).text.length):Lp(i,Ne(r,i.line).text.length)}o(Ue,"clipPos");function Lp(r,i){var u=r.ch;return u==null||u>i?ae(r.line,i):u<0?ae(r.line,0):r}o(Lp,"clipToLen");function tl(r,i){for(var u=[],a=0;athis.maxLookAhead&&(this.maxLookAhead=r),i},In.prototype.baseToken=function(r){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=r;)this.baseTokenPos+=2;var i=this.baseTokens[this.baseTokenPos+1];return{type:i&&i.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-r}},In.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},In.fromSaved=function(r,i,u){return i instanceof ri?new In(r,Fo(r.mode,i.state),u,i.lookAhead):new In(r,Fo(r.mode,i),u)},In.prototype.save=function(r){var i=r!==!1?Fo(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new ri(i,this.maxLookAhead):i};function fu(r,i,u,a){var c=[r.state.modeGen],m={};pu(r,i.text,r.doc.mode,u,function(b,N){return c.push(b,N)},m,a);for(var g=u.state,S=o(function(b){u.baseTokens=c;var N=r.state.overlays[b],A=1,$=0;u.state=!0,pu(r,i.text,N.mode,u,function(z,ee){for(var oe=A;$z&&c.splice(A,1,z,c[A+1],ce),A+=2,$=Math.min(z,ce)}if(!!ee)if(N.opaque)c.splice(oe,A-oe,z,"overlay "+ee),A=oe+2;else for(;oer.options.maxHighlightLength&&Fo(r.doc.mode,a.state),m=fu(r,i,a);c&&(a.state=c),i.stateAfter=a.save(!c),i.styles=m.styles,m.classes?i.styleClasses=m.classes:i.styleClasses&&(i.styleClasses=null),u===r.doc.highlightFrontier&&(r.doc.modeFrontier=Math.max(r.doc.modeFrontier,++r.doc.highlightFrontier))}return i.styles}o(cu,"getLineStyles");function us(r,i,u){var a=r.doc,c=r.display;if(!a.mode.startState)return new In(a,!0,i);var m=kf(r,i,u),g=m>a.first&&Ne(a,m-1).stateAfter,S=g?In.fromSaved(a,g,m):new In(a,Ef(a.mode),m);return a.iter(m,i,function(C){rl(r,C.text,S);var b=S.line;C.stateAfter=b==i-1||b%5==0||b>=c.viewFrom&&bi.start)return m}throw new Error("Mode "+r.name+" failed to advance stream.")}o($l,"readToken");var ql=o(function(r,i,u){this.start=r.start,this.end=r.pos,this.string=r.current(),this.type=i||null,this.state=u},"Token");function Vl(r,i,u,a){var c=r.doc,m=c.mode,g;i=Ue(c,i);var S=Ne(c,i.line),C=us(r,i.line,u),b=new Dt(S.text,r.options.tabSize,C),N;for(a&&(N=[]);(a||b.posr.options.maxHighlightLength?(S=!1,g&&rl(r,i,a,N.pos),N.pos=i.length,A=null):A=Wo($l(u,N,a.state,$),m),$){var z=$[0].name;z&&(A="m-"+(A?z+" "+A:z))}if(!S||b!=A){for(;Cg;--S){if(S<=m.first)return m.first;var C=Ne(m,S-1),b=C.stateAfter;if(b&&(!u||S+(b instanceof ri?b.lookAhead:0)<=m.modeFrontier))return S;var N=_t(C.text,null,r.options.tabSize);(c==null||a>N)&&(c=S-1,a=N)}return c}o(kf,"findStartLine");function Np(r,i){if(r.modeFrontier=Math.min(r.modeFrontier,i),!(r.highlightFrontieru;a--){var c=Ne(r,a).stateAfter;if(c&&(!(c instanceof ri)||a+c.lookAhead=i:m.to>i);(a||(a=[])).push(new Bo(g,m.from,C?null:m.to))}}return a}o(Kl,"markedSpansBefore");function Jh(r,i,u){var a;if(r)for(var c=0;c=i:m.to>i);if(S||m.from==i&&g.type=="bookmark"&&(!u||m.marker.insertLeft)){var C=m.from==null||(g.inclusiveLeft?m.from<=i:m.from0&&S)for(var Te=0;Te0)){var N=[C,1],A=ze(b.from,S.from),$=ze(b.to,S.to);(A<0||!g.inclusiveLeft&&!A)&&N.push({from:b.from,to:S.from}),($>0||!g.inclusiveRight&&!$)&&N.push({from:S.to,to:b.to}),c.splice.apply(c,N),C+=N.length-3}}return c}o(Lf,"removeReadOnlyRanges");function mu(r){var i=r.markedSpans;if(!!i){for(var u=0;ui)&&(!a||Gl(a,m.marker)<0)&&(a=m.marker)}return a}o(vu,"collapsedSpanAround");function nl(r,i,u,a,c){var m=Ne(r,i),g=wi&&m.markedSpans;if(g)for(var S=0;S=0&&A<=0||N<=0&&A>=0)&&(N<=0&&(C.marker.inclusiveRight&&c.inclusiveLeft?ze(b.to,u)>=0:ze(b.to,u)>0)||N>=0&&(C.marker.inclusiveRight&&c.inclusiveLeft?ze(b.from,a)<=0:ze(b.from,a)<0)))return!0}}}o(nl,"conflictingCollapsedRange");function En(r){for(var i;i=Tt(r);)r=i.find(-1,!0).line;return r}o(En,"visualLine");function Mp(r){for(var i;i=Hn(r);)r=i.find(1,!0).line;return r}o(Mp,"visualLineEnd");function Ap(r){for(var i,u;i=Hn(r);)r=i.find(1,!0).line,(u||(u=[])).push(r);return u}o(Ap,"visualLineContinued");function Yl(r,i){var u=Ne(r,i),a=En(u);return u==a?i:dt(a)}o(Yl,"visualLineNo");function Fr(r,i){if(i>r.lastLine())return i;var u=Ne(r,i),a;if(!Ot(r,u))return i;for(;a=Hn(u);)u=a.find(1,!0).line;return dt(u)+1}o(Fr,"visualLineEndNo");function Ot(r,i){var u=wi&&i.markedSpans;if(u){for(var a=void 0,c=0;ci.maxLineLength&&(i.maxLineLength=c,i.maxLine=a)})}o(il,"findMaxLine");var Tr=o(function(r,i,u){this.text=r,fs(this,i),this.height=u?u(this):1},"Line");Tr.prototype.lineNo=function(){return dt(this)},Rr(Tr);function Nf(r,i,u,a){r.text=i,r.stateAfter&&(r.stateAfter=null),r.styles&&(r.styles=null),r.order!=null&&(r.order=null),mu(r),fs(r,u);var c=a?a(r):1;c!=r.height&&yi(r,c)}o(Nf,"updateLine");function Pf(r){r.parent=null,mu(r)}o(Pf,"cleanUpLine");var gu={},yu={};function Xl(r,i){if(!r||/^\s*$/.test(r))return null;var u=i.addModeClass?yu:gu;return u[r]||(u[r]=r.replace(/\S+/g,"cm-$&"))}o(Xl,"interpretTokenStyle");function Ql(r,i){var u=yt("span",null,null,_?"padding-right: .1px":null),a={pre:yt("pre",[u],"CodeMirror-line"),content:u,col:0,pos:0,cm:r,trailingSpace:!1,splitSpaces:r.getOption("lineWrapping")};i.measure={};for(var c=0;c<=(i.rest?i.rest.length:0);c++){var m=c?i.rest[c-1]:i.line,g=void 0;a.pos=0,a.addToken=Dp,Cf(r.display.measure)&&(g=bn(m,r.doc.direction))&&(a.addToken=Rp(a.addToken,g)),a.map=[];var S=i!=r.display.externalMeasured&&dt(m);ro(m,a,cu(r,m,S)),m.styleClasses&&(m.styleClasses.bgClass&&(a.bgClass=Yt(m.styleClasses.bgClass,a.bgClass||"")),m.styleClasses.textClass&&(a.textClass=Yt(m.styleClasses.textClass,a.textClass||""))),a.map.length==0&&a.map.push(0,0,a.content.appendChild(bp(r.display.measure))),c==0?(i.measure.map=a.map,i.measure.cache={}):((i.measure.maps||(i.measure.maps=[])).push(a.map),(i.measure.caches||(i.measure.caches=[])).push({}))}if(_){var C=a.content.lastChild;(/\bcm-tab\b/.test(C.className)||C.querySelector&&C.querySelector(".cm-tab"))&&(a.content.className="cm-tab-wrap-hack")}return Ee(r,"renderLine",r,i.line,a.pre),a.pre.className&&(a.textClass=Yt(a.pre.className,a.textClass||"")),a}o(Ql,"buildLineContent");function Tn(r){var i=be("span","\u2022","cm-invalidchar");return i.title="\\u"+r.charCodeAt(0).toString(16),i.setAttribute("aria-label",i.title),i}o(Tn,"defaultSpecialCharPlaceholder");function Dp(r,i,u,a,c,m,g){if(!!i){var S=r.splitSpaces?Wn(i,r.trailingSpace):i,C=r.cm.state.specialChars,b=!1,N;if(!C.test(i))r.col+=i.length,N=document.createTextNode(S),r.map.push(r.pos,r.pos+i.length,N),p&&w<9&&(b=!0),r.pos+=i.length;else{N=document.createDocumentFragment();for(var A=0;;){C.lastIndex=A;var $=C.exec(i),z=$?$.index-A:i.length-A;if(z){var ee=document.createTextNode(S.slice(A,A+z));p&&w<9?N.appendChild(be("span",[ee])):N.appendChild(ee),r.map.push(r.pos,r.pos+z,ee),r.col+=z,r.pos+=z}if(!$)break;A+=z+1;var oe=void 0;if($[0]==" "){var ce=r.cm.options.tabSize,me=ce-r.col%ce;oe=N.appendChild(be("span",jt(me),"cm-tab")),oe.setAttribute("role","presentation"),oe.setAttribute("cm-text"," "),r.col+=me}else $[0]=="\r"||$[0]==` -`?(oe=N.appendChild(be("span",$[0]=="\r"?"\u240D":"\u2424","cm-invalidchar")),oe.setAttribute("cm-text",$[0]),r.col+=1):(oe=r.cm.options.specialCharPlaceholder($[0]),oe.setAttribute("cm-text",$[0]),p&&w<9?N.appendChild(be("span",[oe])):N.appendChild(oe),r.col+=1);r.map.push(r.pos,r.pos+1,oe),r.pos++}}if(r.trailingSpace=S.charCodeAt(i.length-1)==32,u||a||c||b||m||g){var Ce=u||"";a&&(Ce+=a),c&&(Ce+=c);var ve=be("span",[N],Ce,m);if(g)for(var Te in g)g.hasOwnProperty(Te)&&Te!="style"&&Te!="class"&&ve.setAttribute(Te,g[Te]);return r.content.appendChild(ve)}r.content.appendChild(N)}}o(Dp,"buildToken");function Wn(r,i){if(r.length>1&&!/ /.test(r))return r;for(var u=i,a="",c=0;cb&&A.from<=b));$++);if(A.to>=N)return r(u,a,c,m,g,S,C);r(u,a.slice(0,A.to-b),c,m,null,S,C),m=null,a=a.slice(A.to-b),b=A.to}}}o(Rp,"buildTokenBadBidi");function wu(r,i,u,a){var c=!a&&u.widgetNode;c&&r.map.push(r.pos,r.pos+i,c),!a&&r.cm.display.input.needsContentAttribute&&(c||(c=r.content.appendChild(document.createElement("span"))),c.setAttribute("cm-marker",u.id)),c&&(r.cm.display.input.setUneditable(c),r.content.appendChild(c)),r.pos+=i,r.trailingSpace=!1}o(wu,"buildCollapsedSpan");function ro(r,i,u){var a=r.markedSpans,c=r.text,m=0;if(!a){for(var g=1;gC||tt.collapsed&&De.to==C&&De.from==C)){if(De.to!=null&&De.to!=C&&z>De.to&&(z=De.to,oe=""),tt.className&&(ee+=" "+tt.className),tt.css&&($=($?$+";":"")+tt.css),tt.startStyle&&De.from==C&&(ce+=" "+tt.startStyle),tt.endStyle&&De.to==z&&(Te||(Te=[])).push(tt.endStyle,De.to),tt.title&&((Ce||(Ce={})).title=tt.title),tt.attributes)for(var bt in tt.attributes)(Ce||(Ce={}))[bt]=tt.attributes[bt];tt.collapsed&&(!me||Gl(me.marker,tt)<0)&&(me=De)}else De.from>C&&z>De.from&&(z=De.from)}if(Te)for(var yr=0;yr=S)break;for(var dn=Math.min(S,z);;){if(N){var hn=C+N.length;if(!me){var sr=hn>dn?N.slice(0,dn-C):N;i.addToken(i,sr,A?A+ee:ee,ce,C+sr.length==z?oe:"",$,Ce)}if(hn>=dn){N=N.slice(dn-C),C=dn;break}C=hn,ce=""}N=c.slice(m,m=u[b++]),A=Xl(u[b++],i.cm.options)}}}o(ro,"insertLineContent");function hs(r,i,u){this.line=i,this.rest=Ap(i),this.size=this.rest?dt(xe(this.rest))-u+1:1,this.node=this.text=null,this.hidden=Ot(r,i)}o(hs,"LineView");function ms(r,i,u){for(var a=[],c,m=i;m2&&m.push((C.bottom+b.top)/2-u.top)}}m.push(u.bottom-u.top)}}o(Hp,"ensureLineHeights");function sl(r,i,u){if(r.line==i)return{map:r.measure.map,cache:r.measure.cache};for(var a=0;au)return{map:r.measure.maps[c],cache:r.measure.caches[c],before:!0}}o(sl,"mapFromLineView");function Hi(r,i){i=En(i);var u=dt(i),a=r.display.externalMeasured=new hs(r.doc,i,u);a.lineN=u;var c=a.built=Ql(r,a);return a.text=c.pre,Je(r.display.lineMeasure,c.pre),a}o(Hi,"updateExternalMeasurement");function em(r,i,u,a){return so(r,Wi(r,i),u,a)}o(em,"measureChar");function Wp(r,i){if(i>=r.display.viewFrom&&i=u.lineN&&ii)&&(m=C-S,c=m-1,i>=C&&(g="right")),c!=null){if(a=r[b+2],S==C&&u==(a.insertLeft?"left":"right")&&(g=u),u=="left"&&c==0)for(;b&&r[b-2]==r[b-3]&&r[b-1].insertLeft;)a=r[(b-=3)+2],g="left";if(u=="right"&&c==C-S)for(;b=0&&(u=r[c]).left==u.right;c--);return u}o(rm,"getUsefulRect");function gs(r,i,u,a){var c=Df(i.map,u,a),m=c.node,g=c.start,S=c.end,C=c.collapse,b;if(m.nodeType==3){for(var N=0;N<4;N++){for(;g&&vr(i.line.text.charAt(c.coverStart+g));)--g;for(;c.coverStart+S0&&(C=a="right");var A;r.options.lineWrapping&&(A=m.getClientRects()).length>1?b=A[a=="right"?A.length-1:0]:b=m.getBoundingClientRect()}if(p&&w<9&&!g&&(!b||!b.left&&!b.right)){var $=m.parentNode.getClientRects()[0];$?b={left:$.left,right:$.left+ta(r.display),top:$.top,bottom:$.bottom}:b=tm}for(var z=b.top-i.rect.top,ee=b.bottom-i.rect.top,oe=(z+ee)/2,ce=i.view.measure.heights,me=0;me=a.text.length?(C=a.text.length,b="before"):C<=0&&(C=0,b="after"),!S)return g(b=="before"?C-1:C,b=="before");function N(ee,oe,ce){var me=S[oe],Ce=me.level==1;return g(ce?ee-1:ee,Ce!=ce)}o(N,"getBidi");var A=mi(S,C,b),$=hi,z=N(C,A,b=="before");return $!=null&&(z.other=N(C,$,b!="before")),z}o(Bi,"cursorCoords");function sm(r,i){var u=0;i=Ue(r.doc,i),r.options.lineWrapping||(u=ta(r.display)*i.ch);var a=Ne(r.doc,i.line),c=Ir(a)+Un(r.display);return{left:u,right:u,top:c,bottom:c+a.height}}o(sm,"estimateCoords");function lo(r,i,u,a,c){var m=ae(r,i,u);return m.xRel=c,a&&(m.outside=a),m}o(lo,"PosWithInfo");function q(r,i,u){var a=r.doc;if(u+=r.display.viewOffset,u<0)return lo(a.first,0,null,-1,-1);var c=Fi(a,u),m=a.first+a.size-1;if(c>m)return lo(a.first+a.size-1,Ne(a,m).text.length,null,1,1);i<0&&(i=0);for(var g=Ne(a,c);;){var S=Ye(r,g,c,i,u),C=vu(g,S.ch+(S.xRel>0||S.outside>0?1:0));if(!C)return S;var b=C.find(1);if(b.line==c)return b;g=Ne(a,c=b.line)}}o(q,"coordsChar");function re(r,i,u,a){a-=Ff(i);var c=i.text.length,m=ln(function(g){return so(r,u,g-1).bottom<=a},c,0);return c=ln(function(g){return so(r,u,g).top>a},m,c),{begin:m,end:c}}o(re,"wrappedLineExtent");function Z(r,i,u,a){u||(u=Wi(r,i));var c=If(r,i,so(r,u,a),"line").top;return re(r,i,u,c)}o(Z,"wrappedLineExtentChar");function Oe(r,i,u,a){return r.bottom<=u?!1:r.top>u?!0:(a?r.left:r.right)>i}o(Oe,"boxIsAfter");function Ye(r,i,u,a,c){c-=Ir(i);var m=Wi(r,i),g=Ff(i),S=0,C=i.text.length,b=!0,N=bn(i,r.doc.direction);if(N){var A=(r.options.lineWrapping?kr:St)(r,i,u,m,N,a,c);b=A.level!=1,S=b?A.from:A.to-1,C=b?A.to:A.from-1}var $=null,z=null,ee=ln(function(Fe){var De=so(r,m,Fe);return De.top+=g,De.bottom+=g,Oe(De,a,c,!1)?(De.top<=c&&De.left<=a&&($=Fe,z=De),!0):!1},S,C),oe,ce,me=!1;if(z){var Ce=a-z.left=Te.bottom?1:0}return ee=Ri(i.text,ee,1),lo(u,ee,ce,me,a-oe)}o(Ye,"coordsCharInner");function St(r,i,u,a,c,m,g){var S=ln(function(A){var $=c[A],z=$.level!=1;return Oe(Bi(r,ae(u,z?$.to:$.from,z?"before":"after"),"line",i,a),m,g,!0)},0,c.length-1),C=c[S];if(S>0){var b=C.level!=1,N=Bi(r,ae(u,b?C.from:C.to,b?"after":"before"),"line",i,a);Oe(N,m,g,!0)&&N.top>g&&(C=c[S-1])}return C}o(St,"coordsBidiPart");function kr(r,i,u,a,c,m,g){var S=re(r,i,a,g),C=S.begin,b=S.end;/\s/.test(i.text.charAt(b-1))&&b--;for(var N=null,A=null,$=0;$=b||z.to<=C)){var ee=z.level!=1,oe=so(r,a,ee?Math.min(b,z.to)-1:Math.max(C,z.from)).right,ce=oece)&&(N=z,A=ce)}}return N||(N=c[c.length-1]),N.fromb&&(N={from:N.from,to:b,level:N.level}),N}o(kr,"coordsBidiPartWrapped");var ht;function ys(r){if(r.cachedTextHeight!=null)return r.cachedTextHeight;if(ht==null){ht=be("pre",null,"CodeMirror-line-like");for(var i=0;i<49;++i)ht.appendChild(document.createTextNode("x")),ht.appendChild(be("br"));ht.appendChild(document.createTextNode("x"))}Je(r.measure,ht);var u=ht.offsetHeight/50;return u>3&&(r.cachedTextHeight=u),qe(r.measure),u||1}o(ys,"textHeight");function ta(r){if(r.cachedCharWidth!=null)return r.cachedCharWidth;var i=be("span","xxxxxxxxxx"),u=be("pre",[i],"CodeMirror-line-like");Je(r.measure,u);var a=i.getBoundingClientRect(),c=(a.right-a.left)/10;return c>2&&(r.cachedCharWidth=c),c||10}o(ta,"charWidth");function zn(r){for(var i=r.display,u={},a={},c=i.gutters.clientLeft,m=i.gutters.firstChild,g=0;m;m=m.nextSibling,++g){var S=r.display.gutterSpecs[g].className;u[S]=m.offsetLeft+m.clientLeft+c,a[S]=m.clientWidth}return{fixedPos:ra(i),gutterTotalWidth:i.gutters.offsetWidth,gutterLeft:u,gutterWidth:a,wrapperWidth:i.wrapper.clientWidth}}o(zn,"getDimensions");function ra(r){return r.scroller.getBoundingClientRect().left-r.sizer.getBoundingClientRect().left}o(ra,"compensateForHScroll");function lm(r){var i=ys(r.display),u=r.options.lineWrapping,a=u&&Math.max(5,r.display.scroller.clientWidth/ta(r.display)-3);return function(c){if(Ot(r.doc,c))return 0;var m=0;if(c.widgets)for(var g=0;g0&&(b=Ne(r.doc,C.line).text).length==C.ch){var N=_t(b,b.length,r.options.tabSize)-b.length;C=ae(C.line,Math.max(0,Math.round((m-xi(r.display).left)/ta(r.display))-N))}return C}o(ao,"posFromMouse");function uo(r,i){if(i>=r.display.viewTo||(i-=r.display.viewFrom,i<0))return null;for(var u=r.display.view,a=0;ai)&&(c.updateLineNumbers=i),r.curOp.viewChanged=!0,i>=c.viewTo)wi&&Yl(r.doc,i)c.viewFrom?zo(r):(c.viewFrom+=a,c.viewTo+=a);else if(i<=c.viewFrom&&u>=c.viewTo)zo(r);else if(i<=c.viewFrom){var m=al(r,u,u+a,1);m?(c.view=c.view.slice(m.index),c.viewFrom=m.lineN,c.viewTo+=a):zo(r)}else if(u>=c.viewTo){var g=al(r,i,i,-1);g?(c.view=c.view.slice(0,g.index),c.viewTo=g.lineN):zo(r)}else{var S=al(r,i,i,-1),C=al(r,u,u+a,1);S&&C?(c.view=c.view.slice(0,S.index).concat(ms(r,S.lineN,C.lineN)).concat(c.view.slice(C.index)),c.viewTo+=a):zo(r)}var b=c.externalMeasured;b&&(u=c.lineN&&i=a.viewTo)){var m=a.view[uo(r,i)];if(m.node!=null){var g=m.changes||(m.changes=[]);st(g,u)==-1&&g.push(u)}}}o(xs,"regLineChange");function zo(r){r.display.viewFrom=r.display.viewTo=r.doc.first,r.display.view=[],r.display.viewOffset=0}o(zo,"resetView");function al(r,i,u,a){var c=uo(r,i),m,g=r.display.view;if(!wi||u==r.doc.first+r.doc.size)return{index:c,lineN:u};for(var S=r.display.viewFrom,C=0;C0){if(c==g.length-1)return null;m=S+g[c].size-i,c++}else m=S-i;i+=m,u+=m}for(;Yl(r.doc,u)!=u;){if(c==(a<0?0:g.length-1))return null;u+=a*g[c-(a<0?1:0)].size,c+=a}return{index:c,lineN:u}}o(al,"viewCuttingPoint");function Gy(r,i,u){var a=r.display,c=a.view;c.length==0||i>=a.viewTo||u<=a.viewFrom?(a.view=ms(r,i,u),a.viewFrom=i):(a.viewFrom>i?a.view=ms(r,i,a.viewFrom).concat(a.view):a.viewFromu&&(a.view=a.view.slice(0,uo(r,u)))),a.viewTo=u}o(Gy,"adjustView");function am(r){for(var i=r.display.view,u=0,a=0;a=r.display.viewTo||S.to().line0?i.blinker=setInterval(function(){r.hasFocus()||fl(r),i.cursorDiv.style.visibility=(u=!u)?"":"hidden"},r.options.cursorBlinkRate):r.options.cursorBlinkRate<0&&(i.cursorDiv.style.visibility="hidden")}}o(na,"restartBlink");function Bp(r){r.hasFocus()||(r.display.input.focus(),r.state.focused||ia(r))}o(Bp,"ensureFocus");function Wf(r){r.state.delayingBlurEvent=!0,setTimeout(function(){r.state.delayingBlurEvent&&(r.state.delayingBlurEvent=!1,r.state.focused&&fl(r))},100)}o(Wf,"delayBlurEvent");function ia(r,i){r.state.delayingBlurEvent&&!r.state.draggingText&&(r.state.delayingBlurEvent=!1),r.options.readOnly!="nocursor"&&(r.state.focused||(Ee(r,"focus",r,i),r.state.focused=!0,Ge(r.display.wrapper,"CodeMirror-focused"),!r.curOp&&r.display.selForContextMenu!=r.doc.sel&&(r.display.input.reset(),_&&setTimeout(function(){return r.display.input.reset(!0)},20)),r.display.input.receivedFocus()),na(r))}o(ia,"onFocus");function fl(r,i){r.state.delayingBlurEvent||(r.state.focused&&(Ee(r,"blur",r,i),r.state.focused=!1,we(r.display.wrapper,"CodeMirror-focused")),clearInterval(r.display.blinker),setTimeout(function(){r.state.focused||(r.display.shift=!1)},150))}o(fl,"onBlur");function Ss(r){for(var i=r.display,u=i.lineDiv.offsetTop,a=0;a.005||N<-.005)&&(yi(c.line,g),Cs(c.line),c.rest))for(var A=0;Ar.display.sizerWidth){var $=Math.ceil(S/ta(r.display));$>r.display.maxLineLength&&(r.display.maxLineLength=$,r.display.maxLine=c.line,r.display.maxLineChanged=!0)}}}}o(Ss,"updateHeightsInViewport");function Cs(r){if(r.widgets)for(var i=0;i=g&&(m=Fi(i,Ir(Ne(i,C))-r.wrapper.clientHeight),g=C)}return{from:m,to:Math.max(g,m+1)}}o(cl,"visibleLines");function Yy(r,i){if(!Xt(r,"scrollCursorIntoView")){var u=r.display,a=u.sizer.getBoundingClientRect(),c=null;if(i.top+a.top<0?c=!0:i.bottom+a.top>(window.innerHeight||document.documentElement.clientHeight)&&(c=!1),c!=null&&!te){var m=be("div","\u200B",null,`position: absolute; - top: `+(i.top-u.viewOffset-Un(r.display))+`px; +`,i);c==-1&&(c=r.length);var v=r.slice(i,r.charAt(c-1)=="\r"?c-1:c),g=v.indexOf("\r");g!=-1?(u.push(v.slice(0,g)),i+=g+1):(u.push(v),i=c+1)}return u}:function(r){return r.split(/\r\n?|\n/)},Dp=window.getSelection?function(r){try{return r.selectionStart!=r.selectionEnd}catch(i){return!1}}:function(r){var i;try{i=r.ownerDocument.selection.createRange()}catch(u){}return!i||i.parentElement()!=r?!1:i.compareEndPoints("StartToEnd",i)!=0},Mf=function(){var r=Te("div");return"oncopy"in r?!0:(r.setAttribute("oncopy","return;"),typeof r.oncopy=="function")}(),uu=null;function Rp(r){if(uu!=null)return uu;var i=et(r,Te("span","x")),u=i.getBoundingClientRect(),a=Ue(i,0,1).getBoundingClientRect();return uu=Math.abs(u.left-a.left)>1}o(Rp,"hasBadZoomedRects");var Ul={},ls={};function Fp(r,i){arguments.length>2&&(i.dependencies=Array.prototype.slice.call(arguments,2)),Ul[r]=i}o(Fp,"defineMode");function Af(r,i){ls[r]=i}o(Af,"defineMIME");function Qs(r){if(typeof r=="string"&&ls.hasOwnProperty(r))r=ls[r];else if(r&&typeof r.name=="string"&&ls.hasOwnProperty(r.name)){var i=ls[r.name];typeof i=="string"&&(i={name:i}),r=ei(i,r),r.name=i.name}else{if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+xml$/.test(r))return Qs("application/xml");if(typeof r=="string"&&/^[\w\-]+\/[\w\-]+\+json$/.test(r))return Qs("application/json")}return typeof r=="string"?{name:r}:r||{name:"null"}}o(Qs,"resolveMode");function fu(r,i){i=Qs(i);var u=Ul[i.name];if(!u)return fu(r,"text/plain");var a=u(r,i);if(Ro.hasOwnProperty(i.name)){var c=Ro[i.name];for(var v in c)!c.hasOwnProperty(v)||(a.hasOwnProperty(v)&&(a["_"+v]=a[v]),a[v]=c[v])}if(a.name=i.name,i.helperType&&(a.helperType=i.helperType),i.modeProps)for(var g in i.modeProps)a[g]=i.modeProps[g];return a}o(fu,"getMode");var Ro={};function Ip(r,i){var u=Ro.hasOwnProperty(r)?Ro[r]:Ro[r]={};Kt(i,u)}o(Ip,"extendMode");function Fo(r,i){if(i===!0)return i;if(r.copyState)return r.copyState(i);var u={};for(var a in i){var c=i[a];c instanceof Array&&(c=c.concat([])),u[a]=c}return u}o(Fo,"copyState");function $l(r,i){for(var u;r.innerMode&&(u=r.innerMode(i),!(!u||u.mode==r));)i=u.state,r=u.mode;return u||{mode:r,state:i}}o($l,"innerMode");function Df(r,i,u){return r.startState?r.startState(i,u):!0}o(Df,"startState");var zt=o(function(r,i,u){this.pos=this.start=0,this.string=r,this.tabSize=i||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0,this.lineOracle=u},"StringStream");zt.prototype.eol=function(){return this.pos>=this.string.length},zt.prototype.sol=function(){return this.pos==this.lineStart},zt.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},zt.prototype.next=function(){if(this.posi},zt.prototype.eatSpace=function(){for(var r=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>r},zt.prototype.skipToEnd=function(){this.pos=this.string.length},zt.prototype.skipTo=function(r){var i=this.string.indexOf(r,this.pos);if(i>-1)return this.pos=i,!0},zt.prototype.backUp=function(r){this.pos-=r},zt.prototype.column=function(){return this.lastColumnPos0?null:(v&&i!==!1&&(this.pos+=v[0].length),v)}},zt.prototype.current=function(){return this.string.slice(this.start,this.pos)},zt.prototype.hideFirstChars=function(r,i){this.lineStart+=r;try{return i()}finally{this.lineStart-=r}},zt.prototype.lookAhead=function(r){var i=this.lineOracle;return i&&i.lookAhead(r)},zt.prototype.baseToken=function(){var r=this.lineOracle;return r&&r.baseToken(this.pos)};function Me(r,i){if(i-=r.first,i<0||i>=r.size)throw new Error("There is no line "+(i+r.first)+" in the document.");for(var u=r;!u.lines;)for(var a=0;;++a){var c=u.children[a],v=c.chunkSize();if(i=r.first&&iu?ae(u,Me(r,u).text.length):Hp(i,Me(r,i.line).text.length)}o(Be,"clipPos");function Hp(r,i){var u=r.ch;return u==null||u>i?ae(r.line,i):u<0?ae(r.line,0):r}o(Hp,"clipToLen");function hu(r,i){for(var u=[],a=0;athis.maxLookAhead&&(this.maxLookAhead=r),i},Wn.prototype.baseToken=function(r){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=r;)this.baseTokenPos+=2;var i=this.baseTokens[this.baseTokenPos+1];return{type:i&&i.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-r}},Wn.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},Wn.fromSaved=function(r,i,u){return i instanceof Ho?new Wn(r,Fo(r.mode,i.state),u,i.lookAhead):new Wn(r,Fo(r.mode,i),u)},Wn.prototype.save=function(r){var i=r!==!1?Fo(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new Ho(i,this.maxLookAhead):i};function mu(r,i,u,a){var c=[r.state.modeGen],v={};Vl(r,i.text,r.doc.mode,u,function(b,P){return c.push(b,P)},v,a);for(var g=u.state,S=o(function(b){u.baseTokens=c;var P=r.state.overlays[b],A=1,j=0;u.state=!0,Vl(r,i.text,P.mode,u,function(z,ee){for(var oe=A;jz&&c.splice(A,1,z,c[A+1],ce),A+=2,j=Math.min(z,ce)}if(!!ee)if(P.opaque)c.splice(oe,A-oe,z,"overlay "+ee),A=oe+2;else for(;oer.options.maxHighlightLength&&Fo(r.doc.mode,a.state),v=mu(r,i,a);c&&(a.state=c),i.stateAfter=a.save(!c),i.styles=v.styles,v.classes?i.styleClasses=v.classes:i.styleClasses&&(i.styleClasses=null),u===r.doc.highlightFrontier&&(r.doc.modeFrontier=Math.max(r.doc.modeFrontier,++r.doc.highlightFrontier))}return i.styles}o(ql,"getLineStyles");function Wo(r,i,u){var a=r.doc,c=r.display;if(!a.mode.startState)return new Wn(a,!0,i);var v=Ff(r,i,u),g=v>a.first&&Me(a,v-1).stateAfter,S=g?Wn.fromSaved(a,g,v):new Wn(a,Df(a.mode),v);return a.iter(v,i,function(C){Js(r,C.text,S);var b=S.line;C.stateAfter=b==i-1||b%5==0||b>=c.viewFrom&&bi.start)return v}throw new Error("Mode "+r.name+" failed to advance stream.")}o(el,"readToken");var tl=o(function(r,i,u){this.start=r.start,this.end=r.pos,this.string=r.current(),this.type=i||null,this.state=u},"Token");function us(r,i,u,a){var c=r.doc,v=c.mode,g;i=Be(c,i);var S=Me(c,i.line),C=Wo(r,i.line,u),b=new zt(S.text,r.options.tabSize,C),P;for(a&&(P=[]);(a||b.posr.options.maxHighlightLength?(S=!1,g&&Js(r,i,a,P.pos),P.pos=i.length,A=null):A=no(el(u,P,a.state,j),v),j){var z=j[0].name;z&&(A="m-"+(A?z+" "+A:z))}if(!S||b!=A){for(;Cg;--S){if(S<=v.first)return v.first;var C=Me(v,S-1),b=C.stateAfter;if(b&&(!u||S+(b instanceof Ho?b.lookAhead:0)<=v.modeFrontier))return S;var P=Et(C.text,null,r.options.tabSize);(c==null||a>P)&&(c=S-1,a=P)}return c}o(Ff,"findStartLine");function Wp(r,i){if(r.modeFrontier=Math.min(r.modeFrontier,i),!(r.highlightFrontieru;a--){var c=Me(r,a).stateAfter;if(c&&(!(c instanceof Ho)||a+c.lookAhead=i:v.to>i);(a||(a=[])).push(new Kl(g,v.from,C?null:v.to))}}return a}o(Up,"markedSpansBefore");function $p(r,i,u){var a;if(r)for(var c=0;c=i:v.to>i);if(S||v.from==i&&g.type=="bookmark"&&(!u||v.marker.insertLeft)){var C=v.from==null||(g.inclusiveLeft?v.from<=i:v.from0&&S)for(var Oe=0;Oe0)){var P=[C,1],A=$e(b.from,S.from),j=$e(b.to,S.to);(A<0||!g.inclusiveLeft&&!A)&&P.push({from:b.from,to:S.from}),(j>0||!g.inclusiveRight&&!j)&&P.push({from:S.to,to:b.to}),c.splice.apply(c,P),C+=P.length-3}}return c}o(wu,"removeReadOnlyRanges");function Wf(r){var i=r.markedSpans;if(!!i){for(var u=0;ui)&&(!a||N(a,v.marker)<0)&&(a=v.marker)}return a}o(xu,"collapsedSpanAround");function ye(r,i,u,a,c){var v=Me(r,i),g=an&&v.markedSpans;if(g)for(var S=0;S=0&&A<=0||P<=0&&A>=0)&&(P<=0&&(C.marker.inclusiveRight&&c.inclusiveLeft?$e(b.to,u)>=0:$e(b.to,u)>0)||P>=0&&(C.marker.inclusiveRight&&c.inclusiveLeft?$e(b.from,a)<=0:$e(b.from,a)<0)))return!0}}}o(ye,"conflictingCollapsedRange");function kn(r){for(var i;i=gt(r);)r=i.find(-1,!0).line;return r}o(kn,"visualLine");function am(r){for(var i;i=Tn(r);)r=i.find(1,!0).line;return r}o(am,"visualLineEnd");function um(r){for(var i,u;i=Tn(r);)r=i.find(1,!0).line,(u||(u=[])).push(r);return u}o(um,"visualLineContinued");function Su(r,i){var u=Me(r,i),a=kn(u);return u==a?i:vt(a)}o(Su,"visualLineNo");function zp(r,i){if(i>r.lastLine())return i;var u=Me(r,i),a;if(!Nt(r,u))return i;for(;a=Tn(u);)u=a.find(1,!0).line;return vt(u)+1}o(zp,"visualLineEndNo");function Nt(r,i){var u=an&&i.markedSpans;if(u){for(var a=void 0,c=0;ci.maxLineLength&&(i.maxLineLength=c,i.maxLine=a)})}o(fs,"findMaxLine");var He=o(function(r,i,u){this.text=r,Bf(this,i),this.height=u?u(this):1},"Line");He.prototype.lineNo=function(){return vt(this)},Dr(He);function Uf(r,i,u,a){r.text=i,r.stateAfter&&(r.stateAfter=null),r.styles&&(r.styles=null),r.order!=null&&(r.order=null),Wf(r),Bf(r,u);var c=a?a(r):1;c!=r.height&&ri(r,c)}o(Uf,"updateLine");function xi(r){r.parent=null,Wf(r)}o(xi,"cleanUpLine");var Xl={},il={};function cs(r,i){if(!r||/^\s*$/.test(r))return null;var u=i.addModeClass?il:Xl;return u[r]||(u[r]=r.replace(/\S+/g,"cm-$&"))}o(cs,"interpretTokenStyle");function Cu(r,i){var u=xt("span",null,null,_?"padding-right: .1px":null),a={pre:xt("pre",[u],"CodeMirror-line"),content:u,col:0,pos:0,cm:r,trailingSpace:!1,splitSpaces:r.getOption("lineWrapping")};i.measure={};for(var c=0;c<=(i.rest?i.rest.length:0);c++){var v=c?i.rest[c-1]:i.line,g=void 0;a.pos=0,a.addToken=jp,Pf(r.display.measure)&&(g=En(v,r.doc.direction))&&(a.addToken=$f(a.addToken,g)),a.map=[];var S=i!=r.display.externalMeasured&&vt(v);Hi(v,a,ql(r,v,S)),v.styleClasses&&(v.styleClasses.bgClass&&(a.bgClass=Qt(v.styleClasses.bgClass,a.bgClass||"")),v.styleClasses.textClass&&(a.textClass=Qt(v.styleClasses.textClass,a.textClass||""))),a.map.length==0&&a.map.push(0,0,a.content.appendChild(Ap(r.display.measure))),c==0?(i.measure.map=a.map,i.measure.cache={}):((i.measure.maps||(i.measure.maps=[])).push(a.map),(i.measure.caches||(i.measure.caches=[])).push({}))}if(_){var C=a.content.lastChild;(/\bcm-tab\b/.test(C.className)||C.querySelector&&C.querySelector(".cm-tab"))&&(a.content.className="cm-tab-wrap-hack")}return ke(r,"renderLine",r,i.line,a.pre),a.pre.className&&(a.textClass=Qt(a.pre.className,a.textClass||"")),a}o(Cu,"buildLineContent");function On(r){var i=Te("span","\u2022","cm-invalidchar");return i.title="\\u"+r.charCodeAt(0).toString(16),i.setAttribute("aria-label",i.title),i}o(On,"defaultSpecialCharPlaceholder");function jp(r,i,u,a,c,v,g){if(!!i){var S=r.splitSpaces?Tt(i,r.trailingSpace):i,C=r.cm.state.specialChars,b=!1,P;if(!C.test(i))r.col+=i.length,P=document.createTextNode(S),r.map.push(r.pos,r.pos+i.length,P),p&&x<9&&(b=!0),r.pos+=i.length;else{P=document.createDocumentFragment();for(var A=0;;){C.lastIndex=A;var j=C.exec(i),z=j?j.index-A:i.length-A;if(z){var ee=document.createTextNode(S.slice(A,A+z));p&&x<9?P.appendChild(Te("span",[ee])):P.appendChild(ee),r.map.push(r.pos,r.pos+z,ee),r.col+=z,r.pos+=z}if(!j)break;A+=z+1;var oe=void 0;if(j[0]==" "){var ce=r.cm.options.tabSize,me=ce-r.col%ce;oe=P.appendChild(Te("span",qt(me),"cm-tab")),oe.setAttribute("role","presentation"),oe.setAttribute("cm-text"," "),r.col+=me}else j[0]=="\r"||j[0]==` +`?(oe=P.appendChild(Te("span",j[0]=="\r"?"\u240D":"\u2424","cm-invalidchar")),oe.setAttribute("cm-text",j[0]),r.col+=1):(oe=r.cm.options.specialCharPlaceholder(j[0]),oe.setAttribute("cm-text",j[0]),p&&x<9?P.appendChild(Te("span",[oe])):P.appendChild(oe),r.col+=1);r.map.push(r.pos,r.pos+1,oe),r.pos++}}if(r.trailingSpace=S.charCodeAt(i.length-1)==32,u||a||c||b||v||g){var be=u||"";a&&(be+=a),c&&(be+=c);var ve=Te("span",[P],be,v);if(g)for(var Oe in g)g.hasOwnProperty(Oe)&&Oe!="style"&&Oe!="class"&&ve.setAttribute(Oe,g[Oe]);return r.content.appendChild(ve)}r.content.appendChild(P)}}o(jp,"buildToken");function Tt(r,i){if(r.length>1&&!/ /.test(r))return r;for(var u=i,a="",c=0;cb&&A.from<=b));j++);if(A.to>=P)return r(u,a,c,v,g,S,C);r(u,a.slice(0,A.to-b),c,v,null,S,C),v=null,a=a.slice(A.to-b),b=A.to}}}o($f,"buildTokenBadBidi");function Ql(r,i,u,a){var c=!a&&u.widgetNode;c&&r.map.push(r.pos,r.pos+i,c),!a&&r.cm.display.input.needsContentAttribute&&(c||(c=r.content.appendChild(document.createElement("span"))),c.setAttribute("cm-marker",u.id)),c&&(r.cm.display.input.setUneditable(c),r.content.appendChild(c)),r.pos+=i,r.trailingSpace=!1}o(Ql,"buildCollapsedSpan");function Hi(r,i,u){var a=r.markedSpans,c=r.text,v=0;if(!a){for(var g=1;gC||rt.collapsed&&Re.to==C&&Re.from==C)){if(Re.to!=null&&Re.to!=C&&z>Re.to&&(z=Re.to,oe=""),rt.className&&(ee+=" "+rt.className),rt.css&&(j=(j?j+";":"")+rt.css),rt.startStyle&&Re.from==C&&(ce+=" "+rt.startStyle),rt.endStyle&&Re.to==z&&(Oe||(Oe=[])).push(rt.endStyle,Re.to),rt.title&&((be||(be={})).title=rt.title),rt.attributes)for(var kt in rt.attributes)(be||(be={}))[kt]=rt.attributes[kt];rt.collapsed&&(!me||N(me.marker,rt)<0)&&(me=Re)}else Re.from>C&&z>Re.from&&(z=Re.from)}if(Oe)for(var yr=0;yr=S)break;for(var hn=Math.min(S,z);;){if(P){var mn=C+P.length;if(!me){var ar=mn>hn?P.slice(0,hn-C):P;i.addToken(i,ar,A?A+ee:ee,ce,C+ar.length==z?oe:"",j,be)}if(mn>=hn){P=P.slice(hn-C),C=hn;break}C=mn,ce=""}P=c.slice(v,v=u[b++]),A=cs(u[b++],i.cm.options)}}}o(Hi,"insertLineContent");function ps(r,i,u){this.line=i,this.rest=um(i),this.size=this.rest?vt(Se(this.rest))-u+1:1,this.node=this.text=null,this.hidden=Nt(r,i)}o(ps,"LineView");function ds(r,i,u){for(var a=[],c,v=i;v2&&v.push((C.bottom+b.top)/2-u.top)}}v.push(u.bottom-u.top)}}o(Vf,"ensureLineHeights");function ku(r,i,u){if(r.line==i)return{map:r.measure.map,cache:r.measure.cache};for(var a=0;au)return{map:r.measure.maps[c],cache:r.measure.caches[c],before:!0}}o(ku,"mapFromLineView");function Kp(r,i){i=kn(i);var u=vt(i),a=r.display.externalMeasured=new ps(r.doc,i,u);a.lineN=u;var c=a.built=Cu(r,a);return a.text=c.pre,et(r.display.lineMeasure,c.pre),a}o(Kp,"updateExternalMeasurement");function Kf(r,i,u,a){return ii(r,ni(r,i),u,a)}o(Kf,"measureChar");function Ou(r,i){if(i>=r.display.viewFrom&&i=u.lineN&&ii)&&(v=C-S,c=v-1,i>=C&&(g="right")),c!=null){if(a=r[b+2],S==C&&u==(a.insertLeft?"left":"right")&&(g=u),u=="left"&&c==0)for(;b&&r[b-2]==r[b-3]&&r[b-1].insertLeft;)a=r[(b-=3)+2],g="left";if(u=="right"&&c==C-S)for(;b=0&&(u=r[c]).left==u.right;c--);return u}o(B,"getUsefulRect");function W(r,i,u,a){var c=T(i.map,u,a),v=c.node,g=c.start,S=c.end,C=c.collapse,b;if(v.nodeType==3){for(var P=0;P<4;P++){for(;g&&vr(i.line.text.charAt(c.coverStart+g));)--g;for(;c.coverStart+S0&&(C=a="right");var A;r.options.lineWrapping&&(A=v.getClientRects()).length>1?b=A[a=="right"?A.length-1:0]:b=v.getBoundingClientRect()}if(p&&x<9&&!g&&(!b||!b.left&&!b.right)){var j=v.parentNode.getClientRects()[0];j?b={left:j.left,right:j.left+na(r.display),top:j.top,bottom:j.bottom}:b=y}for(var z=b.top-i.rect.top,ee=b.bottom-i.rect.top,oe=(z+ee)/2,ce=i.view.measure.heights,me=0;me=a.text.length?(C=a.text.length,b="before"):C<=0&&(C=0,b="after"),!S)return g(b=="before"?C-1:C,b=="before");function P(ee,oe,ce){var me=S[oe],be=me.level==1;return g(ce?ee-1:ee,be!=ce)}o(P,"getBidi");var A=gi(S,C,b),j=vi,z=P(C,A,b=="before");return j!=null&&(z.other=P(C,j,b!="before")),z}o(fn,"cursorCoords");function Ci(r,i){var u=0;i=Be(r.doc,i),r.options.lineWrapping||(u=na(r.display)*i.ch);var a=Me(r.doc,i.line),c=_e(a)+sl(r.display);return{left:u,right:u,top:c,bottom:c+a.height}}o(Ci,"estimateCoords");function lo(r,i,u,a,c){var v=ae(r,i,u);return v.xRel=c,a&&(v.outside=a),v}o(lo,"PosWithInfo");function q(r,i,u){var a=r.doc;if(u+=r.display.viewOffset,u<0)return lo(a.first,0,null,-1,-1);var c=ro(a,u),v=a.first+a.size-1;if(c>v)return lo(a.first+a.size-1,Me(a,v).text.length,null,1,1);i<0&&(i=0);for(var g=Me(a,c);;){var S=Xe(r,g,c,i,u),C=xu(g,S.ch+(S.xRel>0||S.outside>0?1:0));if(!C)return S;var b=C.find(1);if(b.line==c)return b;g=Me(a,c=b.line)}}o(q,"coordsChar");function re(r,i,u,a){a-=al(i);var c=i.text.length,v=sn(function(g){return ii(r,u,g-1).bottom<=a},c,0);return c=sn(function(g){return ii(r,u,g).top>a},v,c),{begin:v,end:c}}o(re,"wrappedLineExtent");function Z(r,i,u,a){u||(u=ni(r,i));var c=ul(r,i,ii(r,u,a),"line").top;return re(r,i,u,c)}o(Z,"wrappedLineExtentChar");function Ne(r,i,u,a){return r.bottom<=u?!1:r.top>u?!0:(a?r.left:r.right)>i}o(Ne,"boxIsAfter");function Xe(r,i,u,a,c){c-=_e(i);var v=ni(r,i),g=al(i),S=0,C=i.text.length,b=!0,P=En(i,r.doc.direction);if(P){var A=(r.options.lineWrapping?Tr:_t)(r,i,u,v,P,a,c);b=A.level!=1,S=b?A.from:A.to-1,C=b?A.to:A.from-1}var j=null,z=null,ee=sn(function(Fe){var Re=ii(r,v,Fe);return Re.top+=g,Re.bottom+=g,Ne(Re,a,c,!1)?(Re.top<=c&&Re.left<=a&&(j=Fe,z=Re),!0):!1},S,C),oe,ce,me=!1;if(z){var be=a-z.left=Oe.bottom?1:0}return ee=Fi(i.text,ee,1),lo(u,ee,ce,me,a-oe)}o(Xe,"coordsCharInner");function _t(r,i,u,a,c,v,g){var S=sn(function(A){var j=c[A],z=j.level!=1;return Ne(fn(r,ae(u,z?j.to:j.from,z?"before":"after"),"line",i,a),v,g,!0)},0,c.length-1),C=c[S];if(S>0){var b=C.level!=1,P=fn(r,ae(u,b?C.from:C.to,b?"after":"before"),"line",i,a);Ne(P,v,g,!0)&&P.top>g&&(C=c[S-1])}return C}o(_t,"coordsBidiPart");function Tr(r,i,u,a,c,v,g){var S=re(r,i,a,g),C=S.begin,b=S.end;/\s/.test(i.text.charAt(b-1))&&b--;for(var P=null,A=null,j=0;j=b||z.to<=C)){var ee=z.level!=1,oe=ii(r,a,ee?Math.min(b,z.to)-1:Math.max(C,z.from)).right,ce=oece)&&(P=z,A=ce)}}return P||(P=c[c.length-1]),P.fromb&&(P={from:P.from,to:b,level:P.level}),P}o(Tr,"coordsBidiPartWrapped");var ht;function vs(r){if(r.cachedTextHeight!=null)return r.cachedTextHeight;if(ht==null){ht=Te("pre",null,"CodeMirror-line-like");for(var i=0;i<49;++i)ht.appendChild(document.createTextNode("x")),ht.appendChild(Te("br"));ht.appendChild(document.createTextNode("x"))}et(r.measure,ht);var u=ht.offsetHeight/50;return u>3&&(r.cachedTextHeight=u),qe(r.measure),u||1}o(vs,"textHeight");function na(r){if(r.cachedCharWidth!=null)return r.cachedCharWidth;var i=Te("span","xxxxxxxxxx"),u=Te("pre",[i],"CodeMirror-line-like");et(r.measure,u);var a=i.getBoundingClientRect(),c=(a.right-a.left)/10;return c>2&&(r.cachedCharWidth=c),c||10}o(na,"charWidth");function $n(r){for(var i=r.display,u={},a={},c=i.gutters.clientLeft,v=i.gutters.firstChild,g=0;v;v=v.nextSibling,++g){var S=r.display.gutterSpecs[g].className;u[S]=v.offsetLeft+v.clientLeft+c,a[S]=v.clientWidth}return{fixedPos:ia(i),gutterTotalWidth:i.gutters.offsetWidth,gutterLeft:u,gutterWidth:a,wrapperWidth:i.wrapper.clientWidth}}o($n,"getDimensions");function ia(r){return r.scroller.getBoundingClientRect().left-r.sizer.getBoundingClientRect().left}o(ia,"compensateForHScroll");function fm(r){var i=vs(r.display),u=r.options.lineWrapping,a=u&&Math.max(5,r.display.scroller.clientWidth/na(r.display)-3);return function(c){if(Nt(r.doc,c))return 0;var v=0;if(c.widgets)for(var g=0;g0&&(b=Me(r.doc,C.line).text).length==C.ch){var P=Et(b,b.length,r.options.tabSize)-b.length;C=ae(C.line,Math.max(0,Math.round((v-ll(r.display).left)/na(r.display))-P))}return C}o(ao,"posFromMouse");function uo(r,i){if(i>=r.display.viewTo||(i-=r.display.viewFrom,i<0))return null;for(var u=r.display.view,a=0;ai)&&(c.updateLineNumbers=i),r.curOp.viewChanged=!0,i>=c.viewTo)an&&Su(r.doc,i)c.viewFrom?$o(r):(c.viewFrom+=a,c.viewTo+=a);else if(i<=c.viewFrom&&u>=c.viewTo)$o(r);else if(i<=c.viewFrom){var v=fl(r,u,u+a,1);v?(c.view=c.view.slice(v.index),c.viewFrom=v.lineN,c.viewTo+=a):$o(r)}else if(u>=c.viewTo){var g=fl(r,i,i,-1);g?(c.view=c.view.slice(0,g.index),c.viewTo=g.lineN):$o(r)}else{var S=fl(r,i,i,-1),C=fl(r,u,u+a,1);S&&C?(c.view=c.view.slice(0,S.index).concat(ds(r,S.lineN,C.lineN)).concat(c.view.slice(C.index)),c.viewTo+=a):$o(r)}var b=c.externalMeasured;b&&(u=c.lineN&&i=a.viewTo)){var v=a.view[uo(r,i)];if(v.node!=null){var g=v.changes||(v.changes=[]);at(g,u)==-1&&g.push(u)}}}o(ys,"regLineChange");function $o(r){r.display.viewFrom=r.display.viewTo=r.doc.first,r.display.view=[],r.display.viewOffset=0}o($o,"resetView");function fl(r,i,u,a){var c=uo(r,i),v,g=r.display.view;if(!an||u==r.doc.first+r.doc.size)return{index:c,lineN:u};for(var S=r.display.viewFrom,C=0;C0){if(c==g.length-1)return null;v=S+g[c].size-i,c++}else v=S-i;i+=v,u+=v}for(;Su(r.doc,u)!=u;){if(c==(a<0?0:g.length-1))return null;u+=a*g[c-(a<0?1:0)].size,c+=a}return{index:c,lineN:u}}o(fl,"viewCuttingPoint");function Xy(r,i,u){var a=r.display,c=a.view;c.length==0||i>=a.viewTo||u<=a.viewFrom?(a.view=ds(r,i,u),a.viewFrom=i):(a.viewFrom>i?a.view=ds(r,i,a.viewFrom).concat(a.view):a.viewFromu&&(a.view=a.view.slice(0,uo(r,u)))),a.viewTo=u}o(Xy,"adjustView");function cm(r){for(var i=r.display.view,u=0,a=0;a=r.display.viewTo||S.to().line0?i.blinker=setInterval(function(){r.hasFocus()||pl(r),i.cursorDiv.style.visibility=(u=!u)?"":"hidden"},r.options.cursorBlinkRate):r.options.cursorBlinkRate<0&&(i.cursorDiv.style.visibility="hidden")}}o(oa,"restartBlink");function Gp(r){r.hasFocus()||(r.display.input.focus(),r.state.focused||sa(r))}o(Gp,"ensureFocus");function Gf(r){r.state.delayingBlurEvent=!0,setTimeout(function(){r.state.delayingBlurEvent&&(r.state.delayingBlurEvent=!1,r.state.focused&&pl(r))},100)}o(Gf,"delayBlurEvent");function sa(r,i){r.state.delayingBlurEvent&&!r.state.draggingText&&(r.state.delayingBlurEvent=!1),r.options.readOnly!="nocursor"&&(r.state.focused||(ke(r,"focus",r,i),r.state.focused=!0,Ye(r.display.wrapper,"CodeMirror-focused"),!r.curOp&&r.display.selForContextMenu!=r.doc.sel&&(r.display.input.reset(),_&&setTimeout(function(){return r.display.input.reset(!0)},20)),r.display.input.receivedFocus()),oa(r))}o(sa,"onFocus");function pl(r,i){r.state.delayingBlurEvent||(r.state.focused&&(ke(r,"blur",r,i),r.state.focused=!1,xe(r.display.wrapper,"CodeMirror-focused")),clearInterval(r.display.blinker),setTimeout(function(){r.state.focused||(r.display.shift=!1)},150))}o(pl,"onBlur");function ws(r){for(var i=r.display,u=i.lineDiv.offsetTop,a=0;a.005||P<-.005)&&(ri(c.line,g),xs(c.line),c.rest))for(var A=0;Ar.display.sizerWidth){var j=Math.ceil(S/na(r.display));j>r.display.maxLineLength&&(r.display.maxLineLength=j,r.display.maxLine=c.line,r.display.maxLineChanged=!0)}}}}o(ws,"updateHeightsInViewport");function xs(r){if(r.widgets)for(var i=0;i=g&&(v=ro(i,_e(Me(i,C))-r.wrapper.clientHeight),g=C)}return{from:v,to:Math.max(g,v+1)}}o(dl,"visibleLines");function Qy(r,i){if(!Zt(r,"scrollCursorIntoView")){var u=r.display,a=u.sizer.getBoundingClientRect(),c=null;if(i.top+a.top<0?c=!0:i.bottom+a.top>(window.innerHeight||document.documentElement.clientHeight)&&(c=!1),c!=null&&!te){var v=Te("div","\u200B",null,`position: absolute; + top: `+(i.top-u.viewOffset-sl(r.display))+`px; height: `+(i.bottom-i.top+Vr(r)+u.barHeight)+`px; - left: `+i.left+"px; width: "+Math.max(2,i.right-i.left)+"px;");r.display.lineSpace.appendChild(m),m.scrollIntoView(c),r.display.lineSpace.removeChild(m)}}}o(Yy,"maybeScrollWindow");function Xy(r,i,u,a){a==null&&(a=0);var c;!r.options.lineWrapping&&i==u&&(u=i.sticky=="before"?ae(i.line,i.ch+1,"before"):i,i=i.ch?ae(i.line,i.sticky=="before"?i.ch-1:i.ch,"after"):i);for(var m=0;m<5;m++){var g=!1,S=Bi(r,i),C=!u||u==i?S:Bi(r,u);c={left:Math.min(S.left,C.left),top:Math.min(S.top,C.top)-a,right:Math.max(S.left,C.left),bottom:Math.max(S.bottom,C.bottom)+a};var b=oa(r,c),N=r.doc.scrollTop,A=r.doc.scrollLeft;if(b.scrollTop!=null&&(Zt(r,b.scrollTop),Math.abs(r.doc.scrollTop-N)>1&&(g=!0)),b.scrollLeft!=null&&(pl(r,b.scrollLeft),Math.abs(r.doc.scrollLeft-A)>1&&(g=!0)),!g)break}return c}o(Xy,"scrollPosIntoView");function Qy(r,i){var u=oa(r,i);u.scrollTop!=null&&Zt(r,u.scrollTop),u.scrollLeft!=null&&pl(r,u.scrollLeft)}o(Qy,"scrollIntoView");function oa(r,i){var u=r.display,a=ys(r.display);i.top<0&&(i.top=0);var c=r.curOp&&r.curOp.scrollTop!=null?r.curOp.scrollTop:u.scroller.scrollTop,m=oo(r),g={};i.bottom-i.top>m&&(i.bottom=i.top+m);var S=r.doc.height+gr(u),C=i.topS-a;if(i.topc+m){var N=Math.min(i.top,(b?S:i.bottom)-m);N!=c&&(g.scrollTop=N)}var A=r.options.fixedGutter?0:u.gutters.offsetWidth,$=r.curOp&&r.curOp.scrollLeft!=null?r.curOp.scrollLeft:u.scroller.scrollLeft-A,z=Ii(r)-u.gutters.offsetWidth,ee=i.right-i.left>z;return ee&&(i.right=i.left+z),i.left<10?g.scrollLeft=0:i.left<$?g.scrollLeft=Math.max(0,i.left+A-(ee?0:10)):i.right>z+$-3&&(g.scrollLeft=i.right+(ee?0:10)-z),g}o(oa,"calculateScrollPos");function sa(r,i){i!=null&&(Bf(r),r.curOp.scrollTop=(r.curOp.scrollTop==null?r.doc.scrollTop:r.curOp.scrollTop)+i)}o(sa,"addToScrollTop");function _s(r){Bf(r);var i=r.getCursor();r.curOp.scrollToPos={from:i,to:i,margin:r.options.cursorScrollMargin}}o(_s,"ensureCursorVisible");function _u(r,i,u){(i!=null||u!=null)&&Bf(r),i!=null&&(r.curOp.scrollLeft=i),u!=null&&(r.curOp.scrollTop=u)}o(_u,"scrollToCoords");function fm(r,i){Bf(r),r.curOp.scrollToPos=i}o(fm,"scrollToRange");function Bf(r){var i=r.curOp.scrollToPos;if(i){r.curOp.scrollToPos=null;var u=sm(r,i.from),a=sm(r,i.to);cm(r,u,a,i.margin)}}o(Bf,"resolveScrollToPos");function cm(r,i,u,a){var c=oa(r,{left:Math.min(i.left,u.left),top:Math.min(i.top,u.top)-a,right:Math.max(i.right,u.right),bottom:Math.max(i.bottom,u.bottom)+a});_u(r,c.scrollLeft,c.scrollTop)}o(cm,"scrollToCoordsRange");function Zt(r,i){Math.abs(r.doc.scrollTop-i)<2||(n||zp(r,{top:i}),Kr(r,i,!0),n&&zp(r),co(r,100))}o(Zt,"updateScrollTop");function Kr(r,i,u){i=Math.max(0,Math.min(r.display.scroller.scrollHeight-r.display.scroller.clientHeight,i)),!(r.display.scroller.scrollTop==i&&!u)&&(r.doc.scrollTop=i,r.display.scrollbars.setScrollTop(i),r.display.scroller.scrollTop!=i&&(r.display.scroller.scrollTop=i))}o(Kr,"setScrollTop");function pl(r,i,u,a){i=Math.max(0,Math.min(i,r.display.scroller.scrollWidth-r.display.scroller.clientWidth)),!((u?i==r.doc.scrollLeft:Math.abs(r.doc.scrollLeft-i)<2)&&!a)&&(r.doc.scrollLeft=i,pm(r),r.display.scroller.scrollLeft!=i&&(r.display.scroller.scrollLeft=i),r.display.scrollbars.setScrollLeft(i))}o(pl,"setScrollLeft");function bu(r){var i=r.display,u=i.gutters.offsetWidth,a=Math.round(r.doc.height+gr(r.display));return{clientHeight:i.scroller.clientHeight,viewHeight:i.wrapper.clientHeight,scrollWidth:i.scroller.scrollWidth,clientWidth:i.scroller.clientWidth,viewWidth:i.wrapper.clientWidth,barLeft:r.options.fixedGutter?u:0,docHeight:a,scrollHeight:a+Vr(r)+i.barHeight,nativeBarWidth:i.nativeBarWidth,gutterWidth:u}}o(bu,"measureForScrollbars");var bs=o(function(r,i,u){this.cm=u;var a=this.vert=be("div",[be("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),c=this.horiz=be("div",[be("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a.tabIndex=c.tabIndex=-1,r(a),r(c),H(a,"scroll",function(){a.clientHeight&&i(a.scrollTop,"vertical")}),H(c,"scroll",function(){c.clientWidth&&i(c.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,p&&w<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")},"NativeScrollbars");bs.prototype.update=function(r){var i=r.scrollWidth>r.clientWidth+1,u=r.scrollHeight>r.clientHeight+1,a=r.nativeBarWidth;if(u){this.vert.style.display="block",this.vert.style.bottom=i?a+"px":"0";var c=r.viewHeight-(i?a:0);this.vert.firstChild.style.height=Math.max(0,r.scrollHeight-r.clientHeight+c)+"px"}else this.vert.style.display="",this.vert.firstChild.style.height="0";if(i){this.horiz.style.display="block",this.horiz.style.right=u?a+"px":"0",this.horiz.style.left=r.barLeft+"px";var m=r.viewWidth-r.barLeft-(u?a:0);this.horiz.firstChild.style.width=Math.max(0,r.scrollWidth-r.clientWidth+m)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&r.clientHeight>0&&(a==0&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:u?a:0,bottom:i?a:0}},bs.prototype.setScrollLeft=function(r){this.horiz.scrollLeft!=r&&(this.horiz.scrollLeft=r),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},bs.prototype.setScrollTop=function(r){this.vert.scrollTop!=r&&(this.vert.scrollTop=r),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},bs.prototype.zeroWidthHack=function(){var r=F&&!X?"12px":"18px";this.horiz.style.height=this.vert.style.width=r,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new wt,this.disableVert=new wt},bs.prototype.enableZeroWidthBar=function(r,i,u){r.style.pointerEvents="auto";function a(){var c=r.getBoundingClientRect(),m=u=="vert"?document.elementFromPoint(c.right-1,(c.top+c.bottom)/2):document.elementFromPoint((c.right+c.left)/2,c.bottom-1);m!=r?r.style.pointerEvents="none":i.set(1e3,a)}o(a,"maybeDisable"),i.set(1e3,a)},bs.prototype.clear=function(){var r=this.horiz.parentNode;r.removeChild(this.horiz),r.removeChild(this.vert)};var Eu=o(function(){},"NullScrollbars");Eu.prototype.update=function(){return{bottom:0,right:0}},Eu.prototype.setScrollLeft=function(){},Eu.prototype.setScrollTop=function(){},Eu.prototype.clear=function(){};function Es(r,i){i||(i=bu(r));var u=r.display.barWidth,a=r.display.barHeight;la(r,i);for(var c=0;c<4&&u!=r.display.barWidth||a!=r.display.barHeight;c++)u!=r.display.barWidth&&r.options.lineWrapping&&Ss(r),la(r,bu(r)),u=r.display.barWidth,a=r.display.barHeight}o(Es,"updateScrollbars");function la(r,i){var u=r.display,a=u.scrollbars.update(i);u.sizer.style.paddingRight=(u.barWidth=a.right)+"px",u.sizer.style.paddingBottom=(u.barHeight=a.bottom)+"px",u.heightForcer.style.borderBottom=a.bottom+"px solid transparent",a.right&&a.bottom?(u.scrollbarFiller.style.display="block",u.scrollbarFiller.style.height=a.bottom+"px",u.scrollbarFiller.style.width=a.right+"px"):u.scrollbarFiller.style.display="",a.bottom&&r.options.coverGutterNextToScrollbar&&r.options.fixedGutter?(u.gutterFiller.style.display="block",u.gutterFiller.style.height=a.bottom+"px",u.gutterFiller.style.width=i.gutterWidth+"px"):u.gutterFiller.style.display=""}o(la,"updateScrollbarsInner");var Uf={native:bs,null:Eu};function dl(r){r.display.scrollbars&&(r.display.scrollbars.clear(),r.display.scrollbars.addClass&&we(r.display.wrapper,r.display.scrollbars.addClass)),r.display.scrollbars=new Uf[r.options.scrollbarStyle](function(i){r.display.wrapper.insertBefore(i,r.display.scrollbarFiller),H(i,"mousedown",function(){r.state.focused&&setTimeout(function(){return r.display.input.focus()},0)}),i.setAttribute("cm-not-content","true")},function(i,u){u=="horizontal"?pl(r,i):Zt(r,i)},r),r.display.scrollbars.addClass&&Ge(r.display.wrapper,r.display.scrollbars.addClass)}o(dl,"initScrollbars");var Tu=0;function Ui(r){r.curOp={cm:r,viewChanged:!1,startHeight:r.doc.height,forceUpdate:!1,updateInput:0,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Tu,markArrays:null},no(r.curOp)}o(Ui,"startOperation");function fo(r){var i=r.curOp;i&&Fp(i,function(u){for(var a=0;a=u.viewTo)||u.maxLineChanged&&i.options.lineWrapping,r.update=r.mustUpdate&&new kn(i,r.mustUpdate&&{top:r.scrollTop,ensure:r.scrollToPos},r.forceUpdate)}o(Zy,"endOperation_R1");function Jy(r){r.updatedDisplay=r.mustUpdate&&Up(r.cm,r.update)}o(Jy,"endOperation_W1");function e0(r){var i=r.cm,u=i.display;r.updatedDisplay&&Ss(i),r.barMeasure=bu(i),u.maxLineChanged&&!i.options.lineWrapping&&(r.adjustWidthTo=em(i,u.maxLine,u.maxLine.text.length).left+3,i.display.sizerWidth=r.adjustWidthTo,r.barMeasure.scrollWidth=Math.max(u.scroller.clientWidth,u.sizer.offsetLeft+r.adjustWidthTo+Vr(i)+i.display.barWidth),r.maxScrollLeft=Math.max(0,u.sizer.offsetLeft+r.adjustWidthTo-Ii(i))),(r.updatedDisplay||r.selectionChanged)&&(r.preparedSelection=u.input.prepareSelection())}o(e0,"endOperation_R2");function t0(r){var i=r.cm;r.adjustWidthTo!=null&&(i.display.sizer.style.minWidth=r.adjustWidthTo+"px",r.maxScrollLeft=r.display.viewTo)){var u=+new Date+r.options.workTime,a=us(r,i.highlightFrontier),c=[];i.iter(a.line,Math.min(i.first+i.size,r.display.viewTo+500),function(m){if(a.line>=r.display.viewFrom){var g=m.styles,S=m.text.length>r.options.maxHighlightLength?Fo(i.mode,a.state):null,C=fu(r,m,a,!0);S&&(a.state=S),m.styles=C.styles;var b=m.styleClasses,N=C.classes;N?m.styleClasses=N:b&&(m.styleClasses=null);for(var A=!g||g.length!=m.styles.length||b!=N&&(!b||!N||b.bgClass!=N.bgClass||b.textClass!=N.textClass),$=0;!A&&$u)return co(r,r.options.workDelay),!0}),i.highlightFrontier=a.line,i.modeFrontier=Math.max(i.modeFrontier,a.line),c.length&&Gr(r,function(){for(var m=0;m=u.viewFrom&&i.visible.to<=u.viewTo&&(u.updateLineNumbers==null||u.updateLineNumbers>=u.viewTo)&&u.renderedView==u.view&&am(r)==0)return!1;po(r)&&(zo(r),i.dims=zn(r));var c=a.first+a.size,m=Math.max(i.visible.from-r.options.viewportMargin,a.first),g=Math.min(c,i.visible.to+r.options.viewportMargin);u.viewFromg&&u.viewTo-g<20&&(g=Math.min(c,u.viewTo)),wi&&(m=Yl(r.doc,m),g=Fr(r.doc,g));var S=m!=u.viewFrom||g!=u.viewTo||u.lastWrapHeight!=i.wrapperHeight||u.lastWrapWidth!=i.wrapperWidth;Gy(r,m,g),u.viewOffset=Ir(Ne(r.doc,u.viewFrom)),r.display.mover.style.top=u.viewOffset+"px";var C=am(r);if(!S&&C==0&&!i.force&&u.renderedView==u.view&&(u.updateLineNumbers==null||u.updateLineNumbers>=u.viewTo))return!1;var b=n0(r);return C>4&&(u.lineDiv.style.display="none"),o0(r,u.updateLineNumbers,i.dims),C>4&&(u.lineDiv.style.display=""),u.renderedView=u.view,i0(b),qe(u.cursorDiv),qe(u.selectionDiv),u.gutters.style.height=u.sizer.style.minHeight=0,S&&(u.lastWrapHeight=i.wrapperHeight,u.lastWrapWidth=i.wrapperWidth,co(r,400)),u.updateLineNumbers=null,!0}o(Up,"updateDisplayIfNeeded");function Ts(r,i){for(var u=i.viewport,a=!0;;a=!1){if(!a||!r.options.lineWrapping||i.oldDisplayWidth==Ii(r)){if(u&&u.top!=null&&(u={top:Math.min(r.doc.height+gr(r.display)-oo(r),u.top)}),i.visible=cl(r.display,r.doc,u),i.visible.from>=r.display.viewFrom&&i.visible.to<=r.display.viewTo)break}else a&&(i.visible=cl(r.display,r.doc,u));if(!Up(r,i))break;Ss(r);var c=bu(r);Su(r),Es(r,c),$p(r,c),i.force=!1}i.signal(r,"update",r),(r.display.viewFrom!=r.display.reportedViewFrom||r.display.viewTo!=r.display.reportedViewTo)&&(i.signal(r,"viewportChange",r,r.display.viewFrom,r.display.viewTo),r.display.reportedViewFrom=r.display.viewFrom,r.display.reportedViewTo=r.display.viewTo)}o(Ts,"postUpdateDisplay");function zp(r,i){var u=new kn(r,i);if(Up(r,u)){Ss(r),Ts(r,u);var a=bu(r);Su(r),Es(r,a),$p(r,a),u.finish()}}o(zp,"updateDisplaySimple");function o0(r,i,u){var a=r.display,c=r.options.lineNumbers,m=a.lineDiv,g=m.firstChild;function S(ee){var oe=ee.nextSibling;return _&&F&&r.display.currentWheelTarget==ee?ee.style.display="none":ee.parentNode.removeChild(ee),oe}o(S,"rm");for(var C=a.view,b=a.viewFrom,N=0;N-1&&(z=!1),Af(r,A,b,u)),z&&(qe(A.lineNumber),A.lineNumber.appendChild(document.createTextNode(el(r.options,b)))),g=A.node.nextSibling}b+=A.size}for(;g;)g=S(g)}o(o0,"patchDisplay");function jp(r){var i=r.gutters.offsetWidth;r.sizer.style.marginLeft=i+"px",ir(r,"gutterChanged",r)}o(jp,"updateGutterSpace");function $p(r,i){r.display.sizer.style.minHeight=i.docHeight+"px",r.display.heightForcer.style.top=i.docHeight+"px",r.display.gutters.style.height=i.docHeight+r.display.barHeight+Vr(r)+"px"}o($p,"setDocumentHeight");function pm(r){var i=r.display,u=i.view;if(!(!i.alignWidgets&&(!i.gutters.firstChild||!r.options.fixedGutter))){for(var a=ra(i)-i.scroller.scrollLeft+r.doc.scrollLeft,c=i.gutters.offsetWidth,m=a+"px",g=0;gg.clientWidth,C=g.scrollHeight>g.clientHeight;if(!!(a&&S||c&&C)){if(c&&F&&_){e:for(var b=i.target,N=m.view;b!=g;b=b.parentNode)for(var A=0;A=0&&ze(r,a.to())<=0)return u}return-1};var vt=o(function(r,i){this.anchor=r,this.head=i},"Range");vt.prototype.from=function(){return as(this.anchor,this.head)},vt.prototype.to=function(){return Ho(this.anchor,this.head)},vt.prototype.empty=function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch};function Xr(r,i,u){var a=r&&r.options.selectionsMayTouch,c=i[u];i.sort(function($,z){return ze($.from(),z.from())}),u=st(i,c);for(var m=1;m0:C>=0){var b=as(S.from(),g.from()),N=Ho(S.to(),g.to()),A=S.empty()?g.from()==g.head:S.from()==S.head;m<=u&&--u,i.splice(--m,2,new vt(A?N:b,A?b:N))}}return new ni(i,u)}o(Xr,"normalizeSelection");function ks(r,i){return new ni([new vt(r,i||r)],0)}o(ks,"simpleSelection");function Os(r){return r.text?ae(r.from.line+r.text.length-1,xe(r.text).length+(r.text.length==1?r.from.ch:0)):r.to}o(Os,"changeEnd");function Ci(r,i){if(ze(r,i.from)<0)return r;if(ze(r,i.to)<=0)return Os(i);var u=r.line+i.text.length-(i.to.line-i.from.line)-1,a=r.ch;return r.line==i.to.line&&(a+=Os(i).ch-i.to.ch),ae(u,a)}o(Ci,"adjustForChange");function Vp(r,i){for(var u=[],a=0;a1&&r.remove(S.line+1,ee-1),r.insert(S.line+1,me)}ir(r,"change",r,i)}o($f,"updateDoc");function Ls(r,i,u){function a(c,m,g){if(c.linked)for(var S=0;S1&&!r.done[r.done.length-2].ranges)return r.done.pop(),xe(r.done)}o(u0,"lastChangeEvent");function ho(r,i,u,a){var c=r.history;c.undone.length=0;var m=+new Date,g,S;if((c.lastOp==a||c.lastOrigin==i.origin&&i.origin&&(i.origin.charAt(0)=="+"&&c.lastModTime>m-(r.cm?r.cm.options.historyEventDelay:500)||i.origin.charAt(0)=="*"))&&(g=u0(c,c.lastOp==a)))S=xe(g.changes),ze(i.from,i.to)==0&&ze(i.from,S.to)==0?S.to=Os(i):g.changes.push(Yp(r,i));else{var C=xe(c.done);for((!C||!C.ranges)&&On(r.sel,c.done),g={changes:[Yp(r,i)],generation:c.generation},c.done.push(g);c.done.length>c.undoDepth;)c.done.shift(),c.done[0].ranges||c.done.shift()}c.done.push(u),c.generation=++c.maxGeneration,c.lastModTime=c.lastSelTime=m,c.lastOp=c.lastSelOp=a,c.lastOrigin=c.lastSelOrigin=i.origin,S||Ee(r,"historyAdded")}o(ho,"addChangeToHistory");function Qp(r,i,u,a){var c=i.charAt(0);return c=="*"||c=="+"&&u.ranges.length==a.ranges.length&&u.somethingSelected()==a.somethingSelected()&&new Date-r.history.lastSelTime<=(r.cm?r.cm.options.historyEventDelay:500)}o(Qp,"selectionEventCanBeMerged");function ml(r,i,u,a){var c=r.history,m=a&&a.origin;u==c.lastSelOp||m&&c.lastSelOrigin==m&&(c.lastModTime==c.lastSelTime&&c.lastOrigin==m||Qp(r,m,xe(c.done),i))?c.done[c.done.length-1]=i:On(i,c.done),c.lastSelTime=+new Date,c.lastSelOrigin=m,c.lastSelOp=u,a&&a.clearRedo!==!1&&Xp(c.undone)}o(ml,"addSelectionToHistory");function On(r,i){var u=xe(i);u&&u.ranges&&u.equals(r)||i.push(r)}o(On,"pushSelectionToHistory");function ym(r,i,u,a){var c=i["spans_"+r.id],m=0;r.iter(Math.max(r.first,u),Math.min(r.first+r.size,a),function(g){g.markedSpans&&((c||(c=i["spans_"+r.id]={}))[m]=g.markedSpans),++m})}o(ym,"attachLocalSpans");function wm(r){if(!r)return null;for(var i,u=0;u-1&&(xe(S)[A]=b[A],delete b[A])}}return a}o(ii,"copyHistoryArray");function Vf(r,i,u,a){if(a){var c=r.anchor;if(u){var m=ze(i,c)<0;m!=ze(u,c)<0?(c=i,i=u):m!=ze(i,u)<0&&(i=u)}return new vt(c,i)}else return new vt(u||i,i)}o(Vf,"extendRange");function Kf(r,i,u,a,c){c==null&&(c=r.cm&&(r.cm.display.shift||r.extend)),Hr(r,new ni([Vf(r.sel.primary(),i,u,c)],0),a)}o(Kf,"extendSelection");function Nu(r,i,u){for(var a=[],c=r.cm&&(r.cm.display.shift||r.extend),m=0;m=i.ch:S.to>i.ch))){if(c&&(Ee(C,"beforeCursorEnter"),C.explicitlyCleared))if(m.markedSpans){--g;continue}else break;if(!C.atomic)continue;if(u){var A=C.find(a<0?1:-1),$=void 0;if((a<0?N:b)&&(A=Xf(r,A,-a,A&&A.line==i.line?m:null)),A&&A.line==i.line&&($=ze(A,u))&&(a<0?$<0:$>0))return vl(r,A,i,a,c)}var z=C.find(a<0?-1:1);return(a<0?b:N)&&(z=Xf(r,z,a,z.line==i.line?m:null)),z?vl(r,z,i,a,c):null}}return i}o(vl,"skipAtomicInner");function Wr(r,i,u,a,c){var m=a||1,g=vl(r,i,u,m,c)||!c&&vl(r,i,u,m,!0)||vl(r,i,u,-m,c)||!c&&vl(r,i,u,-m,!0);return g||(r.cantEdit=!0,ae(r.first,0))}o(Wr,"skipAtomic");function Xf(r,i,u,a){return u<0&&i.ch==0?i.line>r.first?Ue(r,ae(i.line-1)):null:u>0&&i.ch==(a||Ne(r,i.line)).text.length?i.line=0;--c)Qf(r,{from:a[c].from,to:a[c].to,text:c?[""]:i.text,origin:i.origin});else Qf(r,i)}}o(fa,"makeChange");function Qf(r,i){if(!(i.text.length==1&&i.text[0]==""&&ze(i.from,i.to)==0)){var u=Vp(r,i);ho(r,i,u,r.cm?r.cm.curOp.id:NaN),pa(r,i,u,Of(r,i));var a=[];Ls(r,function(c,m){!m&&st(a,c.history)==-1&&(bm(c.history,i),a.push(c.history)),pa(c,i,null,Of(c,i))})}}o(Qf,"makeChangeInner");function Zf(r,i,u){var a=r.cm&&r.cm.state.suppressEdits;if(!(a&&!u)){for(var c=r.history,m,g=r.sel,S=i=="undo"?c.done:c.undone,C=i=="undo"?c.undone:c.done,b=0;b=0;--z){var ee=$(z);if(ee)return ee.v}}}}o(Zf,"makeChangeFromHistory");function ca(r,i){if(i!=0&&(r.first+=i,r.sel=new ni(Er(r.sel.ranges,function(c){return new vt(ae(c.anchor.line+i,c.anchor.ch),ae(c.head.line+i,c.head.ch))}),r.sel.primIndex),r.cm)){Ze(r.cm,r.first,r.first-i,i);for(var u=r.cm.display,a=u.viewFrom;ar.lastLine())){if(i.from.linem&&(i={from:i.from,to:ae(m,Ne(r,m).text.length),text:[i.text[0]],origin:i.origin}),i.removed=gi(r,i.from,i.to),u||(u=Vp(r,i)),r.cm?f0(r.cm,i,a):$f(r,i,a),oi(r,u,Ut),r.cantEdit&&Wr(r,ae(r.firstLine(),0))&&(r.cantEdit=!1)}}o(pa,"makeChangeSingleDoc");function f0(r,i,u){var a=r.doc,c=r.display,m=i.from,g=i.to,S=!1,C=m.line;r.options.lineWrapping||(C=dt(En(Ne(a,m.line))),a.iter(C,g.line+1,function(z){if(z==c.maxLine)return S=!0,!0})),a.sel.contains(i.from,i.to)>-1&&Hl(r),$f(a,i,u,lm(r)),r.options.lineWrapping||(a.iter(C,m.line+i.text.length,function(z){var ee=ds(z);ee>c.maxLineLength&&(c.maxLine=z,c.maxLineLength=ee,c.maxLineChanged=!0,S=!1)}),S&&(r.curOp.updateMaxLine=!0)),Np(a,m.line),co(r,400);var b=i.text.length-(g.line-m.line)-1;i.full?Ze(r):m.line==g.line&&i.text.length==1&&!Gp(r.doc,i)?xs(r,m.line,"text"):Ze(r,m.line,g.line+1,b);var N=At(r,"changes"),A=At(r,"change");if(A||N){var $={from:m,to:g,text:i.text,removed:i.removed,origin:i.origin};A&&ir(r,"change",r,$),N&&(r.curOp.changeObjs||(r.curOp.changeObjs=[])).push($)}r.display.selForContextMenu=null}o(f0,"makeChangeSingleDocInEditor");function da(r,i,u,a,c){var m;a||(a=u),ze(a,u)<0&&(m=[a,u],u=m[0],a=m[1]),typeof i=="string"&&(i=r.splitLines(i)),fa(r,{from:u,to:a,text:i,origin:c})}o(da,"replaceRange");function ha(r,i,u,a){u1||!(this.children[0]instanceof ma))){var S=[];this.collapse(S),this.children=[new ma(S)],this.children[0].parent=this}},collapse:function(r){for(var i=0;i50){for(var g=c.lines.length%25+25,S=g;S10);r.parent.maybeSpill()}},iterN:function(r,i,u){for(var a=0;ar.display.maxLineLength&&(r.display.maxLine=b,r.display.maxLineLength=N,r.display.maxLineChanged=!0)}a!=null&&r&&this.collapsed&&Ze(r,a,c+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,r&&Pu(r.doc)),r&&ir(r,"markerCleared",r,this,a,c),i&&fo(r),this.parent&&this.parent.clear()}},Ps.prototype.find=function(r,i){r==null&&this.type=="bookmark"&&(r=1);for(var u,a,c=0;c0||g==0&&m.clearWhenEmpty!==!1)return m;if(m.replacedWith&&(m.collapsed=!0,m.widgetNode=yt("span",[m.replacedWith],"CodeMirror-widget"),a.handleMouseEvents||m.widgetNode.setAttribute("cm-ignore-events","true"),a.insertLeft&&(m.widgetNode.insertLeft=!0)),m.collapsed){if(nl(r,i.line,i,u,m)||i.line!=u.line&&nl(r,u.line,i,u,m))throw new Error("Inserting collapsed marker partially overlapping an existing one");We()}m.addToHistory&&ho(r,{from:i,to:u,origin:"markText"},r.sel,NaN);var S=i.line,C=r.cm,b;if(r.iter(S,u.line+1,function(A){C&&m.collapsed&&!C.options.lineWrapping&&En(A)==C.display.maxLine&&(b=!0),m.collapsed&&S!=i.line&&yi(A,0),Se(A,new Bo(m,S==i.line?i.ch:null,S==u.line?u.ch:null),r.cm&&r.cm.curOp),++S}),m.collapsed&&r.iter(i.line,u.line+1,function(A){Ot(r,A)&&yi(A,0)}),m.clearOnEnter&&H(m,"beforeCursorEnter",function(){return m.clear()}),m.readOnly&&(M(),(r.history.done.length||r.history.undone.length)&&r.clearHistory()),m.collapsed&&(m.id=++Jf,m.atomic=!0),C){if(b&&(C.curOp.updateMaxLine=!0),m.collapsed)Ze(C,i.line,u.line+1);else if(m.className||m.startStyle||m.endStyle||m.css||m.attributes||m.title)for(var N=i.line;N<=u.line;N++)xs(C,N,"text");m.atomic&&Pu(C.doc),ir(C,"markerAdded",C,m)}return m}o(Ms,"markText");var va=o(function(r,i){this.markers=r,this.primary=i;for(var u=0;u=0;C--)fa(this,a[C]);S?Gf(this,S):this.cm&&_s(this.cm)}),undo:T(function(){Zf(this,"undo")}),redo:T(function(){Zf(this,"redo")}),undoSelection:T(function(){Zf(this,"undo",!0)}),redoSelection:T(function(){Zf(this,"redo",!0)}),setExtending:function(r){this.extend=r},getExtending:function(){return this.extend},historySize:function(){for(var r=this.history,i=0,u=0,a=0;a=r.ch)&&i.push(c.marker.parent||c.marker)}return i},findMarks:function(r,i,u){r=Ue(this,r),i=Ue(this,i);var a=[],c=r.line;return this.iter(r.line,i.line+1,function(m){var g=m.markedSpans;if(g)for(var S=0;S=C.to||C.from==null&&c!=r.line||C.from!=null&&c==i.line&&C.from>=i.ch)&&(!u||u(C.marker))&&a.push(C.marker.parent||C.marker)}++c}),a},getAllMarks:function(){var r=[];return this.iter(function(i){var u=i.markedSpans;if(u)for(var a=0;ar)return i=r,!0;r-=m,++u}),Ue(this,ae(u,i))},indexFromPos:function(r){r=Ue(this,r);var i=r.ch;if(r.linei&&(i=r.from),r.to!=null&&r.to-1){i.state.draggingText(r),setTimeout(function(){return i.display.input.focus()},20);return}try{var N=r.dataTransfer.getData("Text");if(N){var A;if(i.state.draggingText&&!i.state.draggingText.copy&&(A=i.listSelections()),oi(i.doc,ks(u,u)),A)for(var $=0;$=0;S--)da(r.doc,"",a[S].from,a[S].to,"+delete");_s(r)})}o(si,"deleteNearSelection");function Fu(r,i,u){var a=Ri(r.text,i+u,u);return a<0||a>r.text.length?null:a}o(Fu,"moveCharLogically");function nc(r,i,u){var a=Fu(r,i.ch,u);return a==null?null:new ae(i.line,a,u<0?"after":"before")}o(nc,"moveLogically");function ga(r,i,u,a,c){if(r){i.doc.direction=="rtl"&&(c=-c);var m=bn(u,i.doc.direction);if(m){var g=c<0?xe(m):m[0],S=c<0==(g.level==1),C=S?"after":"before",b;if(g.level>0||i.doc.direction=="rtl"){var N=Wi(i,u);b=c<0?u.text.length-1:0;var A=so(i,N,b).top;b=ln(function($){return so(i,N,$).top==A},c<0==(g.level==1)?g.from:g.to-1,b),C=="before"&&(b=Fu(u,b,1))}else b=c<0?g.to:g.from;return new ae(a,b,C)}}return new ae(a,c<0?u.text.length:0,c<0?"before":"after")}o(ga,"endOfLine");function Mm(r,i,u,a){var c=bn(i,r.doc.direction);if(!c)return nc(i,u,a);u.ch>=i.text.length?(u.ch=i.text.length,u.sticky="before"):u.ch<=0&&(u.ch=0,u.sticky="after");var m=mi(c,u.ch,u.sticky),g=c[m];if(r.doc.direction=="ltr"&&g.level%2==0&&(a>0?g.to>u.ch:g.from=g.from&&$>=N.begin)){var z=A?"before":"after";return new ae(u.line,$,z)}}var ee=o(function(me,Ce,ve){for(var Te=o(function(bt,yr){return yr?new ae(u.line,S(bt,1),"before"):new ae(u.line,bt,"after")},"getRes");me>=0&&me0==(Fe.level!=1),tt=De?ve.begin:S(ve.end,-1);if(Fe.from<=tt&&tt0?N.end:S(N.begin,-1);return ce!=null&&!(a>0&&ce==i.text.length)&&(oe=ee(a>0?0:c.length-1,a,b(ce)),oe)?oe:null}o(Mm,"moveVisually");var yl={selectAll:Sm,singleSelection:function(r){return r.setSelection(r.getCursor("anchor"),r.getCursor("head"),Ut)},killLine:function(r){return si(r,function(i){if(i.empty()){var u=Ne(r.doc,i.head.line).text.length;return i.head.ch==u&&i.head.line0)c=new ae(c.line,c.ch+1),r.replaceRange(m.charAt(c.ch-1)+m.charAt(c.ch-2),ae(c.line,c.ch-2),c,"+transpose");else if(c.line>r.doc.first){var g=Ne(r.doc,c.line-1).text;g&&(c=new ae(c.line,1),r.replaceRange(m.charAt(0)+r.doc.lineSeparator()+g.charAt(g.length-1),ae(c.line-1,g.length-1),c,"+transpose"))}}u.push(new vt(c,c))}r.setSelections(u)})},newlineAndIndent:function(r){return Gr(r,function(){for(var i=r.listSelections(),u=i.length-1;u>=0;u--)r.replaceRange(r.doc.lineSeparator(),i[u].anchor,i[u].head,"+input");i=r.listSelections();for(var a=0;ar&&ze(i,this.pos)==0&&u==this.button};var Br,jn;function g0(r,i){var u=+new Date;return jn&&jn.compare(u,r,i)?(Br=jn=null,"triple"):Br&&Br.compare(u,r,i)?(jn=new sc(u,r,i),Br=null,"double"):(Br=new sc(u,r,i),jn=null,"single")}o(g0,"clickRepeat");function Im(r){var i=this,u=i.display;if(!(Xt(i,r)||u.activeTouch&&u.input.supportsTouch())){if(u.input.ensurePolled(),u.shift=r.shiftKey,dr(u,r)){_||(u.scroller.draggable=!1,setTimeout(function(){return u.scroller.draggable=!0},100));return}if(!cd(i,r)){var a=ao(i,r),c=Xs(r),m=a?g0(a,c):"single";window.focus(),c==1&&i.state.selectingText&&i.state.selectingText(r),!(a&&lc(i,c,a,m,r))&&(c==1?a?Hm(i,a,m,r):vi(r)==u.scroller&&Qt(r):c==2?(a&&Kf(i.doc,a),setTimeout(function(){return u.input.focus()},20)):c==3&&(de?i.display.input.onContextMenu(r):Wf(i)))}}}o(Im,"onMouseDown");function lc(r,i,u,a,c){var m="Click";return a=="double"?m="Double"+m:a=="triple"&&(m="Triple"+m),m=(i==1?"Left":i==2?"Middle":"Right")+m,ya(r,id(m,c),c,function(g){if(typeof g=="string"&&(g=yl[g]),!g)return!1;var S=!1;try{r.isReadOnly()&&(r.state.suppressEdits=!0),S=g(r,u)!=Bt}finally{r.state.suppressEdits=!1}return S})}o(lc,"handleMappedButton");function wa(r,i,u){var a=r.getOption("configureMouse"),c=a?a(r,i,u):{};if(c.unit==null){var m=K?u.shiftKey&&u.metaKey:u.altKey;c.unit=m?"rectangle":i=="single"?"char":i=="double"?"word":"line"}return(c.extend==null||r.doc.extend)&&(c.extend=r.doc.extend||u.shiftKey),c.addNew==null&&(c.addNew=F?u.metaKey:u.ctrlKey),c.moveOnDrag==null&&(c.moveOnDrag=!(F?u.altKey:u.ctrlKey)),c}o(wa,"configureMouse");function Hm(r,i,u,a){p?setTimeout(Dr(Bp,r),0):r.curOp.focus=Ke();var c=wa(r,u,a),m=r.doc.sel,g;r.options.dragDrop&&ss&&!r.isReadOnly()&&u=="single"&&(g=m.contains(i))>-1&&(ze((g=m.ranges[g]).from(),i)<0||i.xRel>0)&&(ze(g.to(),i)>0||i.xRel<0)?Wm(r,a,i,c):Um(r,a,i,c)}o(Hm,"leftButtonDown");function Wm(r,i,u,a){var c=r.display,m=!1,g=Jt(r,function(b){_&&(c.scroller.draggable=!1),r.state.draggingText=!1,r.state.delayingBlurEvent&&(r.hasFocus()?r.state.delayingBlurEvent=!1:Wf(r)),he(c.wrapper.ownerDocument,"mouseup",g),he(c.wrapper.ownerDocument,"mousemove",S),he(c.scroller,"dragstart",C),he(c.scroller,"drop",g),m||(Qt(b),a.addNew||Kf(r.doc,u,null,null,a.extend),_&&!W||p&&w==9?setTimeout(function(){c.wrapper.ownerDocument.body.focus({preventScroll:!0}),c.input.focus()},20):c.input.focus())}),S=o(function(b){m=m||Math.abs(i.clientX-b.clientX)+Math.abs(i.clientY-b.clientY)>=10},"mouseMove"),C=o(function(){return m=!0},"dragStart");_&&(c.scroller.draggable=!0),r.state.draggingText=g,g.copy=!a.moveOnDrag,H(c.wrapper.ownerDocument,"mouseup",g),H(c.wrapper.ownerDocument,"mousemove",S),H(c.scroller,"dragstart",C),H(c.scroller,"drop",g),r.state.delayingBlurEvent=!0,setTimeout(function(){return c.input.focus()},20),c.scroller.dragDrop&&c.scroller.dragDrop()}o(Wm,"leftButtonStartDrag");function Bm(r,i,u){if(u=="char")return new vt(i,i);if(u=="word")return r.findWordAt(i);if(u=="line")return new vt(ae(i.line,0),Ue(r.doc,ae(i.line+1,0)));var a=u(r,i);return new vt(a.from,a.to)}o(Bm,"rangeForUnit");function Um(r,i,u,a){p&&Wf(r);var c=r.display,m=r.doc;Qt(i);var g,S,C=m.sel,b=C.ranges;if(a.addNew&&!a.extend?(S=m.sel.contains(u),S>-1?g=b[S]:g=new vt(u,u)):(g=m.sel.primary(),S=m.sel.primIndex),a.unit=="rectangle")a.addNew||(g=new vt(u,u)),u=ao(r,i,!0,!0),S=-1;else{var N=Bm(r,u,a.unit);a.extend?g=Vf(g,N.anchor,N.head,a.extend):g=N}a.addNew?S==-1?(S=b.length,Hr(m,Xr(r,b.concat([g]),S),{scroll:!1,origin:"*mouse"})):b.length>1&&b[S].empty()&&a.unit=="char"&&!a.extend?(Hr(m,Xr(r,b.slice(0,S).concat(b.slice(S+1)),0),{scroll:!1,origin:"*mouse"}),C=m.sel):Zp(m,S,g,ne):(S=0,Hr(m,new ni([g],0),ne),C=m.sel);var A=u;function $(ve){if(ze(A,ve)!=0)if(A=ve,a.unit=="rectangle"){for(var Te=[],Fe=r.options.tabSize,De=_t(Ne(m,u.line).text,u.ch,Fe),tt=_t(Ne(m,ve.line).text,ve.ch,Fe),bt=Math.min(De,tt),yr=Math.max(De,tt),Nt=Math.min(u.line,ve.line),dn=Math.min(r.lastLine(),Math.max(u.line,ve.line));Nt<=dn;Nt++){var hn=Ne(m,Nt).text,sr=br(hn,bt,Fe);bt==yr?Te.push(new vt(ae(Nt,sr),ae(Nt,sr))):hn.length>sr&&Te.push(new vt(ae(Nt,sr),ae(Nt,br(hn,yr,Fe))))}Te.length||Te.push(new vt(u,u)),Hr(m,Xr(r,C.ranges.slice(0,S).concat(Te),S),{origin:"*mouse",scroll:!1}),r.scrollIntoView(ve)}else{var Ur=g,Or=Bm(r,ve,a.unit),kt=Ur.anchor,Rt;ze(Or.anchor,kt)>0?(Rt=Or.head,kt=as(Ur.from(),Or.anchor)):(Rt=Or.anchor,kt=Ho(Ur.to(),Or.head));var er=C.ranges.slice(0);er[S]=xa(r,new vt(Ue(m,kt),Rt)),Hr(m,Xr(r,er,S),ne)}}o($,"extendTo");var z=c.wrapper.getBoundingClientRect(),ee=0;function oe(ve){var Te=++ee,Fe=ao(r,ve,!0,a.unit=="rectangle");if(!!Fe)if(ze(Fe,A)!=0){r.curOp.focus=Ke(),$(Fe);var De=cl(c,m);(Fe.line>=De.to||Fe.linez.bottom?20:0;tt&&setTimeout(Jt(r,function(){ee==Te&&(c.scroller.scrollTop+=tt,oe(ve))}),50)}}o(oe,"extend");function ce(ve){r.state.selectingText=!1,ee=1/0,ve&&(Qt(ve),c.input.focus()),he(c.wrapper.ownerDocument,"mousemove",me),he(c.wrapper.ownerDocument,"mouseup",Ce),m.history.lastSelOrigin=null}o(ce,"done");var me=Jt(r,function(ve){ve.buttons===0||!Xs(ve)?ce(ve):oe(ve)}),Ce=Jt(r,ce);r.state.selectingText=Ce,H(c.wrapper.ownerDocument,"mousemove",me),H(c.wrapper.ownerDocument,"mouseup",Ce)}o(Um,"leftButtonSelect");function xa(r,i){var u=i.anchor,a=i.head,c=Ne(r.doc,u.line);if(ze(u,a)==0&&u.sticky==a.sticky)return i;var m=bn(c);if(!m)return i;var g=mi(m,u.ch,u.sticky),S=m[g];if(S.from!=u.ch&&S.to!=u.ch)return i;var C=g+(S.from==u.ch==(S.level!=1)?0:1);if(C==0||C==m.length)return i;var b;if(a.line!=u.line)b=(a.line-u.line)*(r.doc.direction=="ltr"?1:-1)>0;else{var N=mi(m,a.ch,a.sticky),A=N-g||(a.ch-u.ch)*(S.level==1?-1:1);N==C-1||N==C?b=A<0:b=A>0}var $=m[C+(b?-1:0)],z=b==($.level==1),ee=z?$.from:$.to,oe=z?"after":"before";return u.ch==ee&&u.sticky==oe?i:new vt(new ae(u.line,ee,oe),a)}o(xa,"bidiSimplify");function Sa(r,i,u,a){var c,m;if(i.touches)c=i.touches[0].clientX,m=i.touches[0].clientY;else try{c=i.clientX,m=i.clientY}catch($){return!1}if(c>=Math.floor(r.display.gutters.getBoundingClientRect().right))return!1;a&&Qt(i);var g=r.display,S=g.lineDiv.getBoundingClientRect();if(m>S.bottom||!At(r,u))return os(i);m-=S.top-g.viewOffset;for(var C=0;C=c){var N=Fi(r.doc,m),A=r.display.gutterSpecs[C];return Ee(r,u,r,N,A.className,i),os(i)}}}o(Sa,"gutterEvent");function cd(r,i){return Sa(r,i,"gutterClick",!0)}o(cd,"clickInGutter");function pd(r,i){dr(r.display,i)||zm(r,i)||Xt(r,i,"contextmenu")||de||r.display.input.onContextMenu(i)}o(pd,"onContextMenu");function zm(r,i){return At(r,"gutterContextMenu")?Sa(r,i,"gutterContextMenu",!1):!1}o(zm,"contextMenuInGutter");function Iu(r){r.display.wrapper.className=r.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+r.options.theme.replace(/(^|\s)\s*/g," cm-s-"),xu(r)}o(Iu,"themeChanged");var wl={toString:function(){return"CodeMirror.Init"}},Hu={},Ca={};function ac(r){var i=r.optionHandlers;function u(a,c,m,g){r.defaults[a]=c,m&&(i[a]=g?function(S,C,b){b!=wl&&m(S,C,b)}:m)}o(u,"option"),r.defineOption=u,r.Init=wl,u("value","",function(a,c){return a.setValue(c)},!0),u("mode",null,function(a,c){a.doc.modeOption=c,Kp(a)},!0),u("indentUnit",2,Kp,!0),u("indentWithTabs",!1),u("smartIndent",!0),u("tabSize",4,function(a){Ou(a),xu(a),Ze(a)},!0),u("lineSeparator",null,function(a,c){if(a.doc.lineSep=c,!!c){var m=[],g=a.doc.first;a.doc.iter(function(C){for(var b=0;;){var N=C.text.indexOf(c,b);if(N==-1)break;b=N+c.length,m.push(ae(g,N))}g++});for(var S=m.length-1;S>=0;S--)da(a.doc,c,m[S],ae(m[S].line,m[S].ch+c.length))}}),u("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g,function(a,c,m){a.state.specialChars=new RegExp(c.source+(c.test(" ")?"":"| "),"g"),m!=wl&&a.refresh()}),u("specialCharPlaceholder",Tn,function(a){return a.refresh()},!0),u("electricChars",!0),u("inputStyle",P?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),u("spellcheck",!1,function(a,c){return a.getInputField().spellcheck=c},!0),u("autocorrect",!1,function(a,c){return a.getInputField().autocorrect=c},!0),u("autocapitalize",!1,function(a,c){return a.getInputField().autocapitalize=c},!0),u("rtlMoveVisually",!V),u("wholeLineUpdateBefore",!0),u("theme","default",function(a){Iu(a),ku(a)},!0),u("keyMap","default",function(a,c,m){var g=cn(c),S=m!=wl&&cn(m);S&&S.detach&&S.detach(a,g),g.attach&&g.attach(a,S||null)}),u("extraKeys",null),u("configureMouse",null),u("lineWrapping",!1,jm,!0),u("gutters",[],function(a,c){a.display.gutterSpecs=qp(c,a.options.lineNumbers),ku(a)},!0),u("fixedGutter",!0,function(a,c){a.display.gutters.style.left=c?ra(a.display)+"px":"0",a.refresh()},!0),u("coverGutterNextToScrollbar",!1,function(a){return Es(a)},!0),u("scrollbarStyle","native",function(a){dl(a),Es(a),a.display.scrollbars.setScrollTop(a.doc.scrollTop),a.display.scrollbars.setScrollLeft(a.doc.scrollLeft)},!0),u("lineNumbers",!1,function(a,c){a.display.gutterSpecs=qp(a.options.gutters,c),ku(a)},!0),u("firstLineNumber",1,ku,!0),u("lineNumberFormatter",function(a){return a},ku,!0),u("showCursorWhenSelecting",!1,Su,!0),u("resetSelectionOnContextMenu",!0),u("lineWiseCopyCut",!0),u("pasteLinesPerSelection",!0),u("selectionsMayTouch",!1),u("readOnly",!1,function(a,c){c=="nocursor"&&(fl(a),a.display.input.blur()),a.display.input.readOnlyChanged(c)}),u("screenReaderLabel",null,function(a,c){c=c===""?null:c,a.display.input.screenReaderLabelChanged(c)}),u("disableInput",!1,function(a,c){c||a.display.input.reset()},!0),u("dragDrop",!0,y0),u("allowDropFileTypes",null),u("cursorBlinkRate",530),u("cursorScrollMargin",0),u("cursorHeight",1,Su,!0),u("singleCursorHeightPerLine",!0,Su,!0),u("workTime",100),u("workDelay",100),u("flattenSpans",!0,Ou,!0),u("addModeClass",!1,Ou,!0),u("pollInterval",100),u("undoDepth",200,function(a,c){return a.doc.history.undoDepth=c}),u("historyEventDelay",1250),u("viewportMargin",10,function(a){return a.refresh()},!0),u("maxHighlightLength",1e4,Ou,!0),u("moveInputWithCursor",!0,function(a,c){c||a.display.input.resetPosition()}),u("tabindex",null,function(a,c){return a.display.input.getField().tabIndex=c||""}),u("autofocus",null),u("direction","ltr",function(a,c){return a.doc.setDirection(c)},!0),u("phrases",null)}o(ac,"defineOptions");function y0(r,i,u){var a=u&&u!=wl;if(!i!=!a){var c=r.display.dragFunctions,m=i?H:he;m(r.display.scroller,"dragstart",c.start),m(r.display.scroller,"dragenter",c.enter),m(r.display.scroller,"dragover",c.over),m(r.display.scroller,"dragleave",c.leave),m(r.display.scroller,"drop",c.drop)}}o(y0,"dragDropChanged");function jm(r){r.options.lineWrapping?(Ge(r.display.wrapper,"CodeMirror-wrap"),r.display.sizer.style.minWidth="",r.display.sizerWidth=null):(we(r.display.wrapper,"CodeMirror-wrap"),il(r)),ws(r),Ze(r),xu(r),setTimeout(function(){return Es(r)},100)}o(jm,"wrappingChanged");function Lt(r,i){var u=this;if(!(this instanceof Lt))return new Lt(r,i);this.options=i=i?qt(i):{},qt(Hu,i,!1);var a=i.value;typeof a=="string"?a=new fn(a,i.mode,null,i.lineSeparator,i.direction):i.mode&&(a.modeOption=i.mode),this.doc=a;var c=new Lt.inputStyles[i.inputStyle](this),m=this.display=new s0(r,a,c,i);m.wrapper.CodeMirror=this,Iu(this),i.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),dl(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new wt,keySeq:null,specialChars:null},i.autofocus&&!P&&m.input.focus(),p&&w<11&&setTimeout(function(){return u.display.input.reset(!0)},20),$m(this),nd(),Ui(this),this.curOp.forceUpdate=!0,gm(this,a),i.autofocus&&!P||this.hasFocus()?setTimeout(function(){u.hasFocus()&&!u.state.focused&&ia(u)},20):fl(this);for(var g in Ca)Ca.hasOwnProperty(g)&&Ca[g](this,i[g],wl);po(this),i.finishInit&&i.finishInit(this);for(var S=0;S20*20}o(g,"farAway"),H(i.scroller,"touchstart",function(C){if(!Xt(r,C)&&!m(C)&&!cd(r,C)){i.input.ensurePolled(),clearTimeout(u);var b=+new Date;i.activeTouch={start:b,moved:!1,prev:b-a.end<=300?a:null},C.touches.length==1&&(i.activeTouch.left=C.touches[0].pageX,i.activeTouch.top=C.touches[0].pageY)}}),H(i.scroller,"touchmove",function(){i.activeTouch&&(i.activeTouch.moved=!0)}),H(i.scroller,"touchend",function(C){var b=i.activeTouch;if(b&&!dr(i,C)&&b.left!=null&&!b.moved&&new Date-b.start<300){var N=r.coordsChar(i.activeTouch,"page"),A;!b.prev||g(b,b.prev)?A=new vt(N,N):!b.prev.prev||g(b,b.prev.prev)?A=r.findWordAt(N):A=new vt(ae(N.line,0),Ue(r.doc,ae(N.line+1,0))),r.setSelection(A.anchor,A.head),r.focus(),Qt(C)}c()}),H(i.scroller,"touchcancel",c),H(i.scroller,"scroll",function(){i.scroller.clientHeight&&(Zt(r,i.scroller.scrollTop),pl(r,i.scroller.scrollLeft,!0),Ee(r,"scroll",r))}),H(i.scroller,"mousewheel",function(C){return mm(r,C)}),H(i.scroller,"DOMMouseScroll",function(C){return mm(r,C)}),H(i.wrapper,"scroll",function(){return i.wrapper.scrollTop=i.wrapper.scrollLeft=0}),i.dragFunctions={enter:function(C){Xt(r,C)||to(C)},over:function(C){Xt(r,C)||(td(r,C),to(C))},start:function(C){return p0(r,C)},drop:Jt(r,Nm),leave:function(C){Xt(r,C)||rd(r)}};var S=i.input.getField();H(S,"keyup",function(C){return fd.call(r,C)}),H(S,"keydown",Jt(r,Am)),H(S,"keypress",Jt(r,Rm)),H(S,"focus",function(C){return ia(r,C)}),H(S,"blur",function(C){return fl(r,C)})}o($m,"registerEventHandlers");var Wu=[];Lt.defineInitHook=function(r){return Wu.push(r)};function Bu(r,i,u,a){var c=r.doc,m;u==null&&(u="add"),u=="smart"&&(c.mode.indent?m=us(r,i).state:u="prev");var g=r.options.tabSize,S=Ne(c,i),C=_t(S.text,null,g);S.stateAfter&&(S.stateAfter=null);var b=S.text.match(/^\s*/)[0],N;if(!a&&!/\S/.test(S.text))N=0,u="not";else if(u=="smart"&&(N=c.mode.indent(m,S.text.slice(b.length),S.text),N==Bt||N>150)){if(!a)return;u="prev"}u=="prev"?i>c.first?N=_t(Ne(c,i-1).text,null,g):N=0:u=="add"?N=C+r.options.indentUnit:u=="subtract"?N=C-r.options.indentUnit:typeof u=="number"&&(N=C+u),N=Math.max(0,N);var A="",$=0;if(r.options.indentWithTabs)for(var z=Math.floor(N/g);z;--z)$+=g,A+=" ";if($g,C=Zs(i),b=null;if(S&&a.ranges.length>1)if(bi&&bi.text.join(` -`)==i){if(a.ranges.length%bi.text.length==0){b=[];for(var N=0;N=0;$--){var z=a.ranges[$],ee=z.from(),oe=z.to();z.empty()&&(u&&u>0?ee=ae(ee.line,ee.ch-u):r.state.overwrite&&!S?oe=ae(oe.line,Math.min(Ne(m,oe.line).text.length,oe.ch+xe(C).length)):S&&bi&&bi.lineWise&&bi.text.join(` + left: `+i.left+"px; width: "+Math.max(2,i.right-i.left)+"px;");r.display.lineSpace.appendChild(v),v.scrollIntoView(c),r.display.lineSpace.removeChild(v)}}}o(Qy,"maybeScrollWindow");function Zy(r,i,u,a){a==null&&(a=0);var c;!r.options.lineWrapping&&i==u&&(u=i.sticky=="before"?ae(i.line,i.ch+1,"before"):i,i=i.ch?ae(i.line,i.sticky=="before"?i.ch-1:i.ch,"after"):i);for(var v=0;v<5;v++){var g=!1,S=fn(r,i),C=!u||u==i?S:fn(r,u);c={left:Math.min(S.left,C.left),top:Math.min(S.top,C.top)-a,right:Math.max(S.left,C.left),bottom:Math.max(S.bottom,C.bottom)+a};var b=la(r,c),P=r.doc.scrollTop,A=r.doc.scrollLeft;if(b.scrollTop!=null&&(er(r,b.scrollTop),Math.abs(r.doc.scrollTop-P)>1&&(g=!0)),b.scrollLeft!=null&&(hl(r,b.scrollLeft),Math.abs(r.doc.scrollLeft-A)>1&&(g=!0)),!g)break}return c}o(Zy,"scrollPosIntoView");function Jy(r,i){var u=la(r,i);u.scrollTop!=null&&er(r,u.scrollTop),u.scrollLeft!=null&&hl(r,u.scrollLeft)}o(Jy,"scrollIntoView");function la(r,i){var u=r.display,a=vs(r.display);i.top<0&&(i.top=0);var c=r.curOp&&r.curOp.scrollTop!=null?r.curOp.scrollTop:u.scroller.scrollTop,v=ta(r),g={};i.bottom-i.top>v&&(i.bottom=i.top+v);var S=r.doc.height+Rr(u),C=i.topS-a;if(i.topc+v){var P=Math.min(i.top,(b?S:i.bottom)-v);P!=c&&(g.scrollTop=P)}var A=r.options.fixedGutter?0:u.gutters.offsetWidth,j=r.curOp&&r.curOp.scrollLeft!=null?r.curOp.scrollLeft:u.scroller.scrollLeft-A,z=Bi(r)-u.gutters.offsetWidth,ee=i.right-i.left>z;return ee&&(i.right=i.left+z),i.left<10?g.scrollLeft=0:i.leftz+j-3&&(g.scrollLeft=i.right+(ee?0:10)-z),g}o(la,"calculateScrollPos");function aa(r,i){i!=null&&(Yf(r),r.curOp.scrollTop=(r.curOp.scrollTop==null?r.doc.scrollTop:r.curOp.scrollTop)+i)}o(aa,"addToScrollTop");function Ss(r){Yf(r);var i=r.getCursor();r.curOp.scrollToPos={from:i,to:i,margin:r.options.cursorScrollMargin}}o(Ss,"ensureCursorVisible");function Pu(r,i,u){(i!=null||u!=null)&&Yf(r),i!=null&&(r.curOp.scrollLeft=i),u!=null&&(r.curOp.scrollTop=u)}o(Pu,"scrollToCoords");function dm(r,i){Yf(r),r.curOp.scrollToPos=i}o(dm,"scrollToRange");function Yf(r){var i=r.curOp.scrollToPos;if(i){r.curOp.scrollToPos=null;var u=Ci(r,i.from),a=Ci(r,i.to);hm(r,u,a,i.margin)}}o(Yf,"resolveScrollToPos");function hm(r,i,u,a){var c=la(r,{left:Math.min(i.left,u.left),top:Math.min(i.top,u.top)-a,right:Math.max(i.right,u.right),bottom:Math.max(i.bottom,u.bottom)+a});Pu(r,c.scrollLeft,c.scrollTop)}o(hm,"scrollToCoordsRange");function er(r,i){Math.abs(r.doc.scrollTop-i)<2||(n||Xp(r,{top:i}),Kr(r,i,!0),n&&Xp(r),co(r,100))}o(er,"updateScrollTop");function Kr(r,i,u){i=Math.max(0,Math.min(r.display.scroller.scrollHeight-r.display.scroller.clientHeight,i)),!(r.display.scroller.scrollTop==i&&!u)&&(r.doc.scrollTop=i,r.display.scrollbars.setScrollTop(i),r.display.scroller.scrollTop!=i&&(r.display.scroller.scrollTop=i))}o(Kr,"setScrollTop");function hl(r,i,u,a){i=Math.max(0,Math.min(i,r.display.scroller.scrollWidth-r.display.scroller.clientWidth)),!((u?i==r.doc.scrollLeft:Math.abs(r.doc.scrollLeft-i)<2)&&!a)&&(r.doc.scrollLeft=i,mm(r),r.display.scroller.scrollLeft!=i&&(r.display.scroller.scrollLeft=i),r.display.scrollbars.setScrollLeft(i))}o(hl,"setScrollLeft");function Mu(r){var i=r.display,u=i.gutters.offsetWidth,a=Math.round(r.doc.height+Rr(r.display));return{clientHeight:i.scroller.clientHeight,viewHeight:i.wrapper.clientHeight,scrollWidth:i.scroller.scrollWidth,clientWidth:i.scroller.clientWidth,viewWidth:i.wrapper.clientWidth,barLeft:r.options.fixedGutter?u:0,docHeight:a,scrollHeight:a+Vr(r)+i.barHeight,nativeBarWidth:i.nativeBarWidth,gutterWidth:u}}o(Mu,"measureForScrollbars");var Cs=o(function(r,i,u){this.cm=u;var a=this.vert=Te("div",[Te("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),c=this.horiz=Te("div",[Te("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a.tabIndex=c.tabIndex=-1,r(a),r(c),H(a,"scroll",function(){a.clientHeight&&i(a.scrollTop,"vertical")}),H(c,"scroll",function(){c.clientWidth&&i(c.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,p&&x<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")},"NativeScrollbars");Cs.prototype.update=function(r){var i=r.scrollWidth>r.clientWidth+1,u=r.scrollHeight>r.clientHeight+1,a=r.nativeBarWidth;if(u){this.vert.style.display="block",this.vert.style.bottom=i?a+"px":"0";var c=r.viewHeight-(i?a:0);this.vert.firstChild.style.height=Math.max(0,r.scrollHeight-r.clientHeight+c)+"px"}else this.vert.style.display="",this.vert.firstChild.style.height="0";if(i){this.horiz.style.display="block",this.horiz.style.right=u?a+"px":"0",this.horiz.style.left=r.barLeft+"px";var v=r.viewWidth-r.barLeft-(u?a:0);this.horiz.firstChild.style.width=Math.max(0,r.scrollWidth-r.clientWidth+v)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&r.clientHeight>0&&(a==0&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:u?a:0,bottom:i?a:0}},Cs.prototype.setScrollLeft=function(r){this.horiz.scrollLeft!=r&&(this.horiz.scrollLeft=r),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},Cs.prototype.setScrollTop=function(r){this.vert.scrollTop!=r&&(this.vert.scrollTop=r),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},Cs.prototype.zeroWidthHack=function(){var r=R&&!X?"12px":"18px";this.horiz.style.height=this.vert.style.width=r,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new St,this.disableVert=new St},Cs.prototype.enableZeroWidthBar=function(r,i,u){r.style.pointerEvents="auto";function a(){var c=r.getBoundingClientRect(),v=u=="vert"?document.elementFromPoint(c.right-1,(c.top+c.bottom)/2):document.elementFromPoint((c.right+c.left)/2,c.bottom-1);v!=r?r.style.pointerEvents="none":i.set(1e3,a)}o(a,"maybeDisable"),i.set(1e3,a)},Cs.prototype.clear=function(){var r=this.horiz.parentNode;r.removeChild(this.horiz),r.removeChild(this.vert)};var Au=o(function(){},"NullScrollbars");Au.prototype.update=function(){return{bottom:0,right:0}},Au.prototype.setScrollLeft=function(){},Au.prototype.setScrollTop=function(){},Au.prototype.clear=function(){};function _s(r,i){i||(i=Mu(r));var u=r.display.barWidth,a=r.display.barHeight;ua(r,i);for(var c=0;c<4&&u!=r.display.barWidth||a!=r.display.barHeight;c++)u!=r.display.barWidth&&r.options.lineWrapping&&ws(r),ua(r,Mu(r)),u=r.display.barWidth,a=r.display.barHeight}o(_s,"updateScrollbars");function ua(r,i){var u=r.display,a=u.scrollbars.update(i);u.sizer.style.paddingRight=(u.barWidth=a.right)+"px",u.sizer.style.paddingBottom=(u.barHeight=a.bottom)+"px",u.heightForcer.style.borderBottom=a.bottom+"px solid transparent",a.right&&a.bottom?(u.scrollbarFiller.style.display="block",u.scrollbarFiller.style.height=a.bottom+"px",u.scrollbarFiller.style.width=a.right+"px"):u.scrollbarFiller.style.display="",a.bottom&&r.options.coverGutterNextToScrollbar&&r.options.fixedGutter?(u.gutterFiller.style.display="block",u.gutterFiller.style.height=a.bottom+"px",u.gutterFiller.style.width=i.gutterWidth+"px"):u.gutterFiller.style.display=""}o(ua,"updateScrollbarsInner");var Xf={native:Cs,null:Au};function ml(r){r.display.scrollbars&&(r.display.scrollbars.clear(),r.display.scrollbars.addClass&&xe(r.display.wrapper,r.display.scrollbars.addClass)),r.display.scrollbars=new Xf[r.options.scrollbarStyle](function(i){r.display.wrapper.insertBefore(i,r.display.scrollbarFiller),H(i,"mousedown",function(){r.state.focused&&setTimeout(function(){return r.display.input.focus()},0)}),i.setAttribute("cm-not-content","true")},function(i,u){u=="horizontal"?hl(r,i):er(r,i)},r),r.display.scrollbars.addClass&&Ye(r.display.wrapper,r.display.scrollbars.addClass)}o(ml,"initScrollbars");var Du=0;function Ui(r){r.curOp={cm:r,viewChanged:!1,startHeight:r.doc.height,forceUpdate:!1,updateInput:0,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++Du,markArrays:null},io(r.curOp)}o(Ui,"startOperation");function fo(r){var i=r.curOp;i&&zf(i,function(u){for(var a=0;a=u.viewTo)||u.maxLineChanged&&i.options.lineWrapping,r.update=r.mustUpdate&&new Ln(i,r.mustUpdate&&{top:r.scrollTop,ensure:r.scrollToPos},r.forceUpdate)}o(e0,"endOperation_R1");function t0(r){r.updatedDisplay=r.mustUpdate&&Yp(r.cm,r.update)}o(t0,"endOperation_W1");function r0(r){var i=r.cm,u=i.display;r.updatedDisplay&&ws(i),r.barMeasure=Mu(i),u.maxLineChanged&&!i.options.lineWrapping&&(r.adjustWidthTo=Kf(i,u.maxLine,u.maxLine.text.length).left+3,i.display.sizerWidth=r.adjustWidthTo,r.barMeasure.scrollWidth=Math.max(u.scroller.clientWidth,u.sizer.offsetLeft+r.adjustWidthTo+Vr(i)+i.display.barWidth),r.maxScrollLeft=Math.max(0,u.sizer.offsetLeft+r.adjustWidthTo-Bi(i))),(r.updatedDisplay||r.selectionChanged)&&(r.preparedSelection=u.input.prepareSelection())}o(r0,"endOperation_R2");function n0(r){var i=r.cm;r.adjustWidthTo!=null&&(i.display.sizer.style.minWidth=r.adjustWidthTo+"px",r.maxScrollLeft=r.display.viewTo)){var u=+new Date+r.options.workTime,a=Wo(r,i.highlightFrontier),c=[];i.iter(a.line,Math.min(i.first+i.size,r.display.viewTo+500),function(v){if(a.line>=r.display.viewFrom){var g=v.styles,S=v.text.length>r.options.maxHighlightLength?Fo(i.mode,a.state):null,C=mu(r,v,a,!0);S&&(a.state=S),v.styles=C.styles;var b=v.styleClasses,P=C.classes;P?v.styleClasses=P:b&&(v.styleClasses=null);for(var A=!g||g.length!=v.styles.length||b!=P&&(!b||!P||b.bgClass!=P.bgClass||b.textClass!=P.textClass),j=0;!A&&ju)return co(r,r.options.workDelay),!0}),i.highlightFrontier=a.line,i.modeFrontier=Math.max(i.modeFrontier,a.line),c.length&&Gr(r,function(){for(var v=0;v=u.viewFrom&&i.visible.to<=u.viewTo&&(u.updateLineNumbers==null||u.updateLineNumbers>=u.viewTo)&&u.renderedView==u.view&&cm(r)==0)return!1;po(r)&&($o(r),i.dims=$n(r));var c=a.first+a.size,v=Math.max(i.visible.from-r.options.viewportMargin,a.first),g=Math.min(c,i.visible.to+r.options.viewportMargin);u.viewFromg&&u.viewTo-g<20&&(g=Math.min(c,u.viewTo)),an&&(v=Su(r.doc,v),g=zp(r.doc,g));var S=v!=u.viewFrom||g!=u.viewTo||u.lastWrapHeight!=i.wrapperHeight||u.lastWrapWidth!=i.wrapperWidth;Xy(r,v,g),u.viewOffset=_e(Me(r.doc,u.viewFrom)),r.display.mover.style.top=u.viewOffset+"px";var C=cm(r);if(!S&&C==0&&!i.force&&u.renderedView==u.view&&(u.updateLineNumbers==null||u.updateLineNumbers>=u.viewTo))return!1;var b=o0(r);return C>4&&(u.lineDiv.style.display="none"),l0(r,u.updateLineNumbers,i.dims),C>4&&(u.lineDiv.style.display=""),u.renderedView=u.view,s0(b),qe(u.cursorDiv),qe(u.selectionDiv),u.gutters.style.height=u.sizer.style.minHeight=0,S&&(u.lastWrapHeight=i.wrapperHeight,u.lastWrapWidth=i.wrapperWidth,co(r,400)),u.updateLineNumbers=null,!0}o(Yp,"updateDisplayIfNeeded");function bs(r,i){for(var u=i.viewport,a=!0;;a=!1){if(!a||!r.options.lineWrapping||i.oldDisplayWidth==Bi(r)){if(u&&u.top!=null&&(u={top:Math.min(r.doc.height+Rr(r.display)-ta(r),u.top)}),i.visible=dl(r.display,r.doc,u),i.visible.from>=r.display.viewFrom&&i.visible.to<=r.display.viewTo)break}else a&&(i.visible=dl(r.display,r.doc,u));if(!Yp(r,i))break;ws(r);var c=Mu(r);Lu(r),_s(r,c),Zp(r,c),i.force=!1}i.signal(r,"update",r),(r.display.viewFrom!=r.display.reportedViewFrom||r.display.viewTo!=r.display.reportedViewTo)&&(i.signal(r,"viewportChange",r,r.display.viewFrom,r.display.viewTo),r.display.reportedViewFrom=r.display.viewFrom,r.display.reportedViewTo=r.display.viewTo)}o(bs,"postUpdateDisplay");function Xp(r,i){var u=new Ln(r,i);if(Yp(r,u)){ws(r),bs(r,u);var a=Mu(r);Lu(r),_s(r,a),Zp(r,a),u.finish()}}o(Xp,"updateDisplaySimple");function l0(r,i,u){var a=r.display,c=r.options.lineNumbers,v=a.lineDiv,g=v.firstChild;function S(ee){var oe=ee.nextSibling;return _&&R&&r.display.currentWheelTarget==ee?ee.style.display="none":ee.parentNode.removeChild(ee),oe}o(S,"rm");for(var C=a.view,b=a.viewFrom,P=0;P-1&&(z=!1),jf(r,A,b,u)),z&&(qe(A.lineNumber),A.lineNumber.appendChild(document.createTextNode(zl(r.options,b)))),g=A.node.nextSibling}b+=A.size}for(;g;)g=S(g)}o(l0,"patchDisplay");function Qp(r){var i=r.gutters.offsetWidth;r.sizer.style.marginLeft=i+"px",sr(r,"gutterChanged",r)}o(Qp,"updateGutterSpace");function Zp(r,i){r.display.sizer.style.minHeight=i.docHeight+"px",r.display.heightForcer.style.top=i.docHeight+"px",r.display.gutters.style.height=i.docHeight+r.display.barHeight+Vr(r)+"px"}o(Zp,"setDocumentHeight");function mm(r){var i=r.display,u=i.view;if(!(!i.alignWidgets&&(!i.gutters.firstChild||!r.options.fixedGutter))){for(var a=ia(i)-i.scroller.scrollLeft+r.doc.scrollLeft,c=i.gutters.offsetWidth,v=a+"px",g=0;gg.clientWidth,C=g.scrollHeight>g.clientHeight;if(!!(a&&S||c&&C)){if(c&&R&&_){e:for(var b=i.target,P=v.view;b!=g;b=b.parentNode)for(var A=0;A=0&&$e(r,a.to())<=0)return u}return-1};var yt=o(function(r,i){this.anchor=r,this.head=i},"Range");yt.prototype.from=function(){return Zs(this.anchor,this.head)},yt.prototype.to=function(){return as(this.anchor,this.head)},yt.prototype.empty=function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch};function Xr(r,i,u){var a=r&&r.options.selectionsMayTouch,c=i[u];i.sort(function(j,z){return $e(j.from(),z.from())}),u=at(i,c);for(var v=1;v0:C>=0){var b=Zs(S.from(),g.from()),P=as(S.to(),g.to()),A=S.empty()?g.from()==g.head:S.from()==S.head;v<=u&&--u,i.splice(--v,2,new yt(A?P:b,A?b:P))}}return new oi(i,u)}o(Xr,"normalizeSelection");function Es(r,i){return new oi([new yt(r,i||r)],0)}o(Es,"simpleSelection");function Ts(r){return r.text?ae(r.from.line+r.text.length-1,Se(r.text).length+(r.text.length==1?r.from.ch:0)):r.to}o(Ts,"changeEnd");function _i(r,i){if($e(r,i.from)<0)return r;if($e(r,i.to)<=0)return Ts(i);var u=r.line+i.text.length-(i.to.line-i.from.line)-1,a=r.ch;return r.line==i.to.line&&(a+=Ts(i).ch-i.to.ch),ae(u,a)}o(_i,"adjustForChange");function ed(r,i){for(var u=[],a=0;a1&&r.remove(S.line+1,ee-1),r.insert(S.line+1,me)}sr(r,"change",r,i)}o(Jf,"updateDoc");function ks(r,i,u){function a(c,v,g){if(c.linked)for(var S=0;S1&&!r.done[r.done.length-2].ranges)return r.done.pop(),Se(r.done)}o(c0,"lastChangeEvent");function ho(r,i,u,a){var c=r.history;c.undone.length=0;var v=+new Date,g,S;if((c.lastOp==a||c.lastOrigin==i.origin&&i.origin&&(i.origin.charAt(0)=="+"&&c.lastModTime>v-(r.cm?r.cm.options.historyEventDelay:500)||i.origin.charAt(0)=="*"))&&(g=c0(c,c.lastOp==a)))S=Se(g.changes),$e(i.from,i.to)==0&&$e(i.from,S.to)==0?S.to=Ts(i):g.changes.push(nd(r,i));else{var C=Se(c.done);for((!C||!C.ranges)&&Nn(r.sel,c.done),g={changes:[nd(r,i)],generation:c.generation},c.done.push(g);c.done.length>c.undoDepth;)c.done.shift(),c.done[0].ranges||c.done.shift()}c.done.push(u),c.generation=++c.maxGeneration,c.lastModTime=c.lastSelTime=v,c.lastOp=c.lastSelOp=a,c.lastOrigin=c.lastSelOrigin=i.origin,S||ke(r,"historyAdded")}o(ho,"addChangeToHistory");function od(r,i,u,a){var c=i.charAt(0);return c=="*"||c=="+"&&u.ranges.length==a.ranges.length&&u.somethingSelected()==a.somethingSelected()&&new Date-r.history.lastSelTime<=(r.cm?r.cm.options.historyEventDelay:500)}o(od,"selectionEventCanBeMerged");function gl(r,i,u,a){var c=r.history,v=a&&a.origin;u==c.lastSelOp||v&&c.lastSelOrigin==v&&(c.lastModTime==c.lastSelTime&&c.lastOrigin==v||od(r,v,Se(c.done),i))?c.done[c.done.length-1]=i:Nn(i,c.done),c.lastSelTime=+new Date,c.lastSelOrigin=v,c.lastSelOp=u,a&&a.clearRedo!==!1&&id(c.undone)}o(gl,"addSelectionToHistory");function Nn(r,i){var u=Se(i);u&&u.ranges&&u.equals(r)||i.push(r)}o(Nn,"pushSelectionToHistory");function Sm(r,i,u,a){var c=i["spans_"+r.id],v=0;r.iter(Math.max(r.first,u),Math.min(r.first+r.size,a),function(g){g.markedSpans&&((c||(c=i["spans_"+r.id]={}))[v]=g.markedSpans),++v})}o(Sm,"attachLocalSpans");function Cm(r){if(!r)return null;for(var i,u=0;u-1&&(Se(S)[A]=b[A],delete b[A])}}return a}o(si,"copyHistoryArray");function tc(r,i,u,a){if(a){var c=r.anchor;if(u){var v=$e(i,c)<0;v!=$e(u,c)<0?(c=i,i=u):v!=$e(i,u)<0&&(i=u)}return new yt(c,i)}else return new yt(u||i,i)}o(tc,"extendRange");function rc(r,i,u,a,c){c==null&&(c=r.cm&&(r.cm.display.shift||r.extend)),Ir(r,new oi([tc(r.sel.primary(),i,u,c)],0),a)}o(rc,"extendSelection");function Hu(r,i,u){for(var a=[],c=r.cm&&(r.cm.display.shift||r.extend),v=0;v=i.ch:S.to>i.ch))){if(c&&(ke(C,"beforeCursorEnter"),C.explicitlyCleared))if(v.markedSpans){--g;continue}else break;if(!C.atomic)continue;if(u){var A=C.find(a<0?1:-1),j=void 0;if((a<0?P:b)&&(A=oc(r,A,-a,A&&A.line==i.line?v:null)),A&&A.line==i.line&&(j=$e(A,u))&&(a<0?j<0:j>0))return yl(r,A,i,a,c)}var z=C.find(a<0?-1:1);return(a<0?b:P)&&(z=oc(r,z,a,z.line==i.line?v:null)),z?yl(r,z,i,a,c):null}}return i}o(yl,"skipAtomicInner");function Hr(r,i,u,a,c){var v=a||1,g=yl(r,i,u,v,c)||!c&&yl(r,i,u,v,!0)||yl(r,i,u,-v,c)||!c&&yl(r,i,u,-v,!0);return g||(r.cantEdit=!0,ae(r.first,0))}o(Hr,"skipAtomic");function oc(r,i,u,a){return u<0&&i.ch==0?i.line>r.first?Be(r,ae(i.line-1)):null:u>0&&i.ch==(a||Me(r,i.line)).text.length?i.line=0;--c)sc(r,{from:a[c].from,to:a[c].to,text:c?[""]:i.text,origin:i.origin});else sc(r,i)}}o(pa,"makeChange");function sc(r,i){if(!(i.text.length==1&&i.text[0]==""&&$e(i.from,i.to)==0)){var u=ed(r,i);ho(r,i,u,r.cm?r.cm.curOp.id:NaN),ha(r,i,u,yu(r,i));var a=[];ks(r,function(c,v){!v&&at(a,c.history)==-1&&(km(c.history,i),a.push(c.history)),ha(c,i,null,yu(c,i))})}}o(sc,"makeChangeInner");function lc(r,i,u){var a=r.cm&&r.cm.state.suppressEdits;if(!(a&&!u)){for(var c=r.history,v,g=r.sel,S=i=="undo"?c.done:c.undone,C=i=="undo"?c.undone:c.done,b=0;b=0;--z){var ee=j(z);if(ee)return ee.v}}}}o(lc,"makeChangeFromHistory");function da(r,i){if(i!=0&&(r.first+=i,r.sel=new oi(Er(r.sel.ranges,function(c){return new yt(ae(c.anchor.line+i,c.anchor.ch),ae(c.head.line+i,c.head.ch))}),r.sel.primIndex),r.cm)){Je(r.cm,r.first,r.first-i,i);for(var u=r.cm.display,a=u.viewFrom;ar.lastLine())){if(i.from.linev&&(i={from:i.from,to:ae(v,Me(r,v).text.length),text:[i.text[0]],origin:i.origin}),i.removed=wi(r,i.from,i.to),u||(u=ed(r,i)),r.cm?p0(r.cm,i,a):Jf(r,i,a),li(r,u,$t),r.cantEdit&&Hr(r,ae(r.firstLine(),0))&&(r.cantEdit=!1)}}o(ha,"makeChangeSingleDoc");function p0(r,i,u){var a=r.doc,c=r.display,v=i.from,g=i.to,S=!1,C=v.line;r.options.lineWrapping||(C=vt(kn(Me(a,v.line))),a.iter(C,g.line+1,function(z){if(z==c.maxLine)return S=!0,!0})),a.sel.contains(i.from,i.to)>-1&&Bl(r),Jf(a,i,u,fm(r)),r.options.lineWrapping||(a.iter(C,v.line+i.text.length,function(z){var ee=Bo(z);ee>c.maxLineLength&&(c.maxLine=z,c.maxLineLength=ee,c.maxLineChanged=!0,S=!1)}),S&&(r.curOp.updateMaxLine=!0)),Wp(a,v.line),co(r,400);var b=i.text.length-(g.line-v.line)-1;i.full?Je(r):v.line==g.line&&i.text.length==1&&!rd(r.doc,i)?ys(r,v.line,"text"):Je(r,v.line,g.line+1,b);var P=Rt(r,"changes"),A=Rt(r,"change");if(A||P){var j={from:v,to:g,text:i.text,removed:i.removed,origin:i.origin};A&&sr(r,"change",r,j),P&&(r.curOp.changeObjs||(r.curOp.changeObjs=[])).push(j)}r.display.selForContextMenu=null}o(p0,"makeChangeSingleDocInEditor");function ma(r,i,u,a,c){var v;a||(a=u),$e(a,u)<0&&(v=[a,u],u=v[0],a=v[1]),typeof i=="string"&&(i=r.splitLines(i)),pa(r,{from:u,to:a,text:i,origin:c})}o(ma,"replaceRange");function va(r,i,u,a){u1||!(this.children[0]instanceof ga))){var S=[];this.collapse(S),this.children=[new ga(S)],this.children[0].parent=this}},collapse:function(r){for(var i=0;i50){for(var g=c.lines.length%25+25,S=g;S10);r.parent.maybeSpill()}},iterN:function(r,i,u){for(var a=0;ar.display.maxLineLength&&(r.display.maxLine=b,r.display.maxLineLength=P,r.display.maxLineChanged=!0)}a!=null&&r&&this.collapsed&&Je(r,a,c+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,r&&Wu(r.doc)),r&&sr(r,"markerCleared",r,this,a,c),i&&fo(r),this.parent&&this.parent.clear()}},Ls.prototype.find=function(r,i){r==null&&this.type=="bookmark"&&(r=1);for(var u,a,c=0;c0||g==0&&v.clearWhenEmpty!==!1)return v;if(v.replacedWith&&(v.collapsed=!0,v.widgetNode=xt("span",[v.replacedWith],"CodeMirror-widget"),a.handleMouseEvents||v.widgetNode.setAttribute("cm-ignore-events","true"),a.insertLeft&&(v.widgetNode.insertLeft=!0)),v.collapsed){if(ye(r,i.line,i,u,v)||i.line!=u.line&&ye(r,u.line,i,u,v))throw new Error("Inserting collapsed marker partially overlapping an existing one");gu()}v.addToHistory&&ho(r,{from:i,to:u,origin:"markText"},r.sel,NaN);var S=i.line,C=r.cm,b;if(r.iter(S,u.line+1,function(A){C&&v.collapsed&&!C.options.lineWrapping&&kn(A)==C.display.maxLine&&(b=!0),v.collapsed&&S!=i.line&&ri(A,0),If(A,new Kl(v,S==i.line?i.ch:null,S==u.line?u.ch:null),r.cm&&r.cm.curOp),++S}),v.collapsed&&r.iter(i.line,u.line+1,function(A){Nt(r,A)&&ri(A,0)}),v.clearOnEnter&&H(v,"beforeCursorEnter",function(){return v.clear()}),v.readOnly&&(vu(),(r.history.done.length||r.history.undone.length)&&r.clearHistory()),v.collapsed&&(v.id=++ac,v.atomic=!0),C){if(b&&(C.curOp.updateMaxLine=!0),v.collapsed)Je(C,i.line,u.line+1);else if(v.className||v.startStyle||v.endStyle||v.css||v.attributes||v.title)for(var P=i.line;P<=u.line;P++)ys(C,P,"text");v.atomic&&Wu(C.doc),sr(C,"markerAdded",C,v)}return v}o(Ns,"markText");var ya=o(function(r,i){this.markers=r,this.primary=i;for(var u=0;u=0;C--)pa(this,a[C]);S?nc(this,S):this.cm&&Ss(this.cm)}),undo:k(function(){lc(this,"undo")}),redo:k(function(){lc(this,"redo")}),undoSelection:k(function(){lc(this,"undo",!0)}),redoSelection:k(function(){lc(this,"redo",!0)}),setExtending:function(r){this.extend=r},getExtending:function(){return this.extend},historySize:function(){for(var r=this.history,i=0,u=0,a=0;a=r.ch)&&i.push(c.marker.parent||c.marker)}return i},findMarks:function(r,i,u){r=Be(this,r),i=Be(this,i);var a=[],c=r.line;return this.iter(r.line,i.line+1,function(v){var g=v.markedSpans;if(g)for(var S=0;S=C.to||C.from==null&&c!=r.line||C.from!=null&&c==i.line&&C.from>=i.ch)&&(!u||u(C.marker))&&a.push(C.marker.parent||C.marker)}++c}),a},getAllMarks:function(){var r=[];return this.iter(function(i){var u=i.markedSpans;if(u)for(var a=0;ar)return i=r,!0;r-=v,++u}),Be(this,ae(u,i))},indexFromPos:function(r){r=Be(this,r);var i=r.ch;if(r.linei&&(i=r.from),r.to!=null&&r.to-1){i.state.draggingText(r),setTimeout(function(){return i.display.input.focus()},20);return}try{var P=r.dataTransfer.getData("Text");if(P){var A;if(i.state.draggingText&&!i.state.draggingText.copy&&(A=i.listSelections()),li(i.doc,Es(u,u)),A)for(var j=0;j=0;S--)ma(r.doc,"",a[S].from,a[S].to,"+delete");Ss(r)})}o(ai,"deleteNearSelection");function ju(r,i,u){var a=Fi(r.text,i+u,u);return a<0||a>r.text.length?null:a}o(ju,"moveCharLogically");function pc(r,i,u){var a=ju(r,i.ch,u);return a==null?null:new ae(i.line,a,u<0?"after":"before")}o(pc,"moveLogically");function wa(r,i,u,a,c){if(r){i.doc.direction=="rtl"&&(c=-c);var v=En(u,i.doc.direction);if(v){var g=c<0?Se(v):v[0],S=c<0==(g.level==1),C=S?"after":"before",b;if(g.level>0||i.doc.direction=="rtl"){var P=ni(i,u);b=c<0?u.text.length-1:0;var A=ii(i,P,b).top;b=sn(function(j){return ii(i,P,j).top==A},c<0==(g.level==1)?g.from:g.to-1,b),C=="before"&&(b=ju(u,b,1))}else b=c<0?g.to:g.from;return new ae(a,b,C)}}return new ae(a,c<0?u.text.length:0,c<0?"before":"after")}o(wa,"endOfLine");function Rm(r,i,u,a){var c=En(i,r.doc.direction);if(!c)return pc(i,u,a);u.ch>=i.text.length?(u.ch=i.text.length,u.sticky="before"):u.ch<=0&&(u.ch=0,u.sticky="after");var v=gi(c,u.ch,u.sticky),g=c[v];if(r.doc.direction=="ltr"&&g.level%2==0&&(a>0?g.to>u.ch:g.from=g.from&&j>=P.begin)){var z=A?"before":"after";return new ae(u.line,j,z)}}var ee=o(function(me,be,ve){for(var Oe=o(function(kt,yr){return yr?new ae(u.line,S(kt,1),"before"):new ae(u.line,kt,"after")},"getRes");me>=0&&me0==(Fe.level!=1),rt=Re?ve.begin:S(ve.end,-1);if(Fe.from<=rt&&rt0?P.end:S(P.begin,-1);return ce!=null&&!(a>0&&ce==i.text.length)&&(oe=ee(a>0?0:c.length-1,a,b(ce)),oe)?oe:null}o(Rm,"moveVisually");var xl={selectAll:bm,singleSelection:function(r){return r.setSelection(r.getCursor("anchor"),r.getCursor("head"),$t)},killLine:function(r){return ai(r,function(i){if(i.empty()){var u=Me(r.doc,i.head.line).text.length;return i.head.ch==u&&i.head.line0)c=new ae(c.line,c.ch+1),r.replaceRange(v.charAt(c.ch-1)+v.charAt(c.ch-2),ae(c.line,c.ch-2),c,"+transpose");else if(c.line>r.doc.first){var g=Me(r.doc,c.line-1).text;g&&(c=new ae(c.line,1),r.replaceRange(v.charAt(0)+r.doc.lineSeparator()+g.charAt(g.length-1),ae(c.line-1,g.length-1),c,"+transpose"))}}u.push(new yt(c,c))}r.setSelections(u)})},newlineAndIndent:function(r){return Gr(r,function(){for(var i=r.listSelections(),u=i.length-1;u>=0;u--)r.replaceRange(r.doc.lineSeparator(),i[u].anchor,i[u].head,"+input");i=r.listSelections();for(var a=0;ar&&$e(i,this.pos)==0&&u==this.button};var Wr,zn;function w0(r,i){var u=+new Date;return zn&&zn.compare(u,r,i)?(Wr=zn=null,"triple"):Wr&&Wr.compare(u,r,i)?(zn=new mc(u,r,i),Wr=null,"double"):(Wr=new mc(u,r,i),zn=null,"single")}o(w0,"clickRepeat");function Bm(r){var i=this,u=i.display;if(!(Zt(i,r)||u.activeTouch&&u.input.supportsTouch())){if(u.input.ensurePolled(),u.shift=r.shiftKey,Wi(u,r)){_||(u.scroller.draggable=!1,setTimeout(function(){return u.scroller.draggable=!0},100));return}if(!wd(i,r)){var a=ao(i,r),c=Gs(r),v=a?w0(a,c):"single";window.focus(),c==1&&i.state.selectingText&&i.state.selectingText(r),!(a&&vc(i,c,a,v,r))&&(c==1?a?Um(i,a,v,r):yi(r)==u.scroller&&Jt(r):c==2?(a&&rc(i.doc,a),setTimeout(function(){return u.input.focus()},20)):c==3&&(de?i.display.input.onContextMenu(r):Gf(i)))}}}o(Bm,"onMouseDown");function vc(r,i,u,a,c){var v="Click";return a=="double"?v="Double"+v:a=="triple"&&(v="Triple"+v),v=(i==1?"Left":i==2?"Middle":"Right")+v,xa(r,pd(v,c),c,function(g){if(typeof g=="string"&&(g=xl[g]),!g)return!1;var S=!1;try{r.isReadOnly()&&(r.state.suppressEdits=!0),S=g(r,u)!=Ut}finally{r.state.suppressEdits=!1}return S})}o(vc,"handleMappedButton");function Sa(r,i,u){var a=r.getOption("configureMouse"),c=a?a(r,i,u):{};if(c.unit==null){var v=K?u.shiftKey&&u.metaKey:u.altKey;c.unit=v?"rectangle":i=="single"?"char":i=="double"?"word":"line"}return(c.extend==null||r.doc.extend)&&(c.extend=r.doc.extend||u.shiftKey),c.addNew==null&&(c.addNew=R?u.metaKey:u.ctrlKey),c.moveOnDrag==null&&(c.moveOnDrag=!(R?u.altKey:u.ctrlKey)),c}o(Sa,"configureMouse");function Um(r,i,u,a){p?setTimeout(Ar(Gp,r),0):r.curOp.focus=Ke();var c=Sa(r,u,a),v=r.doc.sel,g;r.options.dragDrop&&ss&&!r.isReadOnly()&&u=="single"&&(g=v.contains(i))>-1&&($e((g=v.ranges[g]).from(),i)<0||i.xRel>0)&&($e(g.to(),i)>0||i.xRel<0)?$m(r,a,i,c):jm(r,a,i,c)}o(Um,"leftButtonDown");function $m(r,i,u,a){var c=r.display,v=!1,g=tr(r,function(b){_&&(c.scroller.draggable=!1),r.state.draggingText=!1,r.state.delayingBlurEvent&&(r.hasFocus()?r.state.delayingBlurEvent=!1:Gf(r)),he(c.wrapper.ownerDocument,"mouseup",g),he(c.wrapper.ownerDocument,"mousemove",S),he(c.scroller,"dragstart",C),he(c.scroller,"drop",g),v||(Jt(b),a.addNew||rc(r.doc,u,null,null,a.extend),_&&!U||p&&x==9?setTimeout(function(){c.wrapper.ownerDocument.body.focus({preventScroll:!0}),c.input.focus()},20):c.input.focus())}),S=o(function(b){v=v||Math.abs(i.clientX-b.clientX)+Math.abs(i.clientY-b.clientY)>=10},"mouseMove"),C=o(function(){return v=!0},"dragStart");_&&(c.scroller.draggable=!0),r.state.draggingText=g,g.copy=!a.moveOnDrag,H(c.wrapper.ownerDocument,"mouseup",g),H(c.wrapper.ownerDocument,"mousemove",S),H(c.scroller,"dragstart",C),H(c.scroller,"drop",g),r.state.delayingBlurEvent=!0,setTimeout(function(){return c.input.focus()},20),c.scroller.dragDrop&&c.scroller.dragDrop()}o($m,"leftButtonStartDrag");function zm(r,i,u){if(u=="char")return new yt(i,i);if(u=="word")return r.findWordAt(i);if(u=="line")return new yt(ae(i.line,0),Be(r.doc,ae(i.line+1,0)));var a=u(r,i);return new yt(a.from,a.to)}o(zm,"rangeForUnit");function jm(r,i,u,a){p&&Gf(r);var c=r.display,v=r.doc;Jt(i);var g,S,C=v.sel,b=C.ranges;if(a.addNew&&!a.extend?(S=v.sel.contains(u),S>-1?g=b[S]:g=new yt(u,u)):(g=v.sel.primary(),S=v.sel.primIndex),a.unit=="rectangle")a.addNew||(g=new yt(u,u)),u=ao(r,i,!0,!0),S=-1;else{var P=zm(r,u,a.unit);a.extend?g=tc(g,P.anchor,P.head,a.extend):g=P}a.addNew?S==-1?(S=b.length,Ir(v,Xr(r,b.concat([g]),S),{scroll:!1,origin:"*mouse"})):b.length>1&&b[S].empty()&&a.unit=="char"&&!a.extend?(Ir(v,Xr(r,b.slice(0,S).concat(b.slice(S+1)),0),{scroll:!1,origin:"*mouse"}),C=v.sel):sd(v,S,g,ne):(S=0,Ir(v,new oi([g],0),ne),C=v.sel);var A=u;function j(ve){if($e(A,ve)!=0)if(A=ve,a.unit=="rectangle"){for(var Oe=[],Fe=r.options.tabSize,Re=Et(Me(v,u.line).text,u.ch,Fe),rt=Et(Me(v,ve.line).text,ve.ch,Fe),kt=Math.min(Re,rt),yr=Math.max(Re,rt),Mt=Math.min(u.line,ve.line),hn=Math.min(r.lastLine(),Math.max(u.line,ve.line));Mt<=hn;Mt++){var mn=Me(v,Mt).text,ar=br(mn,kt,Fe);kt==yr?Oe.push(new yt(ae(Mt,ar),ae(Mt,ar))):mn.length>ar&&Oe.push(new yt(ae(Mt,ar),ae(Mt,br(mn,yr,Fe))))}Oe.length||Oe.push(new yt(u,u)),Ir(v,Xr(r,C.ranges.slice(0,S).concat(Oe),S),{origin:"*mouse",scroll:!1}),r.scrollIntoView(ve)}else{var Br=g,kr=zm(r,ve,a.unit),Lt=Br.anchor,Ft;$e(kr.anchor,Lt)>0?(Ft=kr.head,Lt=Zs(Br.from(),kr.anchor)):(Ft=kr.anchor,Lt=as(Br.to(),kr.head));var rr=C.ranges.slice(0);rr[S]=Ca(r,new yt(Be(v,Lt),Ft)),Ir(v,Xr(r,rr,S),ne)}}o(j,"extendTo");var z=c.wrapper.getBoundingClientRect(),ee=0;function oe(ve){var Oe=++ee,Fe=ao(r,ve,!0,a.unit=="rectangle");if(!!Fe)if($e(Fe,A)!=0){r.curOp.focus=Ke(),j(Fe);var Re=dl(c,v);(Fe.line>=Re.to||Fe.linez.bottom?20:0;rt&&setTimeout(tr(r,function(){ee==Oe&&(c.scroller.scrollTop+=rt,oe(ve))}),50)}}o(oe,"extend");function ce(ve){r.state.selectingText=!1,ee=1/0,ve&&(Jt(ve),c.input.focus()),he(c.wrapper.ownerDocument,"mousemove",me),he(c.wrapper.ownerDocument,"mouseup",be),v.history.lastSelOrigin=null}o(ce,"done");var me=tr(r,function(ve){ve.buttons===0||!Gs(ve)?ce(ve):oe(ve)}),be=tr(r,ce);r.state.selectingText=be,H(c.wrapper.ownerDocument,"mousemove",me),H(c.wrapper.ownerDocument,"mouseup",be)}o(jm,"leftButtonSelect");function Ca(r,i){var u=i.anchor,a=i.head,c=Me(r.doc,u.line);if($e(u,a)==0&&u.sticky==a.sticky)return i;var v=En(c);if(!v)return i;var g=gi(v,u.ch,u.sticky),S=v[g];if(S.from!=u.ch&&S.to!=u.ch)return i;var C=g+(S.from==u.ch==(S.level!=1)?0:1);if(C==0||C==v.length)return i;var b;if(a.line!=u.line)b=(a.line-u.line)*(r.doc.direction=="ltr"?1:-1)>0;else{var P=gi(v,a.ch,a.sticky),A=P-g||(a.ch-u.ch)*(S.level==1?-1:1);P==C-1||P==C?b=A<0:b=A>0}var j=v[C+(b?-1:0)],z=b==(j.level==1),ee=z?j.from:j.to,oe=z?"after":"before";return u.ch==ee&&u.sticky==oe?i:new yt(new ae(u.line,ee,oe),a)}o(Ca,"bidiSimplify");function _a(r,i,u,a){var c,v;if(i.touches)c=i.touches[0].clientX,v=i.touches[0].clientY;else try{c=i.clientX,v=i.clientY}catch(j){return!1}if(c>=Math.floor(r.display.gutters.getBoundingClientRect().right))return!1;a&&Jt(i);var g=r.display,S=g.lineDiv.getBoundingClientRect();if(v>S.bottom||!Rt(r,u))return os(i);v-=S.top-g.viewOffset;for(var C=0;C=c){var P=ro(r.doc,v),A=r.display.gutterSpecs[C];return ke(r,u,r,P,A.className,i),os(i)}}}o(_a,"gutterEvent");function wd(r,i){return _a(r,i,"gutterClick",!0)}o(wd,"clickInGutter");function xd(r,i){Wi(r.display,i)||qm(r,i)||Zt(r,i,"contextmenu")||de||r.display.input.onContextMenu(i)}o(xd,"onContextMenu");function qm(r,i){return Rt(r,"gutterContextMenu")?_a(r,i,"gutterContextMenu",!1):!1}o(qm,"contextMenuInGutter");function qu(r){r.display.wrapper.className=r.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+r.options.theme.replace(/(^|\s)\s*/g," cm-s-"),Un(r)}o(qu,"themeChanged");var Sl={toString:function(){return"CodeMirror.Init"}},Vu={},ba={};function gc(r){var i=r.optionHandlers;function u(a,c,v,g){r.defaults[a]=c,v&&(i[a]=g?function(S,C,b){b!=Sl&&v(S,C,b)}:v)}o(u,"option"),r.defineOption=u,r.Init=Sl,u("value","",function(a,c){return a.setValue(c)},!0),u("mode",null,function(a,c){a.doc.modeOption=c,td(a)},!0),u("indentUnit",2,td,!0),u("indentWithTabs",!1),u("smartIndent",!0),u("tabSize",4,function(a){Fu(a),Un(a),Je(a)},!0),u("lineSeparator",null,function(a,c){if(a.doc.lineSep=c,!!c){var v=[],g=a.doc.first;a.doc.iter(function(C){for(var b=0;;){var P=C.text.indexOf(c,b);if(P==-1)break;b=P+c.length,v.push(ae(g,P))}g++});for(var S=v.length-1;S>=0;S--)ma(a.doc,c,v[S],ae(v[S].line,v[S].ch+c.length))}}),u("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\ufeff\ufff9-\ufffc]/g,function(a,c,v){a.state.specialChars=new RegExp(c.source+(c.test(" ")?"":"| "),"g"),v!=Sl&&a.refresh()}),u("specialCharPlaceholder",On,function(a){return a.refresh()},!0),u("electricChars",!0),u("inputStyle",M?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),u("spellcheck",!1,function(a,c){return a.getInputField().spellcheck=c},!0),u("autocorrect",!1,function(a,c){return a.getInputField().autocorrect=c},!0),u("autocapitalize",!1,function(a,c){return a.getInputField().autocapitalize=c},!0),u("rtlMoveVisually",!V),u("wholeLineUpdateBefore",!0),u("theme","default",function(a){qu(a),Ru(a)},!0),u("keyMap","default",function(a,c,v){var g=pn(c),S=v!=Sl&&pn(v);S&&S.detach&&S.detach(a,g),g.attach&&g.attach(a,S||null)}),u("extraKeys",null),u("configureMouse",null),u("lineWrapping",!1,Vm,!0),u("gutters",[],function(a,c){a.display.gutterSpecs=Jp(c,a.options.lineNumbers),Ru(a)},!0),u("fixedGutter",!0,function(a,c){a.display.gutters.style.left=c?ia(a.display)+"px":"0",a.refresh()},!0),u("coverGutterNextToScrollbar",!1,function(a){return _s(a)},!0),u("scrollbarStyle","native",function(a){ml(a),_s(a),a.display.scrollbars.setScrollTop(a.doc.scrollTop),a.display.scrollbars.setScrollLeft(a.doc.scrollLeft)},!0),u("lineNumbers",!1,function(a,c){a.display.gutterSpecs=Jp(a.options.gutters,c),Ru(a)},!0),u("firstLineNumber",1,Ru,!0),u("lineNumberFormatter",function(a){return a},Ru,!0),u("showCursorWhenSelecting",!1,Lu,!0),u("resetSelectionOnContextMenu",!0),u("lineWiseCopyCut",!0),u("pasteLinesPerSelection",!0),u("selectionsMayTouch",!1),u("readOnly",!1,function(a,c){c=="nocursor"&&(pl(a),a.display.input.blur()),a.display.input.readOnlyChanged(c)}),u("screenReaderLabel",null,function(a,c){c=c===""?null:c,a.display.input.screenReaderLabelChanged(c)}),u("disableInput",!1,function(a,c){c||a.display.input.reset()},!0),u("dragDrop",!0,x0),u("allowDropFileTypes",null),u("cursorBlinkRate",530),u("cursorScrollMargin",0),u("cursorHeight",1,Lu,!0),u("singleCursorHeightPerLine",!0,Lu,!0),u("workTime",100),u("workDelay",100),u("flattenSpans",!0,Fu,!0),u("addModeClass",!1,Fu,!0),u("pollInterval",100),u("undoDepth",200,function(a,c){return a.doc.history.undoDepth=c}),u("historyEventDelay",1250),u("viewportMargin",10,function(a){return a.refresh()},!0),u("maxHighlightLength",1e4,Fu,!0),u("moveInputWithCursor",!0,function(a,c){c||a.display.input.resetPosition()}),u("tabindex",null,function(a,c){return a.display.input.getField().tabIndex=c||""}),u("autofocus",null),u("direction","ltr",function(a,c){return a.doc.setDirection(c)},!0),u("phrases",null)}o(gc,"defineOptions");function x0(r,i,u){var a=u&&u!=Sl;if(!i!=!a){var c=r.display.dragFunctions,v=i?H:he;v(r.display.scroller,"dragstart",c.start),v(r.display.scroller,"dragenter",c.enter),v(r.display.scroller,"dragover",c.over),v(r.display.scroller,"dragleave",c.leave),v(r.display.scroller,"drop",c.drop)}}o(x0,"dragDropChanged");function Vm(r){r.options.lineWrapping?(Ye(r.display.wrapper,"CodeMirror-wrap"),r.display.sizer.style.minWidth="",r.display.sizerWidth=null):(xe(r.display.wrapper,"CodeMirror-wrap"),fs(r)),gs(r),Je(r),Un(r),setTimeout(function(){return _s(r)},100)}o(Vm,"wrappingChanged");function Pt(r,i){var u=this;if(!(this instanceof Pt))return new Pt(r,i);this.options=i=i?Kt(i):{},Kt(Vu,i,!1);var a=i.value;typeof a=="string"?a=new cn(a,i.mode,null,i.lineSeparator,i.direction):i.mode&&(a.modeOption=i.mode),this.doc=a;var c=new Pt.inputStyles[i.inputStyle](this),v=this.display=new a0(r,a,c,i);v.wrapper.CodeMirror=this,qu(this),i.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),ml(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new St,keySeq:null,specialChars:null},i.autofocus&&!M&&v.input.focus(),p&&x<11&&setTimeout(function(){return u.display.input.reset(!0)},20),Km(this),cd(),Ui(this),this.curOp.forceUpdate=!0,xm(this,a),i.autofocus&&!M||this.hasFocus()?setTimeout(function(){u.hasFocus()&&!u.state.focused&&sa(u)},20):pl(this);for(var g in ba)ba.hasOwnProperty(g)&&ba[g](this,i[g],Sl);po(this),i.finishInit&&i.finishInit(this);for(var S=0;S20*20}o(g,"farAway"),H(i.scroller,"touchstart",function(C){if(!Zt(r,C)&&!v(C)&&!wd(r,C)){i.input.ensurePolled(),clearTimeout(u);var b=+new Date;i.activeTouch={start:b,moved:!1,prev:b-a.end<=300?a:null},C.touches.length==1&&(i.activeTouch.left=C.touches[0].pageX,i.activeTouch.top=C.touches[0].pageY)}}),H(i.scroller,"touchmove",function(){i.activeTouch&&(i.activeTouch.moved=!0)}),H(i.scroller,"touchend",function(C){var b=i.activeTouch;if(b&&!Wi(i,C)&&b.left!=null&&!b.moved&&new Date-b.start<300){var P=r.coordsChar(i.activeTouch,"page"),A;!b.prev||g(b,b.prev)?A=new yt(P,P):!b.prev.prev||g(b,b.prev.prev)?A=r.findWordAt(P):A=new yt(ae(P.line,0),Be(r.doc,ae(P.line+1,0))),r.setSelection(A.anchor,A.head),r.focus(),Jt(C)}c()}),H(i.scroller,"touchcancel",c),H(i.scroller,"scroll",function(){i.scroller.clientHeight&&(er(r,i.scroller.scrollTop),hl(r,i.scroller.scrollLeft,!0),ke(r,"scroll",r))}),H(i.scroller,"mousewheel",function(C){return ym(r,C)}),H(i.scroller,"DOMMouseScroll",function(C){return ym(r,C)}),H(i.wrapper,"scroll",function(){return i.wrapper.scrollTop=i.wrapper.scrollLeft=0}),i.dragFunctions={enter:function(C){Zt(r,C)||to(C)},over:function(C){Zt(r,C)||(ud(r,C),to(C))},start:function(C){return h0(r,C)},drop:tr(r,Am),leave:function(C){Zt(r,C)||fd(r)}};var S=i.input.getField();H(S,"keyup",function(C){return yd.call(r,C)}),H(S,"keydown",tr(r,Fm)),H(S,"keypress",tr(r,Hm)),H(S,"focus",function(C){return sa(r,C)}),H(S,"blur",function(C){return pl(r,C)})}o(Km,"registerEventHandlers");var Ku=[];Pt.defineInitHook=function(r){return Ku.push(r)};function Gu(r,i,u,a){var c=r.doc,v;u==null&&(u="add"),u=="smart"&&(c.mode.indent?v=Wo(r,i).state:u="prev");var g=r.options.tabSize,S=Me(c,i),C=Et(S.text,null,g);S.stateAfter&&(S.stateAfter=null);var b=S.text.match(/^\s*/)[0],P;if(!a&&!/\S/.test(S.text))P=0,u="not";else if(u=="smart"&&(P=c.mode.indent(v,S.text.slice(b.length),S.text),P==Ut||P>150)){if(!a)return;u="prev"}u=="prev"?i>c.first?P=Et(Me(c,i-1).text,null,g):P=0:u=="add"?P=C+r.options.indentUnit:u=="subtract"?P=C-r.options.indentUnit:typeof u=="number"&&(P=C+u),P=Math.max(0,P);var A="",j=0;if(r.options.indentWithTabs)for(var z=Math.floor(P/g);z;--z)j+=g,A+=" ";if(jg,C=Xs(i),b=null;if(S&&a.ranges.length>1)if(Ei&&Ei.text.join(` +`)==i){if(a.ranges.length%Ei.text.length==0){b=[];for(var P=0;P=0;j--){var z=a.ranges[j],ee=z.from(),oe=z.to();z.empty()&&(u&&u>0?ee=ae(ee.line,ee.ch-u):r.state.overwrite&&!S?oe=ae(oe.line,Math.min(Me(v,oe.line).text.length,oe.ch+Se(C).length)):S&&Ei&&Ei.lineWise&&Ei.text.join(` `)==C.join(` -`)&&(ee=oe=ae(ee.line,0)));var ce={from:ee,to:oe,text:b?b[$%b.length]:C,origin:c||(S?"paste":r.state.cutIncoming>g?"cut":"+input")};fa(r.doc,ce),ir(r,"inputRead",r,ce)}i&&!S&&qm(r,i),_s(r),r.curOp.updateInput<2&&(r.curOp.updateInput=A),r.curOp.typing=!0,r.state.pasteIncoming=r.state.cutIncoming=-1}o(uc,"applyTextInput");function dd(r,i){var u=r.clipboardData&&r.clipboardData.getData("Text");if(u)return r.preventDefault(),!i.isReadOnly()&&!i.options.disableInput&&Gr(i,function(){return uc(i,u,0,null,"paste")}),!0}o(dd,"handlePaste");function qm(r,i){if(!(!r.options.electricChars||!r.options.smartIndent))for(var u=r.doc.sel,a=u.ranges.length-1;a>=0;a--){var c=u.ranges[a];if(!(c.head.ch>100||a&&u.ranges[a-1].head.line==c.head.line)){var m=r.getModeAt(c.head),g=!1;if(m.electricChars){for(var S=0;S-1){g=Bu(r,c.head.line,"smart");break}}else m.electricInput&&m.electricInput.test(Ne(r.doc,c.head.line).text.slice(0,c.head.ch))&&(g=Bu(r,c.head.line,"smart"));g&&ir(r,"electricInput",r,c.head.line)}}}o(qm,"triggerElectric");function hd(r){for(var i=[],u=[],a=0;am&&(Bu(this,S.head.line,a,!0),m=S.head.line,g==this.doc.sel.primIndex&&_s(this));else{var C=S.from(),b=S.to(),N=Math.max(m,C.line);m=Math.min(this.lastLine(),b.line-(b.ch?0:1))+1;for(var A=N;A0&&Zp(this.doc,g,new vt(C,$[g].to()),Ut)}}}),getTokenAt:function(a,c){return Vl(this,a,c)},getLineTokens:function(a,c){return Vl(this,ae(a),c,!0)},getTokenTypeAt:function(a){a=Ue(this.doc,a);var c=cu(this,Ne(this.doc,a.line)),m=0,g=(c.length-1)/2,S=a.ch,C;if(S==0)C=c[2];else for(;;){var b=m+g>>1;if((b?c[b*2-1]:0)>=S)g=b;else if(c[b*2+1]C&&(a=C,g=!0),S=Ne(this.doc,a)}else S=a;return If(this,S,{top:0,left:0},c||"page",m||g).top+(g?this.doc.height-Ir(S):0)},defaultTextHeight:function(){return ys(this.display)},defaultCharWidth:function(){return ta(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(a,c,m,g,S){var C=this.display;a=Bi(this,Ue(this.doc,a));var b=a.bottom,N=a.left;if(c.style.position="absolute",c.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(c),C.sizer.appendChild(c),g=="over")b=a.top;else if(g=="above"||g=="near"){var A=Math.max(C.wrapper.clientHeight,this.doc.height),$=Math.max(C.sizer.clientWidth,C.lineSpace.clientWidth);(g=="above"||a.bottom+c.offsetHeight>A)&&a.top>c.offsetHeight?b=a.top-c.offsetHeight:a.bottom+c.offsetHeight<=A&&(b=a.bottom),N+c.offsetWidth>$&&(N=$-c.offsetWidth)}c.style.top=b+"px",c.style.left=c.style.right="",S=="right"?(N=C.sizer.clientWidth-c.offsetWidth,c.style.right="0px"):(S=="left"?N=0:S=="middle"&&(N=(C.sizer.clientWidth-c.offsetWidth)/2),c.style.left=N+"px"),m&&Qy(this,{left:N,top:b,right:N+c.offsetWidth,bottom:b+c.offsetHeight})},triggerOnKeyDown:Yr(Am),triggerOnKeyPress:Yr(Rm),triggerOnKeyUp:fd,triggerOnMouseDown:Yr(Im),execCommand:function(a){if(yl.hasOwnProperty(a))return yl[a].call(null,this)},triggerElectric:Yr(function(a){qm(this,a)}),findPosH:function(a,c,m,g){var S=1;c<0&&(S=-1,c=-c);for(var C=Ue(this.doc,a),b=0;b0&&N(m.charAt(g-1));)--g;for(;S.5||this.options.lineWrapping)&&ws(this),Ee(this,"refresh",this)}),swapDoc:Yr(function(a){var c=this.doc;return c.cm=null,this.state.selectingText&&this.state.selectingText(),gm(this,a),xu(this),this.display.input.reset(),_u(this,a.scrollLeft,a.scrollTop),this.curOp.forceScroll=!0,ir(this,"swapDoc",this,c),c}),phrase:function(a){var c=this.options.phrases;return c&&Object.prototype.hasOwnProperty.call(c,a)?c[a]:a},getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Rr(r),r.registerHelper=function(a,c,m){u.hasOwnProperty(a)||(u[a]=r[a]={_global:[]}),u[a][c]=m},r.registerGlobalHelper=function(a,c,m,g){r.registerHelper(a,c,g),u[a]._global.push({pred:m,val:g})}}o(Ko,"addEditorMethods");function Uu(r,i,u,a,c){var m=i,g=u,S=Ne(r,i.line),C=c&&r.direction=="rtl"?-u:u;function b(){var Ce=i.line+C;return Ce=r.first+r.size?!1:(i=new ae(Ce,i.ch,i.sticky),S=Ne(r,Ce))}o(b,"findNextLine");function N(Ce){var ve;if(a=="codepoint"){var Te=S.text.charCodeAt(i.ch+(u>0?0:-1));if(isNaN(Te))ve=null;else{var Fe=u>0?Te>=55296&&Te<56320:Te>=56320&&Te<57343;ve=new ae(i.line,Math.max(0,Math.min(S.text.length,i.ch+u*(Fe?2:1))),-u)}}else c?ve=Mm(r.cm,S,i,u):ve=nc(S,i,u);if(ve==null)if(!Ce&&b())i=ga(c,r.cm,S,i.line,C);else return!1;else i=ve;return!0}if(o(N,"moveOnce"),a=="char"||a=="codepoint")N();else if(a=="column")N(!0);else if(a=="word"||a=="group")for(var A=null,$=a=="group",z=r.cm&&r.cm.getHelper(i,"wordChars"),ee=!0;!(u<0&&!N(!ee));ee=!1){var oe=S.text.charAt(i.ch)||` -`,ce=cr(oe,z)?"w":$&&oe==` -`?"n":!$||/\s/.test(oe)?null:"p";if($&&!ee&&!ce&&(ce="s"),A&&A!=ce){u<0&&(u=1,N(),i.sticky="after");break}if(ce&&(A=ce),u>0&&!N(!ee))break}var me=Wr(r,i,m,g,!0);return Ul(m,me)&&(me.hitSide=!0),me}o(Uu,"findPosH");function fc(r,i,u,a){var c=r.doc,m=i.left,g;if(a=="page"){var S=Math.min(r.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight),C=Math.max(S-.5*ys(r.display),3);g=(u>0?i.bottom:i.top)+u*C}else a=="line"&&(g=u>0?i.bottom+3:i.top-3);for(var b;b=q(r,m,g),!!b.outside;){if(u<0?g<=0:g>=c.height){b.hitSide=!0;break}g+=u*5}return b}o(fc,"findPosV");var gt=o(function(r){this.cm=r,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new wt,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null},"ContentEditableInput");gt.prototype.init=function(r){var i=this,u=this,a=u.cm,c=u.div=r.lineDiv;c.contentEditable=!0,Vm(c,a.options.spellcheck,a.options.autocorrect,a.options.autocapitalize);function m(S){for(var C=S.target;C;C=C.parentNode){if(C==c)return!0;if(/\bCodeMirror-(?:line)?widget\b/.test(C.className))break}return!1}o(m,"belongsToInput"),H(c,"paste",function(S){!m(S)||Xt(a,S)||dd(S,a)||w<=11&&setTimeout(Jt(a,function(){return i.updateFromDOM()}),20)}),H(c,"compositionstart",function(S){i.composing={data:S.data,done:!1}}),H(c,"compositionupdate",function(S){i.composing||(i.composing={data:S.data,done:!1})}),H(c,"compositionend",function(S){i.composing&&(S.data!=i.composing.data&&i.readFromDOMSoon(),i.composing.done=!0)}),H(c,"touchstart",function(){return u.forceCompositionEnd()}),H(c,"input",function(){i.composing||i.readFromDOMSoon()});function g(S){if(!(!m(S)||Xt(a,S))){if(a.somethingSelected())Ei({lineWise:!1,text:a.getSelections()}),S.type=="cut"&&a.replaceSelection("",null,"cut");else if(a.options.lineWiseCopyCut){var C=hd(a);Ei({lineWise:!0,text:C.text}),S.type=="cut"&&a.operation(function(){a.setSelections(C.ranges,0,Ut),a.replaceSelection("",null,"cut")})}else return;if(S.clipboardData){S.clipboardData.clearData();var b=bi.text.join(` -`);if(S.clipboardData.setData("Text",b),S.clipboardData.getData("Text")==b){S.preventDefault();return}}var N=Km(),A=N.firstChild;a.display.lineSpace.insertBefore(N,a.display.lineSpace.firstChild),A.value=bi.text.join(` -`);var $=Ke();ut(A),setTimeout(function(){a.display.lineSpace.removeChild(N),$.focus(),$==c&&u.showPrimarySelection()},50)}}o(g,"onCopyCut"),H(c,"copy",g),H(c,"cut",g)},gt.prototype.screenReaderLabelChanged=function(r){r?this.div.setAttribute("aria-label",r):this.div.removeAttribute("aria-label")},gt.prototype.prepareSelection=function(){var r=Cu(this.cm,!1);return r.focus=Ke()==this.div,r},gt.prototype.showSelection=function(r,i){!r||!this.cm.display.view.length||((r.focus||i)&&this.showPrimarySelection(),this.showMultipleSelections(r))},gt.prototype.getSelection=function(){return this.cm.display.wrapper.ownerDocument.getSelection()},gt.prototype.showPrimarySelection=function(){var r=this.getSelection(),i=this.cm,u=i.doc.sel.primary(),a=u.from(),c=u.to();if(i.display.viewTo==i.display.viewFrom||a.line>=i.display.viewTo||c.line=i.display.viewFrom&&zu(i,a)||{node:S[0].measure.map[2],offset:0},b=c.liner.firstLine()&&(a=ae(a.line-1,Ne(r.doc,a.line-1).length)),c.ch==Ne(r.doc,c.line).text.length&&c.linei.viewTo-1)return!1;var m,g,S;a.line==i.viewFrom||(m=uo(r,a.line))==0?(g=dt(i.view[0].line),S=i.view[0].node):(g=dt(i.view[m].line),S=i.view[m-1].node.nextSibling);var C=uo(r,c.line),b,N;if(C==i.view.length-1?(b=i.viewTo-1,N=i.lineDiv.lastChild):(b=dt(i.view[C+1].line)-1,N=i.view[C+1].node.previousSibling),!S)return!1;for(var A=r.doc.splitLines(cc(r,S,N,g,b)),$=gi(r.doc,ae(g,0),ae(b,Ne(r.doc,b).text.length));A.length>1&&$.length>1;)if(xe(A)==xe($))A.pop(),$.pop(),b--;else if(A[0]==$[0])A.shift(),$.shift(),g++;else break;for(var z=0,ee=0,oe=A[0],ce=$[0],me=Math.min(oe.length,ce.length);za.ch&&Ce.charCodeAt(Ce.length-ee-1)==ve.charCodeAt(ve.length-ee-1);)z--,ee++;A[A.length-1]=Ce.slice(0,Ce.length-ee).replace(/^\u200b+/,""),A[0]=A[0].slice(z).replace(/\u200b+$/,"");var Fe=ae(g,z),De=ae(b,$.length?xe($).length-ee:0);if(A.length>1||A[0]||ze(Fe,De))return da(r.doc,A,Fe,De,"+input"),!0},gt.prototype.ensurePolled=function(){this.forceCompositionEnd()},gt.prototype.reset=function(){this.forceCompositionEnd()},gt.prototype.forceCompositionEnd=function(){!this.composing||(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},gt.prototype.readFromDOMSoon=function(){var r=this;this.readDOMTimeout==null&&(this.readDOMTimeout=setTimeout(function(){if(r.readDOMTimeout=null,r.composing)if(r.composing.done)r.composing=null;else return;r.updateFromDOM()},80))},gt.prototype.updateFromDOM=function(){var r=this;(this.cm.isReadOnly()||!this.pollContent())&&Gr(this.cm,function(){return Ze(r.cm)})},gt.prototype.setUneditable=function(r){r.contentEditable="false"},gt.prototype.onKeyPress=function(r){r.charCode==0||this.composing||(r.preventDefault(),this.cm.isReadOnly()||Jt(this.cm,uc)(this.cm,String.fromCharCode(r.charCode==null?r.keyCode:r.charCode),0))},gt.prototype.readOnlyChanged=function(r){this.div.contentEditable=String(r!="nocursor")},gt.prototype.onContextMenu=function(){},gt.prototype.resetPosition=function(){},gt.prototype.needsContentAttribute=!0;function zu(r,i){var u=Wp(r,i.line);if(!u||u.hidden)return null;var a=Ne(r.doc,i.line),c=sl(u,a,i.line),m=bn(a,r.doc.direction),g="left";if(m){var S=mi(m,i.ch);g=S%2?"right":"left"}var C=Df(c.map,i.ch,g);return C.offset=C.collapse=="right"?C.end:C.start,C}o(zu,"posToDOM");function _a(r){for(var i=r;i;i=i.parentNode)if(/CodeMirror-gutter-wrapper/.test(i.className))return!0;return!1}o(_a,"isInGutter");function He(r,i){return i&&(r.bad=!0),r}o(He,"badPos");function cc(r,i,u,a,c){var m="",g=!1,S=r.doc.lineSeparator(),C=!1;function b(z){return function(ee){return ee.id==z}}o(b,"recognizeMarker");function N(){g&&(m+=S,C&&(m+=S),g=C=!1)}o(N,"close");function A(z){z&&(N(),m+=z)}o(A,"addText");function $(z){if(z.nodeType==1){var ee=z.getAttribute("cm-text");if(ee){A(ee);return}var oe=z.getAttribute("cm-marker"),ce;if(oe){var me=r.findMarks(ae(a,0),ae(c+1,0),b(+oe));me.length&&(ce=me[0].find(0))&&A(gi(r.doc,ce.from,ce.to).join(S));return}if(z.getAttribute("contenteditable")=="false")return;var Ce=/^(pre|div|p|li|table|br)$/i.test(z.nodeName);if(!/^br$/i.test(z.nodeName)&&z.textContent.length==0)return;Ce&&N();for(var ve=0;ve=9&&i.hasSelection&&(i.hasSelection=null),u.poll()}),H(c,"paste",function(g){Xt(a,g)||dd(g,a)||(a.state.pasteIncoming=+new Date,u.fastPoll())});function m(g){if(!Xt(a,g)){if(a.somethingSelected())Ei({lineWise:!1,text:a.getSelections()});else if(a.options.lineWiseCopyCut){var S=hd(a);Ei({lineWise:!0,text:S.text}),g.type=="cut"?a.setSelections(S.ranges,null,Ut):(u.prevInput="",c.value=S.text.join(` -`),ut(c))}else return;g.type=="cut"&&(a.state.cutIncoming=+new Date)}}o(m,"prepareCopyCut"),H(c,"cut",m),H(c,"copy",m),H(r.scroller,"paste",function(g){if(!(dr(r,g)||Xt(a,g))){if(!c.dispatchEvent){a.state.pasteIncoming=+new Date,u.focus();return}var S=new Event("paste");S.clipboardData=g.clipboardData,c.dispatchEvent(S)}}),H(r.lineSpace,"selectstart",function(g){dr(r,g)||Qt(g)}),H(c,"compositionstart",function(){var g=a.getCursor("from");u.composing&&u.composing.range.clear(),u.composing={start:g,range:a.markText(g,a.getCursor("to"),{className:"CodeMirror-composing"})}}),H(c,"compositionend",function(){u.composing&&(u.poll(),u.composing.range.clear(),u.composing=null)})},or.prototype.createField=function(r){this.wrapper=Km(),this.textarea=this.wrapper.firstChild},or.prototype.screenReaderLabelChanged=function(r){r?this.textarea.setAttribute("aria-label",r):this.textarea.removeAttribute("aria-label")},or.prototype.prepareSelection=function(){var r=this.cm,i=r.display,u=r.doc,a=Cu(r);if(r.options.moveInputWithCursor){var c=Bi(r,u.sel.primary().head,"div"),m=i.wrapper.getBoundingClientRect(),g=i.lineDiv.getBoundingClientRect();a.teTop=Math.max(0,Math.min(i.wrapper.clientHeight-10,c.top+g.top-m.top)),a.teLeft=Math.max(0,Math.min(i.wrapper.clientWidth-10,c.left+g.left-m.left))}return a},or.prototype.showSelection=function(r){var i=this.cm,u=i.display;Je(u.cursorDiv,r.cursors),Je(u.selectionDiv,r.selection),r.teTop!=null&&(this.wrapper.style.top=r.teTop+"px",this.wrapper.style.left=r.teLeft+"px")},or.prototype.reset=function(r){if(!(this.contextMenuPending||this.composing)){var i=this.cm;if(i.somethingSelected()){this.prevInput="";var u=i.getSelection();this.textarea.value=u,i.state.focused&&ut(this.textarea),p&&w>=9&&(this.hasSelection=u)}else r||(this.prevInput=this.textarea.value="",p&&w>=9&&(this.hasSelection=null))}},or.prototype.getField=function(){return this.textarea},or.prototype.supportsTouch=function(){return!1},or.prototype.focus=function(){if(this.cm.options.readOnly!="nocursor"&&(!P||Ke()!=this.textarea))try{this.textarea.focus()}catch(r){}},or.prototype.blur=function(){this.textarea.blur()},or.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},or.prototype.receivedFocus=function(){this.slowPoll()},or.prototype.slowPoll=function(){var r=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,function(){r.poll(),r.cm.state.focused&&r.slowPoll()})},or.prototype.fastPoll=function(){var r=!1,i=this;i.pollingFast=!0;function u(){var a=i.poll();!a&&!r?(r=!0,i.polling.set(60,u)):(i.pollingFast=!1,i.slowPoll())}o(u,"p"),i.polling.set(20,u)},or.prototype.poll=function(){var r=this,i=this.cm,u=this.textarea,a=this.prevInput;if(this.contextMenuPending||!i.state.focused||Ep(u)&&!a&&!this.composing||i.isReadOnly()||i.options.disableInput||i.state.keySeq)return!1;var c=u.value;if(c==a&&!i.somethingSelected())return!1;if(p&&w>=9&&this.hasSelection===c||F&&/[\uf700-\uf7ff]/.test(c))return i.display.input.reset(),!1;if(i.doc.sel==i.display.selForContextMenu){var m=c.charCodeAt(0);if(m==8203&&!a&&(a="\u200B"),m==8666)return this.reset(),this.cm.execCommand("undo")}for(var g=0,S=Math.min(a.length,c.length);g1e3||c.indexOf(` -`)>-1?u.value=r.prevInput="":r.prevInput=c,r.composing&&(r.composing.range.clear(),r.composing.range=i.markText(r.composing.start,i.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},or.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},or.prototype.onKeyPress=function(){p&&w>=9&&(this.hasSelection=null),this.fastPoll()},or.prototype.onContextMenu=function(r){var i=this,u=i.cm,a=u.display,c=i.textarea;i.contextMenuPending&&i.contextMenuPending();var m=ao(u,r),g=a.scroller.scrollTop;if(!m||Y)return;var S=u.options.resetSelectionOnContextMenu;S&&u.doc.sel.contains(m)==-1&&Jt(u,Hr)(u.doc,ks(m),Ut);var C=c.style.cssText,b=i.wrapper.style.cssText,N=i.wrapper.offsetParent.getBoundingClientRect();i.wrapper.style.cssText="position: static",c.style.cssText=`position: absolute; width: 30px; height: 30px; - top: `+(r.clientY-N.top-5)+"px; left: "+(r.clientX-N.left-5)+`px; +`)&&(ee=oe=ae(ee.line,0)));var ce={from:ee,to:oe,text:b?b[j%b.length]:C,origin:c||(S?"paste":r.state.cutIncoming>g?"cut":"+input")};pa(r.doc,ce),sr(r,"inputRead",r,ce)}i&&!S&&Gm(r,i),Ss(r),r.curOp.updateInput<2&&(r.curOp.updateInput=A),r.curOp.typing=!0,r.state.pasteIncoming=r.state.cutIncoming=-1}o(yc,"applyTextInput");function Sd(r,i){var u=r.clipboardData&&r.clipboardData.getData("Text");if(u)return r.preventDefault(),!i.isReadOnly()&&!i.options.disableInput&&Gr(i,function(){return yc(i,u,0,null,"paste")}),!0}o(Sd,"handlePaste");function Gm(r,i){if(!(!r.options.electricChars||!r.options.smartIndent))for(var u=r.doc.sel,a=u.ranges.length-1;a>=0;a--){var c=u.ranges[a];if(!(c.head.ch>100||a&&u.ranges[a-1].head.line==c.head.line)){var v=r.getModeAt(c.head),g=!1;if(v.electricChars){for(var S=0;S-1){g=Gu(r,c.head.line,"smart");break}}else v.electricInput&&v.electricInput.test(Me(r.doc,c.head.line).text.slice(0,c.head.ch))&&(g=Gu(r,c.head.line,"smart"));g&&sr(r,"electricInput",r,c.head.line)}}}o(Gm,"triggerElectric");function Cd(r){for(var i=[],u=[],a=0;av&&(Gu(this,S.head.line,a,!0),v=S.head.line,g==this.doc.sel.primIndex&&Ss(this));else{var C=S.from(),b=S.to(),P=Math.max(v,C.line);v=Math.min(this.lastLine(),b.line-(b.ch?0:1))+1;for(var A=P;A0&&sd(this.doc,g,new yt(C,j[g].to()),$t)}}}),getTokenAt:function(a,c){return us(this,a,c)},getLineTokens:function(a,c){return us(this,ae(a),c,!0)},getTokenTypeAt:function(a){a=Be(this.doc,a);var c=ql(this,Me(this.doc,a.line)),v=0,g=(c.length-1)/2,S=a.ch,C;if(S==0)C=c[2];else for(;;){var b=v+g>>1;if((b?c[b*2-1]:0)>=S)g=b;else if(c[b*2+1]C&&(a=C,g=!0),S=Me(this.doc,a)}else S=a;return ul(this,S,{top:0,left:0},c||"page",v||g).top+(g?this.doc.height-_e(S):0)},defaultTextHeight:function(){return vs(this.display)},defaultCharWidth:function(){return na(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(a,c,v,g,S){var C=this.display;a=fn(this,Be(this.doc,a));var b=a.bottom,P=a.left;if(c.style.position="absolute",c.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(c),C.sizer.appendChild(c),g=="over")b=a.top;else if(g=="above"||g=="near"){var A=Math.max(C.wrapper.clientHeight,this.doc.height),j=Math.max(C.sizer.clientWidth,C.lineSpace.clientWidth);(g=="above"||a.bottom+c.offsetHeight>A)&&a.top>c.offsetHeight?b=a.top-c.offsetHeight:a.bottom+c.offsetHeight<=A&&(b=a.bottom),P+c.offsetWidth>j&&(P=j-c.offsetWidth)}c.style.top=b+"px",c.style.left=c.style.right="",S=="right"?(P=C.sizer.clientWidth-c.offsetWidth,c.style.right="0px"):(S=="left"?P=0:S=="middle"&&(P=(C.sizer.clientWidth-c.offsetWidth)/2),c.style.left=P+"px"),v&&Jy(this,{left:P,top:b,right:P+c.offsetWidth,bottom:b+c.offsetHeight})},triggerOnKeyDown:Yr(Fm),triggerOnKeyPress:Yr(Hm),triggerOnKeyUp:yd,triggerOnMouseDown:Yr(Bm),execCommand:function(a){if(xl.hasOwnProperty(a))return xl[a].call(null,this)},triggerElectric:Yr(function(a){Gm(this,a)}),findPosH:function(a,c,v,g){var S=1;c<0&&(S=-1,c=-c);for(var C=Be(this.doc,a),b=0;b0&&P(v.charAt(g-1));)--g;for(;S.5||this.options.lineWrapping)&&gs(this),ke(this,"refresh",this)}),swapDoc:Yr(function(a){var c=this.doc;return c.cm=null,this.state.selectingText&&this.state.selectingText(),xm(this,a),Un(this),this.display.input.reset(),Pu(this,a.scrollLeft,a.scrollTop),this.curOp.forceScroll=!0,sr(this,"swapDoc",this,c),c}),phrase:function(a){var c=this.options.phrases;return c&&Object.prototype.hasOwnProperty.call(c,a)?c[a]:a},getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Dr(r),r.registerHelper=function(a,c,v){u.hasOwnProperty(a)||(u[a]=r[a]={_global:[]}),u[a][c]=v},r.registerGlobalHelper=function(a,c,v,g){r.registerHelper(a,c,g),u[a]._global.push({pred:v,val:g})}}o(Ko,"addEditorMethods");function Yu(r,i,u,a,c){var v=i,g=u,S=Me(r,i.line),C=c&&r.direction=="rtl"?-u:u;function b(){var be=i.line+C;return be=r.first+r.size?!1:(i=new ae(be,i.ch,i.sticky),S=Me(r,be))}o(b,"findNextLine");function P(be){var ve;if(a=="codepoint"){var Oe=S.text.charCodeAt(i.ch+(u>0?0:-1));if(isNaN(Oe))ve=null;else{var Fe=u>0?Oe>=55296&&Oe<56320:Oe>=56320&&Oe<57343;ve=new ae(i.line,Math.max(0,Math.min(S.text.length,i.ch+u*(Fe?2:1))),-u)}}else c?ve=Rm(r.cm,S,i,u):ve=pc(S,i,u);if(ve==null)if(!be&&b())i=wa(c,r.cm,S,i.line,C);else return!1;else i=ve;return!0}if(o(P,"moveOnce"),a=="char"||a=="codepoint")P();else if(a=="column")P(!0);else if(a=="word"||a=="group")for(var A=null,j=a=="group",z=r.cm&&r.cm.getHelper(i,"wordChars"),ee=!0;!(u<0&&!P(!ee));ee=!1){var oe=S.text.charAt(i.ch)||` +`,ce=dr(oe,z)?"w":j&&oe==` +`?"n":!j||/\s/.test(oe)?null:"p";if(j&&!ee&&!ce&&(ce="s"),A&&A!=ce){u<0&&(u=1,P(),i.sticky="after");break}if(ce&&(A=ce),u>0&&!P(!ee))break}var me=Hr(r,i,v,g,!0);return pu(v,me)&&(me.hitSide=!0),me}o(Yu,"findPosH");function wc(r,i,u,a){var c=r.doc,v=i.left,g;if(a=="page"){var S=Math.min(r.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight),C=Math.max(S-.5*vs(r.display),3);g=(u>0?i.bottom:i.top)+u*C}else a=="line"&&(g=u>0?i.bottom+3:i.top-3);for(var b;b=q(r,v,g),!!b.outside;){if(u<0?g<=0:g>=c.height){b.hitSide=!0;break}g+=u*5}return b}o(wc,"findPosV");var wt=o(function(r){this.cm=r,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new St,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null},"ContentEditableInput");wt.prototype.init=function(r){var i=this,u=this,a=u.cm,c=u.div=r.lineDiv;c.contentEditable=!0,Ym(c,a.options.spellcheck,a.options.autocorrect,a.options.autocapitalize);function v(S){for(var C=S.target;C;C=C.parentNode){if(C==c)return!0;if(/\bCodeMirror-(?:line)?widget\b/.test(C.className))break}return!1}o(v,"belongsToInput"),H(c,"paste",function(S){!v(S)||Zt(a,S)||Sd(S,a)||x<=11&&setTimeout(tr(a,function(){return i.updateFromDOM()}),20)}),H(c,"compositionstart",function(S){i.composing={data:S.data,done:!1}}),H(c,"compositionupdate",function(S){i.composing||(i.composing={data:S.data,done:!1})}),H(c,"compositionend",function(S){i.composing&&(S.data!=i.composing.data&&i.readFromDOMSoon(),i.composing.done=!0)}),H(c,"touchstart",function(){return u.forceCompositionEnd()}),H(c,"input",function(){i.composing||i.readFromDOMSoon()});function g(S){if(!(!v(S)||Zt(a,S))){if(a.somethingSelected())Ti({lineWise:!1,text:a.getSelections()}),S.type=="cut"&&a.replaceSelection("",null,"cut");else if(a.options.lineWiseCopyCut){var C=Cd(a);Ti({lineWise:!0,text:C.text}),S.type=="cut"&&a.operation(function(){a.setSelections(C.ranges,0,$t),a.replaceSelection("",null,"cut")})}else return;if(S.clipboardData){S.clipboardData.clearData();var b=Ei.text.join(` +`);if(S.clipboardData.setData("Text",b),S.clipboardData.getData("Text")==b){S.preventDefault();return}}var P=Xm(),A=P.firstChild;a.display.lineSpace.insertBefore(P,a.display.lineSpace.firstChild),A.value=Ei.text.join(` +`);var j=Ke();ft(A),setTimeout(function(){a.display.lineSpace.removeChild(P),j.focus(),j==c&&u.showPrimarySelection()},50)}}o(g,"onCopyCut"),H(c,"copy",g),H(c,"cut",g)},wt.prototype.screenReaderLabelChanged=function(r){r?this.div.setAttribute("aria-label",r):this.div.removeAttribute("aria-label")},wt.prototype.prepareSelection=function(){var r=Nu(this.cm,!1);return r.focus=Ke()==this.div,r},wt.prototype.showSelection=function(r,i){!r||!this.cm.display.view.length||((r.focus||i)&&this.showPrimarySelection(),this.showMultipleSelections(r))},wt.prototype.getSelection=function(){return this.cm.display.wrapper.ownerDocument.getSelection()},wt.prototype.showPrimarySelection=function(){var r=this.getSelection(),i=this.cm,u=i.doc.sel.primary(),a=u.from(),c=u.to();if(i.display.viewTo==i.display.viewFrom||a.line>=i.display.viewTo||c.line=i.display.viewFrom&&Xu(i,a)||{node:S[0].measure.map[2],offset:0},b=c.liner.firstLine()&&(a=ae(a.line-1,Me(r.doc,a.line-1).length)),c.ch==Me(r.doc,c.line).text.length&&c.linei.viewTo-1)return!1;var v,g,S;a.line==i.viewFrom||(v=uo(r,a.line))==0?(g=vt(i.view[0].line),S=i.view[0].node):(g=vt(i.view[v].line),S=i.view[v-1].node.nextSibling);var C=uo(r,c.line),b,P;if(C==i.view.length-1?(b=i.viewTo-1,P=i.lineDiv.lastChild):(b=vt(i.view[C+1].line)-1,P=i.view[C+1].node.previousSibling),!S)return!1;for(var A=r.doc.splitLines(xc(r,S,P,g,b)),j=wi(r.doc,ae(g,0),ae(b,Me(r.doc,b).text.length));A.length>1&&j.length>1;)if(Se(A)==Se(j))A.pop(),j.pop(),b--;else if(A[0]==j[0])A.shift(),j.shift(),g++;else break;for(var z=0,ee=0,oe=A[0],ce=j[0],me=Math.min(oe.length,ce.length);za.ch&&be.charCodeAt(be.length-ee-1)==ve.charCodeAt(ve.length-ee-1);)z--,ee++;A[A.length-1]=be.slice(0,be.length-ee).replace(/^\u200b+/,""),A[0]=A[0].slice(z).replace(/\u200b+$/,"");var Fe=ae(g,z),Re=ae(b,j.length?Se(j).length-ee:0);if(A.length>1||A[0]||$e(Fe,Re))return ma(r.doc,A,Fe,Re,"+input"),!0},wt.prototype.ensurePolled=function(){this.forceCompositionEnd()},wt.prototype.reset=function(){this.forceCompositionEnd()},wt.prototype.forceCompositionEnd=function(){!this.composing||(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},wt.prototype.readFromDOMSoon=function(){var r=this;this.readDOMTimeout==null&&(this.readDOMTimeout=setTimeout(function(){if(r.readDOMTimeout=null,r.composing)if(r.composing.done)r.composing=null;else return;r.updateFromDOM()},80))},wt.prototype.updateFromDOM=function(){var r=this;(this.cm.isReadOnly()||!this.pollContent())&&Gr(this.cm,function(){return Je(r.cm)})},wt.prototype.setUneditable=function(r){r.contentEditable="false"},wt.prototype.onKeyPress=function(r){r.charCode==0||this.composing||(r.preventDefault(),this.cm.isReadOnly()||tr(this.cm,yc)(this.cm,String.fromCharCode(r.charCode==null?r.keyCode:r.charCode),0))},wt.prototype.readOnlyChanged=function(r){this.div.contentEditable=String(r!="nocursor")},wt.prototype.onContextMenu=function(){},wt.prototype.resetPosition=function(){},wt.prototype.needsContentAttribute=!0;function Xu(r,i){var u=Ou(r,i.line);if(!u||u.hidden)return null;var a=Me(r.doc,i.line),c=ku(u,a,i.line),v=En(a,r.doc.direction),g="left";if(v){var S=gi(v,i.ch);g=S%2?"right":"left"}var C=T(c.map,i.ch,g);return C.offset=C.collapse=="right"?C.end:C.start,C}o(Xu,"posToDOM");function Ea(r){for(var i=r;i;i=i.parentNode)if(/CodeMirror-gutter-wrapper/.test(i.className))return!0;return!1}o(Ea,"isInGutter");function We(r,i){return i&&(r.bad=!0),r}o(We,"badPos");function xc(r,i,u,a,c){var v="",g=!1,S=r.doc.lineSeparator(),C=!1;function b(z){return function(ee){return ee.id==z}}o(b,"recognizeMarker");function P(){g&&(v+=S,C&&(v+=S),g=C=!1)}o(P,"close");function A(z){z&&(P(),v+=z)}o(A,"addText");function j(z){if(z.nodeType==1){var ee=z.getAttribute("cm-text");if(ee){A(ee);return}var oe=z.getAttribute("cm-marker"),ce;if(oe){var me=r.findMarks(ae(a,0),ae(c+1,0),b(+oe));me.length&&(ce=me[0].find(0))&&A(wi(r.doc,ce.from,ce.to).join(S));return}if(z.getAttribute("contenteditable")=="false")return;var be=/^(pre|div|p|li|table|br)$/i.test(z.nodeName);if(!/^br$/i.test(z.nodeName)&&z.textContent.length==0)return;be&&P();for(var ve=0;ve=9&&i.hasSelection&&(i.hasSelection=null),u.poll()}),H(c,"paste",function(g){Zt(a,g)||Sd(g,a)||(a.state.pasteIncoming=+new Date,u.fastPoll())});function v(g){if(!Zt(a,g)){if(a.somethingSelected())Ti({lineWise:!1,text:a.getSelections()});else if(a.options.lineWiseCopyCut){var S=Cd(a);Ti({lineWise:!0,text:S.text}),g.type=="cut"?a.setSelections(S.ranges,null,$t):(u.prevInput="",c.value=S.text.join(` +`),ft(c))}else return;g.type=="cut"&&(a.state.cutIncoming=+new Date)}}o(v,"prepareCopyCut"),H(c,"cut",v),H(c,"copy",v),H(r.scroller,"paste",function(g){if(!(Wi(r,g)||Zt(a,g))){if(!c.dispatchEvent){a.state.pasteIncoming=+new Date,u.focus();return}var S=new Event("paste");S.clipboardData=g.clipboardData,c.dispatchEvent(S)}}),H(r.lineSpace,"selectstart",function(g){Wi(r,g)||Jt(g)}),H(c,"compositionstart",function(){var g=a.getCursor("from");u.composing&&u.composing.range.clear(),u.composing={start:g,range:a.markText(g,a.getCursor("to"),{className:"CodeMirror-composing"})}}),H(c,"compositionend",function(){u.composing&&(u.poll(),u.composing.range.clear(),u.composing=null)})},lr.prototype.createField=function(r){this.wrapper=Xm(),this.textarea=this.wrapper.firstChild},lr.prototype.screenReaderLabelChanged=function(r){r?this.textarea.setAttribute("aria-label",r):this.textarea.removeAttribute("aria-label")},lr.prototype.prepareSelection=function(){var r=this.cm,i=r.display,u=r.doc,a=Nu(r);if(r.options.moveInputWithCursor){var c=fn(r,u.sel.primary().head,"div"),v=i.wrapper.getBoundingClientRect(),g=i.lineDiv.getBoundingClientRect();a.teTop=Math.max(0,Math.min(i.wrapper.clientHeight-10,c.top+g.top-v.top)),a.teLeft=Math.max(0,Math.min(i.wrapper.clientWidth-10,c.left+g.left-v.left))}return a},lr.prototype.showSelection=function(r){var i=this.cm,u=i.display;et(u.cursorDiv,r.cursors),et(u.selectionDiv,r.selection),r.teTop!=null&&(this.wrapper.style.top=r.teTop+"px",this.wrapper.style.left=r.teLeft+"px")},lr.prototype.reset=function(r){if(!(this.contextMenuPending||this.composing)){var i=this.cm;if(i.somethingSelected()){this.prevInput="";var u=i.getSelection();this.textarea.value=u,i.state.focused&&ft(this.textarea),p&&x>=9&&(this.hasSelection=u)}else r||(this.prevInput=this.textarea.value="",p&&x>=9&&(this.hasSelection=null))}},lr.prototype.getField=function(){return this.textarea},lr.prototype.supportsTouch=function(){return!1},lr.prototype.focus=function(){if(this.cm.options.readOnly!="nocursor"&&(!M||Ke()!=this.textarea))try{this.textarea.focus()}catch(r){}},lr.prototype.blur=function(){this.textarea.blur()},lr.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},lr.prototype.receivedFocus=function(){this.slowPoll()},lr.prototype.slowPoll=function(){var r=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,function(){r.poll(),r.cm.state.focused&&r.slowPoll()})},lr.prototype.fastPoll=function(){var r=!1,i=this;i.pollingFast=!0;function u(){var a=i.poll();!a&&!r?(r=!0,i.polling.set(60,u)):(i.pollingFast=!1,i.slowPoll())}o(u,"p"),i.polling.set(20,u)},lr.prototype.poll=function(){var r=this,i=this.cm,u=this.textarea,a=this.prevInput;if(this.contextMenuPending||!i.state.focused||Dp(u)&&!a&&!this.composing||i.isReadOnly()||i.options.disableInput||i.state.keySeq)return!1;var c=u.value;if(c==a&&!i.somethingSelected())return!1;if(p&&x>=9&&this.hasSelection===c||R&&/[\uf700-\uf7ff]/.test(c))return i.display.input.reset(),!1;if(i.doc.sel==i.display.selForContextMenu){var v=c.charCodeAt(0);if(v==8203&&!a&&(a="\u200B"),v==8666)return this.reset(),this.cm.execCommand("undo")}for(var g=0,S=Math.min(a.length,c.length);g1e3||c.indexOf(` +`)>-1?u.value=r.prevInput="":r.prevInput=c,r.composing&&(r.composing.range.clear(),r.composing.range=i.markText(r.composing.start,i.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},lr.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},lr.prototype.onKeyPress=function(){p&&x>=9&&(this.hasSelection=null),this.fastPoll()},lr.prototype.onContextMenu=function(r){var i=this,u=i.cm,a=u.display,c=i.textarea;i.contextMenuPending&&i.contextMenuPending();var v=ao(u,r),g=a.scroller.scrollTop;if(!v||Y)return;var S=u.options.resetSelectionOnContextMenu;S&&u.doc.sel.contains(v)==-1&&tr(u,Ir)(u.doc,Es(v),$t);var C=c.style.cssText,b=i.wrapper.style.cssText,P=i.wrapper.offsetParent.getBoundingClientRect();i.wrapper.style.cssText="position: static",c.style.cssText=`position: absolute; width: 30px; height: 30px; + top: `+(r.clientY-P.top-5)+"px; left: "+(r.clientX-P.left-5)+`px; z-index: 1000; background: `+(p?"rgba(255, 255, 255, .05)":"transparent")+`; - outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);`;var A;_&&(A=window.scrollY),a.input.focus(),_&&window.scrollTo(null,A),a.input.reset(),u.somethingSelected()||(c.value=i.prevInput=" "),i.contextMenuPending=z,a.selForContextMenu=u.doc.sel,clearTimeout(a.detectingSelectAll);function $(){if(c.selectionStart!=null){var oe=u.somethingSelected(),ce="\u200B"+(oe?c.value:"");c.value="\u21DA",c.value=ce,i.prevInput=oe?"":"\u200B",c.selectionStart=1,c.selectionEnd=ce.length,a.selForContextMenu=u.doc.sel}}o($,"prepareSelectAllHack");function z(){if(i.contextMenuPending==z&&(i.contextMenuPending=!1,i.wrapper.style.cssText=b,c.style.cssText=C,p&&w<9&&a.scrollbars.setScrollTop(a.scroller.scrollTop=g),c.selectionStart!=null)){(!p||p&&w<9)&&$();var oe=0,ce=o(function(){a.selForContextMenu==u.doc.sel&&c.selectionStart==0&&c.selectionEnd>0&&i.prevInput=="\u200B"?Jt(u,Sm)(u):oe++<10?a.detectingSelectAll=setTimeout(ce,500):(a.selForContextMenu=null,a.input.reset())},"poll");a.detectingSelectAll=setTimeout(ce,200)}}if(o(z,"rehide"),p&&w>=9&&$(),de){to(r);var ee=o(function(){he(window,"mouseup",ee),setTimeout(z,20)},"mouseup");H(window,"mouseup",ee)}else setTimeout(z,50)},or.prototype.readOnlyChanged=function(r){r||this.reset(),this.textarea.disabled=r=="nocursor",this.textarea.readOnly=!!r},or.prototype.setUneditable=function(){},or.prototype.needsContentAttribute=!1;function md(r,i){if(i=i?qt(i):{},i.value=r.value,!i.tabindex&&r.tabIndex&&(i.tabindex=r.tabIndex),!i.placeholder&&r.placeholder&&(i.placeholder=r.placeholder),i.autofocus==null){var u=Ke();i.autofocus=u==r||r.getAttribute("autofocus")!=null&&u==document.body}function a(){r.value=S.getValue()}o(a,"save");var c;if(r.form&&(H(r.form,"submit",a),!i.leaveSubmitMethodAlone)){var m=r.form;c=m.submit;try{var g=m.submit=function(){a(),m.submit=c,m.submit(),m.submit=g}}catch(C){}}i.finishInit=function(C){C.save=a,C.getTextArea=function(){return r},C.toTextArea=function(){C.toTextArea=isNaN,a(),r.parentNode.removeChild(C.getWrapperElement()),r.style.display="",r.form&&(he(r.form,"submit",a),!i.leaveSubmitMethodAlone&&typeof r.form.submit=="function"&&(r.form.submit=c))}},r.style.display="none";var S=Lt(function(C){return r.parentNode.insertBefore(C,r.nextSibling)},i);return S}o(md,"fromTextArea");function Gm(r){r.off=he,r.on=H,r.wheelEventPixels=l0,r.Doc=fn,r.splitLines=Zs,r.countColumn=_t,r.findColumn=br,r.isWordChar=Vt,r.Pass=Bt,r.signal=Ee,r.Line=Tr,r.changeEnd=Os,r.scrollbarModel=Uf,r.Pos=ae,r.cmpPos=ze,r.modes=Wl,r.mimeModes=ls,r.resolveMode=Js,r.getMode=au,r.modeExtensions=Ro,r.extendMode=Op,r.copyState=Fo,r.startState=Ef,r.innerMode=Bl,r.commands=yl,r.keyMap=qo,r.keyName=od,r.isModifierKey=rc,r.lookupKey=Vo,r.normalizeKeyMap=h0,r.StringStream=Dt,r.SharedTextMarker=va,r.TextMarker=Ps,r.LineWidget=Au,r.e_preventDefault=Qt,r.e_stopPropagation=ti,r.e_stop=to,r.addClass=Ge,r.contains=Ve,r.rmClass=we,r.keyNames=As}o(Gm,"addLegacyProps"),ac(Lt),Ko(Lt);var pn="iter insert remove copy getEditor constructor".split(" ");for(var pc in fn.prototype)fn.prototype.hasOwnProperty(pc)&&st(pn,pc)<0&&(Lt.prototype[pc]=function(r){return function(){return r.apply(this.doc,arguments)}}(fn.prototype[pc]));return Rr(fn),Lt.inputStyles={textarea:or,contenteditable:gt},Lt.defineMode=function(r){!Lt.defaults.mode&&r!="null"&&(Lt.defaults.mode=r),kp.apply(this,arguments)},Lt.defineMIME=bf,Lt.defineMode("null",function(){return{token:function(r){return r.skipToEnd()}}}),Lt.defineMIME("text/plain","null"),Lt.defineExtension=function(r,i){Lt.prototype[r]=i},Lt.defineDocExtension=function(r,i){fn.prototype[r]=i},Lt.fromTextArea=md,Gm(Lt),Lt.version="5.62.3",Lt})});var Yk=lr((U4,Gk)=>{var iI=typeof Element!="undefined",oI=typeof Map=="function",sI=typeof Set=="function",lI=typeof ArrayBuffer=="function"&&!!ArrayBuffer.isView;function My(e,t){if(e===t)return!0;if(e&&t&&typeof e=="object"&&typeof t=="object"){if(e.constructor!==t.constructor)return!1;var n,l,d;if(Array.isArray(e)){if(n=e.length,n!=t.length)return!1;for(l=n;l--!=0;)if(!My(e[l],t[l]))return!1;return!0}var v;if(oI&&e instanceof Map&&t instanceof Map){if(e.size!==t.size)return!1;for(v=e.entries();!(l=v.next()).done;)if(!t.has(l.value[0]))return!1;for(v=e.entries();!(l=v.next()).done;)if(!My(l.value[1],t.get(l.value[0])))return!1;return!0}if(sI&&e instanceof Set&&t instanceof Set){if(e.size!==t.size)return!1;for(v=e.entries();!(l=v.next()).done;)if(!t.has(l.value[0]))return!1;return!0}if(lI&&ArrayBuffer.isView(e)&&ArrayBuffer.isView(t)){if(n=e.length,n!=t.length)return!1;for(l=n;l--!=0;)if(e[l]!==t[l])return!1;return!0}if(e.constructor===RegExp)return e.source===t.source&&e.flags===t.flags;if(e.valueOf!==Object.prototype.valueOf)return e.valueOf()===t.valueOf();if(e.toString!==Object.prototype.toString)return e.toString()===t.toString();if(d=Object.keys(e),n=d.length,n!==Object.keys(t).length)return!1;for(l=n;l--!=0;)if(!Object.prototype.hasOwnProperty.call(t,d[l]))return!1;if(iI&&e instanceof Element)return!1;for(l=n;l--!=0;)if(!((d[l]==="_owner"||d[l]==="__v"||d[l]==="__o")&&e.$$typeof)&&!My(e[d[l]],t[d[l]]))return!1;return!0}return e!==e&&t!==t}o(My,"equal");Gk.exports=o(function(t,n){try{return My(t,n)}catch(l){if((l.message||"").match(/stack|recursion/i))return console.warn("react-fast-compare cannot handle circular refs"),!1;throw l}},"isEqual")});var lO=lr((ez,sO)=>{"use strict";var gI="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";sO.exports=gI});var cO=lr((tz,fO)=>{"use strict";var yI=lO();function aO(){}o(aO,"emptyFunction");function uO(){}o(uO,"emptyFunctionWithReset");uO.resetWarningCache=aO;fO.exports=function(){function e(l,d,v,p,w,_){if(_!==yI){var O=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw O.name="Invariant Violation",O}}o(e,"shim"),e.isRequired=e;function t(){return e}o(t,"getShim");var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:uO,resetWarningCache:aO};return n.PropTypes=n,n}});var $h=lr((iz,pO)=>{pO.exports=cO()();var rz,nz});var yS=lr((oz,dO)=>{dO.exports=o(function(t,n,l,d){var v=l?l.call(d,t,n):void 0;if(v!==void 0)return!!v;if(t===n)return!0;if(typeof t!="object"||!t||typeof n!="object"||!n)return!1;var p=Object.keys(t),w=Object.keys(n);if(p.length!==w.length)return!1;for(var _=Object.prototype.hasOwnProperty.bind(n),O=0;O=0)&&(n[d]=e[d]);return n}o(Qa,"_objectWithoutPropertiesLoose");var ax=pe(lT()),nr=pe(Re()),pT=pe(cT());var _R=[],bR=[null,null];function ER(e,t){var n=e[1];return[t.payload,n+1]}o(ER,"storeStateUpdatesReducer");function dT(e,t,n){sf(function(){return e.apply(void 0,t)},n)}o(dT,"useIsomorphicLayoutEffectWithArgs");function TR(e,t,n,l,d,v,p){e.current=l,t.current=d,n.current=!1,v.current&&(v.current=null,p())}o(TR,"captureWrapperProps");function kR(e,t,n,l,d,v,p,w,_,O){if(!!e){var D=!1,Y=null,W=o(function(){if(!D){var Q=t.getState(),R,P;try{R=l(Q,d.current)}catch(F){P=F,Y=F}P||(Y=null),R===v.current?p.current||_():(v.current=R,w.current=R,p.current=!0,O({type:"STORE_UPDATED",payload:{error:P}}))}},"checkForUpdates");n.onStateChange=W,n.trySubscribe(),W();var X=o(function(){if(D=!0,n.tryUnsubscribe(),n.onStateChange=null,Y)throw Y},"unsubscribeWrapper");return X}}o(kR,"subscribeUpdates");var OR=o(function(){return[null,0]},"initStateUpdates");function Wg(e,t){t===void 0&&(t={});var n=t,l=n.getDisplayName,d=l===void 0?function(ie){return"ConnectAdvanced("+ie+")"}:l,v=n.methodName,p=v===void 0?"connectAdvanced":v,w=n.renderCountProp,_=w===void 0?void 0:w,O=n.shouldHandleStateChanges,D=O===void 0?!0:O,Y=n.storeKey,W=Y===void 0?"store":Y,X=n.withRef,te=X===void 0?!1:X,Q=n.forwardRef,R=Q===void 0?!1:Q,P=n.context,F=P===void 0?Yn:P,K=Qa(n,["getDisplayName","methodName","renderCountProp","shouldHandleStateChanges","storeKey","withRef","forwardRef","context"]);if(!1)var V;var ue=F;return o(function(de){var ge=de.displayName||de.name||"Component",we=d(ge),qe=To({},K,{getDisplayName:d,methodName:p,renderCountProp:_,shouldHandleStateChanges:D,storeKey:W,displayName:we,wrappedComponentName:ge,WrappedComponent:de}),Je=K.pure;function be(Ge){return e(Ge.dispatch,qe)}o(be,"createChildSelector");var yt=Je?nr.useMemo:function(Ge){return Ge()};function Be(Ge){var Yt=(0,nr.useMemo)(function(){var vr=Ge.reactReduxForwardedRef,Ri=Qa(Ge,["reactReduxForwardedRef"]);return[Ge.context,vr,Ri]},[Ge]),ut=Yt[0],Dr=Yt[1],qt=Yt[2],_t=(0,nr.useMemo)(function(){return ut&&ut.Consumer&&(0,pT.isContextConsumer)(nr.default.createElement(ut.Consumer,null))?ut:ue},[ut,ue]),wt=(0,nr.useContext)(_t),st=Boolean(Ge.store)&&Boolean(Ge.store.getState)&&Boolean(Ge.store.dispatch),_r=Boolean(wt)&&Boolean(wt.store),Bt=st?Ge.store:wt.store,Ut=(0,nr.useMemo)(function(){return be(Bt)},[Bt]),ne=(0,nr.useMemo)(function(){if(!D)return bR;var vr=new Qc(Bt,st?null:wt.subscription),Ri=vr.notifyNestedSubs.bind(vr);return[vr,Ri]},[Bt,st,wt]),et=ne[0],br=ne[1],zt=(0,nr.useMemo)(function(){return st?wt:To({},wt,{subscription:et})},[st,wt,et]),jt=(0,nr.useReducer)(ER,_R,OR),xe=jt[0],Er=xe[0],on=jt[1];if(Er&&Er.error)throw Er.error;var Dn=(0,nr.useRef)(),ei=(0,nr.useRef)(qt),sn=(0,nr.useRef)(),Vt=(0,nr.useRef)(!1),cr=yt(function(){return sn.current&&qt===ei.current?sn.current:Ut(Bt.getState(),qt)},[Bt,Er,qt]);dT(TR,[ei,Dn,Vt,qt,cr,sn,br]),dT(kR,[D,Bt,et,Ut,ei,Dn,Vt,sn,br,on],[Bt,et,Ut]);var ft=(0,nr.useMemo)(function(){return nr.default.createElement(de,To({},cr,{ref:Dr}))},[Dr,de,cr]),Do=(0,nr.useMemo)(function(){return D?nr.default.createElement(_t.Provider,{value:zt},ft):ft},[_t,ft,zt]);return Do}o(Be,"ConnectFunction");var Ve=Je?nr.default.memo(Be):Be;if(Ve.WrappedComponent=de,Ve.displayName=Be.displayName=we,R){var Ke=nr.default.forwardRef(o(function(Yt,ut){return nr.default.createElement(Ve,To({},Yt,{reactReduxForwardedRef:ut}))},"forwardConnectRef"));return Ke.displayName=we,Ke.WrappedComponent=de,(0,ax.default)(Ke,de)}return(0,ax.default)(Ve,de)},"wrapWithConnect")}o(Wg,"connectAdvanced");function hT(e,t){return e===t?e!==0||t!==0||1/e==1/t:e!==e&&t!==t}o(hT,"is");function Zc(e,t){if(hT(e,t))return!0;if(typeof e!="object"||e===null||typeof t!="object"||t===null)return!1;var n=Object.keys(e),l=Object.keys(t);if(n.length!==l.length)return!1;for(var d=0;d=0;l--){var d=t[l](e);if(d)return d}return function(v,p){throw new Error("Invalid value of type "+typeof e+" for "+n+" argument when connecting component "+p.wrappedComponentName+".")}}o(cx,"match");function UR(e,t){return e===t}o(UR,"strictEqual");function zR(e){var t=e===void 0?{}:e,n=t.connectHOC,l=n===void 0?Wg:n,d=t.mapStateToPropsFactories,v=d===void 0?gT:d,p=t.mapDispatchToPropsFactories,w=p===void 0?vT:p,_=t.mergePropsFactories,O=_===void 0?yT:_,D=t.selectorFactory,Y=D===void 0?fx:D;return o(function(X,te,Q,R){R===void 0&&(R={});var P=R,F=P.pure,K=F===void 0?!0:F,V=P.areStatesEqual,ue=V===void 0?UR:V,ie=P.areOwnPropsEqual,de=ie===void 0?Zc:ie,ge=P.areStatePropsEqual,we=ge===void 0?Zc:ge,qe=P.areMergedPropsEqual,Je=qe===void 0?Zc:qe,be=Qa(P,["pure","areStatesEqual","areOwnPropsEqual","areStatePropsEqual","areMergedPropsEqual"]),yt=cx(X,v,"mapStateToProps"),Be=cx(te,w,"mapDispatchToProps"),Ve=cx(Q,O,"mergeProps");return l(Y,To({methodName:"connect",getDisplayName:o(function(Ge){return"Connect("+Ge+")"},"getDisplayName"),shouldHandleStateChanges:Boolean(X),initMapStateToProps:yt,initMapDispatchToProps:Be,initMergeProps:Ve,pure:K,areStatesEqual:ue,areOwnPropsEqual:de,areStatePropsEqual:we,areMergedPropsEqual:Je},be))},"connect")}o(zR,"createConnect");var Ai=zR();var xT=pe(Re());var wT=pe(Re());function Ug(){var e=(0,wT.useContext)(Yn);return e}o(Ug,"useReduxContext");function zg(e){e===void 0&&(e=Yn);var t=e===Yn?Ug:function(){return(0,xT.useContext)(e)};return o(function(){var l=t(),d=l.store;return d},"useStore")}o(zg,"createStoreHook");var px=zg();function ST(e){e===void 0&&(e=Yn);var t=e===Yn?px:zg(e);return o(function(){var l=t();return l.dispatch},"useDispatch")}o(ST,"createDispatchHook");var $s=ST();var Xi=pe(Re());var jR=o(function(t,n){return t===n},"refEquality");function $R(e,t,n,l){var d=(0,Xi.useReducer)(function(te){return te+1},0),v=d[1],p=(0,Xi.useMemo)(function(){return new Qc(n,l)},[n,l]),w=(0,Xi.useRef)(),_=(0,Xi.useRef)(),O=(0,Xi.useRef)(),D=(0,Xi.useRef)(),Y=n.getState(),W;try{if(e!==_.current||Y!==O.current||w.current){var X=e(Y);D.current===void 0||!t(X,D.current)?W=X:W=D.current}else W=D.current}catch(te){throw w.current&&(te.message+=` + outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);`;var A;_&&(A=window.scrollY),a.input.focus(),_&&window.scrollTo(null,A),a.input.reset(),u.somethingSelected()||(c.value=i.prevInput=" "),i.contextMenuPending=z,a.selForContextMenu=u.doc.sel,clearTimeout(a.detectingSelectAll);function j(){if(c.selectionStart!=null){var oe=u.somethingSelected(),ce="\u200B"+(oe?c.value:"");c.value="\u21DA",c.value=ce,i.prevInput=oe?"":"\u200B",c.selectionStart=1,c.selectionEnd=ce.length,a.selForContextMenu=u.doc.sel}}o(j,"prepareSelectAllHack");function z(){if(i.contextMenuPending==z&&(i.contextMenuPending=!1,i.wrapper.style.cssText=b,c.style.cssText=C,p&&x<9&&a.scrollbars.setScrollTop(a.scroller.scrollTop=g),c.selectionStart!=null)){(!p||p&&x<9)&&j();var oe=0,ce=o(function(){a.selForContextMenu==u.doc.sel&&c.selectionStart==0&&c.selectionEnd>0&&i.prevInput=="\u200B"?tr(u,bm)(u):oe++<10?a.detectingSelectAll=setTimeout(ce,500):(a.selForContextMenu=null,a.input.reset())},"poll");a.detectingSelectAll=setTimeout(ce,200)}}if(o(z,"rehide"),p&&x>=9&&j(),de){to(r);var ee=o(function(){he(window,"mouseup",ee),setTimeout(z,20)},"mouseup");H(window,"mouseup",ee)}else setTimeout(z,50)},lr.prototype.readOnlyChanged=function(r){r||this.reset(),this.textarea.disabled=r=="nocursor",this.textarea.readOnly=!!r},lr.prototype.setUneditable=function(){},lr.prototype.needsContentAttribute=!1;function _d(r,i){if(i=i?Kt(i):{},i.value=r.value,!i.tabindex&&r.tabIndex&&(i.tabindex=r.tabIndex),!i.placeholder&&r.placeholder&&(i.placeholder=r.placeholder),i.autofocus==null){var u=Ke();i.autofocus=u==r||r.getAttribute("autofocus")!=null&&u==document.body}function a(){r.value=S.getValue()}o(a,"save");var c;if(r.form&&(H(r.form,"submit",a),!i.leaveSubmitMethodAlone)){var v=r.form;c=v.submit;try{var g=v.submit=function(){a(),v.submit=c,v.submit(),v.submit=g}}catch(C){}}i.finishInit=function(C){C.save=a,C.getTextArea=function(){return r},C.toTextArea=function(){C.toTextArea=isNaN,a(),r.parentNode.removeChild(C.getWrapperElement()),r.style.display="",r.form&&(he(r.form,"submit",a),!i.leaveSubmitMethodAlone&&typeof r.form.submit=="function"&&(r.form.submit=c))}},r.style.display="none";var S=Pt(function(C){return r.parentNode.insertBefore(C,r.nextSibling)},i);return S}o(_d,"fromTextArea");function Qm(r){r.off=he,r.on=H,r.wheelEventPixels=u0,r.Doc=cn,r.splitLines=Xs,r.countColumn=Et,r.findColumn=br,r.isWordChar=Gt,r.Pass=Ut,r.signal=ke,r.Line=He,r.changeEnd=Ts,r.scrollbarModel=Xf,r.Pos=ae,r.cmpPos=$e,r.modes=Ul,r.mimeModes=ls,r.resolveMode=Qs,r.getMode=fu,r.modeExtensions=Ro,r.extendMode=Ip,r.copyState=Fo,r.startState=Df,r.innerMode=$l,r.commands=xl,r.keyMap=qo,r.keyName=dd,r.isModifierKey=cc,r.lookupKey=Vo,r.normalizeKeyMap=v0,r.StringStream=zt,r.SharedTextMarker=ya,r.TextMarker=Ls,r.LineWidget=Uu,r.e_preventDefault=Jt,r.e_stopPropagation=ti,r.e_stop=to,r.addClass=Ye,r.contains=Ve,r.rmClass=xe,r.keyNames=Ps}o(Qm,"addLegacyProps"),gc(Pt),Ko(Pt);var dn="iter insert remove copy getEditor constructor".split(" ");for(var Sc in cn.prototype)cn.prototype.hasOwnProperty(Sc)&&at(dn,Sc)<0&&(Pt.prototype[Sc]=function(r){return function(){return r.apply(this.doc,arguments)}}(cn.prototype[Sc]));return Dr(cn),Pt.inputStyles={textarea:lr,contenteditable:wt},Pt.defineMode=function(r){!Pt.defaults.mode&&r!="null"&&(Pt.defaults.mode=r),Fp.apply(this,arguments)},Pt.defineMIME=Af,Pt.defineMode("null",function(){return{token:function(r){return r.skipToEnd()}}}),Pt.defineMIME("text/plain","null"),Pt.defineExtension=function(r,i){Pt.prototype[r]=i},Pt.defineDocExtension=function(r,i){cn.prototype[r]=i},Pt.fromTextArea=_d,Qm(Pt),Pt.version="5.62.3",Pt})});var eO=ur((X4,Jk)=>{var cI=typeof Element!="undefined",pI=typeof Map=="function",dI=typeof Set=="function",hI=typeof ArrayBuffer=="function"&&!!ArrayBuffer.isView;function Ay(e,t){if(e===t)return!0;if(e&&t&&typeof e=="object"&&typeof t=="object"){if(e.constructor!==t.constructor)return!1;var n,l,d;if(Array.isArray(e)){if(n=e.length,n!=t.length)return!1;for(l=n;l--!=0;)if(!Ay(e[l],t[l]))return!1;return!0}var m;if(pI&&e instanceof Map&&t instanceof Map){if(e.size!==t.size)return!1;for(m=e.entries();!(l=m.next()).done;)if(!t.has(l.value[0]))return!1;for(m=e.entries();!(l=m.next()).done;)if(!Ay(l.value[1],t.get(l.value[0])))return!1;return!0}if(dI&&e instanceof Set&&t instanceof Set){if(e.size!==t.size)return!1;for(m=e.entries();!(l=m.next()).done;)if(!t.has(l.value[0]))return!1;return!0}if(hI&&ArrayBuffer.isView(e)&&ArrayBuffer.isView(t)){if(n=e.length,n!=t.length)return!1;for(l=n;l--!=0;)if(e[l]!==t[l])return!1;return!0}if(e.constructor===RegExp)return e.source===t.source&&e.flags===t.flags;if(e.valueOf!==Object.prototype.valueOf)return e.valueOf()===t.valueOf();if(e.toString!==Object.prototype.toString)return e.toString()===t.toString();if(d=Object.keys(e),n=d.length,n!==Object.keys(t).length)return!1;for(l=n;l--!=0;)if(!Object.prototype.hasOwnProperty.call(t,d[l]))return!1;if(cI&&e instanceof Element)return!1;for(l=n;l--!=0;)if(!((d[l]==="_owner"||d[l]==="__v"||d[l]==="__o")&&e.$$typeof)&&!Ay(e[d[l]],t[d[l]]))return!1;return!0}return e!==e&&t!==t}o(Ay,"equal");Jk.exports=o(function(t,n){try{return Ay(t,n)}catch(l){if((l.message||"").match(/stack|recursion/i))return console.warn("react-fast-compare cannot handle circular refs"),!1;throw l}},"isEqual")});var dO=ur((d$,pO)=>{"use strict";var TI="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED";pO.exports=TI});var gO=ur((h$,vO)=>{"use strict";var kI=dO();function hO(){}o(hO,"emptyFunction");function mO(){}o(mO,"emptyFunctionWithReset");mO.resetWarningCache=hO;vO.exports=function(){function e(l,d,m,p,x,_){if(_!==kI){var O=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw O.name="Invariant Violation",O}}o(e,"shim"),e.isRequired=e;function t(){return e}o(t,"getShim");var n={array:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:mO,resetWarningCache:hO};return n.PropTypes=n,n}});var Jh=ur((g$,yO)=>{yO.exports=gO()();var m$,v$});var _S=ur((y$,wO)=>{wO.exports=o(function(t,n,l,d){var m=l?l.call(d,t,n):void 0;if(m!==void 0)return!!m;if(t===n)return!0;if(typeof t!="object"||!t||typeof n!="object"||!n)return!1;var p=Object.keys(t),x=Object.keys(n);if(p.length!==x.length)return!1;for(var _=Object.prototype.hasOwnProperty.bind(n),O=0;O=0)&&(n[d]=e[d]);return n}o(Ja,"_objectWithoutPropertiesLoose");var fx=pe(pT()),or=pe(De()),gT=pe(vT());var NR=[],PR=[null,null];function MR(e,t){var n=e[1];return[t.payload,n+1]}o(MR,"storeStateUpdatesReducer");function yT(e,t,n){hf(function(){return e.apply(void 0,t)},n)}o(yT,"useIsomorphicLayoutEffectWithArgs");function AR(e,t,n,l,d,m,p){e.current=l,t.current=d,n.current=!1,m.current&&(m.current=null,p())}o(AR,"captureWrapperProps");function DR(e,t,n,l,d,m,p,x,_,O){if(!!e){var D=!1,Y=null,U=o(function(){if(!D){var Q=t.getState(),F,M;try{F=l(Q,d.current)}catch(R){M=R,Y=R}M||(Y=null),F===m.current?p.current||_():(m.current=F,x.current=F,p.current=!0,O({type:"STORE_UPDATED",payload:{error:M}}))}},"checkForUpdates");n.onStateChange=U,n.trySubscribe(),U();var X=o(function(){if(D=!0,n.tryUnsubscribe(),n.onStateChange=null,Y)throw Y},"unsubscribeWrapper");return X}}o(DR,"subscribeUpdates");var RR=o(function(){return[null,0]},"initStateUpdates");function $g(e,t){t===void 0&&(t={});var n=t,l=n.getDisplayName,d=l===void 0?function(ie){return"ConnectAdvanced("+ie+")"}:l,m=n.methodName,p=m===void 0?"connectAdvanced":m,x=n.renderCountProp,_=x===void 0?void 0:x,O=n.shouldHandleStateChanges,D=O===void 0?!0:O,Y=n.storeKey,U=Y===void 0?"store":Y,X=n.withRef,te=X===void 0?!1:X,Q=n.forwardRef,F=Q===void 0?!1:Q,M=n.context,R=M===void 0?Yn:M,K=Ja(n,["getDisplayName","methodName","renderCountProp","shouldHandleStateChanges","storeKey","withRef","forwardRef","context"]);if(!1)var V;var ue=R;return o(function(de){var ge=de.displayName||de.name||"Component",xe=d(ge),qe=To({},K,{getDisplayName:d,methodName:p,renderCountProp:_,shouldHandleStateChanges:D,storeKey:U,displayName:xe,wrappedComponentName:ge,WrappedComponent:de}),et=K.pure;function Te(Ye){return e(Ye.dispatch,qe)}o(Te,"createChildSelector");var xt=et?or.useMemo:function(Ye){return Ye()};function Ue(Ye){var Qt=(0,or.useMemo)(function(){var vr=Ye.reactReduxForwardedRef,Fi=Ja(Ye,["reactReduxForwardedRef"]);return[Ye.context,vr,Fi]},[Ye]),ft=Qt[0],Ar=Qt[1],Kt=Qt[2],Et=(0,or.useMemo)(function(){return ft&&ft.Consumer&&(0,gT.isContextConsumer)(or.default.createElement(ft.Consumer,null))?ft:ue},[ft,ue]),St=(0,or.useContext)(Et),at=Boolean(Ye.store)&&Boolean(Ye.store.getState)&&Boolean(Ye.store.dispatch),_r=Boolean(St)&&Boolean(St.store),Ut=at?Ye.store:St.store,$t=(0,or.useMemo)(function(){return Te(Ut)},[Ut]),ne=(0,or.useMemo)(function(){if(!D)return PR;var vr=new sp(Ut,at?null:St.subscription),Fi=vr.notifyNestedSubs.bind(vr);return[vr,Fi]},[Ut,at,St]),tt=ne[0],br=ne[1],jt=(0,or.useMemo)(function(){return at?St:To({},St,{subscription:tt})},[at,St,tt]),qt=(0,or.useReducer)(MR,NR,RR),Se=qt[0],Er=Se[0],nn=qt[1];if(Er&&Er.error)throw Er.error;var Fn=(0,or.useRef)(),ei=(0,or.useRef)(Kt),on=(0,or.useRef)(),Gt=(0,or.useRef)(!1),dr=xt(function(){return on.current&&Kt===ei.current?on.current:$t(Ut.getState(),Kt)},[Ut,Er,Kt]);yT(AR,[ei,Fn,Gt,Kt,dr,on,br]),yT(DR,[D,Ut,tt,$t,ei,Fn,Gt,on,br,nn],[Ut,tt,$t]);var ct=(0,or.useMemo)(function(){return or.default.createElement(de,To({},dr,{ref:Ar}))},[Ar,de,dr]),Do=(0,or.useMemo)(function(){return D?or.default.createElement(Et.Provider,{value:jt},ct):ct},[Et,ct,jt]);return Do}o(Ue,"ConnectFunction");var Ve=et?or.default.memo(Ue):Ue;if(Ve.WrappedComponent=de,Ve.displayName=Ue.displayName=xe,F){var Ke=or.default.forwardRef(o(function(Qt,ft){return or.default.createElement(Ve,To({},Qt,{reactReduxForwardedRef:ft}))},"forwardConnectRef"));return Ke.displayName=xe,Ke.WrappedComponent=de,(0,fx.default)(Ke,de)}return(0,fx.default)(Ve,de)},"wrapWithConnect")}o($g,"connectAdvanced");function wT(e,t){return e===t?e!==0||t!==0||1/e==1/t:e!==e&&t!==t}o(wT,"is");function lp(e,t){if(wT(e,t))return!0;if(typeof e!="object"||e===null||typeof t!="object"||t===null)return!1;var n=Object.keys(e),l=Object.keys(t);if(n.length!==l.length)return!1;for(var d=0;d=0;l--){var d=t[l](e);if(d)return d}return function(m,p){throw new Error("Invalid value of type "+typeof e+" for "+n+" argument when connecting component "+p.wrappedComponentName+".")}}o(dx,"match");function GR(e,t){return e===t}o(GR,"strictEqual");function YR(e){var t=e===void 0?{}:e,n=t.connectHOC,l=n===void 0?$g:n,d=t.mapStateToPropsFactories,m=d===void 0?CT:d,p=t.mapDispatchToPropsFactories,x=p===void 0?ST:p,_=t.mergePropsFactories,O=_===void 0?_T:_,D=t.selectorFactory,Y=D===void 0?px:D;return o(function(X,te,Q,F){F===void 0&&(F={});var M=F,R=M.pure,K=R===void 0?!0:R,V=M.areStatesEqual,ue=V===void 0?GR:V,ie=M.areOwnPropsEqual,de=ie===void 0?lp:ie,ge=M.areStatePropsEqual,xe=ge===void 0?lp:ge,qe=M.areMergedPropsEqual,et=qe===void 0?lp:qe,Te=Ja(M,["pure","areStatesEqual","areOwnPropsEqual","areStatePropsEqual","areMergedPropsEqual"]),xt=dx(X,m,"mapStateToProps"),Ue=dx(te,x,"mapDispatchToProps"),Ve=dx(Q,O,"mergeProps");return l(Y,To({methodName:"connect",getDisplayName:o(function(Ye){return"Connect("+Ye+")"},"getDisplayName"),shouldHandleStateChanges:Boolean(X),initMapStateToProps:xt,initMapDispatchToProps:Ue,initMergeProps:Ve,pure:K,areStatesEqual:ue,areOwnPropsEqual:de,areStatePropsEqual:xe,areMergedPropsEqual:et},Te))},"connect")}o(YR,"createConnect");var Di=YR();var ET=pe(De());var bT=pe(De());function jg(){var e=(0,bT.useContext)(Yn);return e}o(jg,"useReduxContext");function qg(e){e===void 0&&(e=Yn);var t=e===Yn?jg:function(){return(0,ET.useContext)(e)};return o(function(){var l=t(),d=l.store;return d},"useStore")}o(qg,"createStoreHook");var hx=qg();function TT(e){e===void 0&&(e=Yn);var t=e===Yn?hx:qg(e);return o(function(){var l=t();return l.dispatch},"useDispatch")}o(TT,"createDispatchHook");var $s=TT();var Xi=pe(De());var XR=o(function(t,n){return t===n},"refEquality");function QR(e,t,n,l){var d=(0,Xi.useReducer)(function(te){return te+1},0),m=d[1],p=(0,Xi.useMemo)(function(){return new sp(n,l)},[n,l]),x=(0,Xi.useRef)(),_=(0,Xi.useRef)(),O=(0,Xi.useRef)(),D=(0,Xi.useRef)(),Y=n.getState(),U;try{if(e!==_.current||Y!==O.current||x.current){var X=e(Y);D.current===void 0||!t(X,D.current)?U=X:U=D.current}else U=D.current}catch(te){throw x.current&&(te.message+=` The error may be correlated with this previous error: -`+w.current.stack+` +`+x.current.stack+` -`),te}return sf(function(){_.current=e,O.current=Y,D.current=W,w.current=void 0}),sf(function(){function te(){try{var Q=n.getState(),R=_.current(Q);if(t(R,D.current))return;D.current=R,O.current=Q}catch(P){w.current=P}v()}return o(te,"checkForUpdates"),p.onStateChange=te,p.trySubscribe(),te(),function(){return p.tryUnsubscribe()}},[n,p]),W}o($R,"useSelectorWithStoreAndSubscription");function CT(e){e===void 0&&(e=Yn);var t=e===Yn?Ug:function(){return(0,Xi.useContext)(e)};return o(function(l,d){d===void 0&&(d=jR);var v=t(),p=v.store,w=v.subscription,_=$R(l,d,p,w);return(0,Xi.useDebugValue)(_),_},"useSelector")}o(CT,"createSelectorHook");var dx=CT();var hx=pe(Xa());GE(hx.unstable_batchedUpdates);var An=pe(Re());var _T="UI_FLOWVIEW_SET_TAB",bT="SET_CONTENT_VIEW_FOR",qR={tab:"request",contentViewFor:{}};function mx(e=qR,t){switch(t.type){case bT:return Ft(Le({},e),{contentViewFor:Ft(Le({},e.contentViewFor),{[t.messageId]:t.contentView})});case _T:return Ft(Le({},e),{tab:t.tab?t.tab:"request"});default:return e}}o(mx,"reducer");function lf(e){return{type:_T,tab:e}}o(lf,"selectTab");function jg(e,t){return{type:bT,messageId:e,contentView:t}}o(jg,"setContentViewFor");var ET=pe(xh()),VR=pe(Re());window._=ET.default;window.React=VR;var $g=o(function(e){if(e===0)return"0";for(var t=["b","kb","mb","gb","tb"],n=0;ne);n++);var l;return e%Math.pow(1024,n)==0?l=0:l=1,(e/Math.pow(1024,n)).toFixed(l)+t[n]},"formatSize"),qg=o(function(e){for(var t=e,n=["ms","s","min","h"],l=[1e3,60,60],d=0;Math.abs(t)>=l[d]&&dEt(e,Le({method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)},n));Et.post=(e,t,n={})=>Et(e,Le({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)},n));function Sh(e,...t){return Oa(this,null,function*(){return yield(yield Et(`/commands/${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({arguments:t})})).json()})}o(Sh,"runCommand");var _h={};CC(_h,{ADD:()=>Sx,RECEIVE:()=>bx,REMOVE:()=>_x,SET_FILTER:()=>wx,SET_SORT:()=>xx,UPDATE:()=>Cx,add:()=>YR,defaultState:()=>Vg,receive:()=>ZR,reduce:()=>ep,remove:()=>QR,setFilter:()=>Ex,setSort:()=>kT,update:()=>XR});var yx=pe(TT()),wx="LIST_SET_FILTER",xx="LIST_SET_SORT",Sx="LIST_ADD",Cx="LIST_UPDATE",_x="LIST_REMOVE",bx="LIST_RECEIVE",Vg={byId:{},list:[],listIndex:{},view:[],viewIndex:{}};function ep(e=Vg,t){let{byId:n,list:l,listIndex:d,view:v,viewIndex:p}=e;switch(t.type){case wx:v=(0,yx.default)(l.filter(t.filter),t.sort),p={},v.forEach((O,D)=>{p[O.id]=D});break;case xx:v=(0,yx.default)([...v],t.sort),p={},v.forEach((O,D)=>{p[O.id]=D});break;case Sx:if(t.item.id in n)break;n=Ft(Le({},n),{[t.item.id]:t.item}),d=Ft(Le({},d),{[t.item.id]:l.length}),l=[...l,t.item],t.filter(t.item)&&({view:v,viewIndex:p}=OT(e,t.item,t.sort));break;case Cx:n=Ft(Le({},n),{[t.item.id]:t.item}),l=[...l],l[d[t.item.id]]=t.item;let w=t.item.id in p,_=t.filter(t.item);_&&!w?{view:v,viewIndex:p}=OT(e,t.item,t.sort):!_&&w?{data:v,dataIndex:p}=Tx(v,p,t.item.id):_&&w&&({view:v,viewIndex:p}=JR(e,t.item,t.sort));break;case _x:if(!(t.id in n))break;n=Le({},n),delete n[t.id],{data:l,dataIndex:d}=Tx(l,d,t.id),t.id in p&&({data:v,dataIndex:p}=Tx(v,p,t.id));break;case bx:l=t.list,d={},n={},l.forEach((O,D)=>{n[O.id]=O,d[O.id]=D}),v=l.filter(t.filter).sort(t.sort),p={},v.forEach((O,D)=>{p[O.id]=D});break}return{byId:n,list:l,listIndex:d,view:v,viewIndex:p}}o(ep,"reduce");function Ex(e=Kg,t=Ch){return{type:wx,filter:e,sort:t}}o(Ex,"setFilter");function kT(e=Ch){return{type:xx,sort:e}}o(kT,"setSort");function YR(e,t=Kg,n=Ch){return{type:Sx,item:e,filter:t,sort:n}}o(YR,"add");function XR(e,t=Kg,n=Ch){return{type:Cx,item:e,filter:t,sort:n}}o(XR,"update");function QR(e){return{type:_x,id:e}}o(QR,"remove");function ZR(e,t=Kg,n=Ch){return{type:bx,list:e,filter:t,sort:n}}o(ZR,"receive");function OT(e,t,n){let l=eF(e.view,t,n),d=[...e.view],v=Le({},e.viewIndex);d.splice(l,0,t);for(let p=d.length-1;p>=l;p--)v[d[p].id]=p;return{view:d,viewIndex:v}}o(OT,"sortedInsert");function Tx(e,t,n){let l=t[n],d=[...e],v=Le({},t);delete v[n],d.splice(l,1);for(let p=d.length-1;p>=l;p--)v[d[p].id]=p;return{data:d,dataIndex:v}}o(Tx,"removeData");function JR(e,t,n){let l=[...e.view],d=Le({},e.viewIndex),v=d[t.id];for(l[v]=t;v+10;)l[v]=l[v+1],l[v+1]=t,d[t.id]=v+1,d[l[v].id]=v,++v;for(;v>0&&n(l[v],l[v-1])<0;)l[v]=l[v-1],l[v-1]=t,d[t.id]=v-1,d[l[v].id]=v,--v;return{view:l,viewIndex:d}}o(JR,"sortedUpdate");function eF(e,t,n){let l=0,d=e.length;for(;l>>1;n(t,e[v])>=0?l=v+1:d=v}return l}o(eF,"sortedIndex");function Kg(){return!0}o(Kg,"defaultFilter");function Ch(e,t){return 0}o(Ch,"defaultSort");var LT={http:80,https:443},zr=class{static getContentType(t){var n=zr.get_first_header(t,/^Content-Type$/i);if(n)return n.split(";")[0].trim()}static get_first_header(t,n){let l=t;l._headerLookups||Object.defineProperty(l,"_headerLookups",{value:{},configurable:!1,enumerable:!1,writable:!1});let d=n.toString();if(!(d in l._headerLookups)){let v;for(let p=0;p{switch(e.type){case"http":let t=e.request.contentLength||0;return e.response&&(t+=e.response.contentLength||0),e.websocket&&(t+=e.websocket.messages_meta.contentLength||0),t;case"tcp":return e.messages_meta.contentLength||0}},"getTotalSize"),Gg=o(e=>e.type==="http"&&!e.websocket,"canReplay");var af=function(){"use strict";function e(l,d){function v(){this.constructor=l}o(v,"ctor"),v.prototype=d.prototype,l.prototype=new v}o(e,"peg$subclass");function t(l,d,v,p){this.message=l,this.expected=d,this.found=v,this.location=p,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,t)}o(t,"peg$SyntaxError"),e(t,Error);function n(l){var d=arguments.length>1?arguments[1]:{},v=this,p={},w={start:Gl},_=Gl,O={type:"other",description:"filter expression"},D=o(function(x){return x},"peg$c1"),Y={type:"other",description:"whitespace"},W=/^[ \t\n\r]/,X={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},te={type:"other",description:"control character"},Q=/^[|&!()~"]/,R={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},P={type:"other",description:"optional whitespace"},F="|",K={type:"literal",value:"|",description:'"|"'},V=o(function(x,k){return il(x,k)},"peg$c11"),ue="&",ie={type:"literal",value:"&",description:'"&"'},de=o(function(x,k){return Tr(x,k)},"peg$c14"),ge="!",we={type:"literal",value:"!",description:'"!"'},qe=o(function(x){return Nf(x)},"peg$c17"),Je="(",be={type:"literal",value:"(",description:'"("'},yt=")",Be={type:"literal",value:")",description:'")"'},Ve=o(function(x){return Pf(x)},"peg$c22"),Ke="true",Ge={type:"literal",value:"true",description:'"true"'},Yt=o(function(){return gu},"peg$c25"),ut="false",Dr={type:"literal",value:"false",description:'"false"'},qt=o(function(){return yu},"peg$c28"),_t="~a",wt={type:"literal",value:"~a",description:'"~a"'},st=o(function(){return Ql},"peg$c31"),_r="~b",Bt={type:"literal",value:"~b",description:'"~b"'},Ut=o(function(x){return Dp(x)},"peg$c34"),ne="~bq",et={type:"literal",value:"~bq",description:'"~bq"'},br=o(function(x){return Wn(x)},"peg$c37"),zt="~bs",jt={type:"literal",value:"~bs",description:'"~bs"'},xe=o(function(x){return Rp(x)},"peg$c40"),Er="~c",on={type:"literal",value:"~c",description:'"~c"'},Dn=o(function(x){return Tn(x)},"peg$c43"),ei="~d",sn={type:"literal",value:"~d",description:'"~d"'},Vt=o(function(x){return wu(x)},"peg$c46"),cr="~dst",ft={type:"literal",value:"~dst",description:'"~dst"'},Do=o(function(x){return ro(x)},"peg$c49"),vr="~e",Ri={type:"literal",value:"~e",description:'"~e"'},ln=o(function(){return hs},"peg$c52"),Rn="~h",hi={type:"literal",value:"~h",description:'"~h"'},mi=o(function(x){return ms(x)},"peg$c55"),Fn="~hq",bn={type:"literal",value:"~hq",description:'"~hq"'},Ys=o(function(x){return xt(x)},"peg$c58"),H="~hs",J={type:"literal",value:"~hs",description:'"~hs"'},he=o(function(x){return no(x)},"peg$c61"),Ee="~http",Xt={type:"literal",value:"~http",description:'"~http"'},Hl=o(function(){return Zl},"peg$c64"),At="~m",Rr={type:"literal",value:"~m",description:'"~m"'},Qt=o(function(x){return Fp(x)},"peg$c67"),ti="~marked",os={type:"literal",value:"~marked",description:'"~marked"'},to=o(function(){return io},"peg$c70"),vi="~q",Xs={type:"literal",value:"~q",description:'"~q"'},ss=o(function(){return ir},"peg$c73"),an="~src",bp={type:"literal",value:"~src",description:'"~src"'},Qs=o(function(x){return Af(x)},"peg$c76"),Cf="~s",Zs={type:"literal",value:"~s",description:'"~s"'},Ep=o(function(){return Mf},"peg$c79"),_f="~t",lu={type:"literal",value:"~t",description:'"~t"'},Tp=o(function(x){return vs(x)},"peg$c82"),Wl="~tcp",ls={type:"literal",value:"~tcp",description:'"~tcp"'},kp=o(function(){return ol},"peg$c85"),bf="~tq",Js={type:"literal",value:"~tq",description:'"~tq"'},au=o(function(x){return Uo(x)},"peg$c88"),Ro="~ts",Op={type:"literal",value:"~ts",description:'"~ts"'},Fo=o(function(x){return Ip(x)},"peg$c91"),Bl="~u",Ef={type:"literal",value:"~u",description:'"~u"'},Dt=o(function(x){return Jl(x)},"peg$c94"),Ne="~websocket",gi={type:"literal",value:"~websocket",description:'"~websocket"'},uu=o(function(){return ea},"peg$c97"),yi={type:"other",description:"integer"},dt=/^['"]/,Fi={type:"class",value:`['"]`,description:`['"]`},Io=/^[0-9]/,el={type:"class",value:"[0-9]",description:"[0-9]"},ae=o(function(x){return parseInt(x.join(""),10)},"peg$c103"),ze={type:"other",description:"string"},Ul='"',zl={type:"literal",value:'"',description:'"\\""'},Ho=o(function(x){return x.join("")},"peg$c107"),as="'",jl={type:"literal",value:"'",description:`"'"`},Ue=/^["\\]/,Lp={type:"class",value:'["\\\\]',description:'["\\\\]'},tl={type:"any",description:"any character"},ri=o(function(x){return x},"peg$c113"),In="\\",fu={type:"literal",value:"\\",description:'"\\\\"'},cu=/^['\\]/,us={type:"class",value:"['\\\\]",description:"['\\\\]"},rl=/^['"\\]/,Tf={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},$l="n",ql={type:"literal",value:"n",description:'"n"'},Vl=o(function(){return` -`},"peg$c122"),Wo="r",pu={type:"literal",value:"r",description:'"r"'},kf=o(function(){return"\r"},"peg$c125"),Np="t",du={type:"literal",value:"t",description:'"t"'},wi=o(function(){return" "},"peg$c128"),M=0,We=0,Bo=[{line:1,column:1,seenCR:!1}],un=0,hu=[],Se=0,Kl;if("startRule"in d){if(!(d.startRule in w))throw new Error(`Can't start parsing from rule "`+d.startRule+'".');_=w[d.startRule]}function Jh(){return l.substring(We,M)}o(Jh,"text");function Of(){return fs(We,M)}o(Of,"location");function Pp(x){throw cs(null,[{type:"other",description:x}],l.substring(We,M),fs(We,M))}o(Pp,"expected");function Lf(x){throw cs(x,null,l.substring(We,M),fs(We,M))}o(Lf,"error");function mu(x){var k=Bo[x],U,j;if(k)return k;for(U=x-1;!Bo[U];)U--;for(k=Bo[U],k={line:k.line,column:k.column,seenCR:k.seenCR};Uun&&(un=M,hu=[]),hu.push(x))}o(Pe,"peg$fail");function cs(x,k,U,j){function Bn(dr){var Un=1;for(dr.sort(function(gr,xi){return gr.descriptionxi.description?1:0});Un1?xi.slice(0,-1).join(", ")+" or "+xi[dr.length-1]:xi[0],Ii=Un?'"'+gr(Un)+'"':"end of input","Expected "+Vr+" but "+Ii+" found."}return o(pr,"buildMessage"),k!==null&&Bn(k),new t(x!==null?x:pr(k,U),k,U,j)}o(cs,"peg$buildException");function Gl(){var x,k,U,j;return Se++,x=M,k=Hn(),k!==p?(U=vu(),U!==p?(j=Hn(),j!==p?(We=x,k=D(U),x=k):(M=x,x=p)):(M=x,x=p)):(M=x,x=p),Se--,x===p&&(k=p,Se===0&&Pe(O)),x}o(Gl,"peg$parsestart");function Ae(){var x,k;return Se++,W.test(l.charAt(M))?(x=l.charAt(M),M++):(x=p,Se===0&&Pe(X)),Se--,x===p&&(k=p,Se===0&&Pe(Y)),x}o(Ae,"peg$parsews");function Tt(){var x,k;return Se++,Q.test(l.charAt(M))?(x=l.charAt(M),M++):(x=p,Se===0&&Pe(R)),Se--,x===p&&(k=p,Se===0&&Pe(te)),x}o(Tt,"peg$parsecc");function Hn(){var x,k;for(Se++,x=[],k=Ae();k!==p;)x.push(k),k=Ae();return Se--,x===p&&(k=p,Se===0&&Pe(P)),x}o(Hn,"peg$parse__");function vu(){var x,k,U,j,Bn,pr;return x=M,k=nl(),k!==p?(U=Hn(),U!==p?(l.charCodeAt(M)===124?(j=F,M++):(j=p,Se===0&&Pe(K)),j!==p?(Bn=Hn(),Bn!==p?(pr=vu(),pr!==p?(We=x,k=V(k,pr),x=k):(M=x,x=p)):(M=x,x=p)):(M=x,x=p)):(M=x,x=p)):(M=x,x=p),x===p&&(x=nl()),x}o(vu,"peg$parseOrExpr");function nl(){var x,k,U,j,Bn,pr;if(x=M,k=En(),k!==p?(U=Hn(),U!==p?(l.charCodeAt(M)===38?(j=ue,M++):(j=p,Se===0&&Pe(ie)),j!==p?(Bn=Hn(),Bn!==p?(pr=nl(),pr!==p?(We=x,k=de(k,pr),x=k):(M=x,x=p)):(M=x,x=p)):(M=x,x=p)):(M=x,x=p)):(M=x,x=p),x===p){if(x=M,k=En(),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=nl(),j!==p?(We=x,k=de(k,j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;x===p&&(x=En())}return x}o(nl,"peg$parseAndExpr");function En(){var x,k,U,j;return x=M,l.charCodeAt(M)===33?(k=ge,M++):(k=p,Se===0&&Pe(we)),k!==p?(U=Hn(),U!==p?(j=En(),j!==p?(We=x,k=qe(j),x=k):(M=x,x=p)):(M=x,x=p)):(M=x,x=p),x===p&&(x=Mp()),x}o(En,"peg$parseNotExpr");function Mp(){var x,k,U,j,Bn,pr;return x=M,l.charCodeAt(M)===40?(k=Je,M++):(k=p,Se===0&&Pe(be)),k!==p?(U=Hn(),U!==p?(j=vu(),j!==p?(Bn=Hn(),Bn!==p?(l.charCodeAt(M)===41?(pr=yt,M++):(pr=p,Se===0&&Pe(Be)),pr!==p?(We=x,k=Ve(j),x=k):(M=x,x=p)):(M=x,x=p)):(M=x,x=p)):(M=x,x=p)):(M=x,x=p),x===p&&(x=Ap()),x}o(Mp,"peg$parseBindingExpr");function Ap(){var x,k,U,j;if(x=M,l.substr(M,4)===Ke?(k=Ke,M+=4):(k=p,Se===0&&Pe(Ge)),k!==p&&(We=x,k=Yt()),x=k,x===p&&(x=M,l.substr(M,5)===ut?(k=ut,M+=5):(k=p,Se===0&&Pe(Dr)),k!==p&&(We=x,k=qt()),x=k,x===p&&(x=M,l.substr(M,2)===_t?(k=_t,M+=2):(k=p,Se===0&&Pe(wt)),k!==p&&(We=x,k=st()),x=k,x===p))){if(x=M,l.substr(M,2)===_r?(k=_r,M+=2):(k=p,Se===0&&Pe(Bt)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=Ut(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.substr(M,3)===ne?(k=ne,M+=3):(k=p,Se===0&&Pe(et)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=br(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.substr(M,3)===zt?(k=zt,M+=3):(k=p,Se===0&&Pe(jt)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=xe(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.substr(M,2)===Er?(k=Er,M+=2):(k=p,Se===0&&Pe(on)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Yl(),j!==p?(We=x,k=Dn(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.substr(M,2)===ei?(k=ei,M+=2):(k=p,Se===0&&Pe(sn)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=Vt(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.substr(M,4)===cr?(k=cr,M+=4):(k=p,Se===0&&Pe(ft)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=Do(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p&&(x=M,l.substr(M,2)===vr?(k=vr,M+=2):(k=p,Se===0&&Pe(Ri)),k!==p&&(We=x,k=ln()),x=k,x===p)){if(x=M,l.substr(M,2)===Rn?(k=Rn,M+=2):(k=p,Se===0&&Pe(hi)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=mi(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.substr(M,3)===Fn?(k=Fn,M+=3):(k=p,Se===0&&Pe(bn)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=Ys(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.substr(M,3)===H?(k=H,M+=3):(k=p,Se===0&&Pe(J)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=he(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p&&(x=M,l.substr(M,5)===Ee?(k=Ee,M+=5):(k=p,Se===0&&Pe(Xt)),k!==p&&(We=x,k=Hl()),x=k,x===p)){if(x=M,l.substr(M,2)===At?(k=At,M+=2):(k=p,Se===0&&Pe(Rr)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=Qt(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p&&(x=M,l.substr(M,7)===ti?(k=ti,M+=7):(k=p,Se===0&&Pe(os)),k!==p&&(We=x,k=to()),x=k,x===p&&(x=M,l.substr(M,2)===vi?(k=vi,M+=2):(k=p,Se===0&&Pe(Xs)),k!==p&&(We=x,k=ss()),x=k,x===p))){if(x=M,l.substr(M,4)===an?(k=an,M+=4):(k=p,Se===0&&Pe(bp)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=Qs(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p&&(x=M,l.substr(M,2)===Cf?(k=Cf,M+=2):(k=p,Se===0&&Pe(Zs)),k!==p&&(We=x,k=Ep()),x=k,x===p)){if(x=M,l.substr(M,2)===_f?(k=_f,M+=2):(k=p,Se===0&&Pe(lu)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=Tp(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p&&(x=M,l.substr(M,4)===Wl?(k=Wl,M+=4):(k=p,Se===0&&Pe(ls)),k!==p&&(We=x,k=kp()),x=k,x===p)){if(x=M,l.substr(M,3)===bf?(k=bf,M+=3):(k=p,Se===0&&Pe(Js)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=au(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.substr(M,3)===Ro?(k=Ro,M+=3):(k=p,Se===0&&Pe(Op)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=Fo(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.substr(M,2)===Bl?(k=Bl,M+=2):(k=p,Se===0&&Pe(Ef)),k!==p){if(U=[],j=Ae(),j!==p)for(;j!==p;)U.push(j),j=Ae();else U=p;U!==p?(j=Fr(),j!==p?(We=x,k=Dt(j),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;x===p&&(x=M,l.substr(M,10)===Ne?(k=Ne,M+=10):(k=p,Se===0&&Pe(gi)),k!==p&&(We=x,k=uu()),x=k,x===p&&(x=M,k=Fr(),k!==p&&(We=x,k=Dt(k)),x=k))}}}}}}}}}}}}}}}return x}o(Ap,"peg$parseExpr");function Yl(){var x,k,U,j;if(Se++,x=M,dt.test(l.charAt(M))?(k=l.charAt(M),M++):(k=p,Se===0&&Pe(Fi)),k===p&&(k=null),k!==p){if(U=[],Io.test(l.charAt(M))?(j=l.charAt(M),M++):(j=p,Se===0&&Pe(el)),j!==p)for(;j!==p;)U.push(j),Io.test(l.charAt(M))?(j=l.charAt(M),M++):(j=p,Se===0&&Pe(el));else U=p;U!==p?(dt.test(l.charAt(M))?(j=l.charAt(M),M++):(j=p,Se===0&&Pe(Fi)),j===p&&(j=null),j!==p?(We=x,k=ae(U),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;return Se--,x===p&&(k=p,Se===0&&Pe(yi)),x}o(Yl,"peg$parseIntegerLiteral");function Fr(){var x,k,U,j;if(Se++,x=M,l.charCodeAt(M)===34?(k=Ul,M++):(k=p,Se===0&&Pe(zl)),k!==p){for(U=[],j=Ot();j!==p;)U.push(j),j=Ot();U!==p?(l.charCodeAt(M)===34?(j=Ul,M++):(j=p,Se===0&&Pe(zl)),j!==p?(We=x,k=Ho(U),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p){if(x=M,l.charCodeAt(M)===39?(k=as,M++):(k=p,Se===0&&Pe(jl)),k!==p){for(U=[],j=ps();j!==p;)U.push(j),j=ps();U!==p?(l.charCodeAt(M)===39?(j=as,M++):(j=p,Se===0&&Pe(jl)),j!==p?(We=x,k=Ho(U),x=k):(M=x,x=p)):(M=x,x=p)}else M=x,x=p;if(x===p)if(x=M,k=M,Se++,U=Tt(),Se--,U===p?k=void 0:(M=k,k=p),k!==p){if(U=[],j=Ir(),j!==p)for(;j!==p;)U.push(j),j=Ir();else U=p;U!==p?(We=x,k=Ho(U),x=k):(M=x,x=p)}else M=x,x=p}return Se--,x===p&&(k=p,Se===0&&Pe(ze)),x}o(Fr,"peg$parseStringLiteral");function Ot(){var x,k,U;return x=M,k=M,Se++,Ue.test(l.charAt(M))?(U=l.charAt(M),M++):(U=p,Se===0&&Pe(Lp)),Se--,U===p?k=void 0:(M=k,k=p),k!==p?(l.length>M?(U=l.charAt(M),M++):(U=p,Se===0&&Pe(tl)),U!==p?(We=x,k=ri(U),x=k):(M=x,x=p)):(M=x,x=p),x===p&&(x=M,l.charCodeAt(M)===92?(k=In,M++):(k=p,Se===0&&Pe(fu)),k!==p?(U=ds(),U!==p?(We=x,k=ri(U),x=k):(M=x,x=p)):(M=x,x=p)),x}o(Ot,"peg$parseDoubleStringChar");function ps(){var x,k,U;return x=M,k=M,Se++,cu.test(l.charAt(M))?(U=l.charAt(M),M++):(U=p,Se===0&&Pe(us)),Se--,U===p?k=void 0:(M=k,k=p),k!==p?(l.length>M?(U=l.charAt(M),M++):(U=p,Se===0&&Pe(tl)),U!==p?(We=x,k=ri(U),x=k):(M=x,x=p)):(M=x,x=p),x===p&&(x=M,l.charCodeAt(M)===92?(k=In,M++):(k=p,Se===0&&Pe(fu)),k!==p?(U=ds(),U!==p?(We=x,k=ri(U),x=k):(M=x,x=p)):(M=x,x=p)),x}o(ps,"peg$parseSingleStringChar");function Ir(){var x,k,U;return x=M,k=M,Se++,U=Ae(),Se--,U===p?k=void 0:(M=k,k=p),k!==p?(l.length>M?(U=l.charAt(M),M++):(U=p,Se===0&&Pe(tl)),U!==p?(We=x,k=ri(U),x=k):(M=x,x=p)):(M=x,x=p),x}o(Ir,"peg$parseUnquotedStringChar");function ds(){var x,k;return rl.test(l.charAt(M))?(x=l.charAt(M),M++):(x=p,Se===0&&Pe(Tf)),x===p&&(x=M,l.charCodeAt(M)===110?(k=$l,M++):(k=p,Se===0&&Pe(ql)),k!==p&&(We=x,k=Vl()),x=k,x===p&&(x=M,l.charCodeAt(M)===114?(k=Wo,M++):(k=p,Se===0&&Pe(pu)),k!==p&&(We=x,k=kf()),x=k,x===p&&(x=M,l.charCodeAt(M)===116?(k=Np,M++):(k=p,Se===0&&Pe(du)),k!==p&&(We=x,k=wi()),x=k))),x}o(ds,"peg$parseEscapeSequence");function il(x,k){function U(){return x.apply(this,arguments)||k.apply(this,arguments)}return o(U,"orFilter"),U.desc=x.desc+" or "+k.desc,U}o(il,"or");function Tr(x,k){function U(){return x.apply(this,arguments)&&k.apply(this,arguments)}return o(U,"andFilter"),U.desc=x.desc+" and "+k.desc,U}o(Tr,"and");function Nf(x){function k(){return!x.apply(this,arguments)}return o(k,"notFilter"),k.desc="not "+x.desc,k}o(Nf,"not");function Pf(x){function k(){return x.apply(this,arguments)}return o(k,"bindingFilter"),k.desc="("+x.desc+")",k}o(Pf,"binding");function gu(x){return!0}o(gu,"trueFilter"),gu.desc="true";function yu(x){return!1}o(yu,"falseFilter"),yu.desc="false";var Xl=[new RegExp("text/javascript"),new RegExp("application/x-javascript"),new RegExp("application/javascript"),new RegExp("text/css"),new RegExp("image/.*"),new RegExp("application/x-shockwave-flash")];function Ql(x){if(x.response){for(var k=qs.getContentType(x.response),U=Xl.length;U--;)if(Xl[U].test(k))return!0}return!1}o(Ql,"assetFilter"),Ql.desc="is asset";function Tn(x){function k(U){return U.response&&U.response.status_code===x}return o(k,"responseCodeFilter"),k.desc="resp. code is "+x,k}o(Tn,"responseCode");function Dp(x){x=new RegExp(x,"i");function k(U){return!0}return o(k,"bodyFilter"),k.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",k}o(Dp,"body");function Wn(x){x=new RegExp(x,"i");function k(U){return!0}return o(k,"requestBodyFilter"),k.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",k}o(Wn,"requestBody");function Rp(x){x=new RegExp(x,"i");function k(U){return!0}return o(k,"responseBodyFilter"),k.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",k}o(Rp,"responseBody");function wu(x){x=new RegExp(x,"i");function k(U){return U.request&&(x.test(U.request.host)||x.test(U.request.pretty_host))}return o(k,"domainFilter"),k.desc="domain matches "+x,k}o(wu,"domain");function ro(x){x=new RegExp(x,"i");function k(U){return!!U.server_conn.address&&x.test(U.server_conn.address[0]+":"+U.server_conn.address[1])}return o(k,"destinationFilter"),k.desc="destination address matches "+x,k}o(ro,"destination");function hs(x){return!!x.error}o(hs,"errorFilter"),hs.desc="has error";function ms(x){x=new RegExp(x,"i");function k(U){return U.request&&ko.match_header(U.request,x)||U.response&&qs.match_header(U.response,x)}return o(k,"headerFilter"),k.desc="header matches "+x,k}o(ms,"header");function xt(x){x=new RegExp(x,"i");function k(U){return U.request&&ko.match_header(U.request,x)}return o(k,"requestHeaderFilter"),k.desc="req. header matches "+x,k}o(xt,"requestHeader");function no(x){x=new RegExp(x,"i");function k(U){return U.response&&qs.match_header(U.response,x)}return o(k,"responseHeaderFilter"),k.desc="resp. header matches "+x,k}o(no,"responseHeader");function Zl(x){return x.type==="http"}o(Zl,"httpFilter"),Zl.desc="is an HTTP Flow";function Fp(x){x=new RegExp(x,"i");function k(U){return U.request&&x.test(U.request.method)}return o(k,"methodFilter"),k.desc="method matches "+x,k}o(Fp,"method");function io(x){return x.marked}o(io,"markedFilter"),io.desc="is marked";function ir(x){return x.request&&!x.response}o(ir,"noResponseFilter"),ir.desc="has no response";function Mf(x){return!!x.response}o(Mf,"responseFilter"),Mf.desc="has response";function Af(x){x=new RegExp(x,"i");function k(U){return!!U.client_conn.peername&&x.test(U.client_conn.peername[0]+":"+U.client_conn.peername[1])}return o(k,"sourceFilter"),k.desc="source address matches "+x,k}o(Af,"source");function vs(x){x=new RegExp(x,"i");function k(U){return U.request&&x.test(ko.getContentType(U.request))||U.response&&x.test(qs.getContentType(U.response))}return o(k,"contentTypeFilter"),k.desc="content type matches "+x,k}o(vs,"contentType");function ol(x){return x.type==="tcp"}o(ol,"tcpFilter"),ol.desc="is a TCP Flow";function Uo(x){x=new RegExp(x,"i");function k(U){return U.request&&x.test(ko.getContentType(U.request))}return o(k,"requestContentTypeFilter"),k.desc="req. content type matches "+x,k}o(Uo,"requestContentType");function Ip(x){x=new RegExp(x,"i");function k(U){return U.response&&x.test(qs.getContentType(U.response))}return o(k,"responseContentTypeFilter"),k.desc="resp. content type matches "+x,k}o(Ip,"responseContentType");function Jl(x){x=new RegExp(x,"i");function k(U){return U.request&&x.test(ko.pretty_url(U.request))}return o(k,"urlFilter"),k.desc="url matches "+x,k}o(Jl,"url");function ea(x){return x.type==="websocket"}if(o(ea,"websocketFilter"),ea.desc="is a Websocket Flow",Kl=_(),Kl!==p&&M===l.length)return Kl;throw Kl!==p&&MPx,icon:()=>Zg,method:()=>ey,path:()=>Jg,quickactions:()=>tp,size:()=>ry,status:()=>ty,time:()=>ny,timestamp:()=>iy,tls:()=>Qg});var ur=pe(Re());var Xg=pe(Xn());var Qg=o(({flow:e})=>ur.default.createElement("td",{className:(0,Xg.default)("col-tls",e.client_conn.tls_established?"col-tls-https":"col-tls-http")}),"tls");Qg.headerName="";Qg.sortKey=e=>e.type==="http"&&e.request.scheme;var Zg=o(({flow:e})=>ur.default.createElement("td",{className:"col-icon"},ur.default.createElement("div",{className:(0,Xg.default)("resource-icon",NT(e))})),"icon");Zg.headerName="";Zg.sortKey=e=>NT(e);var NT=o(e=>{if(e.type==="tcp")return"resource-icon-tcp";if(e.websocket)return"resource-icon-websocket";if(!e.response)return"resource-icon-plain";var t=qs.getContentType(e.response)||"";return e.response.status_code===304?"resource-icon-not-modified":300<=e.response.status_code&&e.response.status_code<400?"resource-icon-redirect":t.indexOf("image")>=0?"resource-icon-image":t.indexOf("javascript")>=0?"resource-icon-js":t.indexOf("css")>=0?"resource-icon-css":t.indexOf("html")>=0?"resource-icon-document":"resource-icon-plain"},"getIcon"),PT=o(e=>{var t,n;switch(e.type){case"http":return ko.pretty_url(e.request);case"tcp":return`${e.client_conn.peername.join(":")} \u2194 ${(n=(t=e.server_conn)==null?void 0:t.address)==null?void 0:n.join(":")}`}},"mainPath"),Jg=o(({flow:e})=>{let t;return e.error&&(e.error.msg==="Connection killed."?t=ur.default.createElement("i",{className:"fa fa-fw fa-times pull-right"}):t=ur.default.createElement("i",{className:"fa fa-fw fa-exclamation pull-right"})),ur.default.createElement("td",{className:"col-path"},e.is_replay==="request"&&ur.default.createElement("i",{className:"fa fa-fw fa-repeat pull-right"}),e.intercepted&&ur.default.createElement("i",{className:"fa fa-fw fa-pause pull-right"}),t,ur.default.createElement("span",{className:"marker pull-right"},e.marked),PT(e))},"path");Jg.headerName="Path";Jg.sortKey=e=>PT(e);var ey=o(({flow:e})=>ur.default.createElement("td",{className:"col-method"},e.type==="http"?e.request.method:e.type.toUpperCase()),"method");ey.headerName="Method";ey.sortKey=e=>e.type==="http"?e.request.method:e.type.toUpperCase();var ty=o(({flow:e})=>{let t="darkred";return e.type!=="http"||!e.response?ur.default.createElement("td",{className:"col-status"}):(100<=e.response.status_code&&e.response.status_code<200?t="green":200<=e.response.status_code&&e.response.status_code<300?t="darkgreen":300<=e.response.status_code&&e.response.status_code<400?t="lightblue":(400<=e.response.status_code&&e.response.status_code<500||500<=e.response.status_code&&e.response.status_code<600)&&(t="lightred"),ur.default.createElement("td",{className:"col-status",style:{color:t}},e.response.status_code))},"status");ty.headerName="Status";ty.sortKey=e=>e.type==="http"&&e.response&&e.response.status_code;var ry=o(({flow:e})=>ur.default.createElement("td",{className:"col-size"},$g(Nx(e))),"size");ry.headerName="Size";ry.sortKey=e=>Nx(e);var ny=o(({flow:e})=>{let t=bh(e),n=Lx(e);return ur.default.createElement("td",{className:"col-time"},t&&n?qg(1e3*(n-t)):"...")},"time");ny.headerName="Time";ny.sortKey=e=>{let t=bh(e),n=Lx(e);return t&&n&&n-t};var iy=o(({flow:e})=>{let t=bh(e);return ur.default.createElement("td",{className:"col-start"},t?Qi(t):"...")},"timestamp");iy.headerName="Start time";iy.sortKey=e=>bh(e);var tp=o(({flow:e})=>{let t=$s(),[n,l]=(0,ur.useState)(!1),d=null;return e.intercepted?d=ur.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(rp(e))},ur.default.createElement("i",{className:"fa fa-fw fa-play text-success"})):Gg(e)&&(d=ur.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(np(e))},ur.default.createElement("i",{className:"fa fa-fw fa-repeat text-primary"}))),ur.default.createElement("td",{className:(0,Xg.default)("col-quickactions",{hover:n}),onClick:()=>0},ur.default.createElement("div",null,d))},"quickactions");tp.headerName="";tp.sortKey=e=>0;var Px={icon:Zg,method:ey,path:Jg,quickactions:tp,size:ry,status:ty,time:ny,timestamp:iy,tls:Qg};var nF="FLOWS_ADD",iF="FLOWS_UPDATE",MT="FLOWS_REMOVE",oF="FLOWS_RECEIVE",AT="FLOWS_SELECT",DT="FLOWS_SET_FILTER",RT="FLOWS_SET_SORT",FT="FLOWS_SET_HIGHLIGHT",sF="FLOWS_REQUEST_ACTION",lF=Le({highlight:void 0,filter:void 0,sort:{column:void 0,desc:!1},selected:[]},Vg);function Mx(e=lF,t){switch(t.type){case nF:case iF:case MT:case oF:let n=_h[t.cmd](t.data,IT(e.filter),Ax(e.sort)),l=e.selected;if(t.type===MT&&e.selected.includes(t.data)){if(e.selected.length>1)l=l.filter(d=>d!==t.data);else if(l=[],t.data in e.viewIndex&&e.view.length>1){let d=e.viewIndex[t.data],v;d===e.view.length-1?v=e.view[d-1]:v=e.view[d+1],l.push(v.id)}}return Le(Ft(Le({},e),{selected:l}),ep(e,n));case DT:return Le(Ft(Le({},e),{filter:t.filter}),ep(e,Ex(IT(t.filter),Ax(e.sort))));case FT:return Ft(Le({},e),{highlight:t.highlight});case RT:return Le(Ft(Le({},e),{sort:t.sort}),ep(e,kT(Ax(t.sort))));case AT:return Ft(Le({},e),{selected:t.flowIds});default:return e}}o(Mx,"reducer");function IT(e){if(!!e)return af.parse(e)}o(IT,"makeFilter");function Ax({column:e,desc:t}){if(!e)return(l,d)=>0;let n=Px[e].sortKey;return(l,d)=>{let v=n(l),p=n(d);return v>p?t?-1:1:vEt(`/flows/${e.id}/resume`,{method:"POST"})}o(rp,"resume");function ly(){return e=>Et("/flows/resume",{method:"POST"})}o(ly,"resumeAll");function ay(e){return t=>Et(`/flows/${e.id}/kill`,{method:"POST"})}o(ay,"kill");function WT(){return e=>Et("/flows/kill",{method:"POST"})}o(WT,"killAll");function uy(e){return t=>Et(`/flows/${e.id}`,{method:"DELETE"})}o(uy,"remove");function fy(e){return t=>Et(`/flows/${e.id}/duplicate`,{method:"POST"})}o(fy,"duplicate");function np(e){return t=>Et(`/flows/${e.id}/replay`,{method:"POST"})}o(np,"replay");function cy(e){return t=>Et(`/flows/${e.id}/revert`,{method:"POST"})}o(cy,"revert");function Di(e,t){return n=>Et.put(`/flows/${e.id}`,t)}o(Di,"update");function BT(e,t,n){let l=new FormData;return t=new window.Blob([t],{type:"plain/text"}),l.append("file",t),d=>Et(`/flows/${e.id}/${n}/content.data`,{method:"POST",body:l})}o(BT,"uploadContent");function py(){return e=>Et("/clear",{method:"POST"})}o(py,"clear");function UT(){return window.location.href="/flows/dump",{type:sF}}o(UT,"download");function zT(e){let t=new FormData;return t.append("file",e),n=>Et("/flows/dump",{method:"POST",body:t})}o(zT,"upload");function ff(e){return{type:AT,flowIds:e?[e]:[]}}o(ff,"select");var dy="UI_HIDE_MODAL",jT="UI_SET_ACTIVE_MODAL",aF={activeModal:void 0};function Dx(e=aF,t){switch(t.type){case jT:return Ft(Le({},e),{activeModal:t.activeModal});case dy:return Ft(Le({},e),{activeModal:void 0});default:return e}}o(Dx,"reducer");function $T(e){return{type:jT,activeModal:e}}o($T,"setActiveModal");function hy(){return{type:dy}}o(hy,"hideModal");var Uh=pe(Re());var Wt=pe(Re());var op=pe(Re());var Th=pe(Re()),qT=pe(Xn()),VT=(()=>{let e=document.createElement("div");return e.setAttribute("contenteditable","PLAINTEXT-ONLY"),e.contentEditable==="plaintext-only"?"plaintext-only":"true"})(),ip=!1,Vs=class extends Th.Component{constructor(){super(...arguments);this.input=Th.default.createRef();this.isEditing=o(()=>{var t;return((t=this.input.current)==null?void 0:t.contentEditable)===VT},"isEditing");this.startEditing=o(()=>{if(!this.input.current)return console.error("unreachable");this.isEditing()||(this.suppress_events=!0,this.input.current.blur(),this.input.current.contentEditable=VT,window.requestAnimationFrame(()=>{var l,d;if(!this.input.current)return;this.input.current.focus(),this.suppress_events=!1;let t=document.createRange();t.selectNodeContents(this.input.current);let n=window.getSelection();n==null||n.removeAllRanges(),n==null||n.addRange(t),(d=(l=this.props).onEditStart)==null||d.call(l)}))},"startEditing");this.resetValue=o(()=>{var t,n;if(!this.input.current)return console.error("unreachable");this.input.current.textContent=this.props.content,(n=(t=this.props).onInput)==null||n.call(t,this.props.content)},"resetValue");this.finishEditing=o(()=>{if(!this.input.current)return console.error("unreachable");this.props.onEditDone(this.input.current.textContent||""),this.input.current.blur(),this.input.current.contentEditable="inherit"},"finishEditing");this.onPaste=o(t=>{t.preventDefault();let n=t.clipboardData.getData("text/plain");document.execCommand("insertHTML",!1,n)},"onPaste");this.suppress_events=!1;this.onMouseDown=o(t=>{ip&&console.debug("onMouseDown",this.suppress_events),this.suppress_events=!0,window.addEventListener("mouseup",this.onMouseUp,{once:!0})},"onMouseDown");this.onMouseUp=o(t=>{var d;let n=t.target===this.input.current,l=!((d=window.getSelection())==null?void 0:d.toString());ip&&console.warn("mouseUp",this.suppress_events,n,l),n&&l&&this.startEditing(),this.suppress_events=!1},"onMouseUp");this.onClick=o(t=>{ip&&console.debug("onClick",this.suppress_events)},"onClick");this.onFocus=o(t=>{if(ip&&console.debug("onFocus",this.props.content,this.suppress_events),!this.input.current)throw"unreachable";this.suppress_events||this.startEditing()},"onFocus");this.onInput=o(t=>{var n,l,d;(d=(l=this.props).onInput)==null||d.call(l,((n=this.input.current)==null?void 0:n.textContent)||"")},"onInput");this.onBlur=o(t=>{ip&&console.debug("onBlur",this.props.content,this.suppress_events),!this.suppress_events&&this.finishEditing()},"onBlur");this.onKeyDown=o(t=>{var n,l;switch(ip&&console.debug("keydown",t),t.stopPropagation(),t.key){case"Escape":t.preventDefault(),this.resetValue(),this.finishEditing();break;case"Enter":t.shiftKey||(t.preventDefault(),this.finishEditing());break;default:break}(l=(n=this.props).onKeyDown)==null||l.call(n,t)},"onKeyDown")}render(){let t=(0,qT.default)("inline-input",this.props.className);return Th.default.createElement("span",{ref:this.input,tabIndex:0,className:t,placeholder:this.props.placeholder,onFocus:this.onFocus,onBlur:this.onBlur,onKeyDown:this.onKeyDown,onInput:this.onInput,onPaste:this.onPaste,onMouseDown:this.onMouseDown,onClick:this.onClick},this.props.content)}componentDidUpdate(t){var n,l;t.content!==this.props.content&&((l=(n=this.props).onInput)==null||l.call(n,this.props.content))}};o(Vs,"ValueEditor");var KT=pe(Xn());function cf(e){let[t,n]=(0,op.useState)(e.isValid(e.content)),l=(0,op.useRef)(null),d=o(p=>{var w;e.isValid(p)?e.onEditDone(p):(w=l.current)==null||w.resetValue()},"onEditDone"),v=(0,KT.default)(e.className,t?"has-success":"has-warning");return op.default.createElement(Vs,Ft(Le({},e),{className:v,onInput:p=>n(e.isValid(p)),onEditDone:d,ref:l}))}o(cf,"ValidateEditor");function Rx(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}o(Rx,"_defineProperty");function GT(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);t&&(l=l.filter(function(d){return Object.getOwnPropertyDescriptor(e,d).enumerable})),n.push.apply(n,l)}return n}o(GT,"ownKeys");function my(e){for(var t=1;tn[l.level])));case QT:case cF:return Le(Le({},e),ep(e,_h[t.cmd](t.data,l=>e.filters[l.level])));default:return e}}o(Wx,"reduce");function ek(e){return{type:JT,filter:e}}o(ek,"toggleFilter");function sp(){return{type:ZT}}o(sp,"toggleVisibility");function tk(e,t="web"){let n={id:Math.random().toString(),message:e,level:t};return{type:QT,cmd:"add",data:n}}o(tk,"add");var rk="UI_OPTION_UPDATE_START",nk="UI_OPTION_UPDATE_SUCCESS",ik="UI_OPTION_UPDATE_ERROR",dF={};function Bx(e=dF,t){switch(t.type){case rk:return Ft(Le({},e),{[t.option]:{isUpdating:!0,value:t.value,error:!1}});case nk:return Ft(Le({},e),{[t.option]:void 0});case ik:let n=e[t.option].value;return typeof n=="boolean"&&(n=!n),Ft(Le({},e),{[t.option]:{value:n,isUpdating:!1,error:t.error}});case dy:return{};default:return e}}o(Bx,"reducer");function ok(e,t){return{type:rk,option:e,value:t}}o(ok,"startUpdate");function sk(e){return{type:nk,option:e}}o(sk,"updateSuccess");function lk(e,t){return{type:ik,option:e,error:t}}o(lk,"updateError");var ak=gy({flow:mx,modal:Dx,optionsEditor:Bx});var Zn;(function(v){v.INIT="CONNECTION_INIT",v.FETCHING="CONNECTION_FETCHING",v.ESTABLISHED="CONNECTION_ESTABLISHED",v.ERROR="CONNECTION_ERROR",v.OFFLINE="CONNECTION_OFFLINE"})(Zn||(Zn={}));var hF={state:Zn.INIT,message:void 0};function Ux(e=hF,t){switch(t.type){case Zn.ESTABLISHED:case Zn.FETCHING:case Zn.ERROR:case Zn.OFFLINE:return{state:t.type,message:t.message};default:return e}}o(Ux,"reducer");function uk(){return{type:Zn.FETCHING}}o(uk,"startFetching");function fk(){return{type:Zn.ESTABLISHED}}o(fk,"connectionEstablished");function ck(e){return{type:Zn.ERROR,message:e}}o(ck,"connectionError");var pk={add_upstream_certs_to_client_chain:!1,allow_hosts:[],anticache:!1,anticomp:!1,block_global:!0,block_list:[],block_private:!1,body_size_limit:void 0,cert_passphrase:void 0,certs:[],ciphers_client:void 0,ciphers_server:void 0,client_certs:void 0,client_replay:[],command_history:!0,confdir:"~/.mitmproxy",connection_strategy:"eager",console_focus_follow:!1,content_view_lines_cutoff:512,export_preserve_original_ip:!1,http2:!0,ignore_hosts:[],intercept:void 0,intercept_active:!1,keep_host_header:!1,key_size:2048,listen_host:"",listen_port:8080,map_local:[],map_remote:[],mode:"regular",modify_body:[],modify_headers:[],onboarding:!0,onboarding_host:"mitm.it",onboarding_port:80,proxy_debug:!1,proxyauth:void 0,rawtcp:!0,readfile_filter:void 0,rfile:void 0,save_stream_file:void 0,save_stream_filter:void 0,scripts:[],server:!0,server_replay:[],server_replay_ignore_content:!1,server_replay_ignore_host:!1,server_replay_ignore_params:[],server_replay_ignore_payload_params:[],server_replay_ignore_port:!1,server_replay_kill_extra:!1,server_replay_nopop:!1,server_replay_refresh:!0,server_replay_use_headers:[],showhost:!1,ssl_insecure:!1,ssl_verify_upstream_trusted_ca:void 0,ssl_verify_upstream_trusted_confdir:void 0,stickyauth:void 0,stickycookie:void 0,stream_large_bodies:void 0,tcp_hosts:[],termlog_verbosity:"info",tls_version_client_max:"UNBOUNDED",tls_version_client_min:"TLS1_2",tls_version_server_max:"UNBOUNDED",tls_version_server_min:"TLS1_2",upstream_auth:void 0,upstream_cert:!0,view_filter:void 0,view_order:"time",view_order_reversed:!1,web_columns:["tls","icon","path","method","status","size","time"],web_debug:!1,web_host:"127.0.0.1",web_open_browser:!0,web_port:8081,web_static_viewer:"",websocket:!0};var zx="OPTIONS_RECEIVE",jx="OPTIONS_UPDATE";function $x(e=pk,t){switch(t.type){case zx:let n={};for(let[d,{value:v}]of Object.entries(t.data))n[d]=v;return n;case jx:let l=Le({},e);for(let[d,{value:v}]of Object.entries(t.data))l[d]=v;return l;default:return e}}o($x,"reducer");function mF(e,t,n){return Oa(this,null,function*(){try{let l=yield Et.put("/options",{[e]:t});if(l.status===200)n(sk(e));else throw yield l.text()}catch(l){n(lk(e,l))}})}o(mF,"pureSendUpdate");var vF=mF;function lp(e,t){return n=>{n(ok(e,t)),vF(e,t,n)}}o(lp,"update");function dk(){return e=>Et("/options/save",{method:"POST"})}o(dk,"save");var hk="COMMANDBAR_TOGGLE_VISIBILITY",gF={visible:!1};function qx(e=gF,t){switch(t.type){case hk:return Ft(Le({},e),{visible:!e.visible});default:return e}}o(qx,"reducer");function yy(){return{type:hk}}o(yy,"toggleVisibility");function mk(e){return function(t){var n=t.dispatch,l=t.getState;return function(d){return function(v){return typeof v=="function"?v(n,l,e):d(v)}}}}o(mk,"createThunkMiddleware");var vk=mk();vk.withExtraArgument=mk;var gk=vk;var yF=window.MITMWEB_CONF||{static:!1,version:"1.2.3",contentViews:["Auto","Raw"]};function Vx(e=yF,t){return e}o(Vx,"reducer");var wF={},xF=o((e=wF,t)=>{switch(t.type){case zx:return t.data;case jx:return Le(Le({},e),t.data);default:return e}},"reducer"),yk=xF;var SF=window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||Hx,CF=gy({commandBar:qx,eventLog:Wx,flows:Mx,connection:Ux,ui:ak,options:$x,options_meta:yk,conf:Vx}),_F=o(e=>Ix(CF,e,SF(XT(gk))),"createAppStore"),ap=_F(void 0),$t=o(()=>$s(),"useAppDispatch"),at=dx;var Zi=pe(Re());var wk=pe(xh()),xk=pe(Xn()),Kx=class extends Zi.Component{constructor(){super(...arguments);this.container=Zi.default.createRef();this.nameInput=Zi.default.createRef();this.valueInput=Zi.default.createRef();this.render=o(()=>{let[t,n]=this.props.item;return Zi.default.createElement("div",{ref:this.container,className:"kv-row",onClick:this.onClick,onKeyDownCapture:this.onKeyDown},Zi.default.createElement(Vs,{ref:this.nameInput,className:"kv-key",content:t,onEditStart:this.props.onEditStart,onEditDone:l=>this.props.onEditDone([l,n])}),":\xA0",Zi.default.createElement(Vs,{ref:this.valueInput,className:"kv-value",content:n,onEditStart:this.props.onEditStart,onEditDone:l=>this.props.onEditDone([t,l]),placeholder:"empty"}))},"render");this.onClick=o(t=>{t.target===this.container.current&&this.props.onClickEmptyArea()},"onClick");this.onKeyDown=o(t=>{var n;t.target===((n=this.valueInput.current)==null?void 0:n.input.current)&&t.key==="Tab"&&this.props.onTabNext()},"onKeyDown")}};o(Kx,"Row");var up=class extends Zi.Component{constructor(){super(...arguments);this.rowRefs={};this.state={currentList:this.props.data||[],initialList:this.props.data};this.render=o(()=>{this.rowRefs={};let t=this.state.currentList.map((n,l)=>Zi.default.createElement(Kx,{key:l,item:n,onEditStart:()=>this.currentlyEditing=l,onEditDone:d=>this.onEditDone(l,d),onClickEmptyArea:()=>this.onClickEmptyArea(l),onTabNext:()=>this.onTabNext(l),ref:d=>this.rowRefs[l]=d}));return Zi.default.createElement("div",{className:(0,xk.default)("kv-editor",this.props.className),onMouseDown:this.onMouseDown},t,Zi.default.createElement("div",{onClick:n=>{n.preventDefault(),this.onClickEmptyArea(this.state.currentList.length-1)},className:"kv-add-row fa fa-plus-square-o",role:"button","aria-label":"Add"}))},"render");this.onEditDone=o((t,n)=>{let l=[...this.state.currentList];n[0]?l[t]=n:l.splice(t,1),this.currentlyEditing=void 0,(0,wk.isEqual)(this.state.currentList,l)||this.props.onChange(l),this.setState({currentList:l})},"onEditDone");this.onClickEmptyArea=o(t=>{if(this.justFinishedEditing)return;let n=[...this.state.currentList];n.splice(t+1,0,["",""]),this.setState({currentList:n},()=>{var l,d;return(d=(l=this.rowRefs[t+1])==null?void 0:l.nameInput.current)==null?void 0:d.startEditing()})},"onClickEmptyArea");this.onTabNext=o(t=>{t==this.state.currentList.length-1&&this.onClickEmptyArea(t)},"onTabNext");this.onMouseDown=o(t=>{this.justFinishedEditing=this.currentlyEditing},"onMouseDown")}static getDerivedStateFromProps(t,n){return t.data!==n.initialList?{currentList:t.data||[],initialList:t.data}:null}};o(up,"KeyValueListEditor");var Gt=pe(Re());var kh=pe(Re());var wy=80;function xy(e,t){let[n,l]=(0,kh.useState)(),[d,v]=(0,kh.useState)();return(0,kh.useEffect)(()=>{d&&d.abort();let p=new AbortController;return Et(e,{signal:p.signal}).then(w=>{if(!w.ok)throw`${w.status} ${w.statusText}`.trim();return w.text()}).then(w=>{l(w)}).catch(w=>{p.signal.aborted||l(`Error getting content: ${w}.`)}),v(p),()=>{p.signal.aborted||p.abort()}},[e,t]),n}o(xy,"useContent");var Oh=pe(Re()),Sy=Oh.default.memo(o(function({icon:t,text:n,className:l,title:d,onOpenFile:v,onClick:p}){let w;return Oh.default.createElement("a",{href:"#",onClick:_=>{w.click(),p&&p(_)},className:l,title:d},Oh.default.createElement("i",{className:"fa fa-fw "+t}),n,Oh.default.createElement("input",{ref:_=>w=_,className:"hidden",type:"file",onChange:_=>{_.preventDefault(),_.target.files&&_.target.files.length>0&&v(_.target.files[0]),w.value=""}}))},"FileChooser"));var fp=pe(Re()),Sk=pe(Xn());function Cr({onClick:e,children:t,icon:n,disabled:l,className:d,title:v}){return fp.createElement("button",{className:(0,Sk.default)(d,"btn btn-default"),onClick:l?void 0:e,disabled:l,title:v},n&&fp.createElement(fp.Fragment,null,fp.createElement("i",{className:"fa "+n}),"\xA0"),t)}o(Cr,"Button");var Nh=pe(Re()),kk=pe(Re());var Lh=pe(Re()),_k=pe(Xn()),bk=pe(Ck()),Ek=pe(xh());function Tk(e){return e&&e.replace(/\r\n|\r/g,` -`)}o(Tk,"normalizeLineEndings");var cp=class extends Lh.Component{constructor(t){super(t);this.state={isFocused:!1}}getCodeMirrorInstance(){return this.props.codeMirrorInstance||bk.default}UNSAFE_componentWillMount(){this.props.path&&console.error("Warning: react-codemirror: the `path` prop has been changed to `name`")}componentDidMount(){let t=this.getCodeMirrorInstance();this.codeMirror=t.fromTextArea(this.textareaNode,this.props.options),this.codeMirror.on("change",this.codemirrorValueChanged.bind(this)),this.codeMirror.on("cursorActivity",this.cursorActivity.bind(this)),this.codeMirror.on("focus",this.focusChanged.bind(this,!0)),this.codeMirror.on("blur",this.focusChanged.bind(this,!1)),this.codeMirror.on("scroll",this.scrollChanged.bind(this)),this.codeMirror.setValue(this.props.defaultValue||this.props.value||"")}componentWillUnmount(){this.codeMirror&&this.codeMirror.toTextArea()}UNSAFE_componentWillReceiveProps(t){if(this.codeMirror&&t.value!==void 0&&t.value!==this.props.value&&Tk(this.codeMirror.getValue())!==Tk(t.value))if(this.props.preserveScrollPosition){var n=this.codeMirror.getScrollInfo();this.codeMirror.setValue(t.value),this.codeMirror.scrollTo(n.left,n.top)}else this.codeMirror.setValue(t.value);if(typeof t.options=="object")for(let l in t.options)t.options.hasOwnProperty(l)&&this.setOptionIfChanged(l,t.options[l])}setOptionIfChanged(t,n){let l=this.codeMirror.getOption(t);Ek.default.isEqual(l,n)||this.codeMirror.setOption(t,n)}getCodeMirror(){return this.codeMirror}focus(){this.codeMirror&&this.codeMirror.focus()}focusChanged(t){this.setState({isFocused:t}),this.props.onFocusChange&&this.props.onFocusChange(t)}cursorActivity(t){this.props.onCursorActivity&&this.props.onCursorActivity(t)}scrollChanged(t){this.props.onScroll&&this.props.onScroll(t.getScrollInfo())}codemirrorValueChanged(t,n){this.props.onChange&&n.origin!=="setValue"&&this.props.onChange(t.getValue(),n)}render(){let t=(0,_k.default)("ReactCodeMirror",this.state.isFocused?"ReactCodeMirror--focused":null,this.props.className);return Lh.createElement("div",{className:t},Lh.createElement("textarea",{ref:n=>this.textareaNode=n,name:this.props.name||this.props.path,defaultValue:this.props.value,autoComplete:"off",autoFocus:this.props.autoFocus}))}};o(cp,"CodeMirror"),cp.defaultProps={preserveScrollPosition:!1};var Ph=class extends kk.Component{constructor(){super(...arguments);this.editor=Nh.createRef();this.getContent=o(()=>{var t;return(t=this.editor.current)==null?void 0:t.codeMirror.getValue()},"getContent");this.render=o(()=>{let t={lineNumbers:!0};return Nh.createElement("div",{className:"codeeditor",onKeyDown:n=>n.stopPropagation()},Nh.createElement(cp,{ref:this.editor,value:this.props.initialContent,onChange:()=>0,options:t}))},"render")}};o(Ph,"CodeEditor");var pf=pe(Re()),bF=pf.default.memo(o(function({lines:t,maxLines:n,showMore:l}){return t.length===0?null:pf.default.createElement("pre",null,t.map((d,v)=>v===n?pf.default.createElement("button",{key:"showmore",onClick:l,className:"btn btn-xs btn-info"},pf.default.createElement("i",{className:"fa fa-angle-double-down","aria-hidden":"true"})," Show more"):pf.default.createElement("div",{key:v},d.map(([p,w],_)=>pf.default.createElement("span",{key:_,className:p},w)))))},"LineRenderer")),Cy=bF;var wf=pe(Re());var ci=pe(Re());var _y=pe(Re());var Xx=o(function(t){return t.reduce(function(n,l){var d=l[0],v=l[1];return n[d]=v,n},{})},"fromEntries"),Qx=typeof window!="undefined"&&window.document&&window.document.createElement?_y.useLayoutEffect:_y.useEffect;var ru=pe(Re());var jr="top",Sn="bottom",tn="right",rn="left",by="auto",Za=[jr,Sn,tn,rn],Al="start",Ey="end",Ok="clippingParents",Ty="viewport",pp="popper",Lk="reference",Zx=Za.reduce(function(e,t){return e.concat([t+"-"+Al,t+"-"+Ey])},[]),ky=[].concat(Za,[by]).reduce(function(e,t){return e.concat([t,t+"-"+Al,t+"-"+Ey])},[]),EF="beforeRead",TF="read",kF="afterRead",OF="beforeMain",LF="main",NF="afterMain",PF="beforeWrite",MF="write",AF="afterWrite",Nk=[EF,TF,kF,OF,LF,NF,PF,MF,AF];function Cn(e){return e?(e.nodeName||"").toLowerCase():null}o(Cn,"getNodeName");function Ar(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t&&t.defaultView||window}return e}o(Ar,"getWindow");function Dl(e){var t=Ar(e).Element;return e instanceof t||e instanceof Element}o(Dl,"isElement");function $r(e){var t=Ar(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}o($r,"isHTMLElement");function Oy(e){if(typeof ShadowRoot=="undefined")return!1;var t=Ar(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}o(Oy,"isShadowRoot");function DF(e){var t=e.state;Object.keys(t.elements).forEach(function(n){var l=t.styles[n]||{},d=t.attributes[n]||{},v=t.elements[n];!$r(v)||!Cn(v)||(Object.assign(v.style,l),Object.keys(d).forEach(function(p){var w=d[p];w===!1?v.removeAttribute(p):v.setAttribute(p,w===!0?"":w)}))})}o(DF,"applyStyles");function RF(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(l){var d=t.elements[l],v=t.attributes[l]||{},p=Object.keys(t.styles.hasOwnProperty(l)?t.styles[l]:n[l]),w=p.reduce(function(_,O){return _[O]="",_},{});!$r(d)||!Cn(d)||(Object.assign(d.style,w),Object.keys(v).forEach(function(_){d.removeAttribute(_)}))})}}o(RF,"effect");var Pk={name:"applyStyles",enabled:!0,phase:"write",fn:DF,effect:RF,requires:["computeStyles"]};function _n(e){return e.split("-")[0]}o(_n,"getBasePlacement");var Ja=Math.round;function Oo(e,t){t===void 0&&(t=!1);var n=e.getBoundingClientRect(),l=1,d=1;return $r(e)&&t&&(l=n.width/e.offsetWidth||1,d=n.height/e.offsetHeight||1),{width:Ja(n.width/l),height:Ja(n.height/d),top:Ja(n.top/d),right:Ja(n.right/l),bottom:Ja(n.bottom/d),left:Ja(n.left/l),x:Ja(n.left/l),y:Ja(n.top/d)}}o(Oo,"getBoundingClientRect");function df(e){var t=Oo(e),n=e.offsetWidth,l=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-l)<=1&&(l=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:l}}o(df,"getLayoutRect");function Mh(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&Oy(n)){var l=t;do{if(l&&e.isSameNode(l))return!0;l=l.parentNode||l.host}while(l)}return!1}o(Mh,"contains");function fi(e){return Ar(e).getComputedStyle(e)}o(fi,"getComputedStyle");function Jx(e){return["table","td","th"].indexOf(Cn(e))>=0}o(Jx,"isTableElement");function Mn(e){return((Dl(e)?e.ownerDocument:e.document)||window.document).documentElement}o(Mn,"getDocumentElement");function Rl(e){return Cn(e)==="html"?e:e.assignedSlot||e.parentNode||(Oy(e)?e.host:null)||Mn(e)}o(Rl,"getParentNode");function Mk(e){return!$r(e)||fi(e).position==="fixed"?null:e.offsetParent}o(Mk,"getTrueOffsetParent");function FF(e){var t=navigator.userAgent.toLowerCase().indexOf("firefox")!==-1,n=navigator.userAgent.indexOf("Trident")!==-1;if(n&&$r(e)){var l=fi(e);if(l.position==="fixed")return null}for(var d=Rl(e);$r(d)&&["html","body"].indexOf(Cn(d))<0;){var v=fi(d);if(v.transform!=="none"||v.perspective!=="none"||v.contain==="paint"||["transform","perspective"].indexOf(v.willChange)!==-1||t&&v.willChange==="filter"||t&&v.filter&&v.filter!=="none")return d;d=d.parentNode}return null}o(FF,"getContainingBlock");function es(e){for(var t=Ar(e),n=Mk(e);n&&Jx(n)&&fi(n).position==="static";)n=Mk(n);return n&&(Cn(n)==="html"||Cn(n)==="body"&&fi(n).position==="static")?t:n||FF(e)||t}o(es,"getOffsetParent");function hf(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}o(hf,"getMainAxisFromPlacement");var Lo=Math.max,eu=Math.min,Ah=Math.round;function mf(e,t,n){return Lo(e,eu(t,n))}o(mf,"within");function Dh(){return{top:0,right:0,bottom:0,left:0}}o(Dh,"getFreshSideObject");function Rh(e){return Object.assign({},Dh(),e)}o(Rh,"mergePaddingObject");function Fh(e,t){return t.reduce(function(n,l){return n[l]=e,n},{})}o(Fh,"expandToHashMap");var IF=o(function(t,n){return t=typeof t=="function"?t(Object.assign({},n.rects,{placement:n.placement})):t,Rh(typeof t!="number"?t:Fh(t,Za))},"toPaddingObject");function HF(e){var t,n=e.state,l=e.name,d=e.options,v=n.elements.arrow,p=n.modifiersData.popperOffsets,w=_n(n.placement),_=hf(w),O=[rn,tn].indexOf(w)>=0,D=O?"height":"width";if(!(!v||!p)){var Y=IF(d.padding,n),W=df(v),X=_==="y"?jr:rn,te=_==="y"?Sn:tn,Q=n.rects.reference[D]+n.rects.reference[_]-p[_]-n.rects.popper[D],R=p[_]-n.rects.reference[_],P=es(v),F=P?_==="y"?P.clientHeight||0:P.clientWidth||0:0,K=Q/2-R/2,V=Y[X],ue=F-W[D]-Y[te],ie=F/2-W[D]/2+K,de=mf(V,ie,ue),ge=_;n.modifiersData[l]=(t={},t[ge]=de,t.centerOffset=de-ie,t)}}o(HF,"arrow");function WF(e){var t=e.state,n=e.options,l=n.element,d=l===void 0?"[data-popper-arrow]":l;d!=null&&(typeof d=="string"&&(d=t.elements.popper.querySelector(d),!d)||!Mh(t.elements.popper,d)||(t.elements.arrow=d))}o(WF,"effect");var Ak={name:"arrow",enabled:!0,phase:"main",fn:HF,effect:WF,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};var BF={top:"auto",right:"auto",bottom:"auto",left:"auto"};function UF(e){var t=e.x,n=e.y,l=window,d=l.devicePixelRatio||1;return{x:Ah(Ah(t*d)/d)||0,y:Ah(Ah(n*d)/d)||0}}o(UF,"roundOffsetsByDPR");function Dk(e){var t,n=e.popper,l=e.popperRect,d=e.placement,v=e.offsets,p=e.position,w=e.gpuAcceleration,_=e.adaptive,O=e.roundOffsets,D=O===!0?UF(v):typeof O=="function"?O(v):v,Y=D.x,W=Y===void 0?0:Y,X=D.y,te=X===void 0?0:X,Q=v.hasOwnProperty("x"),R=v.hasOwnProperty("y"),P=rn,F=jr,K=window;if(_){var V=es(n),ue="clientHeight",ie="clientWidth";V===Ar(n)&&(V=Mn(n),fi(V).position!=="static"&&(ue="scrollHeight",ie="scrollWidth")),V=V,d===jr&&(F=Sn,te-=V[ue]-l.height,te*=w?1:-1),d===rn&&(P=tn,W-=V[ie]-l.width,W*=w?1:-1)}var de=Object.assign({position:p},_&&BF);if(w){var ge;return Object.assign({},de,(ge={},ge[F]=R?"0":"",ge[P]=Q?"0":"",ge.transform=(K.devicePixelRatio||1)<2?"translate("+W+"px, "+te+"px)":"translate3d("+W+"px, "+te+"px, 0)",ge))}return Object.assign({},de,(t={},t[F]=R?te+"px":"",t[P]=Q?W+"px":"",t.transform="",t))}o(Dk,"mapToStyles");function zF(e){var t=e.state,n=e.options,l=n.gpuAcceleration,d=l===void 0?!0:l,v=n.adaptive,p=v===void 0?!0:v,w=n.roundOffsets,_=w===void 0?!0:w;if(!1)var O;var D={placement:_n(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:d};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,Dk(Object.assign({},D,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:p,roundOffsets:_})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,Dk(Object.assign({},D,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:_})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}o(zF,"computeStyles");var Rk={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:zF,data:{}};var Ly={passive:!0};function jF(e){var t=e.state,n=e.instance,l=e.options,d=l.scroll,v=d===void 0?!0:d,p=l.resize,w=p===void 0?!0:p,_=Ar(t.elements.popper),O=[].concat(t.scrollParents.reference,t.scrollParents.popper);return v&&O.forEach(function(D){D.addEventListener("scroll",n.update,Ly)}),w&&_.addEventListener("resize",n.update,Ly),function(){v&&O.forEach(function(D){D.removeEventListener("scroll",n.update,Ly)}),w&&_.removeEventListener("resize",n.update,Ly)}}o(jF,"effect");var Fk={name:"eventListeners",enabled:!0,phase:"write",fn:o(function(){},"fn"),effect:jF,data:{}};var $F={left:"right",right:"left",bottom:"top",top:"bottom"};function dp(e){return e.replace(/left|right|bottom|top/g,function(t){return $F[t]})}o(dp,"getOppositePlacement");var qF={start:"end",end:"start"};function Ny(e){return e.replace(/start|end/g,function(t){return qF[t]})}o(Ny,"getOppositeVariationPlacement");function vf(e){var t=Ar(e),n=t.pageXOffset,l=t.pageYOffset;return{scrollLeft:n,scrollTop:l}}o(vf,"getWindowScroll");function gf(e){return Oo(Mn(e)).left+vf(e).scrollLeft}o(gf,"getWindowScrollBarX");function eS(e){var t=Ar(e),n=Mn(e),l=t.visualViewport,d=n.clientWidth,v=n.clientHeight,p=0,w=0;return l&&(d=l.width,v=l.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(p=l.offsetLeft,w=l.offsetTop)),{width:d,height:v,x:p+gf(e),y:w}}o(eS,"getViewportRect");function tS(e){var t,n=Mn(e),l=vf(e),d=(t=e.ownerDocument)==null?void 0:t.body,v=Lo(n.scrollWidth,n.clientWidth,d?d.scrollWidth:0,d?d.clientWidth:0),p=Lo(n.scrollHeight,n.clientHeight,d?d.scrollHeight:0,d?d.clientHeight:0),w=-l.scrollLeft+gf(e),_=-l.scrollTop;return fi(d||n).direction==="rtl"&&(w+=Lo(n.clientWidth,d?d.clientWidth:0)-v),{width:v,height:p,x:w,y:_}}o(tS,"getDocumentRect");function yf(e){var t=fi(e),n=t.overflow,l=t.overflowX,d=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+d+l)}o(yf,"isScrollParent");function Py(e){return["html","body","#document"].indexOf(Cn(e))>=0?e.ownerDocument.body:$r(e)&&yf(e)?e:Py(Rl(e))}o(Py,"getScrollParent");function tu(e,t){var n;t===void 0&&(t=[]);var l=Py(e),d=l===((n=e.ownerDocument)==null?void 0:n.body),v=Ar(l),p=d?[v].concat(v.visualViewport||[],yf(l)?l:[]):l,w=t.concat(p);return d?w:w.concat(tu(Rl(p)))}o(tu,"listScrollParents");function hp(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}o(hp,"rectToClientRect");function VF(e){var t=Oo(e);return t.top=t.top+e.clientTop,t.left=t.left+e.clientLeft,t.bottom=t.top+e.clientHeight,t.right=t.left+e.clientWidth,t.width=e.clientWidth,t.height=e.clientHeight,t.x=t.left,t.y=t.top,t}o(VF,"getInnerBoundingClientRect");function Ik(e,t){return t===Ty?hp(eS(e)):$r(t)?VF(t):hp(tS(Mn(e)))}o(Ik,"getClientRectFromMixedType");function KF(e){var t=tu(Rl(e)),n=["absolute","fixed"].indexOf(fi(e).position)>=0,l=n&&$r(e)?es(e):e;return Dl(l)?t.filter(function(d){return Dl(d)&&Mh(d,l)&&Cn(d)!=="body"}):[]}o(KF,"getClippingParents");function rS(e,t,n){var l=t==="clippingParents"?KF(e):[].concat(t),d=[].concat(l,[n]),v=d[0],p=d.reduce(function(w,_){var O=Ik(e,_);return w.top=Lo(O.top,w.top),w.right=eu(O.right,w.right),w.bottom=eu(O.bottom,w.bottom),w.left=Lo(O.left,w.left),w},Ik(e,v));return p.width=p.right-p.left,p.height=p.bottom-p.top,p.x=p.left,p.y=p.top,p}o(rS,"getClippingRect");function Ks(e){return e.split("-")[1]}o(Ks,"getVariation");function Ih(e){var t=e.reference,n=e.element,l=e.placement,d=l?_n(l):null,v=l?Ks(l):null,p=t.x+t.width/2-n.width/2,w=t.y+t.height/2-n.height/2,_;switch(d){case jr:_={x:p,y:t.y-n.height};break;case Sn:_={x:p,y:t.y+t.height};break;case tn:_={x:t.x+t.width,y:w};break;case rn:_={x:t.x-n.width,y:w};break;default:_={x:t.x,y:t.y}}var O=d?hf(d):null;if(O!=null){var D=O==="y"?"height":"width";switch(v){case Al:_[O]=_[O]-(t[D]/2-n[D]/2);break;case Ey:_[O]=_[O]+(t[D]/2-n[D]/2);break;default:}}return _}o(Ih,"computeOffsets");function ts(e,t){t===void 0&&(t={});var n=t,l=n.placement,d=l===void 0?e.placement:l,v=n.boundary,p=v===void 0?Ok:v,w=n.rootBoundary,_=w===void 0?Ty:w,O=n.elementContext,D=O===void 0?pp:O,Y=n.altBoundary,W=Y===void 0?!1:Y,X=n.padding,te=X===void 0?0:X,Q=Rh(typeof te!="number"?te:Fh(te,Za)),R=D===pp?Lk:pp,P=e.elements.reference,F=e.rects.popper,K=e.elements[W?R:D],V=rS(Dl(K)?K:K.contextElement||Mn(e.elements.popper),p,_),ue=Oo(P),ie=Ih({reference:ue,element:F,strategy:"absolute",placement:d}),de=hp(Object.assign({},F,ie)),ge=D===pp?de:ue,we={top:V.top-ge.top+Q.top,bottom:ge.bottom-V.bottom+Q.bottom,left:V.left-ge.left+Q.left,right:ge.right-V.right+Q.right},qe=e.modifiersData.offset;if(D===pp&&qe){var Je=qe[d];Object.keys(we).forEach(function(be){var yt=[tn,Sn].indexOf(be)>=0?1:-1,Be=[jr,Sn].indexOf(be)>=0?"y":"x";we[be]+=Je[Be]*yt})}return we}o(ts,"detectOverflow");function nS(e,t){t===void 0&&(t={});var n=t,l=n.placement,d=n.boundary,v=n.rootBoundary,p=n.padding,w=n.flipVariations,_=n.allowedAutoPlacements,O=_===void 0?ky:_,D=Ks(l),Y=D?w?Zx:Zx.filter(function(te){return Ks(te)===D}):Za,W=Y.filter(function(te){return O.indexOf(te)>=0});W.length===0&&(W=Y);var X=W.reduce(function(te,Q){return te[Q]=ts(e,{placement:Q,boundary:d,rootBoundary:v,padding:p})[_n(Q)],te},{});return Object.keys(X).sort(function(te,Q){return X[te]-X[Q]})}o(nS,"computeAutoPlacement");function GF(e){if(_n(e)===by)return[];var t=dp(e);return[Ny(e),t,Ny(t)]}o(GF,"getExpandedFallbackPlacements");function YF(e){var t=e.state,n=e.options,l=e.name;if(!t.modifiersData[l]._skip){for(var d=n.mainAxis,v=d===void 0?!0:d,p=n.altAxis,w=p===void 0?!0:p,_=n.fallbackPlacements,O=n.padding,D=n.boundary,Y=n.rootBoundary,W=n.altBoundary,X=n.flipVariations,te=X===void 0?!0:X,Q=n.allowedAutoPlacements,R=t.options.placement,P=_n(R),F=P===R,K=_||(F||!te?[dp(R)]:GF(R)),V=[R].concat(K).reduce(function(st,_r){return st.concat(_n(_r)===by?nS(t,{placement:_r,boundary:D,rootBoundary:Y,padding:O,flipVariations:te,allowedAutoPlacements:Q}):_r)},[]),ue=t.rects.reference,ie=t.rects.popper,de=new Map,ge=!0,we=V[0],qe=0;qe=0,Ve=Be?"width":"height",Ke=ts(t,{placement:Je,boundary:D,rootBoundary:Y,altBoundary:W,padding:O}),Ge=Be?yt?tn:rn:yt?Sn:jr;ue[Ve]>ie[Ve]&&(Ge=dp(Ge));var Yt=dp(Ge),ut=[];if(v&&ut.push(Ke[be]<=0),w&&ut.push(Ke[Ge]<=0,Ke[Yt]<=0),ut.every(function(st){return st})){we=Je,ge=!1;break}de.set(Je,ut)}if(ge)for(var Dr=te?3:1,qt=o(function(_r){var Bt=V.find(function(Ut){var ne=de.get(Ut);if(ne)return ne.slice(0,_r).every(function(et){return et})});if(Bt)return we=Bt,"break"},"_loop"),_t=Dr;_t>0;_t--){var wt=qt(_t);if(wt==="break")break}t.placement!==we&&(t.modifiersData[l]._skip=!0,t.placement=we,t.reset=!0)}}o(YF,"flip");var Hk={name:"flip",enabled:!0,phase:"main",fn:YF,requiresIfExists:["offset"],data:{_skip:!1}};function Wk(e,t,n){return n===void 0&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}o(Wk,"getSideOffsets");function Bk(e){return[jr,tn,Sn,rn].some(function(t){return e[t]>=0})}o(Bk,"isAnySideFullyClipped");function XF(e){var t=e.state,n=e.name,l=t.rects.reference,d=t.rects.popper,v=t.modifiersData.preventOverflow,p=ts(t,{elementContext:"reference"}),w=ts(t,{altBoundary:!0}),_=Wk(p,l),O=Wk(w,d,v),D=Bk(_),Y=Bk(O);t.modifiersData[n]={referenceClippingOffsets:_,popperEscapeOffsets:O,isReferenceHidden:D,hasPopperEscaped:Y},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":D,"data-popper-escaped":Y})}o(XF,"hide");var Uk={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:XF};function QF(e,t,n){var l=_n(e),d=[rn,jr].indexOf(l)>=0?-1:1,v=typeof n=="function"?n(Object.assign({},t,{placement:e})):n,p=v[0],w=v[1];return p=p||0,w=(w||0)*d,[rn,tn].indexOf(l)>=0?{x:w,y:p}:{x:p,y:w}}o(QF,"distanceAndSkiddingToXY");function ZF(e){var t=e.state,n=e.options,l=e.name,d=n.offset,v=d===void 0?[0,0]:d,p=ky.reduce(function(D,Y){return D[Y]=QF(Y,t.rects,v),D},{}),w=p[t.placement],_=w.x,O=w.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=_,t.modifiersData.popperOffsets.y+=O),t.modifiersData[l]=p}o(ZF,"offset");var zk={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:ZF};function JF(e){var t=e.state,n=e.name;t.modifiersData[n]=Ih({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}o(JF,"popperOffsets");var jk={name:"popperOffsets",enabled:!0,phase:"read",fn:JF,data:{}};function iS(e){return e==="x"?"y":"x"}o(iS,"getAltAxis");function eI(e){var t=e.state,n=e.options,l=e.name,d=n.mainAxis,v=d===void 0?!0:d,p=n.altAxis,w=p===void 0?!1:p,_=n.boundary,O=n.rootBoundary,D=n.altBoundary,Y=n.padding,W=n.tether,X=W===void 0?!0:W,te=n.tetherOffset,Q=te===void 0?0:te,R=ts(t,{boundary:_,rootBoundary:O,padding:Y,altBoundary:D}),P=_n(t.placement),F=Ks(t.placement),K=!F,V=hf(P),ue=iS(V),ie=t.modifiersData.popperOffsets,de=t.rects.reference,ge=t.rects.popper,we=typeof Q=="function"?Q(Object.assign({},t.rects,{placement:t.placement})):Q,qe={x:0,y:0};if(!!ie){if(v||w){var Je=V==="y"?jr:rn,be=V==="y"?Sn:tn,yt=V==="y"?"height":"width",Be=ie[V],Ve=ie[V]+R[Je],Ke=ie[V]-R[be],Ge=X?-ge[yt]/2:0,Yt=F===Al?de[yt]:ge[yt],ut=F===Al?-ge[yt]:-de[yt],Dr=t.elements.arrow,qt=X&&Dr?df(Dr):{width:0,height:0},_t=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:Dh(),wt=_t[Je],st=_t[be],_r=mf(0,de[yt],qt[yt]),Bt=K?de[yt]/2-Ge-_r-wt-we:Yt-_r-wt-we,Ut=K?-de[yt]/2+Ge+_r+st+we:ut+_r+st+we,ne=t.elements.arrow&&es(t.elements.arrow),et=ne?V==="y"?ne.clientTop||0:ne.clientLeft||0:0,br=t.modifiersData.offset?t.modifiersData.offset[t.placement][V]:0,zt=ie[V]+Bt-br-et,jt=ie[V]+Ut-br;if(v){var xe=mf(X?eu(Ve,zt):Ve,Be,X?Lo(Ke,jt):Ke);ie[V]=xe,qe[V]=xe-Be}if(w){var Er=V==="x"?jr:rn,on=V==="x"?Sn:tn,Dn=ie[ue],ei=Dn+R[Er],sn=Dn-R[on],Vt=mf(X?eu(ei,zt):ei,Dn,X?Lo(sn,jt):sn);ie[ue]=Vt,qe[ue]=Vt-Dn}}t.modifiersData[l]=qe}}o(eI,"preventOverflow");var $k={name:"preventOverflow",enabled:!0,phase:"main",fn:eI,requiresIfExists:["offset"]};function oS(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}o(oS,"getHTMLElementScroll");function sS(e){return e===Ar(e)||!$r(e)?vf(e):oS(e)}o(sS,"getNodeScroll");function tI(e){var t=e.getBoundingClientRect(),n=t.width/e.offsetWidth||1,l=t.height/e.offsetHeight||1;return n!==1||l!==1}o(tI,"isElementScaled");function lS(e,t,n){n===void 0&&(n=!1);var l=$r(t),d=$r(t)&&tI(t),v=Mn(t),p=Oo(e,d),w={scrollLeft:0,scrollTop:0},_={x:0,y:0};return(l||!l&&!n)&&((Cn(t)!=="body"||yf(v))&&(w=sS(t)),$r(t)?(_=Oo(t,!0),_.x+=t.clientLeft,_.y+=t.clientTop):v&&(_.x=gf(v))),{x:p.left+w.scrollLeft-_.x,y:p.top+w.scrollTop-_.y,width:p.width,height:p.height}}o(lS,"getCompositeRect");function rI(e){var t=new Map,n=new Set,l=[];e.forEach(function(v){t.set(v.name,v)});function d(v){n.add(v.name);var p=[].concat(v.requires||[],v.requiresIfExists||[]);p.forEach(function(w){if(!n.has(w)){var _=t.get(w);_&&d(_)}}),l.push(v)}return o(d,"sort"),e.forEach(function(v){n.has(v.name)||d(v)}),l}o(rI,"order");function aS(e){var t=rI(e);return Nk.reduce(function(n,l){return n.concat(t.filter(function(d){return d.phase===l}))},[])}o(aS,"orderModifiers");function uS(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}o(uS,"debounce");function fS(e){var t=e.reduce(function(n,l){var d=n[l.name];return n[l.name]=d?Object.assign({},d,l,{options:Object.assign({},d.options,l.options),data:Object.assign({},d.data,l.data)}):l,n},{});return Object.keys(t).map(function(n){return t[n]})}o(fS,"mergeByName");var qk={placement:"bottom",modifiers:[],strategy:"absolute"};function Vk(){for(var e=arguments.length,t=new Array(e),n=0;nci.default.createElement("li",{role:"separator",className:"divider"}),"Divider");function pi(l){var d=l,{onClick:e,children:t}=d,n=Fs(d,["onClick","children"]);return ci.default.createElement("li",null,ci.default.createElement("a",Le({href:"#",onClick:o(p=>{p.preventDefault(),e()},"click")},n),t))}o(pi,"MenuItem");var nu=ci.default.memo(o(function(w){var _=w,{text:t,children:n,options:l,className:d,onOpen:v}=_,p=Fs(_,["text","children","options","className","onOpen"]);let[O,D]=(0,ci.useState)(null),[Y,W]=(0,ci.useState)(!1),[X,te]=(0,ci.useState)(null),{styles:Q,attributes:R}=pS(O,X,Le({},l)),P=o(K=>{W(K),v&&v(K)},"setOpen");(0,ci.useEffect)(()=>{!X||document.addEventListener("click",K=>{X.contains(K.target)?document.addEventListener("click",()=>P(!1),{once:!0}):(K.preventDefault(),K.stopPropagation(),P(!1))},{once:!0,capture:!0})},[X]);let F;return Y?F=ci.default.createElement("ul",Le({className:"dropdown-menu show",ref:te,style:Q.popper},R.popper),n):F=null,ci.default.createElement(ci.default.Fragment,null,ci.default.createElement("a",Le({href:"#",ref:D,className:(0,Qk.default)(d,{open:Y}),onClick:K=>{K.preventDefault(),P(!0)}},p),t),F)},"Dropdown"));function Hh({value:e,onChange:t}){let n=at(d=>d.conf.contentViews||[]),l=wf.default.createElement("span",null,wf.default.createElement("i",{className:"fa fa-fw fa-files-o"}),"\xA0",wf.default.createElement("b",null,"View:")," ",e.toLowerCase()," ",wf.default.createElement("span",{className:"caret"}));return wf.default.createElement(nu,{text:l,className:"btn btn-default btn-xs",options:{placement:"top-start"}},n.map(d=>wf.default.createElement(pi,{key:d,onClick:()=>t(d)},d.toLowerCase().replace("_"," "))))}o(Hh,"ViewSelector");function dS({flow:e,message:t}){let n=$t(),l=e.request===t?"request":"response",d=at(te=>te.ui.flow.contentViewFor[e.id+l]||"Auto"),v=(0,Gt.useRef)(null),[p,w]=(0,Gt.useState)(wy),_=(0,Gt.useCallback)(()=>w(Math.max(1024,p*2)),[p]),[O,D]=(0,Gt.useState)(!1),Y;O?Y=zr.getContentURL(e,t):Y=zr.getContentURL(e,t,d,p+1);let W=xy(Y,t.contentHash),X=(0,Gt.useMemo)(()=>{if(W&&!O)try{return JSON.parse(W)}catch(te){return{description:"Network Error",lines:[[["error",`${W}`]]]}}else return},[W]);if(O)return Gt.default.createElement("div",{className:"contentview",key:"edit"},Gt.default.createElement("div",{className:"controls"},Gt.default.createElement("h5",null,"[Editing]"),Gt.default.createElement(Cr,{onClick:o(()=>Oa(this,null,function*(){var R;let Q=(R=v.current)==null?void 0:R.getContent();yield n(Di(e,{[l]:{content:Q}})),D(!1)}),"save"),icon:"fa-check text-success",className:"btn-xs"},"Done"),"\xA0",Gt.default.createElement(Cr,{onClick:()=>D(!1),icon:"fa-times text-danger",className:"btn-xs"},"Cancel")),Gt.default.createElement(Ph,{ref:v,initialContent:W||""}));{let te=X?X.description:"Loading...";return Gt.default.createElement("div",{className:"contentview",key:"view"},Gt.default.createElement("div",{className:"controls"},Gt.default.createElement("h5",null,te),Gt.default.createElement(Cr,{onClick:()=>D(!0),icon:"fa-edit",className:"btn-xs"},"Edit"),"\xA0",Gt.default.createElement(Sy,{icon:"fa-upload",text:"Replace",title:"Upload a file to replace the content.",onOpenFile:Q=>n(BT(e,Q,l)),className:"btn btn-default btn-xs"}),"\xA0",Gt.default.createElement(Hh,{value:d,onChange:Q=>n(jg(e.id+l,Q))})),hS.matches(t)&&Gt.default.createElement(hS,{flow:e,message:t}),Gt.default.createElement(Cy,{lines:(X==null?void 0:X.lines)||[],maxLines:p,showMore:_}))}}o(dS,"HttpMessage");var uI=/^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon)$/i;hS.matches=e=>uI.test(zr.getContentType(e)||"");function hS({flow:e,message:t}){return Gt.default.createElement("div",{className:"flowview-image"},Gt.default.createElement("img",{src:zr.getContentURL(e,t),alt:"preview",className:"img-thumbnail"}))}o(hS,"ViewImage");function fI({flow:e}){let t=$t();return Wt.createElement("div",{className:"first-line request-line"},Wt.createElement("div",null,Wt.createElement(cf,{content:e.request.method,onEditDone:n=>t(Di(e,{request:{method:n}})),isValid:n=>n.length>0}),"\xA0",Wt.createElement(cf,{content:ko.pretty_url(e.request),onEditDone:n=>t(Di(e,{request:Le({path:""},kx(n))})),isValid:n=>{var l;return!!((l=kx(n))==null?void 0:l.host)}}),"\xA0",Wt.createElement(cf,{content:e.request.http_version,onEditDone:n=>t(Di(e,{request:{http_version:n}})),isValid:Ox})))}o(fI,"RequestLine");function cI({flow:e}){let t=$t();return Wt.createElement("div",{className:"first-line response-line"},Wt.createElement(cf,{content:e.response.http_version,onEditDone:n=>t(Di(e,{response:{http_version:n}})),isValid:Ox}),"\xA0",Wt.createElement(cf,{content:e.response.status_code+"",onEditDone:n=>t(Di(e,{response:{code:parseInt(n)}})),isValid:n=>/^\d+$/.test(n)}),e.response.http_version!=="HTTP/2.0"&&Wt.createElement(Wt.Fragment,null,"\xA0",Wt.createElement(Vs,{content:e.response.reason,onEditDone:n=>t(Di(e,{response:{msg:n}}))})))}o(cI,"ResponseLine");function pI({flow:e,message:t}){let n=$t(),l=e.request===t?"request":"response";return Wt.createElement(up,{className:"headers",data:t.headers,onChange:d=>n(Di(e,{[l]:{headers:d}}))})}o(pI,"Headers");function dI({flow:e,message:t}){let n=$t(),l=e.request===t?"request":"response";return!zr.get_first_header(t,/^trailer$/i)?null:Wt.createElement(Wt.Fragment,null,Wt.createElement("hr",null),Wt.createElement("h5",null,"HTTP Trailers"),Wt.createElement(up,{className:"trailers",data:t.trailers,onChange:v=>n(Di(e,{[l]:{trailers:v}}))}))}o(dI,"Trailers");var Jk=Wt.memo(o(function({flow:t,message:n}){let l=t.request===n?"request":"response",d=t.request===n?fI:cI;return Wt.createElement("section",{className:l},Wt.createElement(d,{flow:t}),Wt.createElement(pI,{flow:t,message:n}),Wt.createElement("hr",null),Wt.createElement(dS,{key:t.id+l,flow:t,message:n}),Wt.createElement(dI,{flow:t,message:n}))},"Message"));function mS(){let e=at(t=>t.flows.byId[t.flows.selected[0]]);return Wt.createElement(Jk,{flow:e,message:e.request})}o(mS,"Request");mS.displayName="Request";function vS(){let e=at(t=>t.flows.byId[t.flows.selected[0]]);return Wt.createElement(Jk,{flow:e,message:e.response})}o(vS,"Response");vS.displayName="Response";var _e=pe(Re());function eO({conn:e}){var n,l,d;let t=null;return"address"in e?t=_e.createElement(_e.Fragment,null,_e.createElement("tr",null,_e.createElement("td",null,"Address:"),_e.createElement("td",null,(n=e.address)==null?void 0:n.join(":"))),e.peername&&_e.createElement("tr",null,_e.createElement("td",null,"Resolved address:"),_e.createElement("td",null,e.peername.join(":"))),e.sockname&&_e.createElement("tr",null,_e.createElement("td",null,"Source address:"),_e.createElement("td",null,e.sockname.join(":")))):((l=e.peername)==null?void 0:l[0])&&(t=_e.createElement(_e.Fragment,null,_e.createElement("tr",null,_e.createElement("td",null,"Address:"),_e.createElement("td",null,(d=e.peername)==null?void 0:d.join(":"))))),_e.createElement("table",{className:"connection-table"},_e.createElement("tbody",null,t,e.sni?_e.createElement("tr",null,_e.createElement("td",null,_e.createElement("abbr",{title:"TLS Server Name Indication"},"SNI"),":"),_e.createElement("td",null,e.sni)):null,e.alpn?_e.createElement("tr",null,_e.createElement("td",null,_e.createElement("abbr",{title:"ALPN protocol negotiated"},"ALPN"),":"),_e.createElement("td",null,e.alpn)):null,e.tls_version?_e.createElement("tr",null,_e.createElement("td",null,"TLS Version:"),_e.createElement("td",null,e.tls_version)):null,e.cipher?_e.createElement("tr",null,_e.createElement("td",null,"TLS Cipher:"),_e.createElement("td",null,e.cipher)):null))}o(eO,"ConnectionInfo");function tO(e){return _e.createElement("dl",{className:"cert-attributes"},e.map(([t,n])=>_e.createElement(_e.Fragment,{key:t},_e.createElement("dt",null,t),_e.createElement("dd",null,n))))}o(tO,"attrList");function hI({flow:e}){var n;let t=(n=e.server_conn)==null?void 0:n.cert;return t?_e.createElement(_e.Fragment,null,_e.createElement("h4",{key:"name"},"Server Certificate"),_e.createElement("table",{className:"certificate-table"},_e.createElement("tbody",null,_e.createElement("tr",null,_e.createElement("td",null,"Type"),_e.createElement("td",null,t.keyinfo[0],", ",t.keyinfo[1]," bits")),_e.createElement("tr",null,_e.createElement("td",null,"SHA256 digest"),_e.createElement("td",null,t.sha256)),_e.createElement("tr",null,_e.createElement("td",null,"Valid from"),_e.createElement("td",null,Qi(t.notbefore,{milliseconds:!1}))),_e.createElement("tr",null,_e.createElement("td",null,"Valid to"),_e.createElement("td",null,Qi(t.notafter,{milliseconds:!1}))),_e.createElement("tr",null,_e.createElement("td",null,"Subject Alternative Names"),_e.createElement("td",null,t.altnames.join(", "))),_e.createElement("tr",null,_e.createElement("td",null,"Subject"),_e.createElement("td",null,tO(t.subject))),_e.createElement("tr",null,_e.createElement("td",null,"Issuer"),_e.createElement("td",null,tO(t.issuer))),_e.createElement("tr",null,_e.createElement("td",null,"Serial"),_e.createElement("td",null,t.serial))))):_e.createElement(_e.Fragment,null)}o(hI,"CertificateInfo");function Ay({flow:e}){var t;return _e.createElement("section",{className:"detail"},_e.createElement("h4",null,"Client Connection"),_e.createElement(eO,{conn:e.client_conn}),((t=e.server_conn)==null?void 0:t.address)&&_e.createElement(_e.Fragment,null,_e.createElement("h4",null,"Server Connection"),_e.createElement(eO,{conn:e.server_conn})),_e.createElement(hI,{flow:e}))}o(Ay,"Connection");Ay.displayName="Connection";var Wh=pe(Re());function Dy({flow:e}){return Wh.createElement("section",{className:"error"},Wh.createElement("div",{className:"alert alert-warning"},e.error.msg,Wh.createElement("div",null,Wh.createElement("small",null,Qi(e.error.timestamp)))))}o(Dy,"Error");Dy.displayName="Error";var rs=pe(Re());function mI({t:e,deltaTo:t,title:n}){return e?rs.createElement("tr",null,rs.createElement("td",null,n,":"),rs.createElement("td",null,Qi(e),t&&rs.createElement("span",{className:"text-muted"},"(",qg(1e3*(e-t)),")"))):rs.createElement("tr",null)}o(mI,"TimeStamp");function Ry({flow:e}){var l,d,v,p,w,_;let t;e.type==="http"?t=e.request.timestamp_start:t=e.client_conn.timestamp_start;let n=[{title:"Server conn. initiated",t:(l=e.server_conn)==null?void 0:l.timestamp_start,deltaTo:t},{title:"Server conn. TCP handshake",t:(d=e.server_conn)==null?void 0:d.timestamp_tcp_setup,deltaTo:t},{title:"Server conn. TLS handshake",t:(v=e.server_conn)==null?void 0:v.timestamp_tls_setup,deltaTo:t},{title:"Server conn. closed",t:(p=e.server_conn)==null?void 0:p.timestamp_end,deltaTo:t},{title:"Client conn. established",t:e.client_conn.timestamp_start,deltaTo:e.type==="http"?t:void 0},{title:"Client conn. TLS handshake",t:e.client_conn.timestamp_tls_setup,deltaTo:t},{title:"Client conn. closed",t:e.client_conn.timestamp_end,deltaTo:t}];return e.type==="http"&&n.push({title:"First request byte",t:e.request.timestamp_start},{title:"Request complete",t:e.request.timestamp_end,deltaTo:t},{title:"First response byte",t:(w=e.response)==null?void 0:w.timestamp_start,deltaTo:t},{title:"Response complete",t:(_=e.response)==null?void 0:_.timestamp_end,deltaTo:t}),rs.createElement("section",{className:"timing"},rs.createElement("h4",null,"Timing"),rs.createElement("table",{className:"timing-table"},rs.createElement("tbody",null,n.filter(O=>!!O.t).sort((O,D)=>O.t-D.t).map(O=>rs.createElement(mI,Le({key:O.title},O))))))}o(Ry,"Timing");Ry.displayName="Timing";var iu=pe(Re());var Gs=pe(Re()),mp=pe(Re());function Bh({flow:e,messages_meta:t}){let n=$t(),l=at(O=>O.ui.flow.contentViewFor[e.id+"messages"]||"Auto"),[d,v]=(0,mp.useState)(wy),p=(0,mp.useCallback)(()=>v(Math.max(1024,d*2)),[d]),w=xy(zr.getContentURL(e,"messages",l,d+1),e.id+t.count),_=(0,mp.useMemo)(()=>w&&JSON.parse(w),[w])||[];return Gs.createElement("div",{className:"contentview"},Gs.createElement("div",{className:"controls"},Gs.createElement("h5",null,t.count," Messages"),Gs.createElement(Hh,{value:l,onChange:O=>n(jg(e.id+"messages",O))})),_.map((O,D)=>{let Y=`fa fa-fw fa-arrow-${O.from_client?"right text-primary":"left text-danger"}`,W=Gs.createElement("div",{key:D},Gs.createElement("small",null,Gs.createElement("i",{className:Y}),Gs.createElement("span",{className:"pull-right"},O.timestamp&&Qi(O.timestamp))),Gs.createElement(Cy,{lines:O.lines,maxLines:d,showMore:p}));return d-=O.lines.length,W}))}o(Bh,"Messages");function Fy({flow:e}){return iu.createElement("section",{className:"websocket"},iu.createElement("h4",null,"WebSocket"),iu.createElement(Bh,{flow:e,messages_meta:e.websocket.messages_meta}),iu.createElement(vI,{websocket:e.websocket}))}o(Fy,"WebSocket");Fy.displayName="WebSocket";function vI({websocket:e}){if(!e.timestamp_end)return null;let t=e.close_reason?`(${e.close_reason})`:"";return iu.createElement("div",null,iu.createElement("i",{className:"fa fa-fw fa-window-close text-muted"}),"\xA0 Closed by ",e.closed_by_client?"client":"server"," with code ",e.close_code," ",t,".",iu.createElement("small",{className:"pull-right"},Qi(e.timestamp_end)))}o(vI,"CloseSummary");var rO=pe(Xn());var Iy=pe(Re());function Hy({flow:e}){return Iy.createElement("section",{className:"tcp"},Iy.createElement("h4",null,"TCP Data"),Iy.createElement(Bh,{flow:e,messages_meta:e.messages_meta}))}o(Hy,"TcpMessages");Hy.displayName="TCP Messages";var nO={request:mS,response:vS,error:Dy,connection:Ay,timing:Ry,websocket:Fy,messages:Hy};function Wy(e){let t;switch(e.type){case"http":t=["request","response","websocket"].filter(n=>e[n]);break;case"tcp":t=["messages"];break}return e.error&&t.push("error"),t.push("connection"),t.push("timing"),t}o(Wy,"tabsForFlow");function gS(){let e=$t(),t=at(v=>v.flows.byId[v.flows.selected[0]]),n=Wy(t),l=at(v=>v.ui.flow.tab);n.indexOf(l)<0&&(l==="response"&&t.error?l="error":l==="error"&&"response"in t?l="response":l=n[0]);let d=nO[l];return Uh.createElement("div",{className:"flow-detail"},Uh.createElement("nav",{className:"nav-tabs nav-tabs-sm"},n.map(v=>Uh.createElement("a",{key:v,href:"#",className:(0,rO.default)({active:l===v}),onClick:p=>{p.preventDefault(),e(lf(v))}},nO[v].displayName))),Uh.createElement(d,{flow:t}))}o(gS,"FlowView");function iO(e){if(e.ctrlKey||e.metaKey)return()=>{};let t=e.key;return e.preventDefault(),(n,l)=>{let d=l().flows,v=d.byId[l().flows.selected[0]];switch(t){case"k":case"ArrowUp":n(uf(d,-1));break;case"j":case"ArrowDown":n(uf(d,1));break;case" ":case"PageDown":n(uf(d,10));break;case"PageUp":n(uf(d,-10));break;case"End":n(uf(d,1e10));break;case"Home":n(uf(d,-1e10));break;case"Escape":l().ui.modal.activeModal?n(hy()):n(ff(void 0));break;case"ArrowLeft":{if(!v)break;let p=Wy(v),w=l().ui.flow.tab,_=p[(Math.max(0,p.indexOf(w))-1+p.length)%p.length];n(lf(_));break}case"Tab":case"ArrowRight":{if(!v)break;let p=Wy(v),w=l().ui.flow.tab,_=p[(Math.max(0,p.indexOf(w))+1)%p.length];n(lf(_));break}case"d":{if(!v)return;n(uy(v));break}case"D":{if(!v)return;n(fy(v));break}case"a":{v&&v.intercepted&&n(rp(v));break}case"A":{n(ly());break}case"r":{v&&n(np(v));break}case"v":{v&&v.modified&&n(cy(v));break}case"x":{v&&v.intercepted&&n(ay(v));break}case"X":{n(WT());break}case"z":{n(py());break}default:return}}}o(iO,"onKeyDown");var Gh=pe(Re());var zh=pe(Re()),jh=pe(Xa()),oO=pe(Xn()),vp=class extends zh.Component{constructor(t,n){super(t,n);this.state={applied:!1,startX:0,startY:0},this.onMouseMove=this.onMouseMove.bind(this),this.onMouseDown=this.onMouseDown.bind(this),this.onMouseUp=this.onMouseUp.bind(this),this.onDragEnd=this.onDragEnd.bind(this)}onMouseDown(t){this.setState({startX:t.pageX,startY:t.pageY}),window.addEventListener("mousemove",this.onMouseMove),window.addEventListener("mouseup",this.onMouseUp),window.addEventListener("dragend",this.onDragEnd)}onDragEnd(){jh.default.findDOMNode(this).style.transform="",window.removeEventListener("dragend",this.onDragEnd),window.removeEventListener("mouseup",this.onMouseUp),window.removeEventListener("mousemove",this.onMouseMove)}onMouseUp(t){this.onDragEnd();let n=jh.default.findDOMNode(this),l=n.previousElementSibling,d=l.offsetHeight+t.pageY-this.state.startY;this.props.axis==="x"&&(d=l.offsetWidth+t.pageX-this.state.startX),l.style.flex=`0 0 ${Math.max(0,d)}px`,n.nextElementSibling.style.flex="1 1 auto",this.setState({applied:!0}),this.onResize()}onMouseMove(t){let n=0,l=0;this.props.axis==="x"?n=t.pageX-this.state.startX:l=t.pageY-this.state.startY,jh.default.findDOMNode(this).style.transform=`translate(${n}px, ${l}px)`}onResize(){window.setTimeout(()=>window.dispatchEvent(new CustomEvent("resize")),1)}reset(t){if(!this.state.applied)return;let n=jh.default.findDOMNode(this);n.previousElementSibling.style.flex="",n.nextElementSibling.style.flex="",t||this.setState({applied:!1}),this.onResize()}componentWillUnmount(){this.reset(!0)}render(){return zh.default.createElement("div",{className:(0,oO.default)("splitter",this.props.axis==="x"?"splitter-x":"splitter-y")},zh.default.createElement("div",{onMouseDown:this.onMouseDown,draggable:"true"}))}};o(vp,"Splitter"),vp.defaultProps={axis:"x"};var ns=pe(Re()),Vh=pe($h()),Uy=pe(Xa());var xO=pe(yS());var wS=pe(Xa()),hO=Symbol("shouldStick"),mO=o(e=>e.scrollTop+e.clientHeight===e.scrollHeight,"isAtBottom"),By=o(e=>{var t;return Object.assign((o(t=class extends e{UNSAFE_componentWillUpdate(){let l=wS.default.findDOMNode(this);this[hO]=l.scrollTop&&mO(l),super.UNSAFE_componentWillUpdate&&super.UNSAFE_componentWillUpdate(),super.componentWillUpdate&&super.componentWillUpdate()}componentDidUpdate(){let l=wS.default.findDOMNode(this);this[hO]&&!mO(l)&&(l.scrollTop=l.scrollHeight),super.componentDidUpdate&&super.componentDidUpdate()}},"AutoScrollWrapper"),t.displayName=e.name,t),e)},"default");function gp(e=void 0){if(!e)return{start:0,end:0,paddingTop:0,paddingBottom:0};let{itemCount:t,rowHeight:n,viewportTop:l,viewportHeight:d,itemHeights:v}=e,p=l+d,w=0,_=0,O=0,D=0;if(v)for(let Y=0,W=0;Yw.flows.sort.desc),l=at(w=>w.flows.sort.column),d=at(w=>w.options.web_columns),v=n?"sort-desc":"sort-asc",p=d.map(w=>Eh[w]).filter(w=>w).concat(tp);return qh.createElement("tr",null,p.map(w=>qh.createElement("th",{className:(0,vO.default)(`col-${w.name}`,l===w.name&&v),key:w.name,onClick:()=>t(HT(w.name===l&&n?void 0:w.name,w.name!==l?!1:!n))},w.headerName)))},"FlowTableHead"));var yp=pe(Re()),yO=pe(Xn());var wO=yp.default.memo(o(function({flow:t,selected:n,highlighted:l}){let d=$t(),v=at(O=>O.options.web_columns),p=(0,yO.default)({selected:n,highlighted:l,intercepted:t.intercepted,"has-request":t.type==="http"&&t.request,"has-response":t.type==="http"&&t.response}),w=(0,yp.useCallback)(O=>{let D=O.target;for(;D.parentNode;){if(D.classList.contains("col-quickactions"))return;D=D.parentNode}d(ff(t.id))},[t]),_=v.map(O=>Eh[O]).filter(O=>O).concat(tp);return yp.default.createElement("tr",{className:p,onClick:w},_.map(O=>yp.default.createElement(O,{key:O.name,flow:t})))},"FlowRow"));var Kh=class extends ns.Component{constructor(t,n){super(t,n);this.state={vScroll:gp()},this.onViewportUpdate=this.onViewportUpdate.bind(this)}UNSAFE_componentWillMount(){window.addEventListener("resize",this.onViewportUpdate)}UNSAFE_componentWillUnmount(){window.removeEventListener("resize",this.onViewportUpdate)}componentDidUpdate(){if(this.onViewportUpdate(),!this.shouldScrollIntoView)return;this.shouldScrollIntoView=!1;let{rowHeight:t,flows:n,selected:l}=this.props,d=Uy.default.findDOMNode(this),v=Uy.default.findDOMNode(this.refs.head),p=v?v.offsetHeight:0,w=n.indexOf(l)*t+p,_=w+t,O=d.scrollTop,D=d.offsetHeight;w-pO+D&&(d.scrollTop=_-D)}UNSAFE_componentWillReceiveProps(t){t.selected&&t.selected!==this.props.selected&&(this.shouldScrollIntoView=!0)}onViewportUpdate(){let t=Uy.default.findDOMNode(this),n=t.scrollTop||0,l=gp({viewportTop:n,viewportHeight:t.offsetHeight||0,itemCount:this.props.flows.length,rowHeight:this.props.rowHeight});(this.state.viewportTop!==n||!(0,xO.default)(this.state.vScroll,l))&&this.setState({vScroll:l,viewportTop:n})}render(){let{vScroll:t,viewportTop:n}=this.state,{flows:l,selected:d,highlight:v}=this.props,p=v?af.parse(v):()=>!1;return ns.createElement("div",{className:"flow-table",onScroll:this.onViewportUpdate},ns.createElement("table",null,ns.createElement("thead",{ref:"head",style:{transform:`translateY(${n}px)`}},ns.createElement(gO,null)),ns.createElement("tbody",null,ns.createElement("tr",{style:{height:t.paddingTop}}),l.slice(t.start,t.end).map(w=>ns.createElement(wO,{key:w.id,flow:w,selected:w===d,highlighted:p(w)})),ns.createElement("tr",{style:{height:t.paddingBottom}}))))}};o(Kh,"FlowTable"),gc(Kh,"propTypes",{flows:Vh.default.array.isRequired,rowHeight:Vh.default.number,highlight:Vh.default.string,selected:Vh.default.object}),gc(Kh,"defaultProps",{rowHeight:32});var wI=By(Kh),SO=Ai(e=>({flows:e.flows.view,highlight:e.flows.highlight,selected:e.flows.byId[e.flows.selected[0]]}))(wI);function xS(){let e=at(t=>!!t.flows.byId[t.flows.selected[0]]);return Gh.createElement("div",{className:"main-view"},Gh.createElement(SO,null),e&&Gh.createElement(vp,{key:"splitter"}),e&&Gh.createElement(gS,{key:"flowDetails"}))}o(xS,"MainView");var Po=pe(Re()),kO=pe(Xn());var Jn=pe(Re());var is=pe(Re()),zy=pe(Xa()),CO=pe(Xn());var Ji=pe(Re());var eo=class extends Ji.Component{constructor(t,n){super(t,n);this.state={doc:eo.doc}}componentDidMount(){eo.xhr||(eo.xhr=Et("/filter-help").then(t=>t.json()),eo.xhr.catch(()=>{eo.xhr=null})),this.state.doc||eo.xhr.then(t=>{eo.doc=t,this.setState({doc:t})})}render(){let{doc:t}=this.state;return t?Ji.default.createElement("table",{className:"table table-condensed"},Ji.default.createElement("tbody",null,t.commands.map(n=>Ji.default.createElement("tr",{key:n[1],onClick:l=>this.props.selectHandler(n[0].split(" ")[0]+" ")},Ji.default.createElement("td",null,n[0].replace(" ","\xA0")),Ji.default.createElement("td",null,n[1]))),Ji.default.createElement("tr",{key:"docs-link"},Ji.default.createElement("td",{colSpan:2},Ji.default.createElement("a",{href:"https://mitmproxy.org/docs/latest/concepts-filters/",target:"_blank"},Ji.default.createElement("i",{className:"fa fa-external-link"}),"\xA0 mitmproxy docs"))))):Ji.default.createElement("i",{className:"fa fa-spinner fa-spin"})}};o(eo,"FilterDocs");var xf=class extends is.Component{constructor(t,n){super(t,n);this.state={value:this.props.value,focus:!1,mousefocus:!1},this.onChange=this.onChange.bind(this),this.onFocus=this.onFocus.bind(this),this.onBlur=this.onBlur.bind(this),this.onKeyDown=this.onKeyDown.bind(this),this.onMouseEnter=this.onMouseEnter.bind(this),this.onMouseLeave=this.onMouseLeave.bind(this),this.selectFilter=this.selectFilter.bind(this)}UNSAFE_componentWillReceiveProps(t){this.setState({value:t.value})}isValid(t){try{return t&&af.parse(t),!0}catch(n){return!1}}getDesc(){if(!this.state.value)return is.default.createElement(eo,{selectHandler:this.selectFilter});try{return af.parse(this.state.value).desc}catch(t){return""+t}}onChange(t){let n=t.target.value;this.setState({value:n}),this.isValid(n)&&this.props.onChange(n)}onFocus(){this.setState({focus:!0})}onBlur(){this.setState({focus:!1})}onMouseEnter(){this.setState({mousefocus:!0})}onMouseLeave(){this.setState({mousefocus:!1})}onKeyDown(t){(t.key==="Escape"||t.key==="Enter")&&(this.blur(),this.setState({mousefocus:!1})),t.stopPropagation()}selectFilter(t){this.setState({value:t}),zy.default.findDOMNode(this.refs.input).focus()}blur(){zy.default.findDOMNode(this.refs.input).blur()}select(){zy.default.findDOMNode(this.refs.input).select()}render(){let{type:t,color:n,placeholder:l}=this.props,{value:d,focus:v,mousefocus:p}=this.state;return is.default.createElement("div",{className:(0,CO.default)("filter-input input-group",{"has-error":!this.isValid(d)})},is.default.createElement("span",{className:"input-group-addon"},is.default.createElement("i",{className:"fa fa-fw fa-"+t,style:{color:n}})),is.default.createElement("input",{type:"text",ref:"input",placeholder:l,className:"form-control",value:d,onChange:this.onChange,onFocus:this.onFocus,onBlur:this.onBlur,onKeyDown:this.onKeyDown}),(v||p)&&is.default.createElement("div",{className:"popover bottom",onMouseEnter:this.onMouseEnter,onMouseLeave:this.onMouseLeave},is.default.createElement("div",{className:"arrow"}),is.default.createElement("div",{className:"popover-content"},this.getDesc())))}};o(xf,"FilterInput");wp.title="Start";function wp(){return Jn.createElement("div",{className:"main-menu"},Jn.createElement("div",{className:"menu-group"},Jn.createElement("div",{className:"menu-content"},Jn.createElement(SI,null),Jn.createElement(CI,null)),Jn.createElement("div",{className:"menu-legend"},"Find")),Jn.createElement("div",{className:"menu-group"},Jn.createElement("div",{className:"menu-content"},Jn.createElement(xI,null),Jn.createElement(_I,null)),Jn.createElement("div",{className:"menu-legend"},"Intercept")))}o(wp,"StartMenu");function xI(){let e=$t(),t=at(n=>n.options.intercept);return Jn.createElement(xf,{value:t||"",placeholder:"Intercept",type:"pause",color:"hsl(208, 56%, 53%)",onChange:n=>e(lp("intercept",n))})}o(xI,"InterceptInput");function SI(){let e=$t(),t=at(n=>n.flows.filter);return Jn.createElement(xf,{value:t||"",placeholder:"Search",type:"search",color:"black",onChange:n=>e(oy(n))})}o(SI,"FlowFilterInput");function CI(){let e=$t(),t=at(n=>n.flows.highlight);return Jn.createElement(xf,{value:t||"",placeholder:"Highlight",type:"tag",color:"hsl(48, 100%, 50%)",onChange:n=>e(sy(n))})}o(CI,"HighlightInput");function _I(){let e=$t();return Jn.createElement(Cr,{className:"btn-sm",title:"[a]ccept all",icon:"fa-forward text-success",onClick:()=>e(ly())},"Resume All")}o(_I,"ResumeAll");var qr=pe(Re());var Sf=pe(Re());function SS({value:e,onChange:t,children:n}){return Sf.createElement("div",{className:"menu-entry"},Sf.createElement("label",null,Sf.createElement("input",{type:"checkbox",checked:e,onChange:t}),n))}o(SS,"MenuToggle");function jy({name:e,children:t}){let n=$t(),l=at(d=>d.options[e]);return Sf.createElement(SS,{value:!!l,onChange:()=>n(lp(e,!l))},t)}o(jy,"OptionsToggle");function _O(){let e=$s(),t=at(n=>n.eventLog.visible);return Sf.createElement(SS,{value:t,onChange:()=>e(sp())},"Display Event Log")}o(_O,"EventlogToggle");function bO(){let e=$s(),t=at(n=>n.commandBar.visible);return Sf.createElement(SS,{value:t,onChange:()=>e(yy())},"Display Command Bar")}o(bO,"CommandBarToggle");var CS=pe(Re());function _S({children:e,resource:t}){let n=`https://docs.mitmproxy.org/stable/${t}`;return CS.createElement("a",{target:"_blank",href:n},e||CS.createElement("i",{className:"fa fa-question-circle"}))}o(_S,"DocsLink");var $y=pe(Re());function No({children:e}){return window.MITMWEB_CONF&&window.MITMWEB_CONF.static?null:$y.createElement($y.Fragment,null,e)}o(No,"HideInStatic");qy.title="Options";function qy(){let e=$t(),t=o(()=>$T("OptionModal"),"openOptions");return qr.createElement("div",null,qr.createElement(No,null,qr.createElement("div",{className:"menu-group"},qr.createElement("div",{className:"menu-content"},qr.createElement(Cr,{title:"Open Options",icon:"fa-cogs text-primary",onClick:()=>e(t())},"Edit Options ",qr.createElement("sup",null,"alpha"))),qr.createElement("div",{className:"menu-legend"},"Options Editor")),qr.createElement("div",{className:"menu-group"},qr.createElement("div",{className:"menu-content"},qr.createElement(jy,{name:"anticache"},"Strip cache headers ",qr.createElement(_S,{resource:"overview-features/#anticache"})),qr.createElement(jy,{name:"showhost"},"Use host header for display"),qr.createElement(jy,{name:"ssl_insecure"},"Don't verify server certificates")),qr.createElement("div",{className:"menu-legend"},"Quick Options"))),qr.createElement("div",{className:"menu-group"},qr.createElement("div",{className:"menu-content"},qr.createElement(_O,null),qr.createElement(bO,null)),qr.createElement("div",{className:"menu-legend"},"View Options")))}o(qy,"OptionMenu");var di=pe(Re());var EO=di.memo(o(function(){let t=$s();return di.createElement(nu,{className:"pull-left special",text:"File",options:{placement:"bottom-start"}},di.createElement("li",null,di.createElement(Sy,{icon:"fa-folder-open",text:"\xA0Open...",onClick:n=>n.stopPropagation(),onOpenFile:n=>{t(zT(n)),document.body.click()}})),di.createElement(pi,{onClick:()=>t(UT())},di.createElement("i",{className:"fa fa-fw fa-floppy-o"}),"\xA0Save..."),di.createElement(pi,{onClick:()=>confirm("Delete all flows?")&&t(py())},di.createElement("i",{className:"fa fa-fw fa-trash"}),"\xA0Clear All"),di.createElement(No,null,di.createElement(Zk,null),di.createElement("li",null,di.createElement("a",{href:"http://mitm.it/",target:"_blank"},di.createElement("i",{className:"fa fa-fw fa-external-link"}),"\xA0Install Certificates..."))))},"FileMenu"));var ot=pe(Re());var xp=o((e,t)=>Oa(void 0,null,function*(){let n=yield Sh("export",t,`@${e.id}`);n.value?yield navigator.clipboard.writeText(n.value):n.error?alert(n.error):console.error(n)}),"copy");Sp.title="Flow";function Sp(){let e=$t(),t=at(n=>n.flows.byId[n.flows.selected[0]]);return t?ot.createElement("div",{className:"flow-menu"},ot.createElement(No,null,ot.createElement("div",{className:"menu-group"},ot.createElement("div",{className:"menu-content"},ot.createElement(Cr,{title:"[r]eplay flow",icon:"fa-repeat text-primary",onClick:()=>e(np(t)),disabled:!Gg(t)},"Replay"),ot.createElement(Cr,{title:"[D]uplicate flow",icon:"fa-copy text-info",onClick:()=>e(fy(t))},"Duplicate"),ot.createElement(Cr,{disabled:!t||!t.modified,title:"revert changes to flow [V]",icon:"fa-history text-warning",onClick:()=>e(cy(t))},"Revert"),ot.createElement(Cr,{title:"[d]elete flow",icon:"fa-trash text-danger",onClick:()=>e(uy(t))},"Delete"),ot.createElement(kI,{flow:t})),ot.createElement("div",{className:"menu-legend"},"Flow Modification"))),ot.createElement("div",{className:"menu-group"},ot.createElement("div",{className:"menu-content"},ot.createElement(bI,{flow:t}),ot.createElement(EI,{flow:t})),ot.createElement("div",{className:"menu-legend"},"Export")),ot.createElement(No,null,ot.createElement("div",{className:"menu-group"},ot.createElement("div",{className:"menu-content"},ot.createElement(Cr,{disabled:!t||!t.intercepted,title:"[a]ccept intercepted flow",icon:"fa-play text-success",onClick:()=>e(rp(t))},"Resume"),ot.createElement(Cr,{disabled:!t||!t.intercepted,title:"kill intercepted flow [x]",icon:"fa-times text-danger",onClick:()=>e(ay(t))},"Abort")),ot.createElement("div",{className:"menu-legend"},"Interception")))):ot.createElement("div",null)}o(Sp,"FlowMenu");function bI({flow:e}){var t;if(e.type!=="http")return ot.createElement(Cr,{icon:"fa-download",onClick:()=>0,disabled:!0},"Download");if(e.request.contentLength&&!((t=e.response)==null?void 0:t.contentLength))return ot.createElement(Cr,{icon:"fa-download",onClick:()=>window.location.href=zr.getContentURL(e,e.request)},"Download");if(e.response){let n=e.response;if(!e.request.contentLength&&e.response.contentLength)return ot.createElement(Cr,{icon:"fa-download",onClick:()=>window.location.href=zr.getContentURL(e,n)},"Download");if(e.request.contentLength&&e.response.contentLength)return ot.createElement(nu,{text:ot.createElement(Cr,{icon:"fa-download",onClick:()=>1},"Download\u25BE"),options:{placement:"bottom-start"}},ot.createElement(pi,{onClick:()=>window.location.href=zr.getContentURL(e,e.request)},"Download request"),ot.createElement(pi,{onClick:()=>window.location.href=zr.getContentURL(e,n)},"Download response"))}return null}o(bI,"DownloadButton");function EI({flow:e}){return ot.createElement(nu,{className:"",text:ot.createElement(Cr,{title:"Export flow.",icon:"fa-clone",onClick:()=>1,disabled:e.type==="tcp"},"Export\u25BE"),options:{placement:"bottom-start"}},ot.createElement(pi,{onClick:()=>xp(e,"raw_request")},"Copy raw request"),ot.createElement(pi,{onClick:()=>xp(e,"raw_response")},"Copy raw response"),ot.createElement(pi,{onClick:()=>xp(e,"raw")},"Copy raw request and response"),ot.createElement(pi,{onClick:()=>xp(e,"curl")},"Copy as cURL"),ot.createElement(pi,{onClick:()=>xp(e,"httpie")},"Copy as HTTPie"))}o(EI,"ExportButton");var TI={":red_circle:":"\u{1F534}",":orange_circle:":"\u{1F7E0}",":yellow_circle:":"\u{1F7E1}",":green_circle:":"\u{1F7E2}",":large_blue_circle:":"\u{1F535}",":purple_circle:":"\u{1F7E3}",":brown_circle:":"\u{1F7E4}"};function kI({flow:e}){let t=$t();return ot.createElement(nu,{className:"",text:ot.createElement(Cr,{title:"mark flow",icon:"fa-paint-brush text-success",onClick:()=>1},"Mark\u25BE"),options:{placement:"bottom-start"}},ot.createElement(pi,{onClick:()=>t(Di(e,{marked:""}))},"\u26AA (no marker)"),Object.entries(TI).map(([n,l])=>ot.createElement(pi,{key:n,onClick:()=>t(Di(e,{marked:n}))},l," ",n.replace(/[:_]/g," "))))}o(kI,"MarkButton");var ou=pe(Re());var TO=ou.memo(o(function(){let t=at(l=>l.connection.state),n=at(l=>l.connection.message);switch(t){case Zn.INIT:return ou.createElement("span",{className:"connection-indicator init"},"connecting\u2026");case Zn.FETCHING:return ou.createElement("span",{className:"connection-indicator fetching"},"fetching data\u2026");case Zn.ESTABLISHED:return ou.createElement("span",{className:"connection-indicator established"},"connected");case Zn.ERROR:return ou.createElement("span",{className:"connection-indicator error",title:n},"connection lost");case Zn.OFFLINE:return ou.createElement("span",{className:"connection-indicator offline"},"offline");default:let l=t;throw"unknown connection state"}},"ConnectionIndicator"));function bS(){let e=at(w=>w.flows.selected.filter(_=>_ in w.flows.byId)),[t,n]=(0,Po.useState)(()=>wp),[l,d]=(0,Po.useState)(!1),v=[wp,qy];e.length>0?(l||(n(()=>Sp),d(!0)),v.push(Sp)):(l&&d(!1),t===Sp&&n(()=>wp));function p(w,_){_.preventDefault(),n(()=>w)}return o(p,"handleClick"),Po.default.createElement("header",null,Po.default.createElement("nav",{className:"nav-tabs nav-tabs-lg"},Po.default.createElement(EO,null),v.map(w=>Po.default.createElement("a",{key:w.title,href:"#",className:(0,kO.default)({active:w===t}),onClick:_=>p(w,_)},w.title)),Po.default.createElement(No,null,Po.default.createElement(TO,null))),Po.default.createElement("div",null,Po.default.createElement(t,null)))}o(bS,"Header");var Qe=pe(Re()),OO=pe(Xn());var Vy=function(){"use strict";function e(l,d){function v(){this.constructor=l}o(v,"ctor"),v.prototype=d.prototype,l.prototype=new v}o(e,"peg$subclass");function t(l,d,v,p){this.message=l,this.expected=d,this.found=v,this.location=p,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,t)}o(t,"peg$SyntaxError"),e(t,Error);function n(l){var d=arguments.length>1?arguments[1]:{},v=this,p={},w={Expr:vr},_=vr,O=o(function(H,J){return[H,...J]},"peg$c0"),D=o(function(H){return[H]},"peg$c1"),Y=o(function(){return""},"peg$c2"),W={type:"other",description:"string"},X='"',te={type:"literal",value:'"',description:'"\\""'},Q=o(function(H){return H.join("")},"peg$c6"),R="'",P={type:"literal",value:"'",description:`"'"`},F=/^["\\]/,K={type:"class",value:'["\\\\]',description:'["\\\\]'},V={type:"any",description:"any character"},ue=o(function(H){return H},"peg$c12"),ie="\\",de={type:"literal",value:"\\",description:'"\\\\"'},ge=/^['\\]/,we={type:"class",value:"['\\\\]",description:"['\\\\]"},qe=/^['"\\]/,Je={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},be="n",yt={type:"literal",value:"n",description:'"n"'},Be=o(function(){return` -`},"peg$c21"),Ve="r",Ke={type:"literal",value:"r",description:'"r"'},Ge=o(function(){return"\r"},"peg$c24"),Yt="t",ut={type:"literal",value:"t",description:'"t"'},Dr=o(function(){return" "},"peg$c27"),qt={type:"other",description:"whitespace"},_t=/^[ \t\n\r]/,wt={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},st={type:"other",description:"control character"},_r=/^[|&!()~"]/,Bt={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},Ut={type:"other",description:"optional whitespace"},ne=0,et=0,br=[{line:1,column:1,seenCR:!1}],zt=0,jt=[],xe=0,Er;if("startRule"in d){if(!(d.startRule in w))throw new Error(`Can't start parsing from rule "`+d.startRule+'".');_=w[d.startRule]}function on(){return l.substring(et,ne)}o(on,"text");function Dn(){return cr(et,ne)}o(Dn,"location");function ei(H){throw Do(null,[{type:"other",description:H}],l.substring(et,ne),cr(et,ne))}o(ei,"expected");function sn(H){throw Do(H,null,l.substring(et,ne),cr(et,ne))}o(sn,"error");function Vt(H){var J=br[H],he,Ee;if(J)return J;for(he=H-1;!br[he];)he--;for(J=br[he],J={line:J.line,column:J.column,seenCR:J.seenCR};hezt&&(zt=ne,jt=[]),jt.push(H))}o(ft,"peg$fail");function Do(H,J,he,Ee){function Xt(At){var Rr=1;for(At.sort(function(Qt,ti){return Qt.descriptionti.description?1:0});Rr1?ti.slice(0,-1).join(", ")+" or "+ti[At.length-1]:ti[0],to=Rr?'"'+Qt(Rr)+'"':"end of input","Expected "+os+" but "+to+" found."}return o(Hl,"buildMessage"),J!==null&&Xt(J),new t(H!==null?H:Hl(J,he),J,he,Ee)}o(Do,"peg$buildException");function vr(){var H,J,he,Ee;if(H=ne,J=Ri(),J!==p){if(he=[],Ee=Fn(),Ee!==p)for(;Ee!==p;)he.push(Ee),Ee=Fn();else he=p;he!==p?(Ee=vr(),Ee!==p?(et=H,J=O(J,Ee),H=J):(ne=H,H=p)):(ne=H,H=p)}else ne=H,H=p;if(H===p&&(H=ne,J=Ri(),J!==p&&(et=H,J=D(J)),H=J,H===p)){for(H=ne,J=[],he=Fn();he!==p;)J.push(he),he=Fn();J!==p&&(et=H,J=Y()),H=J}return H}o(vr,"peg$parseExpr");function Ri(){var H,J,he,Ee;if(xe++,H=ne,l.charCodeAt(ne)===34?(J=X,ne++):(J=p,xe===0&&ft(te)),J!==p){for(he=[],Ee=ln();Ee!==p;)he.push(Ee),Ee=ln();he!==p?(l.charCodeAt(ne)===34?(Ee=X,ne++):(Ee=p,xe===0&&ft(te)),Ee!==p?(et=H,J=Q(he),H=J):(ne=H,H=p)):(ne=H,H=p)}else ne=H,H=p;if(H===p){if(H=ne,l.charCodeAt(ne)===39?(J=R,ne++):(J=p,xe===0&&ft(P)),J!==p){for(he=[],Ee=Rn();Ee!==p;)he.push(Ee),Ee=Rn();he!==p?(l.charCodeAt(ne)===39?(Ee=R,ne++):(Ee=p,xe===0&&ft(P)),Ee!==p?(et=H,J=Q(he),H=J):(ne=H,H=p)):(ne=H,H=p)}else ne=H,H=p;if(H===p){if(H=ne,J=ne,xe++,he=bn(),xe--,he===p?J=void 0:(ne=J,J=p),J!==p){if(he=[],Ee=hi(),Ee!==p)for(;Ee!==p;)he.push(Ee),Ee=hi();else he=p;he!==p?(et=H,J=Q(he),H=J):(ne=H,H=p)}else ne=H,H=p;if(H===p){if(H=ne,l.charCodeAt(ne)===34?(J=X,ne++):(J=p,xe===0&&ft(te)),J!==p){for(he=[],Ee=ln();Ee!==p;)he.push(Ee),Ee=ln();he!==p?(et=H,J=Q(he),H=J):(ne=H,H=p)}else ne=H,H=p;if(H===p)if(H=ne,l.charCodeAt(ne)===39?(J=R,ne++):(J=p,xe===0&&ft(P)),J!==p){for(he=[],Ee=Rn();Ee!==p;)he.push(Ee),Ee=Rn();he!==p?(et=H,J=Q(he),H=J):(ne=H,H=p)}else ne=H,H=p}}}return xe--,H===p&&(J=p,xe===0&&ft(W)),H}o(Ri,"peg$parseStringLiteral");function ln(){var H,J,he;return H=ne,J=ne,xe++,F.test(l.charAt(ne))?(he=l.charAt(ne),ne++):(he=p,xe===0&&ft(K)),xe--,he===p?J=void 0:(ne=J,J=p),J!==p?(l.length>ne?(he=l.charAt(ne),ne++):(he=p,xe===0&&ft(V)),he!==p?(et=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p),H===p&&(H=ne,l.charCodeAt(ne)===92?(J=ie,ne++):(J=p,xe===0&&ft(de)),J!==p?(he=mi(),he!==p?(et=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p)),H}o(ln,"peg$parseDoubleStringChar");function Rn(){var H,J,he;return H=ne,J=ne,xe++,ge.test(l.charAt(ne))?(he=l.charAt(ne),ne++):(he=p,xe===0&&ft(we)),xe--,he===p?J=void 0:(ne=J,J=p),J!==p?(l.length>ne?(he=l.charAt(ne),ne++):(he=p,xe===0&&ft(V)),he!==p?(et=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p),H===p&&(H=ne,l.charCodeAt(ne)===92?(J=ie,ne++):(J=p,xe===0&&ft(de)),J!==p?(he=mi(),he!==p?(et=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p)),H}o(Rn,"peg$parseSingleStringChar");function hi(){var H,J,he;return H=ne,J=ne,xe++,he=Fn(),xe--,he===p?J=void 0:(ne=J,J=p),J!==p?(l.length>ne?(he=l.charAt(ne),ne++):(he=p,xe===0&&ft(V)),he!==p?(et=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p),H}o(hi,"peg$parseUnquotedStringChar");function mi(){var H,J;return qe.test(l.charAt(ne))?(H=l.charAt(ne),ne++):(H=p,xe===0&&ft(Je)),H===p&&(H=ne,l.charCodeAt(ne)===110?(J=be,ne++):(J=p,xe===0&&ft(yt)),J!==p&&(et=H,J=Be()),H=J,H===p&&(H=ne,l.charCodeAt(ne)===114?(J=Ve,ne++):(J=p,xe===0&&ft(Ke)),J!==p&&(et=H,J=Ge()),H=J,H===p&&(H=ne,l.charCodeAt(ne)===116?(J=Yt,ne++):(J=p,xe===0&&ft(ut)),J!==p&&(et=H,J=Dr()),H=J))),H}o(mi,"peg$parseEscapeSequence");function Fn(){var H,J;return xe++,_t.test(l.charAt(ne))?(H=l.charAt(ne),ne++):(H=p,xe===0&&ft(wt)),xe--,H===p&&(J=p,xe===0&&ft(qt)),H}o(Fn,"peg$parsews");function bn(){var H,J;return xe++,_r.test(l.charAt(ne))?(H=l.charAt(ne),ne++):(H=p,xe===0&&ft(Bt)),xe--,H===p&&(J=p,xe===0&&ft(st)),H}o(bn,"peg$parsecc");function Ys(){var H,J;for(xe++,H=[],J=Fn();J!==p;)H.push(J),J=Fn();return xe--,H===p&&(J=p,xe===0&&ft(Ut)),H}if(o(Ys,"peg$parse__"),Er=_(),Er!==p&&ne===l.length)return Er;throw Er!==p&&ne{t&&t.current.addEventListener("DOMNodeInserted",n=>{let l=n.currentTarget;l.scroll({top:l.scrollHeight,behavior:"auto"})})},[]),Qe.default.createElement("div",{className:"command-result",ref:t},e.map((n,l)=>Qe.default.createElement("div",{key:l},Qe.default.createElement("div",null,Qe.default.createElement("strong",null,"$ ",n.command)),n.result)))}o(OI,"Results");function LI({nextArgs:e,currentArg:t,help:n,description:l,availableCommands:d}){let v=[];for(let p=0;p0&&Qe.default.createElement("div",null,Qe.default.createElement("strong",null,"Argument suggestion:")," ",v),(n==null?void 0:n.includes("->"))&&Qe.default.createElement("div",null,Qe.default.createElement("strong",null,"Signature help: "),n),l&&Qe.default.createElement("div",null,"# ",l),Qe.default.createElement("div",null,Qe.default.createElement("strong",null,"Available Commands: "),Qe.default.createElement("p",{className:"available-commands"},JSON.stringify(d)))))}o(LI,"CommandHelp");function TS(){let[e,t]=(0,Qe.useState)(""),[n,l]=(0,Qe.useState)(""),[d,v]=(0,Qe.useState)(0),[p,w]=(0,Qe.useState)([]),[_,O]=(0,Qe.useState)([]),[D,Y]=(0,Qe.useState)({}),[W,X]=(0,Qe.useState)([]),[te,Q]=(0,Qe.useState)(0),[R,P]=(0,Qe.useState)(""),[F,K]=(0,Qe.useState)(""),[V,ue]=(0,Qe.useState)([]),[ie,de]=(0,Qe.useState)([]),[ge,we]=(0,Qe.useState)(void 0);(0,Qe.useEffect)(()=>{Et("/commands",{method:"GET"}).then(Be=>Be.json()).then(Be=>{Y(Be),w(ES(Be)),O(Object.keys(Be))}).catch(Be=>console.error(Be))},[]),(0,Qe.useEffect)(()=>{Sh("commands.history.get").then(Be=>{de(Be.value)}).catch(Be=>console.error(Be))},[]);let qe=o((Be,Ve)=>{var ut,Dr,qt;let Ke=Vy.parse(Ve),Ge=Vy.parse(Be);P((ut=D[Ke[0]])==null?void 0:ut.signature_help),K(((Dr=D[Ke[0]])==null?void 0:Dr.help)||""),w(ES(D,Ge[0])),O(ES(D,Ke[0]));let Yt=(qt=D[Ke[0]])==null?void 0:qt.parameters.map(_t=>_t.name);Yt&&(X([Ke[0],...Yt]),Q(Ke.length-1))},"parseCommand"),Je=o(Be=>{t(Be.target.value),l(Be.target.value),v(0)},"onChange"),be=o(Be=>{if(Be.key==="Enter"){let[Ve,...Ke]=Vy.parse(e);de([...ie,e]),Sh("commands.history.add",e).catch(()=>0),Et.post(`/commands/${Ve}`,{arguments:Ke}).then(Ge=>Ge.json()).then(Ge=>{we(void 0),X([]),ue([...V,{command:e,result:JSON.stringify(Ge.value||Ge.error)}])}).catch(Ge=>{we(void 0),X([]),ue([...V,{command:e,result:Ge.toString()}])}),P(""),K(""),t(""),l(""),v(0),w(_)}if(Be.key==="ArrowUp"){let Ve;ge===void 0?Ve=ie.length-1:Ve=Math.max(0,ge-1),t(ie[Ve]),l(ie[Ve]),we(Ve)}if(Be.key==="ArrowDown"){if(ge===void 0)return;if(ge==ie.length-1)t(""),l(""),we(void 0);else{let Ve=ge+1;t(ie[Ve]),l(ie[Ve]),we(Ve)}}Be.key==="Tab"&&(t(p[d]),v((d+1)%p.length),Be.preventDefault()),Be.stopPropagation()},"onKeyDown"),yt=o(Be=>{if(!e){O(Object.keys(D));return}qe(n,e),Be.stopPropagation()},"onKeyUp");return Qe.default.createElement("div",{className:"command"},Qe.default.createElement("div",{className:"command-title"},"Command Result"),Qe.default.createElement(OI,{results:V}),Qe.default.createElement(LI,{nextArgs:W,currentArg:te,help:R,description:F,availableCommands:_}),Qe.default.createElement("div",{className:(0,OO.default)("command-input input-group")},Qe.default.createElement("span",{className:"input-group-addon"},Qe.default.createElement("i",{className:"fa fa-fw fa-terminal"})),Qe.default.createElement("input",{type:"text",placeholder:"Enter command",className:"form-control",value:e||"",onChange:Je,onKeyDown:be,onKeyUp:yt})))}o(TS,"CommandBar");var Il=pe(Re()),Cp=pe($h());var kS=pe(Re());function OS({checked:e,onToggle:t,text:n}){return kS.default.createElement("div",{className:"btn btn-toggle "+(e?"btn-primary":"btn-default"),onClick:t},kS.default.createElement("i",{className:"fa fa-fw "+(e?"fa-check-square-o":"fa-square-o")}),"\xA0",n)}o(OS,"ToggleButton");var Fl=pe(Re()),LS=pe($h()),LO=pe(Xa()),NO=pe(yS());var Yh=class extends Fl.Component{constructor(t){super(t);this.heights={},this.state={vScroll:gp()},this.onViewportUpdate=this.onViewportUpdate.bind(this)}componentDidMount(){window.addEventListener("resize",this.onViewportUpdate),this.onViewportUpdate()}componentWillUnmount(){window.removeEventListener("resize",this.onViewportUpdate)}componentDidUpdate(){this.onViewportUpdate()}onViewportUpdate(){let t=LO.default.findDOMNode(this),n=gp({itemCount:this.props.events.length,rowHeight:this.props.rowHeight,viewportTop:t.scrollTop,viewportHeight:t.offsetHeight,itemHeights:this.props.events.map(l=>this.heights[l.id])});(0,NO.default)(this.state.vScroll,n)||this.setState({vScroll:n})}setHeight(t,n){if(n&&!this.heights[t]){let l=n.offsetHeight;this.heights[t]!==l&&(this.heights[t]=l,this.onViewportUpdate())}}render(){let{vScroll:t}=this.state,{events:n}=this.props;return Fl.default.createElement("pre",{onScroll:this.onViewportUpdate},Fl.default.createElement("div",{style:{height:t.paddingTop}}),n.slice(t.start,t.end).map(l=>Fl.default.createElement("div",{key:l.id,ref:d=>this.setHeight(l.id,d)},Fl.default.createElement(NI,{event:l}),l.message)),Fl.default.createElement("div",{style:{height:t.paddingBottom}}))}};o(Yh,"EventLogList"),Yh.propTypes={events:LS.default.array.isRequired,rowHeight:LS.default.number},Yh.defaultProps={rowHeight:18};function NI({event:e}){let t={web:"html5",debug:"bug",warn:"exclamation-triangle",error:"ban"}[e.level]||"info";return Fl.default.createElement("i",{className:`fa fa-fw fa-${t}`})}o(NI,"LogIcon");var PO=By(Yh);var Xh=class extends Il.Component{constructor(t,n){super(t,n);this.state={height:this.props.defaultHeight},this.onDragStart=this.onDragStart.bind(this),this.onDragMove=this.onDragMove.bind(this),this.onDragStop=this.onDragStop.bind(this)}onDragStart(t){t.preventDefault(),this.dragStart=this.state.height+t.pageY,window.addEventListener("mousemove",this.onDragMove),window.addEventListener("mouseup",this.onDragStop),window.addEventListener("dragend",this.onDragStop)}onDragMove(t){t.preventDefault(),this.setState({height:this.dragStart-t.pageY})}onDragStop(t){t.preventDefault(),window.removeEventListener("mousemove",this.onDragMove)}render(){let{height:t}=this.state,{filters:n,events:l,toggleFilter:d,close:v}=this.props;return Il.default.createElement("div",{className:"eventlog",style:{height:t}},Il.default.createElement("div",{onMouseDown:this.onDragStart},"Eventlog",Il.default.createElement("div",{className:"pull-right"},["debug","info","web","warn","error"].map(p=>Il.default.createElement(OS,{key:p,text:p,checked:n[p],onToggle:()=>d(p)})),Il.default.createElement("i",{onClick:v,className:"fa fa-close"}))),Il.default.createElement(PO,{events:l}))}};o(Xh,"PureEventLog"),gc(Xh,"propTypes",{filters:Cp.default.object.isRequired,events:Cp.default.array.isRequired,toggleFilter:Cp.default.func.isRequired,close:Cp.default.func.isRequired,defaultHeight:Cp.default.number}),gc(Xh,"defaultProps",{defaultHeight:200});var MO=Ai(e=>({filters:e.eventLog.filters,events:e.eventLog.view}),{close:sp,toggleFilter:ek})(Xh);var nn=pe(Re());function NS(){let e=at(P=>P.conf.version),{mode:t,intercept:n,showhost:l,upstream_cert:d,rawtcp:v,http2:p,websocket:w,anticache:_,anticomp:O,stickyauth:D,stickycookie:Y,stream_large_bodies:W,listen_host:X,listen_port:te,server:Q,ssl_insecure:R}=at(P=>P.options);return nn.createElement("footer",null,t&&t!=="regular"&&nn.createElement("span",{className:"label label-success"},t," mode"),n&&nn.createElement("span",{className:"label label-success"},"Intercept: ",n),R&&nn.createElement("span",{className:"label label-danger"},"ssl_insecure"),l&&nn.createElement("span",{className:"label label-success"},"showhost"),!d&&nn.createElement("span",{className:"label label-success"},"no-upstream-cert"),!v&&nn.createElement("span",{className:"label label-success"},"no-raw-tcp"),!p&&nn.createElement("span",{className:"label label-success"},"no-http2"),!w&&nn.createElement("span",{className:"label label-success"},"no-websocket"),_&&nn.createElement("span",{className:"label label-success"},"anticache"),O&&nn.createElement("span",{className:"label label-success"},"anticomp"),D&&nn.createElement("span",{className:"label label-success"},"stickyauth: ",D),Y&&nn.createElement("span",{className:"label label-success"},"stickycookie: ",Y),W&&nn.createElement("span",{className:"label label-success"},"stream: ",$g(W)),nn.createElement("div",{className:"pull-right"},nn.createElement(No,null,Q&&nn.createElement("span",{className:"label label-primary",title:"HTTP Proxy Server Address"},X||"*",":",te)),nn.createElement("span",{className:"label label-default",title:"Mitmproxy Version"},"mitmproxy ",e)))}o(NS,"Footer");var FS=pe(Re());var RS=pe(Re());var _p=pe(Re());function PS({children:e}){return _p.createElement("div",null,_p.createElement("div",{className:"modal-backdrop fade in"}),_p.createElement("div",{className:"modal modal-visible",id:"optionsModal",tabIndex:"-1",role:"dialog","aria-labelledby":"options"},_p.createElement("div",{className:"modal-dialog modal-lg",role:"document"},_p.createElement("div",{className:"modal-content"},e))))}o(PS,"ModalLayout");var fr=pe(Re());var Mo=pe(Re()),Ao=pe($h());var AO=pe(Xn()),PI=o(e=>{e.key!=="Escape"&&e.stopPropagation()},"stopPropagation");MS.propTypes={value:Ao.default.bool.isRequired,onChange:Ao.default.func.isRequired};function MS(l){var d=l,{value:e,onChange:t}=d,n=Fs(d,["value","onChange"]);return Mo.default.createElement("div",{className:"checkbox"},Mo.default.createElement("label",null,Mo.default.createElement("input",Le({type:"checkbox",checked:e,onChange:v=>t(v.target.checked)},n)),"Enable"))}o(MS,"BooleanOption");AS.propTypes={value:Ao.default.string,onChange:Ao.default.func.isRequired};function AS(l){var d=l,{value:e,onChange:t}=d,n=Fs(d,["value","onChange"]);return Mo.default.createElement("input",Le({type:"text",value:e||"",onChange:v=>t(v.target.value)},n))}o(AS,"StringOption");function MI(e){return function(l){var d=l,{onChange:t}=d,n=Fs(d,["onChange"]);return Mo.default.createElement(e,Le({onChange:v=>t(v||null)},n))}}o(MI,"Optional");DO.propTypes={value:Ao.default.number.isRequired,onChange:Ao.default.func.isRequired};function DO(l){var d=l,{value:e,onChange:t}=d,n=Fs(d,["value","onChange"]);return Mo.default.createElement("input",Le({type:"number",value:e,onChange:v=>t(parseInt(v.target.value))},n))}o(DO,"NumberOption");RO.propTypes={value:Ao.default.string.isRequired,onChange:Ao.default.func.isRequired};function RO(d){var v=d,{value:e,onChange:t,choices:n}=v,l=Fs(v,["value","onChange","choices"]);return Mo.default.createElement("select",Le({onChange:p=>t(p.target.value),value:e},l),n.map(p=>Mo.default.createElement("option",{key:p,value:p},p)))}o(RO,"ChoicesOption");FO.propTypes={value:Ao.default.arrayOf(Ao.default.string).isRequired,onChange:Ao.default.func.isRequired};function FO(l){var d=l,{value:e,onChange:t}=d,n=Fs(d,["value","onChange"]);let v=Math.max(e.length,1);return Mo.default.createElement("textarea",Le({rows:v,value:e.join(` +`),te}return hf(function(){_.current=e,O.current=Y,D.current=U,x.current=void 0}),hf(function(){function te(){try{var Q=n.getState(),F=_.current(Q);if(t(F,D.current))return;D.current=F,O.current=Q}catch(M){x.current=M}m()}return o(te,"checkForUpdates"),p.onStateChange=te,p.trySubscribe(),te(),function(){return p.tryUnsubscribe()}},[n,p]),U}o(QR,"useSelectorWithStoreAndSubscription");function kT(e){e===void 0&&(e=Yn);var t=e===Yn?jg:function(){return(0,Xi.useContext)(e)};return o(function(l,d){d===void 0&&(d=XR);var m=t(),p=m.store,x=m.subscription,_=QR(l,d,p,x);return(0,Xi.useDebugValue)(_),_},"useSelector")}o(kT,"createSelectorHook");var mx=kT();var vx=pe(Za());JE(vx.unstable_batchedUpdates);var Rn=pe(De());var OT="UI_FLOWVIEW_SET_TAB",LT="SET_CONTENT_VIEW_FOR",ZR={tab:"request",contentViewFor:{}};function gx(e=ZR,t){switch(t.type){case LT:return It(Pe({},e),{contentViewFor:It(Pe({},e.contentViewFor),{[t.messageId]:t.contentView})});case OT:return It(Pe({},e),{tab:t.tab?t.tab:"request"});default:return e}}o(gx,"reducer");function mf(e){return{type:OT,tab:e}}o(mf,"selectTab");function Vg(e,t){return{type:LT,messageId:e,contentView:t}}o(Vg,"setContentViewFor");var NT=pe(Oh()),JR=pe(De());window._=NT.default;window.React=JR;var Kg=o(function(e){if(e===0)return"0";for(var t=["b","kb","mb","gb","tb"],n=0;ne);n++);var l;return e%Math.pow(1024,n)==0?l=0:l=1,(e/Math.pow(1024,n)).toFixed(l)+t[n]},"formatSize"),Gg=o(function(e){for(var t=e,n=["ms","s","min","h"],l=[1e3,60,60],d=0;Math.abs(t)>=l[d]&&dOt(e,Pe({method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)},n));Ot.post=(e,t,n={})=>Ot(e,Pe({method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)},n));function vf(e,...t){return Na(this,null,function*(){return yield(yield Ot(`/commands/${e}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({arguments:t})})).json()})}o(vf,"runCommand");var Nh={};kC(Nh,{ADD:()=>_x,RECEIVE:()=>Tx,REMOVE:()=>Ex,SET_FILTER:()=>Sx,SET_SORT:()=>Cx,UPDATE:()=>bx,add:()=>rF,defaultState:()=>Yg,receive:()=>oF,reduce:()=>up,remove:()=>iF,setFilter:()=>kx,setSort:()=>MT,update:()=>nF});var xx=pe(PT()),Sx="LIST_SET_FILTER",Cx="LIST_SET_SORT",_x="LIST_ADD",bx="LIST_UPDATE",Ex="LIST_REMOVE",Tx="LIST_RECEIVE",Yg={byId:{},list:[],listIndex:{},view:[],viewIndex:{}};function up(e=Yg,t){let{byId:n,list:l,listIndex:d,view:m,viewIndex:p}=e;switch(t.type){case Sx:m=(0,xx.default)(l.filter(t.filter),t.sort),p={},m.forEach((O,D)=>{p[O.id]=D});break;case Cx:m=(0,xx.default)([...m],t.sort),p={},m.forEach((O,D)=>{p[O.id]=D});break;case _x:if(t.item.id in n)break;n=It(Pe({},n),{[t.item.id]:t.item}),d=It(Pe({},d),{[t.item.id]:l.length}),l=[...l,t.item],t.filter(t.item)&&({view:m,viewIndex:p}=AT(e,t.item,t.sort));break;case bx:n=It(Pe({},n),{[t.item.id]:t.item}),l=[...l],l[d[t.item.id]]=t.item;let x=t.item.id in p,_=t.filter(t.item);_&&!x?{view:m,viewIndex:p}=AT(e,t.item,t.sort):!_&&x?{data:m,dataIndex:p}=Ox(m,p,t.item.id):_&&x&&({view:m,viewIndex:p}=sF(e,t.item,t.sort));break;case Ex:if(!(t.id in n))break;n=Pe({},n),delete n[t.id],{data:l,dataIndex:d}=Ox(l,d,t.id),t.id in p&&({data:m,dataIndex:p}=Ox(m,p,t.id));break;case Tx:l=t.list,d={},n={},l.forEach((O,D)=>{n[O.id]=O,d[O.id]=D}),m=l.filter(t.filter).sort(t.sort),p={},m.forEach((O,D)=>{p[O.id]=D});break}return{byId:n,list:l,listIndex:d,view:m,viewIndex:p}}o(up,"reduce");function kx(e=Xg,t=Lh){return{type:Sx,filter:e,sort:t}}o(kx,"setFilter");function MT(e=Lh){return{type:Cx,sort:e}}o(MT,"setSort");function rF(e,t=Xg,n=Lh){return{type:_x,item:e,filter:t,sort:n}}o(rF,"add");function nF(e,t=Xg,n=Lh){return{type:bx,item:e,filter:t,sort:n}}o(nF,"update");function iF(e){return{type:Ex,id:e}}o(iF,"remove");function oF(e,t=Xg,n=Lh){return{type:Tx,list:e,filter:t,sort:n}}o(oF,"receive");function AT(e,t,n){let l=lF(e.view,t,n),d=[...e.view],m=Pe({},e.viewIndex);d.splice(l,0,t);for(let p=d.length-1;p>=l;p--)m[d[p].id]=p;return{view:d,viewIndex:m}}o(AT,"sortedInsert");function Ox(e,t,n){let l=t[n],d=[...e],m=Pe({},t);delete m[n],d.splice(l,1);for(let p=d.length-1;p>=l;p--)m[d[p].id]=p;return{data:d,dataIndex:m}}o(Ox,"removeData");function sF(e,t,n){let l=[...e.view],d=Pe({},e.viewIndex),m=d[t.id];for(l[m]=t;m+10;)l[m]=l[m+1],l[m+1]=t,d[t.id]=m+1,d[l[m].id]=m,++m;for(;m>0&&n(l[m],l[m-1])<0;)l[m]=l[m-1],l[m-1]=t,d[t.id]=m-1,d[l[m].id]=m,--m;return{view:l,viewIndex:d}}o(sF,"sortedUpdate");function lF(e,t,n){let l=0,d=e.length;for(;l>>1;n(t,e[m])>=0?l=m+1:d=m}return l}o(lF,"sortedIndex");function Xg(){return!0}o(Xg,"defaultFilter");function Lh(e,t){return 0}o(Lh,"defaultSort");var DT={http:80,https:443},Ur=class{static getContentType(t){var n=Ur.get_first_header(t,/^Content-Type$/i);if(n)return n.split(";")[0].trim()}static get_first_header(t,n){let l=t;l._headerLookups||Object.defineProperty(l,"_headerLookups",{value:{},configurable:!1,enumerable:!1,writable:!1});let d=n.toString();if(!(d in l._headerLookups)){let m;for(let p=0;p{var t,n;switch(e.type){case"http":let l=e.request.contentLength||0;return e.response&&(l+=e.response.contentLength||0),e.websocket&&(l+=e.websocket.messages_meta.contentLength||0),l;case"tcp":return e.messages_meta.contentLength||0;case"dns":return(n=(t=e.response)==null?void 0:t.size)!=null?n:0}},"getTotalSize"),Qg=o(e=>e.type==="http"&&!e.websocket,"canReplay");var gf=function(){"use strict";function e(l,d){function m(){this.constructor=l}o(m,"ctor"),m.prototype=d.prototype,l.prototype=new m}o(e,"peg$subclass");function t(l,d,m,p){this.message=l,this.expected=d,this.found=m,this.location=p,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,t)}o(t,"peg$SyntaxError"),e(t,Error);function n(l){var d=arguments.length>1?arguments[1]:{},m=this,p={},x={start:fs},_=fs,O={type:"other",description:"filter expression"},D=o(function(y){return y},"peg$c1"),Y={type:"other",description:"whitespace"},U=/^[ \t\n\r]/,X={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},te={type:"other",description:"control character"},Q=/^[|&!()~"]/,F={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},M={type:"other",description:"optional whitespace"},R="|",K={type:"literal",value:"|",description:'"|"'},V=o(function(y,T){return ds(y,T)},"peg$c11"),ue="&",ie={type:"literal",value:"&",description:'"&"'},de=o(function(y,T){return Ct(y,T)},"peg$c14"),ge="!",xe={type:"literal",value:"!",description:'"!"'},qe=o(function(y){return io(y)},"peg$c17"),et="(",Te={type:"literal",value:"(",description:'"("'},xt=")",Ue={type:"literal",value:")",description:'")"'},Ve=o(function(y){return _u(y)},"peg$c22"),Ke="~all",Ye={type:"literal",value:"~all",description:'"~all"'},Qt=o(function(){return zf},"peg$c25"),ft="~a",Ar={type:"literal",value:"~a",description:'"~a"'},Kt=o(function(){return sr},"peg$c28"),Et="~b",St={type:"literal",value:"~b",description:'"~b"'},at=o(function(y){return qp(y)},"peg$c31"),_r="~bq",Ut={type:"literal",value:"~bq",description:'"~bq"'},$t=o(function(y){return jf(y)},"peg$c34"),ne="~bs",tt={type:"literal",value:"~bs",description:'"~bs"'},br=o(function(y){return hs(y)},"peg$c37"),jt="~c",qt={type:"literal",value:"~c",description:'"~c"'},Se=o(function(y){return Zl(y)},"peg$c40"),Er="~comment",nn={type:"literal",value:"~comment",description:'"~comment"'},Fn=o(function(y){return Uo(y)},"peg$c43"),ei="~d",on={type:"literal",value:"~d",description:'"~d"'},Gt=o(function(y){return Vp(y)},"peg$c46"),dr="~dns",ct={type:"literal",value:"~dns",description:'"~dns"'},Do=o(function(){return ol},"peg$c49"),vr="~dst",Fi={type:"literal",value:"~dst",description:'"~dst"'},sn=o(function(y){return bu(y)},"peg$c52"),In="~e",vi={type:"literal",value:"~e",description:'"~e"'},gi=o(function(){return Jl},"peg$c55"),Hn="~h",En={type:"literal",value:"~h",description:'"~h"'},Ks=o(function(y){return Eu(y)},"peg$c58"),H="~hq",J={type:"literal",value:"~hq",description:'"~hq"'},he=o(function(y){return Tu(y)},"peg$c61"),ke="~hs",Zt={type:"literal",value:"~hs",description:'"~hs"'},Bl=o(function(y){return qf(y)},"peg$c64"),Rt="~http",Dr={type:"literal",value:"~http",description:'"~http"'},Jt=o(function(){return ea},"peg$c67"),ti="~marked",os={type:"literal",value:"~marked",description:'"~marked"'},to=o(function(){return so},"peg$c70"),yi="~marker",Gs={type:"literal",value:"~marker",description:'"~marker"'},ss=o(function(y){return Wi(y)},"peg$c73"),ln="~m",Ap={type:"literal",value:"~m",description:'"~m"'},Ys=o(function(y){return sl(y)},"peg$c76"),Pf="~q",Xs={type:"literal",value:"~q",description:'"~q"'},Dp=o(function(){return Rr},"peg$c79"),Mf="~replayq",uu={type:"literal",value:"~replayq",description:'"~replayq"'},Rp=o(function(){return ll},"peg$c82"),Ul="~replays",ls={type:"literal",value:"~replays",description:'"~replays"'},Fp=o(function(){return Vr},"peg$c85"),Af="~replay",Qs={type:"literal",value:"~replay",description:'"~replay"'},fu=o(function(){return Bi},"peg$c88"),Ro="~src",Ip={type:"literal",value:"~src",description:'"~src"'},Fo=o(function(y){return ta(y)},"peg$c91"),$l="~s",Df={type:"literal",value:"~s",description:'"~s"'},zt=o(function(){return Vf},"peg$c94"),Me="~tcp",wi={type:"literal",value:"~tcp",description:'"~tcp"'},cu=o(function(){return ku},"peg$c97"),ri="~tq",vt={type:"literal",value:"~tq",description:'"~tq"'},ro=o(function(y){return Kp(y)},"peg$c100"),Io="~ts",zl={type:"literal",value:"~ts",description:'"~ts"'},ae=o(function(y){return Kf(y)},"peg$c103"),$e="~t",pu={type:"literal",value:"~t",description:'"~t"'},du=o(function(y){return Ou(y)},"peg$c106"),as="~u",Zs={type:"literal",value:"~u",description:'"~u"'},jl=o(function(y){return ni(y)},"peg$c109"),Be="~websocket",Hp={type:"literal",value:"~websocket",description:'"~websocket"'},hu=o(function(){return ii},"peg$c112"),Ho={type:"other",description:"integer"},Wn=/^['"]/,mu={type:"class",value:`['"]`,description:`['"]`},ql=/^[0-9]/,Wo={type:"class",value:"[0-9]",description:"[0-9]"},Js=o(function(y){return parseInt(y.join(""),10)},"peg$c118"),Rf={type:"other",description:"string"},el='"',tl={type:"literal",value:'"',description:'"\\""'},us=o(function(y){return y.join("")},"peg$c122"),no="'",Vl={type:"literal",value:"'",description:`"'"`},Ff=/^["\\]/,Wp={type:"class",value:'["\\\\]',description:'["\\\\]'},rl={type:"any",description:"any character"},an=o(function(y){return y},"peg$c128"),vu="\\",gu={type:"literal",value:"\\",description:'"\\\\"'},Kl=/^['\\]/,nl={type:"class",value:"['\\\\]",description:"['\\\\]"},Bp=/^['"\\]/,If={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},Up="n",$p={type:"literal",value:"n",description:'"n"'},yu=o(function(){return` +`},"peg$c137"),Hf="r",wu={type:"literal",value:"r",description:'"r"'},Wf=o(function(){return"\r"},"peg$c140"),Bf="t",Gl={type:"literal",value:"t",description:'"t"'},Yl=o(function(){return" "},"peg$c143"),N=0,Ce=0,gt=[{line:1,column:1,seenCR:!1}],Tn=0,xu=[],ye=0,kn;if("startRule"in d){if(!(d.startRule in x))throw new Error(`Can't start parsing from rule "`+d.startRule+'".');_=x[d.startRule]}function am(){return l.substring(Ce,N)}o(am,"text");function um(){return Ii(Ce,N)}o(um,"location");function Su(y){throw Bo(null,[{type:"other",description:y}],l.substring(Ce,N),Ii(Ce,N))}o(Su,"expected");function zp(y){throw Bo(y,null,l.substring(Ce,N),Ii(Ce,N))}o(zp,"error");function Nt(y){var T=gt[y],B,W;if(T)return T;for(B=y-1;!gt[B];)B--;for(T=gt[B],T={line:T.line,column:T.column,seenCR:T.seenCR};BTn&&(Tn=N,xu=[]),xu.push(y))}o(_e,"peg$fail");function Bo(y,T,B,W){function Fr(Bn){var Un=1;for(Bn.sort(function(Si,gr){return Si.descriptiongr.description?1:0});Un1?gr.slice(0,-1).join(", ")+" or "+gr[Bn.length-1]:gr[0],ul=Un?'"'+Si(Un)+'"':"end of input","Expected "+al+" but "+ul+" found."}return o(un,"buildMessage"),T!==null&&Fr(T),new t(y!==null?y:un(T,B),T,B,W)}o(Bo,"peg$buildException");function fs(){var y,T,B,W;return ye++,y=N,T=xi(),T!==p?(B=Xl(),B!==p?(W=xi(),W!==p?(Ce=y,T=D(B),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),ye--,y===p&&(T=p,ye===0&&_e(O)),y}o(fs,"peg$parsestart");function He(){var y,T;return ye++,U.test(l.charAt(N))?(y=l.charAt(N),N++):(y=p,ye===0&&_e(X)),ye--,y===p&&(T=p,ye===0&&_e(Y)),y}o(He,"peg$parsews");function Uf(){var y,T;return ye++,Q.test(l.charAt(N))?(y=l.charAt(N),N++):(y=p,ye===0&&_e(F)),ye--,y===p&&(T=p,ye===0&&_e(te)),y}o(Uf,"peg$parsecc");function xi(){var y,T;for(ye++,y=[],T=He();T!==p;)y.push(T),T=He();return ye--,y===p&&(T=p,ye===0&&_e(M)),y}o(xi,"peg$parse__");function Xl(){var y,T,B,W,Fr,un;return y=N,T=il(),T!==p?(B=xi(),B!==p?(l.charCodeAt(N)===124?(W=R,N++):(W=p,ye===0&&_e(K)),W!==p?(Fr=xi(),Fr!==p?(un=Xl(),un!==p?(Ce=y,T=V(T,un),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),y===p&&(y=il()),y}o(Xl,"peg$parseOrExpr");function il(){var y,T,B,W,Fr,un;if(y=N,T=cs(),T!==p?(B=xi(),B!==p?(l.charCodeAt(N)===38?(W=ue,N++):(W=p,ye===0&&_e(ie)),W!==p?(Fr=xi(),Fr!==p?(un=il(),un!==p?(Ce=y,T=de(T,un),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),y===p){if(y=N,T=cs(),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=il(),W!==p?(Ce=y,T=de(T,W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;y===p&&(y=cs())}return y}o(il,"peg$parseAndExpr");function cs(){var y,T,B,W;return y=N,l.charCodeAt(N)===33?(T=ge,N++):(T=p,ye===0&&_e(xe)),T!==p?(B=xi(),B!==p?(W=cs(),W!==p?(Ce=y,T=qe(W),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),y===p&&(y=Cu()),y}o(cs,"peg$parseNotExpr");function Cu(){var y,T,B,W,Fr,un;return y=N,l.charCodeAt(N)===40?(T=et,N++):(T=p,ye===0&&_e(Te)),T!==p?(B=xi(),B!==p?(W=Xl(),W!==p?(Fr=xi(),Fr!==p?(l.charCodeAt(N)===41?(un=xt,N++):(un=p,ye===0&&_e(Ue)),un!==p?(Ce=y,T=Ve(W),y=T):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p)):(N=y,y=p),y===p&&(y=On()),y}o(Cu,"peg$parseBindingExpr");function On(){var y,T,B,W;if(y=N,l.substr(N,4)===Ke?(T=Ke,N+=4):(T=p,ye===0&&_e(Ye)),T!==p&&(Ce=y,T=Qt()),y=T,y===p&&(y=N,l.substr(N,2)===ft?(T=ft,N+=2):(T=p,ye===0&&_e(Ar)),T!==p&&(Ce=y,T=Kt()),y=T,y===p)){if(y=N,l.substr(N,2)===Et?(T=Et,N+=2):(T=p,ye===0&&_e(St)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=at(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===_r?(T=_r,N+=3):(T=p,ye===0&&_e(Ut)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=$t(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===ne?(T=ne,N+=3):(T=p,ye===0&&_e(tt)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=br(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===jt?(T=jt,N+=2):(T=p,ye===0&&_e(qt)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=jp(),W!==p?(Ce=y,T=Se(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,8)===Er?(T=Er,N+=8):(T=p,ye===0&&_e(nn)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Fn(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===ei?(T=ei,N+=2):(T=p,ye===0&&_e(on)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Gt(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,4)===dr?(T=dr,N+=4):(T=p,ye===0&&_e(ct)),T!==p&&(Ce=y,T=Do()),y=T,y===p)){if(y=N,l.substr(N,4)===vr?(T=vr,N+=4):(T=p,ye===0&&_e(Fi)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=sn(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,2)===In?(T=In,N+=2):(T=p,ye===0&&_e(vi)),T!==p&&(Ce=y,T=gi()),y=T,y===p)){if(y=N,l.substr(N,2)===Hn?(T=Hn,N+=2):(T=p,ye===0&&_e(En)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Ks(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===H?(T=H,N+=3):(T=p,ye===0&&_e(J)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=he(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===ke?(T=ke,N+=3):(T=p,ye===0&&_e(Zt)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Bl(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,5)===Rt?(T=Rt,N+=5):(T=p,ye===0&&_e(Dr)),T!==p&&(Ce=y,T=Jt()),y=T,y===p&&(y=N,l.substr(N,7)===ti?(T=ti,N+=7):(T=p,ye===0&&_e(os)),T!==p&&(Ce=y,T=to()),y=T,y===p))){if(y=N,l.substr(N,7)===yi?(T=yi,N+=7):(T=p,ye===0&&_e(Gs)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=ss(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===ln?(T=ln,N+=2):(T=p,ye===0&&_e(Ap)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Ys(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,2)===Pf?(T=Pf,N+=2):(T=p,ye===0&&_e(Xs)),T!==p&&(Ce=y,T=Dp()),y=T,y===p&&(y=N,l.substr(N,8)===Mf?(T=Mf,N+=8):(T=p,ye===0&&_e(uu)),T!==p&&(Ce=y,T=Rp()),y=T,y===p&&(y=N,l.substr(N,8)===Ul?(T=Ul,N+=8):(T=p,ye===0&&_e(ls)),T!==p&&(Ce=y,T=Fp()),y=T,y===p&&(y=N,l.substr(N,7)===Af?(T=Af,N+=7):(T=p,ye===0&&_e(Qs)),T!==p&&(Ce=y,T=fu()),y=T,y===p))))){if(y=N,l.substr(N,4)===Ro?(T=Ro,N+=4):(T=p,ye===0&&_e(Ip)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=Fo(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p&&(y=N,l.substr(N,2)===$l?(T=$l,N+=2):(T=p,ye===0&&_e(Df)),T!==p&&(Ce=y,T=zt()),y=T,y===p&&(y=N,l.substr(N,4)===Me?(T=Me,N+=4):(T=p,ye===0&&_e(wi)),T!==p&&(Ce=y,T=cu()),y=T,y===p))){if(y=N,l.substr(N,3)===ri?(T=ri,N+=3):(T=p,ye===0&&_e(vt)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=ro(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,3)===Io?(T=Io,N+=3):(T=p,ye===0&&_e(zl)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=ae(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===$e?(T=$e,N+=2):(T=p,ye===0&&_e(pu)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=du(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.substr(N,2)===as?(T=as,N+=2):(T=p,ye===0&&_e(Zs)),T!==p){if(B=[],W=He(),W!==p)for(;W!==p;)B.push(W),W=He();else B=p;B!==p?(W=Tt(),W!==p?(Ce=y,T=jl(W),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;y===p&&(y=N,l.substr(N,10)===Be?(T=Be,N+=10):(T=p,ye===0&&_e(Hp)),T!==p&&(Ce=y,T=hu()),y=T,y===p&&(y=N,T=Tt(),T!==p&&(Ce=y,T=jl(T)),y=T))}}}}}}}}}}}}}}}}}return y}o(On,"peg$parseExpr");function jp(){var y,T,B,W;if(ye++,y=N,Wn.test(l.charAt(N))?(T=l.charAt(N),N++):(T=p,ye===0&&_e(mu)),T===p&&(T=null),T!==p){if(B=[],ql.test(l.charAt(N))?(W=l.charAt(N),N++):(W=p,ye===0&&_e(Wo)),W!==p)for(;W!==p;)B.push(W),ql.test(l.charAt(N))?(W=l.charAt(N),N++):(W=p,ye===0&&_e(Wo));else B=p;B!==p?(Wn.test(l.charAt(N))?(W=l.charAt(N),N++):(W=p,ye===0&&_e(mu)),W===p&&(W=null),W!==p?(Ce=y,T=Js(B),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;return ye--,y===p&&(T=p,ye===0&&_e(Ho)),y}o(jp,"peg$parseIntegerLiteral");function Tt(){var y,T,B,W;if(ye++,y=N,l.charCodeAt(N)===34?(T=el,N++):(T=p,ye===0&&_e(tl)),T!==p){for(B=[],W=$f();W!==p;)B.push(W),W=$f();B!==p?(l.charCodeAt(N)===34?(W=el,N++):(W=p,ye===0&&_e(tl)),W!==p?(Ce=y,T=us(B),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p){if(y=N,l.charCodeAt(N)===39?(T=no,N++):(T=p,ye===0&&_e(Vl)),T!==p){for(B=[],W=Ql();W!==p;)B.push(W),W=Ql();B!==p?(l.charCodeAt(N)===39?(W=no,N++):(W=p,ye===0&&_e(Vl)),W!==p?(Ce=y,T=us(B),y=T):(N=y,y=p)):(N=y,y=p)}else N=y,y=p;if(y===p)if(y=N,T=N,ye++,B=Uf(),ye--,B===p?T=void 0:(N=T,T=p),T!==p){if(B=[],W=Hi(),W!==p)for(;W!==p;)B.push(W),W=Hi();else B=p;B!==p?(Ce=y,T=us(B),y=T):(N=y,y=p)}else N=y,y=p}return ye--,y===p&&(T=p,ye===0&&_e(Rf)),y}o(Tt,"peg$parseStringLiteral");function $f(){var y,T,B;return y=N,T=N,ye++,Ff.test(l.charAt(N))?(B=l.charAt(N),N++):(B=p,ye===0&&_e(Wp)),ye--,B===p?T=void 0:(N=T,T=p),T!==p?(l.length>N?(B=l.charAt(N),N++):(B=p,ye===0&&_e(rl)),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p),y===p&&(y=N,l.charCodeAt(N)===92?(T=vu,N++):(T=p,ye===0&&_e(gu)),T!==p?(B=ps(),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p)),y}o($f,"peg$parseDoubleStringChar");function Ql(){var y,T,B;return y=N,T=N,ye++,Kl.test(l.charAt(N))?(B=l.charAt(N),N++):(B=p,ye===0&&_e(nl)),ye--,B===p?T=void 0:(N=T,T=p),T!==p?(l.length>N?(B=l.charAt(N),N++):(B=p,ye===0&&_e(rl)),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p),y===p&&(y=N,l.charCodeAt(N)===92?(T=vu,N++):(T=p,ye===0&&_e(gu)),T!==p?(B=ps(),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p)),y}o(Ql,"peg$parseSingleStringChar");function Hi(){var y,T,B;return y=N,T=N,ye++,B=He(),ye--,B===p?T=void 0:(N=T,T=p),T!==p?(l.length>N?(B=l.charAt(N),N++):(B=p,ye===0&&_e(rl)),B!==p?(Ce=y,T=an(B),y=T):(N=y,y=p)):(N=y,y=p),y}o(Hi,"peg$parseUnquotedStringChar");function ps(){var y,T;return Bp.test(l.charAt(N))?(y=l.charAt(N),N++):(y=p,ye===0&&_e(If)),y===p&&(y=N,l.charCodeAt(N)===110?(T=Up,N++):(T=p,ye===0&&_e($p)),T!==p&&(Ce=y,T=yu()),y=T,y===p&&(y=N,l.charCodeAt(N)===114?(T=Hf,N++):(T=p,ye===0&&_e(wu)),T!==p&&(Ce=y,T=Wf()),y=T,y===p&&(y=N,l.charCodeAt(N)===116?(T=Bf,N++):(T=p,ye===0&&_e(Gl)),T!==p&&(Ce=y,T=Yl()),y=T))),y}o(ps,"peg$parseEscapeSequence");function ds(y,T){function B(){return y.apply(this,arguments)||T.apply(this,arguments)}return o(B,"orFilter"),B.desc=y.desc+" or "+T.desc,B}o(ds,"or");function Ct(y,T){function B(){return y.apply(this,arguments)&&T.apply(this,arguments)}return o(B,"andFilter"),B.desc=y.desc+" and "+T.desc,B}o(Ct,"and");function io(y){function T(){return!y.apply(this,arguments)}return o(T,"notFilter"),T.desc="not "+y.desc,T}o(io,"not");function _u(y){function T(){return y.apply(this,arguments)}return o(T,"bindingFilter"),T.desc="("+y.desc+")",T}o(_u,"binding");function zf(y){return!0}o(zf,"allFilter"),zf.desc="all flows";var oo=[new RegExp("text/javascript"),new RegExp("application/x-javascript"),new RegExp("application/javascript"),new RegExp("text/css"),new RegExp("image/.*"),new RegExp("font/.*"),new RegExp("application/font.*")];function sr(y){if(y.response){for(var T=zs.getContentType(y.response),B=oo.length;B--;)if(oo[B].test(T))return!0}return!1}o(sr,"assetFilter"),sr.desc="is asset";function qp(y){y=new RegExp(y,"i");function T(B){return!0}return o(T,"bodyFilter"),T.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",T}o(qp,"body");function jf(y){y=new RegExp(y,"i");function T(B){return!0}return o(T,"requestBodyFilter"),T.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",T}o(jf,"requestBody");function hs(y){y=new RegExp(y,"i");function T(B){return!0}return o(T,"responseBodyFilter"),T.desc="body filters are not implemented yet, see https://github.com/mitmproxy/mitmweb/issues/10",T}o(hs,"responseBody");function Zl(y){function T(B){return B.response&&B.response.status_code===y}return o(T,"responseCodeFilter"),T.desc="resp. code is "+y,T}o(Zl,"responseCode");function Uo(y){y=new RegExp(y,"i");function T(B){return y.test(B.comment)}return o(T,"commentFilter"),T.desc="comment matches "+y,T}o(Uo,"comment");function Vp(y){y=new RegExp(y,"i");function T(B){return B.request&&(y.test(B.request.host)||y.test(B.request.pretty_host))}return o(T,"domainFilter"),T.desc="domain matches "+y,T}o(Vp,"domain");function ol(y){return y.type==="dns"}o(ol,"dnsFilter"),ol.desc="is a DNS Flow";function bu(y){y=new RegExp(y,"i");function T(B){return!!B.server_conn.address&&y.test(B.server_conn.address[0]+":"+B.server_conn.address[1])}return o(T,"destinationFilter"),T.desc="destination address matches "+y,T}o(bu,"destination");function Jl(y){return!!y.error}o(Jl,"errorFilter"),Jl.desc="has error";function Eu(y){y=new RegExp(y,"i");function T(B){return B.request&&ko.match_header(B.request,y)||B.response&&zs.match_header(B.response,y)}return o(T,"headerFilter"),T.desc="header matches "+y,T}o(Eu,"header");function Tu(y){y=new RegExp(y,"i");function T(B){return B.request&&ko.match_header(B.request,y)}return o(T,"requestHeaderFilter"),T.desc="req. header matches "+y,T}o(Tu,"requestHeader");function qf(y){y=new RegExp(y,"i");function T(B){return B.response&&zs.match_header(B.response,y)}return o(T,"responseHeaderFilter"),T.desc="resp. header matches "+y,T}o(qf,"responseHeader");function ea(y){return y.type==="http"}o(ea,"httpFilter"),ea.desc="is an HTTP Flow";function so(y){return y.marked}o(so,"markedFilter"),so.desc="is marked";function Wi(y){y=new RegExp(y,"i");function T(B){return y.test(B.marked)}return o(T,"markerFilter"),T.desc="marker matches "+y,T}o(Wi,"marker");function sl(y){y=new RegExp(y,"i");function T(B){return B.request&&y.test(B.request.method)}return o(T,"methodFilter"),T.desc="method matches "+y,T}o(sl,"method");function Rr(y){return y.request&&!y.response}o(Rr,"noResponseFilter"),Rr.desc="has no response";function ll(y){return y.is_replay==="request"}o(ll,"clientReplayFilter"),ll.desc="request has been replayed";function Vr(y){return y.is_replay==="response"}o(Vr,"serverReplayFilter"),Vr.desc="response has been replayed";function Bi(y){return!!y.is_replay}o(Bi,"replayFilter"),Bi.desc="flow has been replayed";function ta(y){y=new RegExp(y,"i");function T(B){return!!B.client_conn.peername&&y.test(B.client_conn.peername[0]+":"+B.client_conn.peername[1])}return o(T,"sourceFilter"),T.desc="source address matches "+y,T}o(ta,"source");function Vf(y){return!!y.response}o(Vf,"responseFilter"),Vf.desc="has response";function ku(y){return y.type==="tcp"}o(ku,"tcpFilter"),ku.desc="is a TCP Flow";function Kp(y){y=new RegExp(y,"i");function T(B){return B.request&&y.test(ko.getContentType(B.request))}return o(T,"requestContentTypeFilter"),T.desc="req. content type matches "+y,T}o(Kp,"requestContentType");function Kf(y){y=new RegExp(y,"i");function T(B){return B.response&&y.test(zs.getContentType(B.response))}return o(T,"responseContentTypeFilter"),T.desc="resp. content type matches "+y,T}o(Kf,"responseContentType");function Ou(y){y=new RegExp(y,"i");function T(B){return B.request&&y.test(ko.getContentType(B.request))||B.response&&y.test(zs.getContentType(B.response))}return o(T,"contentTypeFilter"),T.desc="content type matches "+y,T}o(Ou,"contentType");function ni(y){y=new RegExp(y,"i");function T(B){var W;if(B.type==="dns"){let Fr=(W=B.request)==null?void 0:W.questions[0];return Fr&&y.test(Fr.name)}return B.request&&y.test(ko.pretty_url(B.request))}return o(T,"urlFilter"),T.desc="url matches "+y,T}o(ni,"url");function ii(y){return!!y.websocket}if(o(ii,"websocketFilter"),ii.desc="is a Websocket Flow",kn=_(),kn!==p&&N===l.length)return kn;throw kn!==p&&NAx,icon:()=>ty,method:()=>Mh,path:()=>ry,quickactions:()=>fp,size:()=>ny,status:()=>Ah,time:()=>iy,timestamp:()=>oy,tls:()=>ey});var cr=pe(De());var Jg=pe(Xn());var ey=o(({flow:e})=>cr.default.createElement("td",{className:(0,Jg.default)("col-tls",e.client_conn.tls_established?"col-tls-https":"col-tls-http")}),"tls");ey.headerName="";ey.sortKey=e=>e.type==="http"&&e.request.scheme;var ty=o(({flow:e})=>cr.default.createElement("td",{className:"col-icon"},cr.default.createElement("div",{className:(0,Jg.default)("resource-icon",RT(e))})),"icon");ty.headerName="";ty.sortKey=e=>RT(e);var RT=o(e=>{if(e.type==="tcp"||e.type==="dns")return`resource-icon-${e.type}`;if(e.websocket)return"resource-icon-websocket";if(!e.response)return"resource-icon-plain";var t=zs.getContentType(e.response)||"";return e.response.status_code===304?"resource-icon-not-modified":300<=e.response.status_code&&e.response.status_code<400?"resource-icon-redirect":t.indexOf("image")>=0?"resource-icon-image":t.indexOf("javascript")>=0?"resource-icon-js":t.indexOf("css")>=0?"resource-icon-css":t.indexOf("html")>=0?"resource-icon-document":"resource-icon-plain"},"getIcon"),FT=o(e=>{var t,n,l,d;switch(e.type){case"http":return ko.pretty_url(e.request);case"tcp":return`${e.client_conn.peername.join(":")} \u2194 ${(n=(t=e.server_conn)==null?void 0:t.address)==null?void 0:n.join(":")}`;case"dns":return`${e.request.questions.map(m=>`${m.name} ${m.type}`).join(", ")} = ${((d=(l=e.response)==null?void 0:l.answers.map(m=>m.data).join(", "))!=null?d:"...")||"?"}`}},"mainPath"),ry=o(({flow:e})=>{let t;return e.error&&(e.error.msg==="Connection killed."?t=cr.default.createElement("i",{className:"fa fa-fw fa-times pull-right"}):t=cr.default.createElement("i",{className:"fa fa-fw fa-exclamation pull-right"})),cr.default.createElement("td",{className:"col-path"},e.is_replay==="request"&&cr.default.createElement("i",{className:"fa fa-fw fa-repeat pull-right"}),e.intercepted&&cr.default.createElement("i",{className:"fa fa-fw fa-pause pull-right"}),t,cr.default.createElement("span",{className:"marker pull-right"},e.marked),FT(e))},"path");ry.headerName="Path";ry.sortKey=e=>FT(e);var Mh=o(({flow:e})=>cr.default.createElement("td",{className:"col-method"},Mh.sortKey(e)),"method");Mh.headerName="Method";Mh.sortKey=e=>{switch(e.type){case"http":return e.websocket?e.client_conn.tls_established?"WSS":"WS":e.request.method;case"dns":return e.request.op_code;default:return e.type.toUpperCase()}};var Ah=o(({flow:e})=>{let t="darkred";return e.type!=="http"&&e.type!="dns"||!e.response?cr.default.createElement("td",{className:"col-status"}):(100<=e.response.status_code&&e.response.status_code<200?t="green":200<=e.response.status_code&&e.response.status_code<300?t="darkgreen":300<=e.response.status_code&&e.response.status_code<400?t="lightblue":(400<=e.response.status_code&&e.response.status_code<500||500<=e.response.status_code&&e.response.status_code<600)&&(t="red"),cr.default.createElement("td",{className:"col-status",style:{color:t}},Ah.sortKey(e)))},"status");Ah.headerName="Status";Ah.sortKey=e=>{var t,n;switch(e.type){case"http":return(t=e.response)==null?void 0:t.status_code;case"dns":return(n=e.response)==null?void 0:n.response_code;default:return}};var ny=o(({flow:e})=>cr.default.createElement("td",{className:"col-size"},Kg(Mx(e))),"size");ny.headerName="Size";ny.sortKey=e=>Mx(e);var iy=o(({flow:e})=>{let t=Ph(e),n=Px(e);return cr.default.createElement("td",{className:"col-time"},t&&n?Gg(1e3*(n-t)):"...")},"time");iy.headerName="Time";iy.sortKey=e=>{let t=Ph(e),n=Px(e);return t&&n&&n-t};var oy=o(({flow:e})=>{let t=Ph(e);return cr.default.createElement("td",{className:"col-timestamp"},t?Qi(t):"...")},"timestamp");oy.headerName="Start time";oy.sortKey=e=>Ph(e);var fp=o(({flow:e})=>{let t=$s(),[n,l]=(0,cr.useState)(!1),d=null;return e.intercepted?d=cr.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(cp(e))},cr.default.createElement("i",{className:"fa fa-fw fa-play text-success"})):Qg(e)&&(d=cr.default.createElement("a",{href:"#",className:"quickaction",onClick:()=>t(pp(e))},cr.default.createElement("i",{className:"fa fa-fw fa-repeat text-primary"}))),cr.default.createElement("td",{className:(0,Jg.default)("col-quickactions",{hover:n}),onClick:()=>0},cr.default.createElement("div",null,d))},"quickactions");fp.headerName="";fp.sortKey=e=>0;var Ax={icon:ty,method:Mh,path:ry,quickactions:fp,size:ny,status:Ah,time:iy,timestamp:oy,tls:ey};var fF="FLOWS_ADD",cF="FLOWS_UPDATE",IT="FLOWS_REMOVE",pF="FLOWS_RECEIVE",HT="FLOWS_SELECT",WT="FLOWS_SET_FILTER",BT="FLOWS_SET_SORT",UT="FLOWS_SET_HIGHLIGHT",dF="FLOWS_REQUEST_ACTION",hF=Pe({highlight:void 0,filter:void 0,sort:{column:void 0,desc:!1},selected:[]},Yg);function Dx(e=hF,t){switch(t.type){case fF:case cF:case IT:case pF:let n=Nh[t.cmd](t.data,$T(e.filter),Rx(e.sort)),l=e.selected;if(t.type===IT&&e.selected.includes(t.data)){if(e.selected.length>1)l=l.filter(d=>d!==t.data);else if(l=[],t.data in e.viewIndex&&e.view.length>1){let d=e.viewIndex[t.data],m;d===e.view.length-1?m=e.view[d-1]:m=e.view[d+1],l.push(m.id)}}return Pe(It(Pe({},e),{selected:l}),up(e,n));case WT:return Pe(It(Pe({},e),{filter:t.filter}),up(e,kx($T(t.filter),Rx(e.sort))));case UT:return It(Pe({},e),{highlight:t.highlight});case BT:return Pe(It(Pe({},e),{sort:t.sort}),up(e,MT(Rx(t.sort))));case HT:return It(Pe({},e),{selected:t.flowIds});default:return e}}o(Dx,"reducer");function $T(e){if(!!e)return gf.parse(e)}o($T,"makeFilter");function Rx({column:e,desc:t}){if(!e)return(l,d)=>0;let n=Ax[e].sortKey;return(l,d)=>{let m=n(l),p=n(d);return m>p?t?-1:1:mOt(`/flows/${e.id}/resume`,{method:"POST"})}o(cp,"resume");function ay(){return e=>Ot("/flows/resume",{method:"POST"})}o(ay,"resumeAll");function uy(e){return t=>Ot(`/flows/${e.id}/kill`,{method:"POST"})}o(uy,"kill");function jT(){return e=>Ot("/flows/kill",{method:"POST"})}o(jT,"killAll");function fy(e){return t=>Ot(`/flows/${e.id}`,{method:"DELETE"})}o(fy,"remove");function cy(e){return t=>Ot(`/flows/${e.id}/duplicate`,{method:"POST"})}o(cy,"duplicate");function pp(e){return t=>Ot(`/flows/${e.id}/replay`,{method:"POST"})}o(pp,"replay");function py(e){return t=>Ot(`/flows/${e.id}/revert`,{method:"POST"})}o(py,"revert");function Ri(e,t){return n=>Ot.put(`/flows/${e.id}`,t)}o(Ri,"update");function qT(e,t,n){let l=new FormData;return t=new window.Blob([t],{type:"plain/text"}),l.append("file",t),d=>Ot(`/flows/${e.id}/${n}/content.data`,{method:"POST",body:l})}o(qT,"uploadContent");function dy(){return e=>Ot("/clear",{method:"POST"})}o(dy,"clear");function VT(){return window.location.href="/flows/dump",{type:dF}}o(VT,"download");function KT(e){let t=new FormData;return t.append("file",e),n=>Ot("/flows/dump",{method:"POST",body:t})}o(KT,"upload");function wf(e){return{type:HT,flowIds:e?[e]:[]}}o(wf,"select");var hy="UI_HIDE_MODAL",GT="UI_SET_ACTIVE_MODAL",mF={activeModal:void 0};function Fx(e=mF,t){switch(t.type){case GT:return It(Pe({},e),{activeModal:t.activeModal});case hy:return It(Pe({},e),{activeModal:void 0});default:return e}}o(Fx,"reducer");function YT(e){return{type:GT,activeModal:e}}o(YT,"setActiveModal");function my(){return{type:hy}}o(my,"hideModal");var Xh=pe(De());var Bt=pe(De());var hp=pe(De());var Rh=pe(De()),XT=pe(Xn()),QT=(()=>{let e=document.createElement("div");return e.setAttribute("contenteditable","PLAINTEXT-ONLY"),e.contentEditable==="plaintext-only"?"plaintext-only":"true"})(),dp=!1,js=class extends Rh.Component{constructor(){super(...arguments);this.input=Rh.default.createRef();this.isEditing=o(()=>{var t;return((t=this.input.current)==null?void 0:t.contentEditable)===QT},"isEditing");this.startEditing=o(()=>{if(!this.input.current)return console.error("unreachable");this.isEditing()||(this.suppress_events=!0,this.input.current.blur(),this.input.current.contentEditable=QT,window.requestAnimationFrame(()=>{var l,d;if(!this.input.current)return;this.input.current.focus(),this.suppress_events=!1;let t=document.createRange();t.selectNodeContents(this.input.current);let n=window.getSelection();n==null||n.removeAllRanges(),n==null||n.addRange(t),(d=(l=this.props).onEditStart)==null||d.call(l)}))},"startEditing");this.resetValue=o(()=>{var t,n;if(!this.input.current)return console.error("unreachable");this.input.current.textContent=this.props.content,(n=(t=this.props).onInput)==null||n.call(t,this.props.content)},"resetValue");this.finishEditing=o(()=>{if(!this.input.current)return console.error("unreachable");this.props.onEditDone(this.input.current.textContent||""),this.input.current.blur(),this.input.current.contentEditable="inherit"},"finishEditing");this.onPaste=o(t=>{t.preventDefault();let n=t.clipboardData.getData("text/plain");document.execCommand("insertHTML",!1,n)},"onPaste");this.suppress_events=!1;this.onMouseDown=o(t=>{dp&&console.debug("onMouseDown",this.suppress_events),this.suppress_events=!0,window.addEventListener("mouseup",this.onMouseUp,{once:!0})},"onMouseDown");this.onMouseUp=o(t=>{var d;let n=t.target===this.input.current,l=!((d=window.getSelection())==null?void 0:d.toString());dp&&console.warn("mouseUp",this.suppress_events,n,l),n&&l&&this.startEditing(),this.suppress_events=!1},"onMouseUp");this.onClick=o(t=>{dp&&console.debug("onClick",this.suppress_events)},"onClick");this.onFocus=o(t=>{if(dp&&console.debug("onFocus",this.props.content,this.suppress_events),!this.input.current)throw"unreachable";this.suppress_events||this.startEditing()},"onFocus");this.onInput=o(t=>{var n,l,d;(d=(l=this.props).onInput)==null||d.call(l,((n=this.input.current)==null?void 0:n.textContent)||"")},"onInput");this.onBlur=o(t=>{dp&&console.debug("onBlur",this.props.content,this.suppress_events),!this.suppress_events&&this.finishEditing()},"onBlur");this.onKeyDown=o(t=>{var n,l;switch(dp&&console.debug("keydown",t),t.stopPropagation(),t.key){case"Escape":t.preventDefault(),this.resetValue(),this.finishEditing();break;case"Enter":t.shiftKey||(t.preventDefault(),this.finishEditing());break;default:break}(l=(n=this.props).onKeyDown)==null||l.call(n,t)},"onKeyDown")}render(){let t=(0,XT.default)("inline-input",this.props.className);return Rh.default.createElement("span",{ref:this.input,tabIndex:0,className:t,placeholder:this.props.placeholder,onFocus:this.onFocus,onBlur:this.onBlur,onKeyDown:this.onKeyDown,onInput:this.onInput,onPaste:this.onPaste,onMouseDown:this.onMouseDown,onClick:this.onClick},this.props.content)}componentDidUpdate(t){var n,l;t.content!==this.props.content&&((l=(n=this.props).onInput)==null||l.call(n,this.props.content))}};o(js,"ValueEditor");var ZT=pe(Xn());function xf(e){let[t,n]=(0,hp.useState)(e.isValid(e.content)),l=(0,hp.useRef)(null),d=o(p=>{var x;e.isValid(p)?e.onEditDone(p):(x=l.current)==null||x.resetValue()},"onEditDone"),m=(0,ZT.default)(e.className,t?"has-success":"has-warning");return hp.default.createElement(js,It(Pe({},e),{className:m,onInput:p=>n(e.isValid(p)),onEditDone:d,ref:l}))}o(xf,"ValidateEditor");function Ix(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}o(Ix,"_defineProperty");function JT(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var l=Object.getOwnPropertySymbols(e);t&&(l=l.filter(function(d){return Object.getOwnPropertyDescriptor(e,d).enumerable})),n.push.apply(n,l)}return n}o(JT,"ownKeys");function vy(e){for(var t=1;tn[l.level])));case rk:case yF:return Pe(Pe({},e),up(e,Nh[t.cmd](t.data,l=>e.filters[l.level])));default:return e}}o(Ux,"reduce");function ok(e){return{type:ik,filter:e}}o(ok,"toggleFilter");function mp(){return{type:nk}}o(mp,"toggleVisibility");function sk(e,t="web"){let n={id:Math.random().toString(),message:e,level:t};return{type:rk,cmd:"add",data:n}}o(sk,"add");var lk="UI_OPTION_UPDATE_START",ak="UI_OPTION_UPDATE_SUCCESS",uk="UI_OPTION_UPDATE_ERROR",xF={};function $x(e=xF,t){switch(t.type){case lk:return It(Pe({},e),{[t.option]:{isUpdating:!0,value:t.value,error:!1}});case ak:return It(Pe({},e),{[t.option]:void 0});case uk:let n=e[t.option].value;return typeof n=="boolean"&&(n=!n),It(Pe({},e),{[t.option]:{value:n,isUpdating:!1,error:t.error}});case hy:return{};default:return e}}o($x,"reducer");function fk(e,t){return{type:lk,option:e,value:t}}o(fk,"startUpdate");function ck(e){return{type:ak,option:e}}o(ck,"updateSuccess");function pk(e,t){return{type:uk,option:e,error:t}}o(pk,"updateError");var dk=yy({flow:gx,modal:Fx,optionsEditor:$x});var Zn;(function(m){m.INIT="CONNECTION_INIT",m.FETCHING="CONNECTION_FETCHING",m.ESTABLISHED="CONNECTION_ESTABLISHED",m.ERROR="CONNECTION_ERROR",m.OFFLINE="CONNECTION_OFFLINE"})(Zn||(Zn={}));var SF={state:Zn.INIT,message:void 0};function zx(e=SF,t){switch(t.type){case Zn.ESTABLISHED:case Zn.FETCHING:case Zn.ERROR:case Zn.OFFLINE:return{state:t.type,message:t.message};default:return e}}o(zx,"reducer");function hk(){return{type:Zn.FETCHING}}o(hk,"startFetching");function mk(){return{type:Zn.ESTABLISHED}}o(mk,"connectionEstablished");function vk(e){return{type:Zn.ERROR,message:e}}o(vk,"connectionError");var gk={add_upstream_certs_to_client_chain:!1,allow_hosts:[],anticache:!1,anticomp:!1,block_global:!0,block_list:[],block_private:!1,body_size_limit:void 0,cert_passphrase:void 0,certs:[],ciphers_client:void 0,ciphers_server:void 0,client_certs:void 0,client_replay:[],client_replay_concurrency:1,command_history:!0,confdir:"~/.mitmproxy",connection_strategy:"eager",console_focus_follow:!1,content_view_lines_cutoff:512,dns_listen_host:"",dns_listen_port:53,dns_mode:"regular",dns_server:!1,export_preserve_original_ip:!1,http2:!0,http2_ping_keepalive:58,ignore_hosts:[],intercept:void 0,intercept_active:!1,keep_host_header:!1,key_size:2048,listen_host:"",listen_port:8080,map_local:[],map_remote:[],mode:"regular",modify_body:[],modify_headers:[],normalize_outbound_headers:!0,onboarding:!0,onboarding_host:"mitm.it",onboarding_port:80,proxy_debug:!1,proxyauth:void 0,rawtcp:!0,readfile_filter:void 0,rfile:void 0,save_stream_file:void 0,save_stream_filter:void 0,scripts:[],server:!0,server_replay:[],server_replay_ignore_content:!1,server_replay_ignore_host:!1,server_replay_ignore_params:[],server_replay_ignore_payload_params:[],server_replay_ignore_port:!1,server_replay_kill_extra:!1,server_replay_nopop:!1,server_replay_refresh:!0,server_replay_use_headers:[],showhost:!1,ssl_insecure:!1,ssl_verify_upstream_trusted_ca:void 0,ssl_verify_upstream_trusted_confdir:void 0,stickyauth:void 0,stickycookie:void 0,stream_large_bodies:void 0,tcp_hosts:[],termlog_verbosity:"info",tls_version_client_max:"UNBOUNDED",tls_version_client_min:"TLS1_2",tls_version_server_max:"UNBOUNDED",tls_version_server_min:"TLS1_2",upstream_auth:void 0,upstream_cert:!0,validate_inbound_headers:!0,view_filter:void 0,view_order:"time",view_order_reversed:!1,web_columns:["tls","icon","path","method","status","size","time"],web_debug:!1,web_host:"127.0.0.1",web_open_browser:!0,web_port:8081,web_static_viewer:"",websocket:!0};var jx="OPTIONS_RECEIVE",qx="OPTIONS_UPDATE";function Vx(e=gk,t){switch(t.type){case jx:let n={};for(let[d,{value:m}]of Object.entries(t.data))n[d]=m;return n;case qx:let l=Pe({},e);for(let[d,{value:m}]of Object.entries(t.data))l[d]=m;return l;default:return e}}o(Vx,"reducer");function CF(e,t,n){return Na(this,null,function*(){try{let l=yield Ot.put("/options",{[e]:t});if(l.status===200)n(ck(e));else throw yield l.text()}catch(l){n(pk(e,l))}})}o(CF,"pureSendUpdate");var _F=CF;function vp(e,t){return n=>{n(fk(e,t)),_F(e,t,n)}}o(vp,"update");function yk(){return e=>Ot("/options/save",{method:"POST"})}o(yk,"save");var wk="COMMANDBAR_TOGGLE_VISIBILITY",bF={visible:!1};function Kx(e=bF,t){switch(t.type){case wk:return It(Pe({},e),{visible:!e.visible});default:return e}}o(Kx,"reducer");function wy(){return{type:wk}}o(wy,"toggleVisibility");function xk(e){return function(t){var n=t.dispatch,l=t.getState;return function(d){return function(m){return typeof m=="function"?m(n,l,e):d(m)}}}}o(xk,"createThunkMiddleware");var Sk=xk();Sk.withExtraArgument=xk;var Ck=Sk;var EF=window.MITMWEB_CONF||{static:!1,version:"1.2.3",contentViews:["Auto","Raw"]};function Gx(e=EF,t){return e}o(Gx,"reducer");var TF={},kF=o((e=TF,t)=>{switch(t.type){case jx:return t.data;case qx:return Pe(Pe({},e),t.data);default:return e}},"reducer"),_k=kF;var OF=window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||Bx,LF=yy({commandBar:Kx,eventLog:Ux,flows:Dx,connection:zx,ui:dk,options:Vx,options_meta:_k,conf:Gx}),NF=o(e=>Wx(LF,e,OF(tk(Ck))),"createAppStore"),gp=NF(void 0),Vt=o(()=>$s(),"useAppDispatch"),it=mx;var Zi=pe(De());var bk=pe(Oh()),Ek=pe(Xn()),Yx=class extends Zi.Component{constructor(){super(...arguments);this.container=Zi.default.createRef();this.nameInput=Zi.default.createRef();this.valueInput=Zi.default.createRef();this.render=o(()=>{let[t,n]=this.props.item;return Zi.default.createElement("div",{ref:this.container,className:"kv-row",onClick:this.onClick,onKeyDownCapture:this.onKeyDown},Zi.default.createElement(js,{ref:this.nameInput,className:"kv-key",content:t,onEditStart:this.props.onEditStart,onEditDone:l=>this.props.onEditDone([l,n])}),":\xA0",Zi.default.createElement(js,{ref:this.valueInput,className:"kv-value",content:n,onEditStart:this.props.onEditStart,onEditDone:l=>this.props.onEditDone([t,l]),placeholder:"empty"}))},"render");this.onClick=o(t=>{t.target===this.container.current&&this.props.onClickEmptyArea()},"onClick");this.onKeyDown=o(t=>{var n;t.target===((n=this.valueInput.current)==null?void 0:n.input.current)&&t.key==="Tab"&&this.props.onTabNext()},"onKeyDown")}};o(Yx,"Row");var yp=class extends Zi.Component{constructor(){super(...arguments);this.rowRefs={};this.state={currentList:this.props.data||[],initialList:this.props.data};this.render=o(()=>{this.rowRefs={};let t=this.state.currentList.map((n,l)=>Zi.default.createElement(Yx,{key:l,item:n,onEditStart:()=>this.currentlyEditing=l,onEditDone:d=>this.onEditDone(l,d),onClickEmptyArea:()=>this.onClickEmptyArea(l),onTabNext:()=>this.onTabNext(l),ref:d=>this.rowRefs[l]=d}));return Zi.default.createElement("div",{className:(0,Ek.default)("kv-editor",this.props.className),onMouseDown:this.onMouseDown},t,Zi.default.createElement("div",{onClick:n=>{n.preventDefault(),this.onClickEmptyArea(this.state.currentList.length-1)},className:"kv-add-row fa fa-plus-square-o",role:"button","aria-label":"Add"}))},"render");this.onEditDone=o((t,n)=>{let l=[...this.state.currentList];n[0]?l[t]=n:l.splice(t,1),this.currentlyEditing=void 0,(0,bk.isEqual)(this.state.currentList,l)||this.props.onChange(l),this.setState({currentList:l})},"onEditDone");this.onClickEmptyArea=o(t=>{if(this.justFinishedEditing)return;let n=[...this.state.currentList];n.splice(t+1,0,["",""]),this.setState({currentList:n},()=>{var l,d;return(d=(l=this.rowRefs[t+1])==null?void 0:l.nameInput.current)==null?void 0:d.startEditing()})},"onClickEmptyArea");this.onTabNext=o(t=>{t==this.state.currentList.length-1&&this.onClickEmptyArea(t)},"onTabNext");this.onMouseDown=o(t=>{this.justFinishedEditing=this.currentlyEditing},"onMouseDown")}static getDerivedStateFromProps(t,n){return t.data!==n.initialList?{currentList:t.data||[],initialList:t.data}:null}};o(yp,"KeyValueListEditor");var Xt=pe(De());var Fh=pe(De());var xy=80;function Sy(e,t){let[n,l]=(0,Fh.useState)(),[d,m]=(0,Fh.useState)();return(0,Fh.useEffect)(()=>{d&&d.abort();let p=new AbortController;return Ot(e,{signal:p.signal}).then(x=>{if(!x.ok)throw`${x.status} ${x.statusText}`.trim();return x.text()}).then(x=>{l(x)}).catch(x=>{p.signal.aborted||l(`Error getting content: ${x}.`)}),m(p),()=>{p.signal.aborted||p.abort()}},[e,t]),n}o(Sy,"useContent");var Ih=pe(De()),Cy=Ih.default.memo(o(function({icon:t,text:n,className:l,title:d,onOpenFile:m,onClick:p}){let x;return Ih.default.createElement("a",{href:"#",onClick:_=>{x.click(),p&&p(_)},className:l,title:d},Ih.default.createElement("i",{className:"fa fa-fw "+t}),n,Ih.default.createElement("input",{ref:_=>x=_,className:"hidden",type:"file",onChange:_=>{_.preventDefault(),_.target.files&&_.target.files.length>0&&m(_.target.files[0]),x.value=""}}))},"FileChooser"));var wp=pe(De()),Tk=pe(Xn());function Cr({onClick:e,children:t,icon:n,disabled:l,className:d,title:m}){return wp.createElement("button",{className:(0,Tk.default)(d,"btn btn-default"),onClick:l?void 0:e,disabled:l,title:m},n&&wp.createElement(wp.Fragment,null,wp.createElement("i",{className:"fa "+n}),"\xA0"),t)}o(Cr,"Button");var Wh=pe(De()),Mk=pe(De());var Hh=pe(De()),Ok=pe(Xn()),Lk=pe(kk()),Nk=pe(Oh());function Pk(e){return e&&e.replace(/\r\n|\r/g,` +`)}o(Pk,"normalizeLineEndings");var xp=class extends Hh.Component{constructor(t){super(t);this.state={isFocused:!1}}getCodeMirrorInstance(){return this.props.codeMirrorInstance||Lk.default}UNSAFE_componentWillMount(){this.props.path&&console.error("Warning: react-codemirror: the `path` prop has been changed to `name`")}componentDidMount(){let t=this.getCodeMirrorInstance();this.codeMirror=t.fromTextArea(this.textareaNode,this.props.options),this.codeMirror.on("change",this.codemirrorValueChanged.bind(this)),this.codeMirror.on("cursorActivity",this.cursorActivity.bind(this)),this.codeMirror.on("focus",this.focusChanged.bind(this,!0)),this.codeMirror.on("blur",this.focusChanged.bind(this,!1)),this.codeMirror.on("scroll",this.scrollChanged.bind(this)),this.codeMirror.setValue(this.props.defaultValue||this.props.value||"")}componentWillUnmount(){this.codeMirror&&this.codeMirror.toTextArea()}UNSAFE_componentWillReceiveProps(t){if(this.codeMirror&&t.value!==void 0&&t.value!==this.props.value&&Pk(this.codeMirror.getValue())!==Pk(t.value))if(this.props.preserveScrollPosition){var n=this.codeMirror.getScrollInfo();this.codeMirror.setValue(t.value),this.codeMirror.scrollTo(n.left,n.top)}else this.codeMirror.setValue(t.value);if(typeof t.options=="object")for(let l in t.options)t.options.hasOwnProperty(l)&&this.setOptionIfChanged(l,t.options[l])}setOptionIfChanged(t,n){let l=this.codeMirror.getOption(t);Nk.default.isEqual(l,n)||this.codeMirror.setOption(t,n)}getCodeMirror(){return this.codeMirror}focus(){this.codeMirror&&this.codeMirror.focus()}focusChanged(t){this.setState({isFocused:t}),this.props.onFocusChange&&this.props.onFocusChange(t)}cursorActivity(t){this.props.onCursorActivity&&this.props.onCursorActivity(t)}scrollChanged(t){this.props.onScroll&&this.props.onScroll(t.getScrollInfo())}codemirrorValueChanged(t,n){this.props.onChange&&n.origin!=="setValue"&&this.props.onChange(t.getValue(),n)}render(){let t=(0,Ok.default)("ReactCodeMirror",this.state.isFocused?"ReactCodeMirror--focused":null,this.props.className);return Hh.createElement("div",{className:t},Hh.createElement("textarea",{ref:n=>this.textareaNode=n,name:this.props.name||this.props.path,defaultValue:this.props.value,autoComplete:"off",autoFocus:this.props.autoFocus}))}};o(xp,"CodeMirror"),xp.defaultProps={preserveScrollPosition:!1};var Bh=class extends Mk.Component{constructor(){super(...arguments);this.editor=Wh.createRef();this.getContent=o(()=>{var t;return(t=this.editor.current)==null?void 0:t.codeMirror.getValue()},"getContent");this.render=o(()=>{let t={lineNumbers:!0};return Wh.createElement("div",{className:"codeeditor",onKeyDown:n=>n.stopPropagation()},Wh.createElement(xp,{ref:this.editor,value:this.props.initialContent,onChange:()=>0,options:t}))},"render")}};o(Bh,"CodeEditor");var Sf=pe(De()),PF=Sf.default.memo(o(function({lines:t,maxLines:n,showMore:l}){return t.length===0?null:Sf.default.createElement("pre",null,t.map((d,m)=>m===n?Sf.default.createElement("button",{key:"showmore",onClick:l,className:"btn btn-xs btn-info"},Sf.default.createElement("i",{className:"fa fa-angle-double-down","aria-hidden":"true"})," Show more"):Sf.default.createElement("div",{key:m},d.map(([p,x],_)=>Sf.default.createElement("span",{key:_,className:p},x)))))},"LineRenderer")),_y=PF;var Of=pe(De());var di=pe(De());var by=pe(De());var Zx=o(function(t){return t.reduce(function(n,l){var d=l[0],m=l[1];return n[d]=m,n},{})},"fromEntries"),Jx=typeof window!="undefined"&&window.document&&window.document.createElement?by.useLayoutEffect:by.useEffect;var iu=pe(De());var $r="top",Cn="bottom",tn="right",rn="left",Ey="auto",eu=[$r,Cn,tn,rn],Rl="start",Ty="end",Ak="clippingParents",ky="viewport",Sp="popper",Dk="reference",eS=eu.reduce(function(e,t){return e.concat([t+"-"+Rl,t+"-"+Ty])},[]),Oy=[].concat(eu,[Ey]).reduce(function(e,t){return e.concat([t,t+"-"+Rl,t+"-"+Ty])},[]),MF="beforeRead",AF="read",DF="afterRead",RF="beforeMain",FF="main",IF="afterMain",HF="beforeWrite",WF="write",BF="afterWrite",Rk=[MF,AF,DF,RF,FF,IF,HF,WF,BF];function _n(e){return e?(e.nodeName||"").toLowerCase():null}o(_n,"getNodeName");function Mr(e){if(e==null)return window;if(e.toString()!=="[object Window]"){var t=e.ownerDocument;return t&&t.defaultView||window}return e}o(Mr,"getWindow");function Fl(e){var t=Mr(e).Element;return e instanceof t||e instanceof Element}o(Fl,"isElement");function zr(e){var t=Mr(e).HTMLElement;return e instanceof t||e instanceof HTMLElement}o(zr,"isHTMLElement");function Ly(e){if(typeof ShadowRoot=="undefined")return!1;var t=Mr(e).ShadowRoot;return e instanceof t||e instanceof ShadowRoot}o(Ly,"isShadowRoot");function UF(e){var t=e.state;Object.keys(t.elements).forEach(function(n){var l=t.styles[n]||{},d=t.attributes[n]||{},m=t.elements[n];!zr(m)||!_n(m)||(Object.assign(m.style,l),Object.keys(d).forEach(function(p){var x=d[p];x===!1?m.removeAttribute(p):m.setAttribute(p,x===!0?"":x)}))})}o(UF,"applyStyles");function $F(e){var t=e.state,n={popper:{position:t.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(t.elements.popper.style,n.popper),t.styles=n,t.elements.arrow&&Object.assign(t.elements.arrow.style,n.arrow),function(){Object.keys(t.elements).forEach(function(l){var d=t.elements[l],m=t.attributes[l]||{},p=Object.keys(t.styles.hasOwnProperty(l)?t.styles[l]:n[l]),x=p.reduce(function(_,O){return _[O]="",_},{});!zr(d)||!_n(d)||(Object.assign(d.style,x),Object.keys(m).forEach(function(_){d.removeAttribute(_)}))})}}o($F,"effect");var Fk={name:"applyStyles",enabled:!0,phase:"write",fn:UF,effect:$F,requires:["computeStyles"]};function bn(e){return e.split("-")[0]}o(bn,"getBasePlacement");var tu=Math.round;function Oo(e,t){t===void 0&&(t=!1);var n=e.getBoundingClientRect(),l=1,d=1;return zr(e)&&t&&(l=n.width/e.offsetWidth||1,d=n.height/e.offsetHeight||1),{width:tu(n.width/l),height:tu(n.height/d),top:tu(n.top/d),right:tu(n.right/l),bottom:tu(n.bottom/d),left:tu(n.left/l),x:tu(n.left/l),y:tu(n.top/d)}}o(Oo,"getBoundingClientRect");function Cf(e){var t=Oo(e),n=e.offsetWidth,l=e.offsetHeight;return Math.abs(t.width-n)<=1&&(n=t.width),Math.abs(t.height-l)<=1&&(l=t.height),{x:e.offsetLeft,y:e.offsetTop,width:n,height:l}}o(Cf,"getLayoutRect");function Uh(e,t){var n=t.getRootNode&&t.getRootNode();if(e.contains(t))return!0;if(n&&Ly(n)){var l=t;do{if(l&&e.isSameNode(l))return!0;l=l.parentNode||l.host}while(l)}return!1}o(Uh,"contains");function pi(e){return Mr(e).getComputedStyle(e)}o(pi,"getComputedStyle");function tS(e){return["table","td","th"].indexOf(_n(e))>=0}o(tS,"isTableElement");function Dn(e){return((Fl(e)?e.ownerDocument:e.document)||window.document).documentElement}o(Dn,"getDocumentElement");function Il(e){return _n(e)==="html"?e:e.assignedSlot||e.parentNode||(Ly(e)?e.host:null)||Dn(e)}o(Il,"getParentNode");function Ik(e){return!zr(e)||pi(e).position==="fixed"?null:e.offsetParent}o(Ik,"getTrueOffsetParent");function zF(e){var t=navigator.userAgent.toLowerCase().indexOf("firefox")!==-1,n=navigator.userAgent.indexOf("Trident")!==-1;if(n&&zr(e)){var l=pi(e);if(l.position==="fixed")return null}for(var d=Il(e);zr(d)&&["html","body"].indexOf(_n(d))<0;){var m=pi(d);if(m.transform!=="none"||m.perspective!=="none"||m.contain==="paint"||["transform","perspective"].indexOf(m.willChange)!==-1||t&&m.willChange==="filter"||t&&m.filter&&m.filter!=="none")return d;d=d.parentNode}return null}o(zF,"getContainingBlock");function es(e){for(var t=Mr(e),n=Ik(e);n&&tS(n)&&pi(n).position==="static";)n=Ik(n);return n&&(_n(n)==="html"||_n(n)==="body"&&pi(n).position==="static")?t:n||zF(e)||t}o(es,"getOffsetParent");function _f(e){return["top","bottom"].indexOf(e)>=0?"x":"y"}o(_f,"getMainAxisFromPlacement");var Lo=Math.max,ru=Math.min,$h=Math.round;function bf(e,t,n){return Lo(e,ru(t,n))}o(bf,"within");function zh(){return{top:0,right:0,bottom:0,left:0}}o(zh,"getFreshSideObject");function jh(e){return Object.assign({},zh(),e)}o(jh,"mergePaddingObject");function qh(e,t){return t.reduce(function(n,l){return n[l]=e,n},{})}o(qh,"expandToHashMap");var jF=o(function(t,n){return t=typeof t=="function"?t(Object.assign({},n.rects,{placement:n.placement})):t,jh(typeof t!="number"?t:qh(t,eu))},"toPaddingObject");function qF(e){var t,n=e.state,l=e.name,d=e.options,m=n.elements.arrow,p=n.modifiersData.popperOffsets,x=bn(n.placement),_=_f(x),O=[rn,tn].indexOf(x)>=0,D=O?"height":"width";if(!(!m||!p)){var Y=jF(d.padding,n),U=Cf(m),X=_==="y"?$r:rn,te=_==="y"?Cn:tn,Q=n.rects.reference[D]+n.rects.reference[_]-p[_]-n.rects.popper[D],F=p[_]-n.rects.reference[_],M=es(m),R=M?_==="y"?M.clientHeight||0:M.clientWidth||0:0,K=Q/2-F/2,V=Y[X],ue=R-U[D]-Y[te],ie=R/2-U[D]/2+K,de=bf(V,ie,ue),ge=_;n.modifiersData[l]=(t={},t[ge]=de,t.centerOffset=de-ie,t)}}o(qF,"arrow");function VF(e){var t=e.state,n=e.options,l=n.element,d=l===void 0?"[data-popper-arrow]":l;d!=null&&(typeof d=="string"&&(d=t.elements.popper.querySelector(d),!d)||!Uh(t.elements.popper,d)||(t.elements.arrow=d))}o(VF,"effect");var Hk={name:"arrow",enabled:!0,phase:"main",fn:qF,effect:VF,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};var KF={top:"auto",right:"auto",bottom:"auto",left:"auto"};function GF(e){var t=e.x,n=e.y,l=window,d=l.devicePixelRatio||1;return{x:$h($h(t*d)/d)||0,y:$h($h(n*d)/d)||0}}o(GF,"roundOffsetsByDPR");function Wk(e){var t,n=e.popper,l=e.popperRect,d=e.placement,m=e.offsets,p=e.position,x=e.gpuAcceleration,_=e.adaptive,O=e.roundOffsets,D=O===!0?GF(m):typeof O=="function"?O(m):m,Y=D.x,U=Y===void 0?0:Y,X=D.y,te=X===void 0?0:X,Q=m.hasOwnProperty("x"),F=m.hasOwnProperty("y"),M=rn,R=$r,K=window;if(_){var V=es(n),ue="clientHeight",ie="clientWidth";V===Mr(n)&&(V=Dn(n),pi(V).position!=="static"&&(ue="scrollHeight",ie="scrollWidth")),V=V,d===$r&&(R=Cn,te-=V[ue]-l.height,te*=x?1:-1),d===rn&&(M=tn,U-=V[ie]-l.width,U*=x?1:-1)}var de=Object.assign({position:p},_&&KF);if(x){var ge;return Object.assign({},de,(ge={},ge[R]=F?"0":"",ge[M]=Q?"0":"",ge.transform=(K.devicePixelRatio||1)<2?"translate("+U+"px, "+te+"px)":"translate3d("+U+"px, "+te+"px, 0)",ge))}return Object.assign({},de,(t={},t[R]=F?te+"px":"",t[M]=Q?U+"px":"",t.transform="",t))}o(Wk,"mapToStyles");function YF(e){var t=e.state,n=e.options,l=n.gpuAcceleration,d=l===void 0?!0:l,m=n.adaptive,p=m===void 0?!0:m,x=n.roundOffsets,_=x===void 0?!0:x;if(!1)var O;var D={placement:bn(t.placement),popper:t.elements.popper,popperRect:t.rects.popper,gpuAcceleration:d};t.modifiersData.popperOffsets!=null&&(t.styles.popper=Object.assign({},t.styles.popper,Wk(Object.assign({},D,{offsets:t.modifiersData.popperOffsets,position:t.options.strategy,adaptive:p,roundOffsets:_})))),t.modifiersData.arrow!=null&&(t.styles.arrow=Object.assign({},t.styles.arrow,Wk(Object.assign({},D,{offsets:t.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:_})))),t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-placement":t.placement})}o(YF,"computeStyles");var Bk={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:YF,data:{}};var Ny={passive:!0};function XF(e){var t=e.state,n=e.instance,l=e.options,d=l.scroll,m=d===void 0?!0:d,p=l.resize,x=p===void 0?!0:p,_=Mr(t.elements.popper),O=[].concat(t.scrollParents.reference,t.scrollParents.popper);return m&&O.forEach(function(D){D.addEventListener("scroll",n.update,Ny)}),x&&_.addEventListener("resize",n.update,Ny),function(){m&&O.forEach(function(D){D.removeEventListener("scroll",n.update,Ny)}),x&&_.removeEventListener("resize",n.update,Ny)}}o(XF,"effect");var Uk={name:"eventListeners",enabled:!0,phase:"write",fn:o(function(){},"fn"),effect:XF,data:{}};var QF={left:"right",right:"left",bottom:"top",top:"bottom"};function Cp(e){return e.replace(/left|right|bottom|top/g,function(t){return QF[t]})}o(Cp,"getOppositePlacement");var ZF={start:"end",end:"start"};function Py(e){return e.replace(/start|end/g,function(t){return ZF[t]})}o(Py,"getOppositeVariationPlacement");function Ef(e){var t=Mr(e),n=t.pageXOffset,l=t.pageYOffset;return{scrollLeft:n,scrollTop:l}}o(Ef,"getWindowScroll");function Tf(e){return Oo(Dn(e)).left+Ef(e).scrollLeft}o(Tf,"getWindowScrollBarX");function rS(e){var t=Mr(e),n=Dn(e),l=t.visualViewport,d=n.clientWidth,m=n.clientHeight,p=0,x=0;return l&&(d=l.width,m=l.height,/^((?!chrome|android).)*safari/i.test(navigator.userAgent)||(p=l.offsetLeft,x=l.offsetTop)),{width:d,height:m,x:p+Tf(e),y:x}}o(rS,"getViewportRect");function nS(e){var t,n=Dn(e),l=Ef(e),d=(t=e.ownerDocument)==null?void 0:t.body,m=Lo(n.scrollWidth,n.clientWidth,d?d.scrollWidth:0,d?d.clientWidth:0),p=Lo(n.scrollHeight,n.clientHeight,d?d.scrollHeight:0,d?d.clientHeight:0),x=-l.scrollLeft+Tf(e),_=-l.scrollTop;return pi(d||n).direction==="rtl"&&(x+=Lo(n.clientWidth,d?d.clientWidth:0)-m),{width:m,height:p,x,y:_}}o(nS,"getDocumentRect");function kf(e){var t=pi(e),n=t.overflow,l=t.overflowX,d=t.overflowY;return/auto|scroll|overlay|hidden/.test(n+d+l)}o(kf,"isScrollParent");function My(e){return["html","body","#document"].indexOf(_n(e))>=0?e.ownerDocument.body:zr(e)&&kf(e)?e:My(Il(e))}o(My,"getScrollParent");function nu(e,t){var n;t===void 0&&(t=[]);var l=My(e),d=l===((n=e.ownerDocument)==null?void 0:n.body),m=Mr(l),p=d?[m].concat(m.visualViewport||[],kf(l)?l:[]):l,x=t.concat(p);return d?x:x.concat(nu(Il(p)))}o(nu,"listScrollParents");function _p(e){return Object.assign({},e,{left:e.x,top:e.y,right:e.x+e.width,bottom:e.y+e.height})}o(_p,"rectToClientRect");function JF(e){var t=Oo(e);return t.top=t.top+e.clientTop,t.left=t.left+e.clientLeft,t.bottom=t.top+e.clientHeight,t.right=t.left+e.clientWidth,t.width=e.clientWidth,t.height=e.clientHeight,t.x=t.left,t.y=t.top,t}o(JF,"getInnerBoundingClientRect");function $k(e,t){return t===ky?_p(rS(e)):zr(t)?JF(t):_p(nS(Dn(e)))}o($k,"getClientRectFromMixedType");function eI(e){var t=nu(Il(e)),n=["absolute","fixed"].indexOf(pi(e).position)>=0,l=n&&zr(e)?es(e):e;return Fl(l)?t.filter(function(d){return Fl(d)&&Uh(d,l)&&_n(d)!=="body"}):[]}o(eI,"getClippingParents");function iS(e,t,n){var l=t==="clippingParents"?eI(e):[].concat(t),d=[].concat(l,[n]),m=d[0],p=d.reduce(function(x,_){var O=$k(e,_);return x.top=Lo(O.top,x.top),x.right=ru(O.right,x.right),x.bottom=ru(O.bottom,x.bottom),x.left=Lo(O.left,x.left),x},$k(e,m));return p.width=p.right-p.left,p.height=p.bottom-p.top,p.x=p.left,p.y=p.top,p}o(iS,"getClippingRect");function qs(e){return e.split("-")[1]}o(qs,"getVariation");function Vh(e){var t=e.reference,n=e.element,l=e.placement,d=l?bn(l):null,m=l?qs(l):null,p=t.x+t.width/2-n.width/2,x=t.y+t.height/2-n.height/2,_;switch(d){case $r:_={x:p,y:t.y-n.height};break;case Cn:_={x:p,y:t.y+t.height};break;case tn:_={x:t.x+t.width,y:x};break;case rn:_={x:t.x-n.width,y:x};break;default:_={x:t.x,y:t.y}}var O=d?_f(d):null;if(O!=null){var D=O==="y"?"height":"width";switch(m){case Rl:_[O]=_[O]-(t[D]/2-n[D]/2);break;case Ty:_[O]=_[O]+(t[D]/2-n[D]/2);break;default:}}return _}o(Vh,"computeOffsets");function ts(e,t){t===void 0&&(t={});var n=t,l=n.placement,d=l===void 0?e.placement:l,m=n.boundary,p=m===void 0?Ak:m,x=n.rootBoundary,_=x===void 0?ky:x,O=n.elementContext,D=O===void 0?Sp:O,Y=n.altBoundary,U=Y===void 0?!1:Y,X=n.padding,te=X===void 0?0:X,Q=jh(typeof te!="number"?te:qh(te,eu)),F=D===Sp?Dk:Sp,M=e.elements.reference,R=e.rects.popper,K=e.elements[U?F:D],V=iS(Fl(K)?K:K.contextElement||Dn(e.elements.popper),p,_),ue=Oo(M),ie=Vh({reference:ue,element:R,strategy:"absolute",placement:d}),de=_p(Object.assign({},R,ie)),ge=D===Sp?de:ue,xe={top:V.top-ge.top+Q.top,bottom:ge.bottom-V.bottom+Q.bottom,left:V.left-ge.left+Q.left,right:ge.right-V.right+Q.right},qe=e.modifiersData.offset;if(D===Sp&&qe){var et=qe[d];Object.keys(xe).forEach(function(Te){var xt=[tn,Cn].indexOf(Te)>=0?1:-1,Ue=[$r,Cn].indexOf(Te)>=0?"y":"x";xe[Te]+=et[Ue]*xt})}return xe}o(ts,"detectOverflow");function oS(e,t){t===void 0&&(t={});var n=t,l=n.placement,d=n.boundary,m=n.rootBoundary,p=n.padding,x=n.flipVariations,_=n.allowedAutoPlacements,O=_===void 0?Oy:_,D=qs(l),Y=D?x?eS:eS.filter(function(te){return qs(te)===D}):eu,U=Y.filter(function(te){return O.indexOf(te)>=0});U.length===0&&(U=Y);var X=U.reduce(function(te,Q){return te[Q]=ts(e,{placement:Q,boundary:d,rootBoundary:m,padding:p})[bn(Q)],te},{});return Object.keys(X).sort(function(te,Q){return X[te]-X[Q]})}o(oS,"computeAutoPlacement");function tI(e){if(bn(e)===Ey)return[];var t=Cp(e);return[Py(e),t,Py(t)]}o(tI,"getExpandedFallbackPlacements");function rI(e){var t=e.state,n=e.options,l=e.name;if(!t.modifiersData[l]._skip){for(var d=n.mainAxis,m=d===void 0?!0:d,p=n.altAxis,x=p===void 0?!0:p,_=n.fallbackPlacements,O=n.padding,D=n.boundary,Y=n.rootBoundary,U=n.altBoundary,X=n.flipVariations,te=X===void 0?!0:X,Q=n.allowedAutoPlacements,F=t.options.placement,M=bn(F),R=M===F,K=_||(R||!te?[Cp(F)]:tI(F)),V=[F].concat(K).reduce(function(at,_r){return at.concat(bn(_r)===Ey?oS(t,{placement:_r,boundary:D,rootBoundary:Y,padding:O,flipVariations:te,allowedAutoPlacements:Q}):_r)},[]),ue=t.rects.reference,ie=t.rects.popper,de=new Map,ge=!0,xe=V[0],qe=0;qe=0,Ve=Ue?"width":"height",Ke=ts(t,{placement:et,boundary:D,rootBoundary:Y,altBoundary:U,padding:O}),Ye=Ue?xt?tn:rn:xt?Cn:$r;ue[Ve]>ie[Ve]&&(Ye=Cp(Ye));var Qt=Cp(Ye),ft=[];if(m&&ft.push(Ke[Te]<=0),x&&ft.push(Ke[Ye]<=0,Ke[Qt]<=0),ft.every(function(at){return at})){xe=et,ge=!1;break}de.set(et,ft)}if(ge)for(var Ar=te?3:1,Kt=o(function(_r){var Ut=V.find(function($t){var ne=de.get($t);if(ne)return ne.slice(0,_r).every(function(tt){return tt})});if(Ut)return xe=Ut,"break"},"_loop"),Et=Ar;Et>0;Et--){var St=Kt(Et);if(St==="break")break}t.placement!==xe&&(t.modifiersData[l]._skip=!0,t.placement=xe,t.reset=!0)}}o(rI,"flip");var zk={name:"flip",enabled:!0,phase:"main",fn:rI,requiresIfExists:["offset"],data:{_skip:!1}};function jk(e,t,n){return n===void 0&&(n={x:0,y:0}),{top:e.top-t.height-n.y,right:e.right-t.width+n.x,bottom:e.bottom-t.height+n.y,left:e.left-t.width-n.x}}o(jk,"getSideOffsets");function qk(e){return[$r,tn,Cn,rn].some(function(t){return e[t]>=0})}o(qk,"isAnySideFullyClipped");function nI(e){var t=e.state,n=e.name,l=t.rects.reference,d=t.rects.popper,m=t.modifiersData.preventOverflow,p=ts(t,{elementContext:"reference"}),x=ts(t,{altBoundary:!0}),_=jk(p,l),O=jk(x,d,m),D=qk(_),Y=qk(O);t.modifiersData[n]={referenceClippingOffsets:_,popperEscapeOffsets:O,isReferenceHidden:D,hasPopperEscaped:Y},t.attributes.popper=Object.assign({},t.attributes.popper,{"data-popper-reference-hidden":D,"data-popper-escaped":Y})}o(nI,"hide");var Vk={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:nI};function iI(e,t,n){var l=bn(e),d=[rn,$r].indexOf(l)>=0?-1:1,m=typeof n=="function"?n(Object.assign({},t,{placement:e})):n,p=m[0],x=m[1];return p=p||0,x=(x||0)*d,[rn,tn].indexOf(l)>=0?{x,y:p}:{x:p,y:x}}o(iI,"distanceAndSkiddingToXY");function oI(e){var t=e.state,n=e.options,l=e.name,d=n.offset,m=d===void 0?[0,0]:d,p=Oy.reduce(function(D,Y){return D[Y]=iI(Y,t.rects,m),D},{}),x=p[t.placement],_=x.x,O=x.y;t.modifiersData.popperOffsets!=null&&(t.modifiersData.popperOffsets.x+=_,t.modifiersData.popperOffsets.y+=O),t.modifiersData[l]=p}o(oI,"offset");var Kk={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:oI};function sI(e){var t=e.state,n=e.name;t.modifiersData[n]=Vh({reference:t.rects.reference,element:t.rects.popper,strategy:"absolute",placement:t.placement})}o(sI,"popperOffsets");var Gk={name:"popperOffsets",enabled:!0,phase:"read",fn:sI,data:{}};function sS(e){return e==="x"?"y":"x"}o(sS,"getAltAxis");function lI(e){var t=e.state,n=e.options,l=e.name,d=n.mainAxis,m=d===void 0?!0:d,p=n.altAxis,x=p===void 0?!1:p,_=n.boundary,O=n.rootBoundary,D=n.altBoundary,Y=n.padding,U=n.tether,X=U===void 0?!0:U,te=n.tetherOffset,Q=te===void 0?0:te,F=ts(t,{boundary:_,rootBoundary:O,padding:Y,altBoundary:D}),M=bn(t.placement),R=qs(t.placement),K=!R,V=_f(M),ue=sS(V),ie=t.modifiersData.popperOffsets,de=t.rects.reference,ge=t.rects.popper,xe=typeof Q=="function"?Q(Object.assign({},t.rects,{placement:t.placement})):Q,qe={x:0,y:0};if(!!ie){if(m||x){var et=V==="y"?$r:rn,Te=V==="y"?Cn:tn,xt=V==="y"?"height":"width",Ue=ie[V],Ve=ie[V]+F[et],Ke=ie[V]-F[Te],Ye=X?-ge[xt]/2:0,Qt=R===Rl?de[xt]:ge[xt],ft=R===Rl?-ge[xt]:-de[xt],Ar=t.elements.arrow,Kt=X&&Ar?Cf(Ar):{width:0,height:0},Et=t.modifiersData["arrow#persistent"]?t.modifiersData["arrow#persistent"].padding:zh(),St=Et[et],at=Et[Te],_r=bf(0,de[xt],Kt[xt]),Ut=K?de[xt]/2-Ye-_r-St-xe:Qt-_r-St-xe,$t=K?-de[xt]/2+Ye+_r+at+xe:ft+_r+at+xe,ne=t.elements.arrow&&es(t.elements.arrow),tt=ne?V==="y"?ne.clientTop||0:ne.clientLeft||0:0,br=t.modifiersData.offset?t.modifiersData.offset[t.placement][V]:0,jt=ie[V]+Ut-br-tt,qt=ie[V]+$t-br;if(m){var Se=bf(X?ru(Ve,jt):Ve,Ue,X?Lo(Ke,qt):Ke);ie[V]=Se,qe[V]=Se-Ue}if(x){var Er=V==="x"?$r:rn,nn=V==="x"?Cn:tn,Fn=ie[ue],ei=Fn+F[Er],on=Fn-F[nn],Gt=bf(X?ru(ei,jt):ei,Fn,X?Lo(on,qt):on);ie[ue]=Gt,qe[ue]=Gt-Fn}}t.modifiersData[l]=qe}}o(lI,"preventOverflow");var Yk={name:"preventOverflow",enabled:!0,phase:"main",fn:lI,requiresIfExists:["offset"]};function lS(e){return{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}}o(lS,"getHTMLElementScroll");function aS(e){return e===Mr(e)||!zr(e)?Ef(e):lS(e)}o(aS,"getNodeScroll");function aI(e){var t=e.getBoundingClientRect(),n=t.width/e.offsetWidth||1,l=t.height/e.offsetHeight||1;return n!==1||l!==1}o(aI,"isElementScaled");function uS(e,t,n){n===void 0&&(n=!1);var l=zr(t),d=zr(t)&&aI(t),m=Dn(t),p=Oo(e,d),x={scrollLeft:0,scrollTop:0},_={x:0,y:0};return(l||!l&&!n)&&((_n(t)!=="body"||kf(m))&&(x=aS(t)),zr(t)?(_=Oo(t,!0),_.x+=t.clientLeft,_.y+=t.clientTop):m&&(_.x=Tf(m))),{x:p.left+x.scrollLeft-_.x,y:p.top+x.scrollTop-_.y,width:p.width,height:p.height}}o(uS,"getCompositeRect");function uI(e){var t=new Map,n=new Set,l=[];e.forEach(function(m){t.set(m.name,m)});function d(m){n.add(m.name);var p=[].concat(m.requires||[],m.requiresIfExists||[]);p.forEach(function(x){if(!n.has(x)){var _=t.get(x);_&&d(_)}}),l.push(m)}return o(d,"sort"),e.forEach(function(m){n.has(m.name)||d(m)}),l}o(uI,"order");function fS(e){var t=uI(e);return Rk.reduce(function(n,l){return n.concat(t.filter(function(d){return d.phase===l}))},[])}o(fS,"orderModifiers");function cS(e){var t;return function(){return t||(t=new Promise(function(n){Promise.resolve().then(function(){t=void 0,n(e())})})),t}}o(cS,"debounce");function pS(e){var t=e.reduce(function(n,l){var d=n[l.name];return n[l.name]=d?Object.assign({},d,l,{options:Object.assign({},d.options,l.options),data:Object.assign({},d.data,l.data)}):l,n},{});return Object.keys(t).map(function(n){return t[n]})}o(pS,"mergeByName");var Xk={placement:"bottom",modifiers:[],strategy:"absolute"};function Qk(){for(var e=arguments.length,t=new Array(e),n=0;ndi.default.createElement("li",{role:"separator",className:"divider"}),"Divider");function hi(l){var d=l,{onClick:e,children:t}=d,n=Ds(d,["onClick","children"]);return di.default.createElement("li",null,di.default.createElement("a",Pe({href:"#",onClick:o(p=>{p.preventDefault(),e()},"click")},n),t))}o(hi,"MenuItem");var ou=di.default.memo(o(function(x){var _=x,{text:t,children:n,options:l,className:d,onOpen:m}=_,p=Ds(_,["text","children","options","className","onOpen"]);let[O,D]=(0,di.useState)(null),[Y,U]=(0,di.useState)(!1),[X,te]=(0,di.useState)(null),{styles:Q,attributes:F}=hS(O,X,Pe({},l)),M=o(K=>{U(K),m&&m(K)},"setOpen");(0,di.useEffect)(()=>{!X||document.addEventListener("click",K=>{X.contains(K.target)?document.addEventListener("click",()=>M(!1),{once:!0}):(K.preventDefault(),K.stopPropagation(),M(!1))},{once:!0,capture:!0})},[X]);let R;return Y?R=di.default.createElement("ul",Pe({className:"dropdown-menu show",ref:te,style:Q.popper},F.popper),n):R=null,di.default.createElement(di.default.Fragment,null,di.default.createElement("a",Pe({href:"#",ref:D,className:(0,rO.default)(d,{open:Y}),onClick:K=>{K.preventDefault(),M(!0)}},p),t),R)},"Dropdown"));function Kh({value:e,onChange:t}){let n=it(d=>d.conf.contentViews||[]),l=Of.default.createElement("span",null,Of.default.createElement("i",{className:"fa fa-fw fa-files-o"}),"\xA0",Of.default.createElement("b",null,"View:")," ",e.toLowerCase()," ",Of.default.createElement("span",{className:"caret"}));return Of.default.createElement(ou,{text:l,className:"btn btn-default btn-xs",options:{placement:"top-start"}},n.map(d=>Of.default.createElement(hi,{key:d,onClick:()=>t(d)},d.toLowerCase().replace("_"," "))))}o(Kh,"ViewSelector");function mS({flow:e,message:t}){let n=Vt(),l=e.request===t?"request":"response",d=it(te=>te.ui.flow.contentViewFor[e.id+l]||"Auto"),m=(0,Xt.useRef)(null),[p,x]=(0,Xt.useState)(xy),_=(0,Xt.useCallback)(()=>x(Math.max(1024,p*2)),[p]),[O,D]=(0,Xt.useState)(!1),Y;O?Y=Ur.getContentURL(e,t):Y=Ur.getContentURL(e,t,d,p+1);let U=Sy(Y,t.contentHash),X=(0,Xt.useMemo)(()=>{if(U&&!O)try{return JSON.parse(U)}catch(te){return{description:"Network Error",lines:[[["error",`${U}`]]]}}else return},[U]);if(O)return Xt.default.createElement("div",{className:"contentview",key:"edit"},Xt.default.createElement("div",{className:"controls"},Xt.default.createElement("h5",null,"[Editing]"),Xt.default.createElement(Cr,{onClick:o(()=>Na(this,null,function*(){var F;let Q=(F=m.current)==null?void 0:F.getContent();yield n(Ri(e,{[l]:{content:Q}})),D(!1)}),"save"),icon:"fa-check text-success",className:"btn-xs"},"Done"),"\xA0",Xt.default.createElement(Cr,{onClick:()=>D(!1),icon:"fa-times text-danger",className:"btn-xs"},"Cancel")),Xt.default.createElement(Bh,{ref:m,initialContent:U||""}));{let te=X?X.description:"Loading...";return Xt.default.createElement("div",{className:"contentview",key:"view"},Xt.default.createElement("div",{className:"controls"},Xt.default.createElement("h5",null,te),Xt.default.createElement(Cr,{onClick:()=>D(!0),icon:"fa-edit",className:"btn-xs"},"Edit"),"\xA0",Xt.default.createElement(Cy,{icon:"fa-upload",text:"Replace",title:"Upload a file to replace the content.",onOpenFile:Q=>n(qT(e,Q,l)),className:"btn btn-default btn-xs"}),"\xA0",Xt.default.createElement(Kh,{value:d,onChange:Q=>n(Vg(e.id+l,Q))})),vS.matches(t)&&Xt.default.createElement(vS,{flow:e,message:t}),Xt.default.createElement(_y,{lines:(X==null?void 0:X.lines)||[],maxLines:p,showMore:_}))}}o(mS,"HttpMessage");var vI=/^image\/(png|jpe?g|gif|webp|vnc.microsoft.icon|x-icon)$/i;vS.matches=e=>vI.test(Ur.getContentType(e)||"");function vS({flow:e,message:t}){return Xt.default.createElement("div",{className:"flowview-image"},Xt.default.createElement("img",{src:Ur.getContentURL(e,t),alt:"preview",className:"img-thumbnail"}))}o(vS,"ViewImage");function gI({flow:e}){let t=Vt();return Bt.createElement("div",{className:"first-line request-line"},Bt.createElement("div",null,Bt.createElement(xf,{content:e.request.method,onEditDone:n=>t(Ri(e,{request:{method:n}})),isValid:n=>n.length>0}),"\xA0",Bt.createElement(xf,{content:ko.pretty_url(e.request),onEditDone:n=>t(Ri(e,{request:Pe({path:""},Lx(n))})),isValid:n=>{var l;return!!((l=Lx(n))==null?void 0:l.host)}}),"\xA0",Bt.createElement(xf,{content:e.request.http_version,onEditDone:n=>t(Ri(e,{request:{http_version:n}})),isValid:Nx})))}o(gI,"RequestLine");function yI({flow:e}){let t=Vt();return Bt.createElement("div",{className:"first-line response-line"},Bt.createElement(xf,{content:e.response.http_version,onEditDone:n=>t(Ri(e,{response:{http_version:n}})),isValid:Nx}),"\xA0",Bt.createElement(xf,{content:e.response.status_code+"",onEditDone:n=>t(Ri(e,{response:{code:parseInt(n)}})),isValid:n=>/^\d+$/.test(n)}),e.response.http_version!=="HTTP/2.0"&&Bt.createElement(Bt.Fragment,null,"\xA0",Bt.createElement(js,{content:e.response.reason,onEditDone:n=>t(Ri(e,{response:{msg:n}}))})))}o(yI,"ResponseLine");function wI({flow:e,message:t}){let n=Vt(),l=e.request===t?"request":"response";return Bt.createElement(yp,{className:"headers",data:t.headers,onChange:d=>n(Ri(e,{[l]:{headers:d}}))})}o(wI,"Headers");function xI({flow:e,message:t}){let n=Vt(),l=e.request===t?"request":"response";return!Ur.get_first_header(t,/^trailer$/i)?null:Bt.createElement(Bt.Fragment,null,Bt.createElement("hr",null),Bt.createElement("h5",null,"HTTP Trailers"),Bt.createElement(yp,{className:"trailers",data:t.trailers,onChange:m=>n(Ri(e,{[l]:{trailers:m}}))}))}o(xI,"Trailers");var iO=Bt.memo(o(function({flow:t,message:n}){let l=t.request===n?"request":"response",d=t.request===n?gI:yI;return Bt.createElement("section",{className:l},Bt.createElement(d,{flow:t}),Bt.createElement(wI,{flow:t,message:n}),Bt.createElement("hr",null),Bt.createElement(mS,{key:t.id+l,flow:t,message:n}),Bt.createElement(xI,{flow:t,message:n}))},"Message"));function gS(){let e=it(t=>t.flows.byId[t.flows.selected[0]]);return Bt.createElement(iO,{flow:e,message:e.request})}o(gS,"Request");gS.displayName="Request";function yS(){let e=it(t=>t.flows.byId[t.flows.selected[0]]);return Bt.createElement(iO,{flow:e,message:e.response})}o(yS,"Response");yS.displayName="Response";var Ge=pe(De());var SI=o(({message:e})=>Ge.createElement("div",null,e.query?e.op_code:e.response_code,"\xA0",e.truncation?"(Truncated)":""),"Summary"),CI=o(({message:e})=>Ge.createElement(Ge.Fragment,null,Ge.createElement("h5",null,e.recursion_desired?"Recursive ":"","Question"),Ge.createElement("table",null,Ge.createElement("thead",null,Ge.createElement("tr",null,Ge.createElement("th",null,"Name"),Ge.createElement("th",null,"Type"),Ge.createElement("th",null,"Class"))),Ge.createElement("tbody",null,e.questions.map(t=>Ge.createElement("tr",{key:t.name},Ge.createElement("td",null,t.name),Ge.createElement("td",null,t.type),Ge.createElement("td",null,t.class)))))),"Questions"),wS=o(({name:e,values:t})=>Ge.createElement(Ge.Fragment,null,Ge.createElement("h5",null,e),t.length>0?Ge.createElement("table",null,Ge.createElement("thead",null,Ge.createElement("tr",null,Ge.createElement("th",null,"Name"),Ge.createElement("th",null,"Type"),Ge.createElement("th",null,"Class"),Ge.createElement("th",null,"TTL"),Ge.createElement("th",null,"Data"))),Ge.createElement("tbody",null,t.map(n=>Ge.createElement("tr",{key:n.name},Ge.createElement("td",null,n.name),Ge.createElement("td",null,n.type),Ge.createElement("td",null,n.class),Ge.createElement("td",null,n.ttl),Ge.createElement("td",null,n.data))))):"\u2014"),"ResourceRecords"),oO=o(({type:e,message:t})=>Ge.createElement("section",{className:"dns-"+e},Ge.createElement("div",{className:`first-line ${e}-line`},Ge.createElement(SI,{message:t})),Ge.createElement(CI,{message:t}),Ge.createElement("hr",null),Ge.createElement(wS,{name:`${t.authoritative_answer?"Authoritative ":""}${t.recursion_available?"Recursive ":""}Answer`,values:t.answers}),Ge.createElement("hr",null),Ge.createElement(wS,{name:"Authority",values:t.authorities}),Ge.createElement("hr",null),Ge.createElement(wS,{name:"Additional",values:t.additionals})),"Message");function xS(){let e=it(t=>t.flows.byId[t.flows.selected[0]]);return Ge.createElement(oO,{type:"request",message:e.request})}o(xS,"Request");xS.displayName="Request";function SS(){let e=it(t=>t.flows.byId[t.flows.selected[0]]);return Ge.createElement(oO,{type:"response",message:e.response})}o(SS,"Response");SS.displayName="Response";var Ee=pe(De());function sO({conn:e}){var n,l,d;let t=null;return"address"in e?t=Ee.createElement(Ee.Fragment,null,Ee.createElement("tr",null,Ee.createElement("td",null,"Address:"),Ee.createElement("td",null,(n=e.address)==null?void 0:n.join(":"))),e.peername&&Ee.createElement("tr",null,Ee.createElement("td",null,"Resolved address:"),Ee.createElement("td",null,e.peername.join(":"))),e.sockname&&Ee.createElement("tr",null,Ee.createElement("td",null,"Source address:"),Ee.createElement("td",null,e.sockname.join(":")))):((l=e.peername)==null?void 0:l[0])&&(t=Ee.createElement(Ee.Fragment,null,Ee.createElement("tr",null,Ee.createElement("td",null,"Address:"),Ee.createElement("td",null,(d=e.peername)==null?void 0:d.join(":"))))),Ee.createElement("table",{className:"connection-table"},Ee.createElement("tbody",null,t,e.sni?Ee.createElement("tr",null,Ee.createElement("td",null,Ee.createElement("abbr",{title:"TLS Server Name Indication"},"SNI"),":"),Ee.createElement("td",null,e.sni)):null,e.alpn?Ee.createElement("tr",null,Ee.createElement("td",null,Ee.createElement("abbr",{title:"ALPN protocol negotiated"},"ALPN"),":"),Ee.createElement("td",null,e.alpn)):null,e.tls_version?Ee.createElement("tr",null,Ee.createElement("td",null,"TLS Version:"),Ee.createElement("td",null,e.tls_version)):null,e.cipher?Ee.createElement("tr",null,Ee.createElement("td",null,"TLS Cipher:"),Ee.createElement("td",null,e.cipher)):null))}o(sO,"ConnectionInfo");function lO(e){return Ee.createElement("dl",{className:"cert-attributes"},e.map(([t,n])=>Ee.createElement(Ee.Fragment,{key:t},Ee.createElement("dt",null,t),Ee.createElement("dd",null,n))))}o(lO,"attrList");function _I({flow:e}){var n;let t=(n=e.server_conn)==null?void 0:n.cert;return t?Ee.createElement(Ee.Fragment,null,Ee.createElement("h4",{key:"name"},"Server Certificate"),Ee.createElement("table",{className:"certificate-table"},Ee.createElement("tbody",null,Ee.createElement("tr",null,Ee.createElement("td",null,"Type"),Ee.createElement("td",null,t.keyinfo[0],", ",t.keyinfo[1]," bits")),Ee.createElement("tr",null,Ee.createElement("td",null,"SHA256 digest"),Ee.createElement("td",null,t.sha256)),Ee.createElement("tr",null,Ee.createElement("td",null,"Valid from"),Ee.createElement("td",null,Qi(t.notbefore,{milliseconds:!1}))),Ee.createElement("tr",null,Ee.createElement("td",null,"Valid to"),Ee.createElement("td",null,Qi(t.notafter,{milliseconds:!1}))),Ee.createElement("tr",null,Ee.createElement("td",null,"Subject Alternative Names"),Ee.createElement("td",null,t.altnames.join(", "))),Ee.createElement("tr",null,Ee.createElement("td",null,"Subject"),Ee.createElement("td",null,lO(t.subject))),Ee.createElement("tr",null,Ee.createElement("td",null,"Issuer"),Ee.createElement("td",null,lO(t.issuer))),Ee.createElement("tr",null,Ee.createElement("td",null,"Serial"),Ee.createElement("td",null,t.serial))))):Ee.createElement(Ee.Fragment,null)}o(_I,"CertificateInfo");function Dy({flow:e}){var t;return Ee.createElement("section",{className:"detail"},Ee.createElement("h4",null,"Client Connection"),Ee.createElement(sO,{conn:e.client_conn}),((t=e.server_conn)==null?void 0:t.address)&&Ee.createElement(Ee.Fragment,null,Ee.createElement("h4",null,"Server Connection"),Ee.createElement(sO,{conn:e.server_conn})),Ee.createElement(_I,{flow:e}))}o(Dy,"Connection");Dy.displayName="Connection";var Gh=pe(De());function Ry({flow:e}){return Gh.createElement("section",{className:"error"},Gh.createElement("div",{className:"alert alert-warning"},e.error.msg,Gh.createElement("div",null,Gh.createElement("small",null,Qi(e.error.timestamp)))))}o(Ry,"Error");Ry.displayName="Error";var rs=pe(De());function bI({t:e,deltaTo:t,title:n}){return e?rs.createElement("tr",null,rs.createElement("td",null,n,":"),rs.createElement("td",null,Qi(e),t&&rs.createElement("span",{className:"text-muted"},"(",Gg(1e3*(e-t)),")"))):rs.createElement("tr",null)}o(bI,"TimeStamp");function Fy({flow:e}){var l,d,m,p,x,_;let t;e.type==="http"?t=e.request.timestamp_start:t=e.client_conn.timestamp_start;let n=[{title:"Server conn. initiated",t:(l=e.server_conn)==null?void 0:l.timestamp_start,deltaTo:t},{title:"Server conn. TCP handshake",t:(d=e.server_conn)==null?void 0:d.timestamp_tcp_setup,deltaTo:t},{title:"Server conn. TLS handshake",t:(m=e.server_conn)==null?void 0:m.timestamp_tls_setup,deltaTo:t},{title:"Server conn. closed",t:(p=e.server_conn)==null?void 0:p.timestamp_end,deltaTo:t},{title:"Client conn. established",t:e.client_conn.timestamp_start,deltaTo:e.type==="http"?t:void 0},{title:"Client conn. TLS handshake",t:e.client_conn.timestamp_tls_setup,deltaTo:t},{title:"Client conn. closed",t:e.client_conn.timestamp_end,deltaTo:t}];return e.type==="http"&&n.push({title:"First request byte",t:e.request.timestamp_start},{title:"Request complete",t:e.request.timestamp_end,deltaTo:t},{title:"First response byte",t:(x=e.response)==null?void 0:x.timestamp_start,deltaTo:t},{title:"Response complete",t:(_=e.response)==null?void 0:_.timestamp_end,deltaTo:t}),rs.createElement("section",{className:"timing"},rs.createElement("h4",null,"Timing"),rs.createElement("table",{className:"timing-table"},rs.createElement("tbody",null,n.filter(O=>!!O.t).sort((O,D)=>O.t-D.t).map(O=>rs.createElement(bI,Pe({key:O.title},O))))))}o(Fy,"Timing");Fy.displayName="Timing";var su=pe(De());var Vs=pe(De()),bp=pe(De());function Yh({flow:e,messages_meta:t}){let n=Vt(),l=it(O=>O.ui.flow.contentViewFor[e.id+"messages"]||"Auto"),[d,m]=(0,bp.useState)(xy),p=(0,bp.useCallback)(()=>m(Math.max(1024,d*2)),[d]),x=Sy(Ur.getContentURL(e,"messages",l,d+1),e.id+t.count),_=(0,bp.useMemo)(()=>x&&JSON.parse(x),[x])||[];return Vs.createElement("div",{className:"contentview"},Vs.createElement("div",{className:"controls"},Vs.createElement("h5",null,t.count," Messages"),Vs.createElement(Kh,{value:l,onChange:O=>n(Vg(e.id+"messages",O))})),_.map((O,D)=>{let Y=`fa fa-fw fa-arrow-${O.from_client?"right text-primary":"left text-danger"}`,U=Vs.createElement("div",{key:D},Vs.createElement("small",null,Vs.createElement("i",{className:Y}),Vs.createElement("span",{className:"pull-right"},O.timestamp&&Qi(O.timestamp))),Vs.createElement(_y,{lines:O.lines,maxLines:d,showMore:p}));return d-=O.lines.length,U}))}o(Yh,"Messages");function Iy({flow:e}){return su.createElement("section",{className:"websocket"},su.createElement("h4",null,"WebSocket"),su.createElement(Yh,{flow:e,messages_meta:e.websocket.messages_meta}),su.createElement(EI,{websocket:e.websocket}))}o(Iy,"WebSocket");Iy.displayName="WebSocket";function EI({websocket:e}){if(!e.timestamp_end)return null;let t=e.close_reason?`(${e.close_reason})`:"";return su.createElement("div",null,su.createElement("i",{className:"fa fa-fw fa-window-close text-muted"}),"\xA0 Closed by ",e.closed_by_client?"client":"server"," with code ",e.close_code," ",t,".",su.createElement("small",{className:"pull-right"},Qi(e.timestamp_end)))}o(EI,"CloseSummary");var aO=pe(Xn());var Hy=pe(De());function Wy({flow:e}){return Hy.createElement("section",{className:"tcp"},Hy.createElement("h4",null,"TCP Data"),Hy.createElement(Yh,{flow:e,messages_meta:e.messages_meta}))}o(Wy,"TcpMessages");Wy.displayName="TCP Messages";var uO={request:gS,response:yS,error:Ry,connection:Dy,timing:Fy,websocket:Iy,messages:Wy,dnsrequest:xS,dnsresponse:SS};function By(e){let t;switch(e.type){case"http":t=["request","response","websocket"].filter(n=>e[n]);break;case"tcp":t=["messages"];break;case"dns":t=["request","response"].filter(n=>e[n]).map(n=>"dns"+n);break}return e.error&&t.push("error"),t.push("connection"),t.push("timing"),t}o(By,"tabsForFlow");function CS(){let e=Vt(),t=it(m=>m.flows.byId[m.flows.selected[0]]),n=By(t),l=it(m=>m.ui.flow.tab);n.indexOf(l)<0&&(l==="response"&&t.error?l="error":l==="error"&&"response"in t?l="response":l=n[0]);let d=uO[l];return Xh.createElement("div",{className:"flow-detail"},Xh.createElement("nav",{className:"nav-tabs nav-tabs-sm"},n.map(m=>Xh.createElement("a",{key:m,href:"#",className:(0,aO.default)({active:l===m}),onClick:p=>{p.preventDefault(),e(mf(m))}},uO[m].displayName))),Xh.createElement(d,{flow:t}))}o(CS,"FlowView");function fO(e){if(e.ctrlKey||e.metaKey)return()=>{};let t=e.key;return e.preventDefault(),(n,l)=>{let d=l().flows,m=d.byId[l().flows.selected[0]];switch(t){case"k":case"ArrowUp":n(yf(d,-1));break;case"j":case"ArrowDown":n(yf(d,1));break;case" ":case"PageDown":n(yf(d,10));break;case"PageUp":n(yf(d,-10));break;case"End":n(yf(d,1e10));break;case"Home":n(yf(d,-1e10));break;case"Escape":l().ui.modal.activeModal?n(my()):n(wf(void 0));break;case"ArrowLeft":{if(!m)break;let p=By(m),x=l().ui.flow.tab,_=p[(Math.max(0,p.indexOf(x))-1+p.length)%p.length];n(mf(_));break}case"Tab":case"ArrowRight":{if(!m)break;let p=By(m),x=l().ui.flow.tab,_=p[(Math.max(0,p.indexOf(x))+1)%p.length];n(mf(_));break}case"d":{if(!m)return;n(fy(m));break}case"n":{vf("view.flows.create","get","https://example.com/");break}case"D":{if(!m)return;n(cy(m));break}case"a":{m&&m.intercepted&&n(cp(m));break}case"A":{n(ay());break}case"r":{m&&n(pp(m));break}case"v":{m&&m.modified&&n(py(m));break}case"x":{m&&m.intercepted&&n(uy(m));break}case"X":{n(jT());break}case"z":{n(dy());break}default:return}}}o(fO,"onKeyDown");var nm=pe(De());var Qh=pe(De()),Zh=pe(Za()),cO=pe(Xn()),Ep=class extends Qh.Component{constructor(t,n){super(t,n);this.state={applied:!1,startX:0,startY:0},this.onMouseMove=this.onMouseMove.bind(this),this.onMouseDown=this.onMouseDown.bind(this),this.onMouseUp=this.onMouseUp.bind(this),this.onDragEnd=this.onDragEnd.bind(this)}onMouseDown(t){this.setState({startX:t.pageX,startY:t.pageY}),window.addEventListener("mousemove",this.onMouseMove),window.addEventListener("mouseup",this.onMouseUp),window.addEventListener("dragend",this.onDragEnd)}onDragEnd(){Zh.default.findDOMNode(this).style.transform="",window.removeEventListener("dragend",this.onDragEnd),window.removeEventListener("mouseup",this.onMouseUp),window.removeEventListener("mousemove",this.onMouseMove)}onMouseUp(t){this.onDragEnd();let n=Zh.default.findDOMNode(this),l=n.previousElementSibling,d=l.offsetHeight+t.pageY-this.state.startY;this.props.axis==="x"&&(d=l.offsetWidth+t.pageX-this.state.startX),l.style.flex=`0 0 ${Math.max(0,d)}px`,n.nextElementSibling.style.flex="1 1 auto",this.setState({applied:!0}),this.onResize()}onMouseMove(t){let n=0,l=0;this.props.axis==="x"?n=t.pageX-this.state.startX:l=t.pageY-this.state.startY,Zh.default.findDOMNode(this).style.transform=`translate(${n}px, ${l}px)`}onResize(){window.setTimeout(()=>window.dispatchEvent(new CustomEvent("resize")),1)}reset(t){if(!this.state.applied)return;let n=Zh.default.findDOMNode(this);n.previousElementSibling.style.flex="",n.nextElementSibling.style.flex="",t||this.setState({applied:!1}),this.onResize()}componentWillUnmount(){this.reset(!0)}render(){return Qh.default.createElement("div",{className:(0,cO.default)("splitter",this.props.axis==="x"?"splitter-x":"splitter-y")},Qh.default.createElement("div",{onMouseDown:this.onMouseDown,draggable:"true"}))}};o(Ep,"Splitter"),Ep.defaultProps={axis:"x"};var ns=pe(De()),tm=pe(Jh()),$y=pe(Za());var TO=pe(_S());var bS=pe(Za()),xO=Symbol("shouldStick"),SO=o(e=>e.scrollTop+e.clientHeight===e.scrollHeight,"isAtBottom"),Uy=o(e=>{var t;return Object.assign((o(t=class extends e{UNSAFE_componentWillUpdate(){let l=bS.default.findDOMNode(this);this[xO]=l.scrollTop&&SO(l),super.UNSAFE_componentWillUpdate&&super.UNSAFE_componentWillUpdate(),super.componentWillUpdate&&super.componentWillUpdate()}componentDidUpdate(){let l=bS.default.findDOMNode(this);this[xO]&&!SO(l)&&(l.scrollTop=l.scrollHeight),super.componentDidUpdate&&super.componentDidUpdate()}},"AutoScrollWrapper"),t.displayName=e.name,t),e)},"default");function Tp(e=void 0){if(!e)return{start:0,end:0,paddingTop:0,paddingBottom:0};let{itemCount:t,rowHeight:n,viewportTop:l,viewportHeight:d,itemHeights:m}=e,p=l+d,x=0,_=0,O=0,D=0;if(m)for(let Y=0,U=0;Yx.flows.sort.desc),l=it(x=>x.flows.sort.column),d=it(x=>x.options.web_columns),m=n?"sort-desc":"sort-asc",p=d.map(x=>Dh[x]).filter(x=>x).concat(fp);return em.createElement("tr",null,p.map(x=>em.createElement("th",{className:(0,CO.default)(`col-${x.name}`,l===x.name&&m),key:x.name,onClick:()=>t(zT(x.name===l&&n?void 0:x.name,x.name!==l?!1:!n))},x.headerName)))},"FlowTableHead"));var kp=pe(De()),bO=pe(Xn());var EO=kp.default.memo(o(function({flow:t,selected:n,highlighted:l}){let d=Vt(),m=it(O=>O.options.web_columns),p=(0,bO.default)({selected:n,highlighted:l,intercepted:t.intercepted,"has-request":t.type==="http"&&t.request,"has-response":t.type==="http"&&t.response}),x=(0,kp.useCallback)(O=>{let D=O.target;for(;D.parentNode;){if(D.classList.contains("col-quickactions"))return;D=D.parentNode}d(wf(t.id))},[t]),_=m.map(O=>Dh[O]).filter(O=>O).concat(fp);return kp.default.createElement("tr",{className:p,onClick:x},_.map(O=>kp.default.createElement(O,{key:O.name,flow:t})))},"FlowRow"));var rm=class extends ns.Component{constructor(t,n){super(t,n);this.state={vScroll:Tp()},this.onViewportUpdate=this.onViewportUpdate.bind(this)}UNSAFE_componentWillMount(){window.addEventListener("resize",this.onViewportUpdate)}UNSAFE_componentWillUnmount(){window.removeEventListener("resize",this.onViewportUpdate)}componentDidUpdate(){if(this.onViewportUpdate(),!this.shouldScrollIntoView)return;this.shouldScrollIntoView=!1;let{rowHeight:t,flows:n,selected:l}=this.props,d=$y.default.findDOMNode(this),m=$y.default.findDOMNode(this.refs.head),p=m?m.offsetHeight:0,x=n.indexOf(l)*t+p,_=x+t,O=d.scrollTop,D=d.offsetHeight;x-pO+D&&(d.scrollTop=_-D)}UNSAFE_componentWillReceiveProps(t){t.selected&&t.selected!==this.props.selected&&(this.shouldScrollIntoView=!0)}onViewportUpdate(){let t=$y.default.findDOMNode(this),n=t.scrollTop||0,l=Tp({viewportTop:n,viewportHeight:t.offsetHeight||0,itemCount:this.props.flows.length,rowHeight:this.props.rowHeight});(this.state.viewportTop!==n||!(0,TO.default)(this.state.vScroll,l))&&this.setState({vScroll:l,viewportTop:n})}render(){let{vScroll:t,viewportTop:n}=this.state,{flows:l,selected:d,highlight:m}=this.props,p=m?gf.parse(m):()=>!1;return ns.createElement("div",{className:"flow-table",onScroll:this.onViewportUpdate},ns.createElement("table",null,ns.createElement("thead",{ref:"head",style:{transform:`translateY(${n}px)`}},ns.createElement(_O,null)),ns.createElement("tbody",null,ns.createElement("tr",{style:{height:t.paddingTop}}),l.slice(t.start,t.end).map(x=>ns.createElement(EO,{key:x.id,flow:x,selected:x===d,highlighted:p(x)})),ns.createElement("tr",{style:{height:t.paddingBottom}}))))}};o(rm,"FlowTable"),Tc(rm,"propTypes",{flows:tm.default.array.isRequired,rowHeight:tm.default.number,highlight:tm.default.string,selected:tm.default.object}),Tc(rm,"defaultProps",{rowHeight:32});var OI=Uy(rm),kO=Di(e=>({flows:e.flows.view,highlight:e.flows.highlight,selected:e.flows.byId[e.flows.selected[0]]}))(OI);function ES(){let e=it(t=>!!t.flows.byId[t.flows.selected[0]]);return nm.createElement("div",{className:"main-view"},nm.createElement(kO,null),e&&nm.createElement(Ep,{key:"splitter"}),e&&nm.createElement(CS,{key:"flowDetails"}))}o(ES,"MainView");var Po=pe(De()),DO=pe(Xn());var Jn=pe(De());var is=pe(De()),zy=pe(Za()),OO=pe(Xn());var Ji=pe(De());var eo=class extends Ji.Component{constructor(t,n){super(t,n);this.state={doc:eo.doc}}componentDidMount(){eo.xhr||(eo.xhr=Ot("/filter-help").then(t=>t.json()),eo.xhr.catch(()=>{eo.xhr=null})),this.state.doc||eo.xhr.then(t=>{eo.doc=t,this.setState({doc:t})})}render(){let{doc:t}=this.state;return t?Ji.default.createElement("table",{className:"table table-condensed"},Ji.default.createElement("tbody",null,t.commands.map(n=>Ji.default.createElement("tr",{key:n[1],onClick:l=>this.props.selectHandler(n[0].split(" ")[0]+" ")},Ji.default.createElement("td",null,n[0].replace(" ","\xA0")),Ji.default.createElement("td",null,n[1]))),Ji.default.createElement("tr",{key:"docs-link"},Ji.default.createElement("td",{colSpan:2},Ji.default.createElement("a",{href:"https://mitmproxy.org/docs/latest/concepts-filters/",target:"_blank"},Ji.default.createElement("i",{className:"fa fa-external-link"}),"\xA0 mitmproxy docs"))))):Ji.default.createElement("i",{className:"fa fa-spinner fa-spin"})}};o(eo,"FilterDocs");var Lf=class extends is.Component{constructor(t,n){super(t,n);this.state={value:this.props.value,focus:!1,mousefocus:!1},this.onChange=this.onChange.bind(this),this.onFocus=this.onFocus.bind(this),this.onBlur=this.onBlur.bind(this),this.onKeyDown=this.onKeyDown.bind(this),this.onMouseEnter=this.onMouseEnter.bind(this),this.onMouseLeave=this.onMouseLeave.bind(this),this.selectFilter=this.selectFilter.bind(this)}UNSAFE_componentWillReceiveProps(t){this.setState({value:t.value})}isValid(t){try{return t&&gf.parse(t),!0}catch(n){return!1}}getDesc(){if(!this.state.value)return is.default.createElement(eo,{selectHandler:this.selectFilter});try{return gf.parse(this.state.value).desc}catch(t){return""+t}}onChange(t){let n=t.target.value;this.setState({value:n}),this.isValid(n)&&this.props.onChange(n)}onFocus(){this.setState({focus:!0})}onBlur(){this.setState({focus:!1})}onMouseEnter(){this.setState({mousefocus:!0})}onMouseLeave(){this.setState({mousefocus:!1})}onKeyDown(t){(t.key==="Escape"||t.key==="Enter")&&(this.blur(),this.setState({mousefocus:!1})),t.stopPropagation()}selectFilter(t){this.setState({value:t}),zy.default.findDOMNode(this.refs.input).focus()}blur(){zy.default.findDOMNode(this.refs.input).blur()}select(){zy.default.findDOMNode(this.refs.input).select()}render(){let{type:t,color:n,placeholder:l}=this.props,{value:d,focus:m,mousefocus:p}=this.state;return is.default.createElement("div",{className:(0,OO.default)("filter-input input-group",{"has-error":!this.isValid(d)})},is.default.createElement("span",{className:"input-group-addon"},is.default.createElement("i",{className:"fa fa-fw fa-"+t,style:{color:n}})),is.default.createElement("input",{type:"text",ref:"input",placeholder:l,className:"form-control",value:d,onChange:this.onChange,onFocus:this.onFocus,onBlur:this.onBlur,onKeyDown:this.onKeyDown}),(m||p)&&is.default.createElement("div",{className:"popover bottom",onMouseEnter:this.onMouseEnter,onMouseLeave:this.onMouseLeave},is.default.createElement("div",{className:"arrow"}),is.default.createElement("div",{className:"popover-content"},this.getDesc())))}};o(Lf,"FilterInput");Op.title="Start";function Op(){return Jn.createElement("div",{className:"main-menu"},Jn.createElement("div",{className:"menu-group"},Jn.createElement("div",{className:"menu-content"},Jn.createElement(NI,null),Jn.createElement(PI,null)),Jn.createElement("div",{className:"menu-legend"},"Find")),Jn.createElement("div",{className:"menu-group"},Jn.createElement("div",{className:"menu-content"},Jn.createElement(LI,null),Jn.createElement(MI,null)),Jn.createElement("div",{className:"menu-legend"},"Intercept")))}o(Op,"StartMenu");function LI(){let e=Vt(),t=it(n=>n.options.intercept);return Jn.createElement(Lf,{value:t||"",placeholder:"Intercept",type:"pause",color:"hsl(208, 56%, 53%)",onChange:n=>e(vp("intercept",n))})}o(LI,"InterceptInput");function NI(){let e=Vt(),t=it(n=>n.flows.filter);return Jn.createElement(Lf,{value:t||"",placeholder:"Search",type:"search",color:"black",onChange:n=>e(sy(n))})}o(NI,"FlowFilterInput");function PI(){let e=Vt(),t=it(n=>n.flows.highlight);return Jn.createElement(Lf,{value:t||"",placeholder:"Highlight",type:"tag",color:"hsl(48, 100%, 50%)",onChange:n=>e(ly(n))})}o(PI,"HighlightInput");function MI(){let e=Vt();return Jn.createElement(Cr,{className:"btn-sm",title:"[a]ccept all",icon:"fa-forward text-success",onClick:()=>e(ay())},"Resume All")}o(MI,"ResumeAll");var jr=pe(De());var Nf=pe(De());function TS({value:e,onChange:t,children:n}){return Nf.createElement("div",{className:"menu-entry"},Nf.createElement("label",null,Nf.createElement("input",{type:"checkbox",checked:e,onChange:t}),n))}o(TS,"MenuToggle");function jy({name:e,children:t}){let n=Vt(),l=it(d=>d.options[e]);return Nf.createElement(TS,{value:!!l,onChange:()=>n(vp(e,!l))},t)}o(jy,"OptionsToggle");function LO(){let e=$s(),t=it(n=>n.eventLog.visible);return Nf.createElement(TS,{value:t,onChange:()=>e(mp())},"Display Event Log")}o(LO,"EventlogToggle");function NO(){let e=$s(),t=it(n=>n.commandBar.visible);return Nf.createElement(TS,{value:t,onChange:()=>e(wy())},"Display Command Bar")}o(NO,"CommandBarToggle");var kS=pe(De());function OS({children:e,resource:t}){let n=`https://docs.mitmproxy.org/stable/${t}`;return kS.createElement("a",{target:"_blank",href:n},e||kS.createElement("i",{className:"fa fa-question-circle"}))}o(OS,"DocsLink");var qy=pe(De());function No({children:e}){return window.MITMWEB_CONF&&window.MITMWEB_CONF.static?null:qy.createElement(qy.Fragment,null,e)}o(No,"HideInStatic");Vy.title="Options";function Vy(){let e=Vt(),t=o(()=>YT("OptionModal"),"openOptions");return jr.createElement("div",null,jr.createElement(No,null,jr.createElement("div",{className:"menu-group"},jr.createElement("div",{className:"menu-content"},jr.createElement(Cr,{title:"Open Options",icon:"fa-cogs text-primary",onClick:()=>e(t())},"Edit Options ",jr.createElement("sup",null,"alpha"))),jr.createElement("div",{className:"menu-legend"},"Options Editor")),jr.createElement("div",{className:"menu-group"},jr.createElement("div",{className:"menu-content"},jr.createElement(jy,{name:"anticache"},"Strip cache headers ",jr.createElement(OS,{resource:"overview-features/#anticache"})),jr.createElement(jy,{name:"showhost"},"Use host header for display"),jr.createElement(jy,{name:"ssl_insecure"},"Don't verify server certificates")),jr.createElement("div",{className:"menu-legend"},"Quick Options"))),jr.createElement("div",{className:"menu-group"},jr.createElement("div",{className:"menu-content"},jr.createElement(LO,null),jr.createElement(NO,null)),jr.createElement("div",{className:"menu-legend"},"View Options")))}o(Vy,"OptionMenu");var mi=pe(De());var PO=mi.memo(o(function(){let t=$s();return mi.createElement(ou,{className:"pull-left special",text:"File",options:{placement:"bottom-start"}},mi.createElement("li",null,mi.createElement(Cy,{icon:"fa-folder-open",text:"\xA0Open...",onClick:n=>n.stopPropagation(),onOpenFile:n=>{t(KT(n)),document.body.click()}})),mi.createElement(hi,{onClick:()=>t(VT())},mi.createElement("i",{className:"fa fa-fw fa-floppy-o"}),"\xA0Save..."),mi.createElement(hi,{onClick:()=>confirm("Delete all flows?")&&t(dy())},mi.createElement("i",{className:"fa fa-fw fa-trash"}),"\xA0Clear All"),mi.createElement(No,null,mi.createElement(nO,null),mi.createElement("li",null,mi.createElement("a",{href:"http://mitm.it/",target:"_blank"},mi.createElement("i",{className:"fa fa-fw fa-external-link"}),"\xA0Install Certificates..."))))},"FileMenu"));var lt=pe(De());function MO(e){if(navigator.clipboard&&window.isSecureContext)return navigator.clipboard.writeText(e);{let t=document.createElement("textarea");t.value=e,t.style.position="absolute",t.style.opacity="0",document.body.appendChild(t);try{return t.focus(),t.select(),document.execCommand("copy"),Promise.resolve()}catch(n){return alert(e),Promise.reject(n)}finally{t.remove()}}}o(MO,"copyToClipboard");var Lp=o((e,t)=>Na(void 0,null,function*(){let n=yield vf("export",t,`@${e.id}`);n.value?yield MO(n.value):n.error?alert(n.error):console.error(n)}),"copy");Np.title="Flow";function Np(){let e=Vt(),t=it(n=>n.flows.byId[n.flows.selected[0]]);return t?lt.createElement("div",{className:"flow-menu"},lt.createElement(No,null,lt.createElement("div",{className:"menu-group"},lt.createElement("div",{className:"menu-content"},lt.createElement(Cr,{title:"[r]eplay flow",icon:"fa-repeat text-primary",onClick:()=>e(pp(t)),disabled:!Qg(t)},"Replay"),lt.createElement(Cr,{title:"[D]uplicate flow",icon:"fa-copy text-info",onClick:()=>e(cy(t))},"Duplicate"),lt.createElement(Cr,{disabled:!t||!t.modified,title:"revert changes to flow [V]",icon:"fa-history text-warning",onClick:()=>e(py(t))},"Revert"),lt.createElement(Cr,{title:"[d]elete flow",icon:"fa-trash text-danger",onClick:()=>e(fy(t))},"Delete"),lt.createElement(FI,{flow:t})),lt.createElement("div",{className:"menu-legend"},"Flow Modification"))),lt.createElement("div",{className:"menu-group"},lt.createElement("div",{className:"menu-content"},lt.createElement(AI,{flow:t}),lt.createElement(DI,{flow:t})),lt.createElement("div",{className:"menu-legend"},"Export")),lt.createElement(No,null,lt.createElement("div",{className:"menu-group"},lt.createElement("div",{className:"menu-content"},lt.createElement(Cr,{disabled:!t||!t.intercepted,title:"[a]ccept intercepted flow",icon:"fa-play text-success",onClick:()=>e(cp(t))},"Resume"),lt.createElement(Cr,{disabled:!t||!t.intercepted,title:"kill intercepted flow [x]",icon:"fa-times text-danger",onClick:()=>e(uy(t))},"Abort")),lt.createElement("div",{className:"menu-legend"},"Interception")))):lt.createElement("div",null)}o(Np,"FlowMenu");var Ky=o(e=>{let t=window.open(e,"_blank","noopener,noreferrer");t&&(t.opener=null)},"openInNewTab");function AI({flow:e}){var t;if(e.type!=="http")return lt.createElement(Cr,{icon:"fa-download",onClick:()=>0,disabled:!0},"Download");if(e.request.contentLength&&!((t=e.response)==null?void 0:t.contentLength))return lt.createElement(Cr,{icon:"fa-download",onClick:()=>Ky(Ur.getContentURL(e,e.request))},"Download");if(e.response){let n=e.response;if(!e.request.contentLength&&e.response.contentLength)return lt.createElement(Cr,{icon:"fa-download",onClick:()=>Ky(Ur.getContentURL(e,n))},"Download");if(e.request.contentLength&&e.response.contentLength)return lt.createElement(ou,{text:lt.createElement(Cr,{icon:"fa-download",onClick:()=>1},"Download\u25BE"),options:{placement:"bottom-start"}},lt.createElement(hi,{onClick:()=>Ky(Ur.getContentURL(e,e.request))},"Download request"),lt.createElement(hi,{onClick:()=>Ky(Ur.getContentURL(e,n))},"Download response"))}return null}o(AI,"DownloadButton");function DI({flow:e}){return lt.createElement(ou,{className:"",text:lt.createElement(Cr,{title:"Export flow.",icon:"fa-clone",onClick:()=>1,disabled:e.type!=="http"},"Export\u25BE"),options:{placement:"bottom-start"}},lt.createElement(hi,{onClick:()=>Lp(e,"raw_request")},"Copy raw request"),lt.createElement(hi,{onClick:()=>Lp(e,"raw_response")},"Copy raw response"),lt.createElement(hi,{onClick:()=>Lp(e,"raw")},"Copy raw request and response"),lt.createElement(hi,{onClick:()=>Lp(e,"curl")},"Copy as cURL"),lt.createElement(hi,{onClick:()=>Lp(e,"httpie")},"Copy as HTTPie"))}o(DI,"ExportButton");var RI={":red_circle:":"\u{1F534}",":orange_circle:":"\u{1F7E0}",":yellow_circle:":"\u{1F7E1}",":green_circle:":"\u{1F7E2}",":large_blue_circle:":"\u{1F535}",":purple_circle:":"\u{1F7E3}",":brown_circle:":"\u{1F7E4}"};function FI({flow:e}){let t=Vt();return lt.createElement(ou,{className:"",text:lt.createElement(Cr,{title:"mark flow",icon:"fa-paint-brush text-success",onClick:()=>1},"Mark\u25BE"),options:{placement:"bottom-start"}},lt.createElement(hi,{onClick:()=>t(Ri(e,{marked:""}))},"\u26AA (no marker)"),Object.entries(RI).map(([n,l])=>lt.createElement(hi,{key:n,onClick:()=>t(Ri(e,{marked:n}))},l," ",n.replace(/[:_]/g," "))))}o(FI,"MarkButton");var lu=pe(De());var AO=lu.memo(o(function(){let t=it(l=>l.connection.state),n=it(l=>l.connection.message);switch(t){case Zn.INIT:return lu.createElement("span",{className:"connection-indicator init"},"connecting\u2026");case Zn.FETCHING:return lu.createElement("span",{className:"connection-indicator fetching"},"fetching data\u2026");case Zn.ESTABLISHED:return lu.createElement("span",{className:"connection-indicator established"},"connected");case Zn.ERROR:return lu.createElement("span",{className:"connection-indicator error",title:n},"connection lost");case Zn.OFFLINE:return lu.createElement("span",{className:"connection-indicator offline"},"offline");default:let l=t;throw"unknown connection state"}},"ConnectionIndicator"));function LS(){let e=it(x=>x.flows.selected.filter(_=>_ in x.flows.byId)),[t,n]=(0,Po.useState)(()=>Op),[l,d]=(0,Po.useState)(!1),m=[Op,Vy];e.length>0?(l||(n(()=>Np),d(!0)),m.push(Np)):(l&&d(!1),t===Np&&n(()=>Op));function p(x,_){_.preventDefault(),n(()=>x)}return o(p,"handleClick"),Po.default.createElement("header",null,Po.default.createElement("nav",{className:"nav-tabs nav-tabs-lg"},Po.default.createElement(PO,null),m.map(x=>Po.default.createElement("a",{key:x.title,href:"#",className:(0,DO.default)({active:x===t}),onClick:_=>p(x,_)},x.title)),Po.default.createElement(No,null,Po.default.createElement(AO,null))),Po.default.createElement("div",null,Po.default.createElement(t,null)))}o(LS,"Header");var Ze=pe(De()),RO=pe(Xn());var Gy=function(){"use strict";function e(l,d){function m(){this.constructor=l}o(m,"ctor"),m.prototype=d.prototype,l.prototype=new m}o(e,"peg$subclass");function t(l,d,m,p){this.message=l,this.expected=d,this.found=m,this.location=p,this.name="SyntaxError",typeof Error.captureStackTrace=="function"&&Error.captureStackTrace(this,t)}o(t,"peg$SyntaxError"),e(t,Error);function n(l){var d=arguments.length>1?arguments[1]:{},m=this,p={},x={Expr:vr},_=vr,O=o(function(H,J){return[H,...J]},"peg$c0"),D=o(function(H){return[H]},"peg$c1"),Y=o(function(){return""},"peg$c2"),U={type:"other",description:"string"},X='"',te={type:"literal",value:'"',description:'"\\""'},Q=o(function(H){return H.join("")},"peg$c6"),F="'",M={type:"literal",value:"'",description:`"'"`},R=/^["\\]/,K={type:"class",value:'["\\\\]',description:'["\\\\]'},V={type:"any",description:"any character"},ue=o(function(H){return H},"peg$c12"),ie="\\",de={type:"literal",value:"\\",description:'"\\\\"'},ge=/^['\\]/,xe={type:"class",value:"['\\\\]",description:"['\\\\]"},qe=/^['"\\]/,et={type:"class",value:`['"\\\\]`,description:`['"\\\\]`},Te="n",xt={type:"literal",value:"n",description:'"n"'},Ue=o(function(){return` +`},"peg$c21"),Ve="r",Ke={type:"literal",value:"r",description:'"r"'},Ye=o(function(){return"\r"},"peg$c24"),Qt="t",ft={type:"literal",value:"t",description:'"t"'},Ar=o(function(){return" "},"peg$c27"),Kt={type:"other",description:"whitespace"},Et=/^[ \t\n\r]/,St={type:"class",value:"[ \\t\\n\\r]",description:"[ \\t\\n\\r]"},at={type:"other",description:"control character"},_r=/^[|&!()~"]/,Ut={type:"class",value:'[|&!()~"]',description:'[|&!()~"]'},$t={type:"other",description:"optional whitespace"},ne=0,tt=0,br=[{line:1,column:1,seenCR:!1}],jt=0,qt=[],Se=0,Er;if("startRule"in d){if(!(d.startRule in x))throw new Error(`Can't start parsing from rule "`+d.startRule+'".');_=x[d.startRule]}function nn(){return l.substring(tt,ne)}o(nn,"text");function Fn(){return dr(tt,ne)}o(Fn,"location");function ei(H){throw Do(null,[{type:"other",description:H}],l.substring(tt,ne),dr(tt,ne))}o(ei,"expected");function on(H){throw Do(H,null,l.substring(tt,ne),dr(tt,ne))}o(on,"error");function Gt(H){var J=br[H],he,ke;if(J)return J;for(he=H-1;!br[he];)he--;for(J=br[he],J={line:J.line,column:J.column,seenCR:J.seenCR};hejt&&(jt=ne,qt=[]),qt.push(H))}o(ct,"peg$fail");function Do(H,J,he,ke){function Zt(Rt){var Dr=1;for(Rt.sort(function(Jt,ti){return Jt.descriptionti.description?1:0});Dr1?ti.slice(0,-1).join(", ")+" or "+ti[Rt.length-1]:ti[0],to=Dr?'"'+Jt(Dr)+'"':"end of input","Expected "+os+" but "+to+" found."}return o(Bl,"buildMessage"),J!==null&&Zt(J),new t(H!==null?H:Bl(J,he),J,he,ke)}o(Do,"peg$buildException");function vr(){var H,J,he,ke;if(H=ne,J=Fi(),J!==p){if(he=[],ke=Hn(),ke!==p)for(;ke!==p;)he.push(ke),ke=Hn();else he=p;he!==p?(ke=vr(),ke!==p?(tt=H,J=O(J,ke),H=J):(ne=H,H=p)):(ne=H,H=p)}else ne=H,H=p;if(H===p&&(H=ne,J=Fi(),J!==p&&(tt=H,J=D(J)),H=J,H===p)){for(H=ne,J=[],he=Hn();he!==p;)J.push(he),he=Hn();J!==p&&(tt=H,J=Y()),H=J}return H}o(vr,"peg$parseExpr");function Fi(){var H,J,he,ke;if(Se++,H=ne,l.charCodeAt(ne)===34?(J=X,ne++):(J=p,Se===0&&ct(te)),J!==p){for(he=[],ke=sn();ke!==p;)he.push(ke),ke=sn();he!==p?(l.charCodeAt(ne)===34?(ke=X,ne++):(ke=p,Se===0&&ct(te)),ke!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)):(ne=H,H=p)}else ne=H,H=p;if(H===p){if(H=ne,l.charCodeAt(ne)===39?(J=F,ne++):(J=p,Se===0&&ct(M)),J!==p){for(he=[],ke=In();ke!==p;)he.push(ke),ke=In();he!==p?(l.charCodeAt(ne)===39?(ke=F,ne++):(ke=p,Se===0&&ct(M)),ke!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)):(ne=H,H=p)}else ne=H,H=p;if(H===p){if(H=ne,J=ne,Se++,he=En(),Se--,he===p?J=void 0:(ne=J,J=p),J!==p){if(he=[],ke=vi(),ke!==p)for(;ke!==p;)he.push(ke),ke=vi();else he=p;he!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)}else ne=H,H=p;if(H===p){if(H=ne,l.charCodeAt(ne)===34?(J=X,ne++):(J=p,Se===0&&ct(te)),J!==p){for(he=[],ke=sn();ke!==p;)he.push(ke),ke=sn();he!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)}else ne=H,H=p;if(H===p)if(H=ne,l.charCodeAt(ne)===39?(J=F,ne++):(J=p,Se===0&&ct(M)),J!==p){for(he=[],ke=In();ke!==p;)he.push(ke),ke=In();he!==p?(tt=H,J=Q(he),H=J):(ne=H,H=p)}else ne=H,H=p}}}return Se--,H===p&&(J=p,Se===0&&ct(U)),H}o(Fi,"peg$parseStringLiteral");function sn(){var H,J,he;return H=ne,J=ne,Se++,R.test(l.charAt(ne))?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(K)),Se--,he===p?J=void 0:(ne=J,J=p),J!==p?(l.length>ne?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(V)),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p),H===p&&(H=ne,l.charCodeAt(ne)===92?(J=ie,ne++):(J=p,Se===0&&ct(de)),J!==p?(he=gi(),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p)),H}o(sn,"peg$parseDoubleStringChar");function In(){var H,J,he;return H=ne,J=ne,Se++,ge.test(l.charAt(ne))?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(xe)),Se--,he===p?J=void 0:(ne=J,J=p),J!==p?(l.length>ne?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(V)),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p),H===p&&(H=ne,l.charCodeAt(ne)===92?(J=ie,ne++):(J=p,Se===0&&ct(de)),J!==p?(he=gi(),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p)),H}o(In,"peg$parseSingleStringChar");function vi(){var H,J,he;return H=ne,J=ne,Se++,he=Hn(),Se--,he===p?J=void 0:(ne=J,J=p),J!==p?(l.length>ne?(he=l.charAt(ne),ne++):(he=p,Se===0&&ct(V)),he!==p?(tt=H,J=ue(he),H=J):(ne=H,H=p)):(ne=H,H=p),H}o(vi,"peg$parseUnquotedStringChar");function gi(){var H,J;return qe.test(l.charAt(ne))?(H=l.charAt(ne),ne++):(H=p,Se===0&&ct(et)),H===p&&(H=ne,l.charCodeAt(ne)===110?(J=Te,ne++):(J=p,Se===0&&ct(xt)),J!==p&&(tt=H,J=Ue()),H=J,H===p&&(H=ne,l.charCodeAt(ne)===114?(J=Ve,ne++):(J=p,Se===0&&ct(Ke)),J!==p&&(tt=H,J=Ye()),H=J,H===p&&(H=ne,l.charCodeAt(ne)===116?(J=Qt,ne++):(J=p,Se===0&&ct(ft)),J!==p&&(tt=H,J=Ar()),H=J))),H}o(gi,"peg$parseEscapeSequence");function Hn(){var H,J;return Se++,Et.test(l.charAt(ne))?(H=l.charAt(ne),ne++):(H=p,Se===0&&ct(St)),Se--,H===p&&(J=p,Se===0&&ct(Kt)),H}o(Hn,"peg$parsews");function En(){var H,J;return Se++,_r.test(l.charAt(ne))?(H=l.charAt(ne),ne++):(H=p,Se===0&&ct(Ut)),Se--,H===p&&(J=p,Se===0&&ct(at)),H}o(En,"peg$parsecc");function Ks(){var H,J;for(Se++,H=[],J=Hn();J!==p;)H.push(J),J=Hn();return Se--,H===p&&(J=p,Se===0&&ct($t)),H}if(o(Ks,"peg$parse__"),Er=_(),Er!==p&&ne===l.length)return Er;throw Er!==p&&ne{t&&t.current.addEventListener("DOMNodeInserted",n=>{let l=n.currentTarget;l.scroll({top:l.scrollHeight,behavior:"auto"})})},[]),Ze.default.createElement("div",{className:"command-result",ref:t},e.map((n,l)=>Ze.default.createElement("div",{key:l},Ze.default.createElement("div",null,Ze.default.createElement("strong",null,"$ ",n.command)),n.result)))}o(II,"Results");function HI({nextArgs:e,currentArg:t,help:n,description:l,availableCommands:d}){let m=[];for(let p=0;p0&&Ze.default.createElement("div",null,Ze.default.createElement("strong",null,"Argument suggestion:")," ",m),(n==null?void 0:n.includes("->"))&&Ze.default.createElement("div",null,Ze.default.createElement("strong",null,"Signature help: "),n),l&&Ze.default.createElement("div",null,"# ",l),Ze.default.createElement("div",null,Ze.default.createElement("strong",null,"Available Commands: "),Ze.default.createElement("p",{className:"available-commands"},JSON.stringify(d)))))}o(HI,"CommandHelp");function PS(){let[e,t]=(0,Ze.useState)(""),[n,l]=(0,Ze.useState)(""),[d,m]=(0,Ze.useState)(0),[p,x]=(0,Ze.useState)([]),[_,O]=(0,Ze.useState)([]),[D,Y]=(0,Ze.useState)({}),[U,X]=(0,Ze.useState)([]),[te,Q]=(0,Ze.useState)(0),[F,M]=(0,Ze.useState)(""),[R,K]=(0,Ze.useState)(""),[V,ue]=(0,Ze.useState)([]),[ie,de]=(0,Ze.useState)([]),[ge,xe]=(0,Ze.useState)(void 0);(0,Ze.useEffect)(()=>{Ot("/commands",{method:"GET"}).then(Ue=>Ue.json()).then(Ue=>{Y(Ue),x(NS(Ue)),O(Object.keys(Ue))}).catch(Ue=>console.error(Ue))},[]),(0,Ze.useEffect)(()=>{vf("commands.history.get").then(Ue=>{de(Ue.value)}).catch(Ue=>console.error(Ue))},[]);let qe=o((Ue,Ve)=>{var ft,Ar,Kt;let Ke=Gy.parse(Ve),Ye=Gy.parse(Ue);M((ft=D[Ke[0]])==null?void 0:ft.signature_help),K(((Ar=D[Ke[0]])==null?void 0:Ar.help)||""),x(NS(D,Ye[0])),O(NS(D,Ke[0]));let Qt=(Kt=D[Ke[0]])==null?void 0:Kt.parameters.map(Et=>Et.name);Qt&&(X([Ke[0],...Qt]),Q(Ke.length-1))},"parseCommand"),et=o(Ue=>{t(Ue.target.value),l(Ue.target.value),m(0)},"onChange"),Te=o(Ue=>{if(Ue.key==="Enter"){let[Ve,...Ke]=Gy.parse(e);de([...ie,e]),vf("commands.history.add",e).catch(()=>0),Ot.post(`/commands/${Ve}`,{arguments:Ke}).then(Ye=>Ye.json()).then(Ye=>{xe(void 0),X([]),ue([...V,{command:e,result:JSON.stringify(Ye.value||Ye.error)}])}).catch(Ye=>{xe(void 0),X([]),ue([...V,{command:e,result:Ye.toString()}])}),M(""),K(""),t(""),l(""),m(0),x(_)}if(Ue.key==="ArrowUp"){let Ve;ge===void 0?Ve=ie.length-1:Ve=Math.max(0,ge-1),t(ie[Ve]),l(ie[Ve]),xe(Ve)}if(Ue.key==="ArrowDown"){if(ge===void 0)return;if(ge==ie.length-1)t(""),l(""),xe(void 0);else{let Ve=ge+1;t(ie[Ve]),l(ie[Ve]),xe(Ve)}}Ue.key==="Tab"&&(t(p[d]),m((d+1)%p.length),Ue.preventDefault()),Ue.stopPropagation()},"onKeyDown"),xt=o(Ue=>{if(!e){O(Object.keys(D));return}qe(n,e),Ue.stopPropagation()},"onKeyUp");return Ze.default.createElement("div",{className:"command"},Ze.default.createElement("div",{className:"command-title"},"Command Result"),Ze.default.createElement(II,{results:V}),Ze.default.createElement(HI,{nextArgs:U,currentArg:te,help:F,description:R,availableCommands:_}),Ze.default.createElement("div",{className:(0,RO.default)("command-input input-group")},Ze.default.createElement("span",{className:"input-group-addon"},Ze.default.createElement("i",{className:"fa fa-fw fa-terminal"})),Ze.default.createElement("input",{type:"text",placeholder:"Enter command",className:"form-control",value:e||"",onChange:et,onKeyDown:Te,onKeyUp:xt})))}o(PS,"CommandBar");var Wl=pe(De()),Pp=pe(Jh());var MS=pe(De());function AS({checked:e,onToggle:t,text:n}){return MS.default.createElement("div",{className:"btn btn-toggle "+(e?"btn-primary":"btn-default"),onClick:t},MS.default.createElement("i",{className:"fa fa-fw "+(e?"fa-check-square-o":"fa-square-o")}),"\xA0",n)}o(AS,"ToggleButton");var Hl=pe(De()),DS=pe(Jh()),FO=pe(Za()),IO=pe(_S());var im=class extends Hl.Component{constructor(t){super(t);this.heights={},this.state={vScroll:Tp()},this.onViewportUpdate=this.onViewportUpdate.bind(this)}componentDidMount(){window.addEventListener("resize",this.onViewportUpdate),this.onViewportUpdate()}componentWillUnmount(){window.removeEventListener("resize",this.onViewportUpdate)}componentDidUpdate(){this.onViewportUpdate()}onViewportUpdate(){let t=FO.default.findDOMNode(this),n=Tp({itemCount:this.props.events.length,rowHeight:this.props.rowHeight,viewportTop:t.scrollTop,viewportHeight:t.offsetHeight,itemHeights:this.props.events.map(l=>this.heights[l.id])});(0,IO.default)(this.state.vScroll,n)||this.setState({vScroll:n})}setHeight(t,n){if(n&&!this.heights[t]){let l=n.offsetHeight;this.heights[t]!==l&&(this.heights[t]=l,this.onViewportUpdate())}}render(){let{vScroll:t}=this.state,{events:n}=this.props;return Hl.default.createElement("pre",{onScroll:this.onViewportUpdate},Hl.default.createElement("div",{style:{height:t.paddingTop}}),n.slice(t.start,t.end).map(l=>Hl.default.createElement("div",{key:l.id,ref:d=>this.setHeight(l.id,d)},Hl.default.createElement(WI,{event:l}),l.message)),Hl.default.createElement("div",{style:{height:t.paddingBottom}}))}};o(im,"EventLogList"),im.propTypes={events:DS.default.array.isRequired,rowHeight:DS.default.number},im.defaultProps={rowHeight:18};function WI({event:e}){let t={web:"html5",debug:"bug",warn:"exclamation-triangle",error:"ban"}[e.level]||"info";return Hl.default.createElement("i",{className:`fa fa-fw fa-${t}`})}o(WI,"LogIcon");var HO=Uy(im);var om=class extends Wl.Component{constructor(t,n){super(t,n);this.state={height:this.props.defaultHeight},this.onDragStart=this.onDragStart.bind(this),this.onDragMove=this.onDragMove.bind(this),this.onDragStop=this.onDragStop.bind(this)}onDragStart(t){t.preventDefault(),this.dragStart=this.state.height+t.pageY,window.addEventListener("mousemove",this.onDragMove),window.addEventListener("mouseup",this.onDragStop),window.addEventListener("dragend",this.onDragStop)}onDragMove(t){t.preventDefault(),this.setState({height:this.dragStart-t.pageY})}onDragStop(t){t.preventDefault(),window.removeEventListener("mousemove",this.onDragMove)}render(){let{height:t}=this.state,{filters:n,events:l,toggleFilter:d,close:m}=this.props;return Wl.default.createElement("div",{className:"eventlog",style:{height:t}},Wl.default.createElement("div",{onMouseDown:this.onDragStart},"Eventlog",Wl.default.createElement("div",{className:"pull-right"},["debug","info","web","warn","error"].map(p=>Wl.default.createElement(AS,{key:p,text:p,checked:n[p],onToggle:()=>d(p)})),Wl.default.createElement("i",{onClick:m,className:"fa fa-close"}))),Wl.default.createElement(HO,{events:l}))}};o(om,"PureEventLog"),Tc(om,"propTypes",{filters:Pp.default.object.isRequired,events:Pp.default.array.isRequired,toggleFilter:Pp.default.func.isRequired,close:Pp.default.func.isRequired,defaultHeight:Pp.default.number}),Tc(om,"defaultProps",{defaultHeight:200});var WO=Di(e=>({filters:e.eventLog.filters,events:e.eventLog.view}),{close:mp,toggleFilter:ok})(om);var qr=pe(De());function RS(){let e=it(R=>R.conf.version),{mode:t,intercept:n,showhost:l,upstream_cert:d,rawtcp:m,dns_server:p,http2:x,websocket:_,anticache:O,anticomp:D,stickyauth:Y,stickycookie:U,stream_large_bodies:X,listen_host:te,listen_port:Q,server:F,ssl_insecure:M}=it(R=>R.options);return qr.createElement("footer",null,t&&t!=="regular"&&qr.createElement("span",{className:"label label-success"},t," mode"),n&&qr.createElement("span",{className:"label label-success"},"Intercept: ",n),M&&qr.createElement("span",{className:"label label-danger"},"ssl_insecure"),l&&qr.createElement("span",{className:"label label-success"},"showhost"),!d&&qr.createElement("span",{className:"label label-success"},"no-upstream-cert"),!m&&qr.createElement("span",{className:"label label-success"},"no-raw-tcp"),p&&qr.createElement("span",{className:"label label-success"},"dns-server"),!x&&qr.createElement("span",{className:"label label-success"},"no-http2"),!_&&qr.createElement("span",{className:"label label-success"},"no-websocket"),O&&qr.createElement("span",{className:"label label-success"},"anticache"),D&&qr.createElement("span",{className:"label label-success"},"anticomp"),Y&&qr.createElement("span",{className:"label label-success"},"stickyauth: ",Y),U&&qr.createElement("span",{className:"label label-success"},"stickycookie: ",U),X&&qr.createElement("span",{className:"label label-success"},"stream: ",Kg(X)),qr.createElement("div",{className:"pull-right"},qr.createElement(No,null,F&&qr.createElement("span",{className:"label label-primary",title:"HTTP Proxy Server Address"},te||"*",":",Q)),qr.createElement("span",{className:"label label-default",title:"Mitmproxy Version"},"mitmproxy ",e)))}o(RS,"Footer");var US=pe(De());var BS=pe(De());var Mp=pe(De());function FS({children:e}){return Mp.createElement("div",null,Mp.createElement("div",{className:"modal-backdrop fade in"}),Mp.createElement("div",{className:"modal modal-visible",id:"optionsModal",tabIndex:-1,role:"dialog","aria-labelledby":"options"},Mp.createElement("div",{className:"modal-dialog modal-lg",role:"document"},Mp.createElement("div",{className:"modal-content"},e))))}o(FS,"ModalLayout");var pr=pe(De());var Mo=pe(De()),Ao=pe(Jh());var BO=pe(Xn()),BI=o(e=>{e.key!=="Escape"&&e.stopPropagation()},"stopPropagation");IS.propTypes={value:Ao.default.bool.isRequired,onChange:Ao.default.func.isRequired};function IS(l){var d=l,{value:e,onChange:t}=d,n=Ds(d,["value","onChange"]);return Mo.default.createElement("div",{className:"checkbox"},Mo.default.createElement("label",null,Mo.default.createElement("input",Pe({type:"checkbox",checked:e,onChange:m=>t(m.target.checked)},n)),"Enable"))}o(IS,"BooleanOption");HS.propTypes={value:Ao.default.string,onChange:Ao.default.func.isRequired};function HS(l){var d=l,{value:e,onChange:t}=d,n=Ds(d,["value","onChange"]);return Mo.default.createElement("input",Pe({type:"text",value:e||"",onChange:m=>t(m.target.value)},n))}o(HS,"StringOption");function UI(e){return function(l){var d=l,{onChange:t}=d,n=Ds(d,["onChange"]);return Mo.default.createElement(e,Pe({onChange:m=>t(m||null)},n))}}o(UI,"Optional");UO.propTypes={value:Ao.default.number.isRequired,onChange:Ao.default.func.isRequired};function UO(l){var d=l,{value:e,onChange:t}=d,n=Ds(d,["value","onChange"]);return Mo.default.createElement("input",Pe({type:"number",value:e,onChange:m=>t(parseInt(m.target.value))},n))}o(UO,"NumberOption");$O.propTypes={value:Ao.default.string.isRequired,onChange:Ao.default.func.isRequired};function $O(d){var m=d,{value:e,onChange:t,choices:n}=m,l=Ds(m,["value","onChange","choices"]);return Mo.default.createElement("select",Pe({onChange:p=>t(p.target.value),value:e},l),n.map(p=>Mo.default.createElement("option",{key:p,value:p},p)))}o($O,"ChoicesOption");zO.propTypes={value:Ao.default.arrayOf(Ao.default.string).isRequired,onChange:Ao.default.func.isRequired};function zO(l){var d=l,{value:e,onChange:t}=d,n=Ds(d,["value","onChange"]);let m=Math.max(e.length,1);return Mo.default.createElement("textarea",Pe({rows:m,value:e.join(` `),onChange:p=>t(p.target.value.split(` -`))},n))}o(FO,"StringSequenceOption");var AI={bool:MS,str:AS,int:DO,"optional str":MI(AS),"sequence of str":FO};function DI({choices:e,type:t,value:n,onChange:l,name:d,error:v}){let p,w={};if(e)p=RO,w.choices=e;else if(p=AI[t],!p)throw`unknown option type ${t}`;return p!==MS&&(w.className="form-control"),Mo.default.createElement("div",{className:(0,AO.default)({"has-error":v})},Mo.default.createElement(p,Le({name:d,value:n,onChange:l,onKeyDown:PI},w)))}o(DI,"PureOption");var IO=Ai((e,{name:t})=>Le(Le({},e.options_meta[t]),e.ui.optionsEditor[t]),(e,{name:t})=>({onChange:n=>e(lp(t,n))}))(DI);var Ky=pe(xh());function RI({help:e}){return fr.default.createElement("div",{className:"help-block small"},e)}o(RI,"PureOptionHelp");var FI=Ai((e,{name:t})=>({help:e.options_meta[t].help}))(RI);function II({error:e}){return e?fr.default.createElement("div",{className:"small text-danger"},e):null}o(II,"PureOptionError");var HI=Ai((e,{name:t})=>({error:e.ui.optionsEditor[t]&&e.ui.optionsEditor[t].error}))(II);function WI({value:e,defaultVal:t}){if(e===t)return null;if(typeof t=="boolean")t=t?"true":"false";else if(Array.isArray(t)){if(Ky.default.isEmpty(Ky.default.compact(e))&&Ky.default.isEmpty(t))return null;t="[ ]"}else t===""?t='""':t===null&&(t="null");return fr.default.createElement("div",{className:"small"},"Default: ",fr.default.createElement("strong",null," ",t," ")," ")}o(WI,"PureOptionDefault");var BI=Ai((e,{name:t})=>({value:e.options[t],defaultVal:e.options_meta[t].default}))(WI),DS=class extends fr.Component{constructor(t,n){super(t,n);this.state={title:"Options"}}componentWillUnmount(){}render(){let{hideModal:t,options:n}=this.props,{title:l}=this.state;return fr.default.createElement("div",null,fr.default.createElement("div",{className:"modal-header"},fr.default.createElement("button",{type:"button",className:"close","data-dismiss":"modal",onClick:()=>{t()}},fr.default.createElement("i",{className:"fa fa-fw fa-times"})),fr.default.createElement("div",{className:"modal-title"},fr.default.createElement("h4",null,l))),fr.default.createElement("div",{className:"modal-body"},fr.default.createElement("div",{className:"form-horizontal"},n.map(d=>fr.default.createElement("div",{key:d,className:"form-group"},fr.default.createElement("div",{className:"col-xs-6"},fr.default.createElement("label",{htmlFor:d},d),fr.default.createElement(FI,{name:d})),fr.default.createElement("div",{className:"col-xs-6"},fr.default.createElement(IO,{name:d}),fr.default.createElement(HI,{name:d}),fr.default.createElement(BI,{name:d})))))),fr.default.createElement("div",{className:"modal-footer"}))}};o(DS,"PureOptionModal");var HO=Ai(e=>({options:Object.keys(e.options_meta).sort()}),{hideModal:hy,save:dk})(DS);function UI(){return RS.createElement(PS,null,RS.createElement(HO,null))}o(UI,"OptionModal");var WO=[UI];function IS(){let e=at(n=>n.ui.modal.activeModal),t=WO.find(n=>n.name===e);return e?FS.createElement(t,null):FS.createElement("div",null)}o(IS,"PureModal");var HS=class extends An.Component{constructor(){super(...arguments);this.state={};this.render=o(()=>{var l;let{showEventLog:t,showCommandBar:n}=this.props;return this.state.error?(console.log("ERR",this.state),An.default.createElement("div",{className:"container"},An.default.createElement("h1",null,"mitmproxy has crashed."),An.default.createElement("pre",null,this.state.error.stack,An.default.createElement("br",null),An.default.createElement("br",null),"Component Stack:",(l=this.state.errorInfo)==null?void 0:l.componentStack),An.default.createElement("p",null,"Please lodge a bug report at ",An.default.createElement("a",{href:"https://github.com/mitmproxy/mitmproxy/issues"},"https://github.com/mitmproxy/mitmproxy/issues"),"."))):An.default.createElement("div",{id:"container",tabIndex:0},An.default.createElement(bS,null),An.default.createElement(xS,null),n&&An.default.createElement(TS,{key:"commandbar"}),t&&An.default.createElement(MO,{key:"eventlog"}),An.default.createElement(NS,null),An.default.createElement(IS,null))},"render")}componentDidMount(){window.addEventListener("keydown",this.props.onKeyDown)}componentWillUnmount(){window.removeEventListener("keydown",this.props.onKeyDown)}componentDidCatch(t,n){this.setState({error:t,errorInfo:n})}};o(HS,"ProxyAppMain");var BO=Ai(e=>({showEventLog:e.eventLog.visible,showCommandBar:e.commandBar.visible}),{onKeyDown:iO})(HS);var su={SEARCH:"s",HIGHLIGHT:"h",SHOW_EVENTLOG:"e",SHOW_COMMANDBAR:"c"};function zI(e){let[t,n]=window.location.hash.substr(1).split("?",2),l=t.substr(1).split("/");if(l[0]==="flows"&&l.length==3){let[d,v]=l.slice(1);e.dispatch(ff(d)),e.dispatch(lf(v))}n&&n.split("&").forEach(d=>{let[v,p]=d.split("=",2);switch(v){case su.SEARCH:e.dispatch(oy(p));break;case su.HIGHLIGHT:e.dispatch(sy(p));break;case su.SHOW_EVENTLOG:e.getState().eventLog.visible||e.dispatch(sp());break;case su.SHOW_COMMANDBAR:e.getState().commandBar.visible||e.dispatch(yy());break;default:console.error(`unimplemented query arg: ${d}`)}})}o(zI,"updateStoreFromUrl");function jI(e){let t=e.getState(),n={[su.SEARCH]:t.flows.filter,[su.HIGHLIGHT]:t.flows.highlight,[su.SHOW_EVENTLOG]:t.eventLog.visible,[su.SHOW_COMMANDBAR]:t.commandBar.visible},l=Object.keys(n).filter(p=>n[p]).map(p=>`${p}=${n[p]}`).join("&"),d;t.flows.selected.length>0?d=`/flows/${t.flows.selected[0]}/${t.ui.flow.tab}`:d="/flows",l&&(d+="?"+l);let v=window.location.pathname;v==="blank"&&(v="/"),window.location.hash.substr(1)!==d&&history.replaceState(void 0,"",`${v}#${d}`)}o(jI,"updateUrlFromStore");function WS(e){zI(e),e.subscribe(()=>jI(e))}o(WS,"initialize");var $I="reset",Qh=class{constructor(t){this.activeFetches={},this.store=t,this.connect()}connect(){this.socket=new WebSocket(location.origin.replace("http","ws")+"/updates"),this.socket.addEventListener("open",()=>this.onOpen()),this.socket.addEventListener("close",t=>this.onClose(t)),this.socket.addEventListener("message",t=>this.onMessage(JSON.parse(t.data))),this.socket.addEventListener("error",t=>this.onError(t))}onOpen(){this.fetchData("flows"),this.fetchData("events"),this.fetchData("options"),this.store.dispatch(uk())}fetchData(t){let n=[];this.activeFetches[t]=n,Et(`./${t}`).then(l=>l.json()).then(l=>{this.activeFetches[t]===n&&this.receive(t,l)})}onMessage(t){if(t.cmd===$I)return this.fetchData(t.resource);if(t.resource in this.activeFetches)this.activeFetches[t.resource].push(t);else{let n=`${t.resource}_${t.cmd}`.toUpperCase();this.store.dispatch(Le({type:n},t))}}receive(t,n){let l=`${t}_RECEIVE`.toUpperCase();this.store.dispatch({type:l,cmd:"receive",resource:t,data:n});let d=this.activeFetches[t];delete this.activeFetches[t],d.forEach(v=>this.onMessage(v)),Object.keys(this.activeFetches).length===0&&this.store.dispatch(fk())}onClose(t){this.store.dispatch(ck(`Connection closed at ${new Date().toUTCString()} with error code ${t.code}.`)),console.error("websocket connection closed",t)}onError(t){console.error("websocket connection errored",arguments)}};o(Qh,"WebsocketBackend");var Zh=class{constructor(t){this.store=t,this.onOpen()}onOpen(){this.fetchData("flows"),this.fetchData("options")}fetchData(t){Et(`./${t}`).then(n=>n.json()).then(n=>{this.receive(t,n)})}receive(t,n){let l=`${t}_RECEIVE`.toUpperCase();this.store.dispatch({type:l,cmd:"receive",resource:t,data:n})}};o(Zh,"StaticBackend");WS(ap);window.MITMWEB_STATIC?window.backend=new Zh(ap):window.backend=new Qh(ap);window.addEventListener("error",e=>{ap.dispatch(tk(`${e.message} -${e.error.stack}`))});document.addEventListener("DOMContentLoaded",()=>{(0,UO.render)(BS.createElement(J1,{store:ap},BS.createElement(BO,null)),document.getElementById("mitmproxy"))});})(); +`))},n))}o(zO,"StringSequenceOption");var $I={bool:IS,str:HS,int:UO,"optional str":UI(HS),"sequence of str":zO};function zI({choices:e,type:t,value:n,onChange:l,name:d,error:m}){let p,x={};if(e)p=$O,x.choices=e;else if(p=$I[t],!p)throw`unknown option type ${t}`;return p!==IS&&(x.className="form-control"),Mo.default.createElement("div",{className:(0,BO.default)({"has-error":m})},Mo.default.createElement(p,Pe({name:d,value:n,onChange:l,onKeyDown:BI},x)))}o(zI,"PureOption");var jO=Di((e,{name:t})=>Pe(Pe({},e.options_meta[t]),e.ui.optionsEditor[t]),(e,{name:t})=>({onChange:n=>e(vp(t,n))}))(zI);var Yy=pe(Oh());function jI({help:e}){return pr.default.createElement("div",{className:"help-block small"},e)}o(jI,"PureOptionHelp");var qI=Di((e,{name:t})=>({help:e.options_meta[t].help}))(jI);function VI({error:e}){return e?pr.default.createElement("div",{className:"small text-danger"},e):null}o(VI,"PureOptionError");var KI=Di((e,{name:t})=>({error:e.ui.optionsEditor[t]&&e.ui.optionsEditor[t].error}))(VI);function GI({value:e,defaultVal:t}){if(e===t)return null;if(typeof t=="boolean")t=t?"true":"false";else if(Array.isArray(t)){if(Yy.default.isEmpty(Yy.default.compact(e))&&Yy.default.isEmpty(t))return null;t="[ ]"}else t===""?t='""':t===null&&(t="null");return pr.default.createElement("div",{className:"small"},"Default: ",pr.default.createElement("strong",null," ",t," ")," ")}o(GI,"PureOptionDefault");var YI=Di((e,{name:t})=>({value:e.options[t],defaultVal:e.options_meta[t].default}))(GI),WS=class extends pr.Component{constructor(t,n){super(t,n);this.state={title:"Options"}}componentWillUnmount(){}render(){let{hideModal:t,options:n}=this.props,{title:l}=this.state;return pr.default.createElement("div",null,pr.default.createElement("div",{className:"modal-header"},pr.default.createElement("button",{type:"button",className:"close","data-dismiss":"modal",onClick:()=>{t()}},pr.default.createElement("i",{className:"fa fa-fw fa-times"})),pr.default.createElement("div",{className:"modal-title"},pr.default.createElement("h4",null,l))),pr.default.createElement("div",{className:"modal-body"},pr.default.createElement("div",{className:"form-horizontal"},n.map(d=>pr.default.createElement("div",{key:d,className:"form-group"},pr.default.createElement("div",{className:"col-xs-6"},pr.default.createElement("label",{htmlFor:d},d),pr.default.createElement(qI,{name:d})),pr.default.createElement("div",{className:"col-xs-6"},pr.default.createElement(jO,{name:d}),pr.default.createElement(KI,{name:d}),pr.default.createElement(YI,{name:d})))))),pr.default.createElement("div",{className:"modal-footer"}))}};o(WS,"PureOptionModal");var qO=Di(e=>({options:Object.keys(e.options_meta).sort()}),{hideModal:my,save:yk})(WS);function XI(){return BS.createElement(FS,null,BS.createElement(qO,null))}o(XI,"OptionModal");var VO=[XI];function $S(){let e=it(n=>n.ui.modal.activeModal),t=VO.find(n=>n.name===e);return e&&t!==void 0?US.createElement(t,null):US.createElement("div",null)}o($S,"PureModal");var zS=class extends Rn.Component{constructor(){super(...arguments);this.state={};this.render=o(()=>{var l;let{showEventLog:t,showCommandBar:n}=this.props;return this.state.error?(console.log("ERR",this.state),Rn.default.createElement("div",{className:"container"},Rn.default.createElement("h1",null,"mitmproxy has crashed."),Rn.default.createElement("pre",null,this.state.error.stack,Rn.default.createElement("br",null),Rn.default.createElement("br",null),"Component Stack:",(l=this.state.errorInfo)==null?void 0:l.componentStack),Rn.default.createElement("p",null,"Please lodge a bug report at ",Rn.default.createElement("a",{href:"https://github.com/mitmproxy/mitmproxy/issues"},"https://github.com/mitmproxy/mitmproxy/issues"),"."))):Rn.default.createElement("div",{id:"container",tabIndex:0},Rn.default.createElement(LS,null),Rn.default.createElement(ES,null),n&&Rn.default.createElement(PS,{key:"commandbar"}),t&&Rn.default.createElement(WO,{key:"eventlog"}),Rn.default.createElement(RS,null),Rn.default.createElement($S,null))},"render")}componentDidMount(){window.addEventListener("keydown",this.props.onKeyDown)}componentWillUnmount(){window.removeEventListener("keydown",this.props.onKeyDown)}componentDidCatch(t,n){this.setState({error:t,errorInfo:n})}};o(zS,"ProxyAppMain");var KO=Di(e=>({showEventLog:e.eventLog.visible,showCommandBar:e.commandBar.visible}),{onKeyDown:fO})(zS);var au={SEARCH:"s",HIGHLIGHT:"h",SHOW_EVENTLOG:"e",SHOW_COMMANDBAR:"c"};function QI(e){let[t,n]=window.location.hash.substr(1).split("?",2),l=t.substr(1).split("/");if(l[0]==="flows"&&l.length==3){let[d,m]=l.slice(1);e.dispatch(wf(d)),e.dispatch(mf(m))}n&&n.split("&").forEach(d=>{let[m,p]=d.split("=",2);switch(p=decodeURIComponent(p),m){case au.SEARCH:e.dispatch(sy(p));break;case au.HIGHLIGHT:e.dispatch(ly(p));break;case au.SHOW_EVENTLOG:e.getState().eventLog.visible||e.dispatch(mp());break;case au.SHOW_COMMANDBAR:e.getState().commandBar.visible||e.dispatch(wy());break;default:console.error(`unimplemented query arg: ${d}`)}})}o(QI,"updateStoreFromUrl");function ZI(e){let t=e.getState(),n={[au.SEARCH]:t.flows.filter,[au.HIGHLIGHT]:t.flows.highlight,[au.SHOW_EVENTLOG]:t.eventLog.visible,[au.SHOW_COMMANDBAR]:t.commandBar.visible},l=Object.keys(n).filter(p=>n[p]).map(p=>`${p}=${encodeURIComponent(n[p])}`).join("&"),d;t.flows.selected.length>0?d=`/flows/${t.flows.selected[0]}/${t.ui.flow.tab}`:d="/flows",l&&(d+="?"+l);let m=window.location.pathname;m==="blank"&&(m="/"),window.location.hash.substr(1)!==d&&history.replaceState(void 0,"",`${m}#${d}`)}o(ZI,"updateUrlFromStore");function jS(e){QI(e),e.subscribe(()=>ZI(e))}o(jS,"initialize");var JI="reset",sm=class{constructor(t){this.activeFetches={},this.store=t,this.connect()}connect(){this.socket=new WebSocket(location.origin.replace("http","ws")+"/updates"),this.socket.addEventListener("open",()=>this.onOpen()),this.socket.addEventListener("close",t=>this.onClose(t)),this.socket.addEventListener("message",t=>this.onMessage(JSON.parse(t.data))),this.socket.addEventListener("error",t=>this.onError(t))}onOpen(){this.fetchData("flows"),this.fetchData("events"),this.fetchData("options"),this.store.dispatch(hk())}fetchData(t){let n=[];this.activeFetches[t]=n,Ot(`./${t}`).then(l=>l.json()).then(l=>{this.activeFetches[t]===n&&this.receive(t,l)})}onMessage(t){if(t.cmd===JI)return this.fetchData(t.resource);if(t.resource in this.activeFetches)this.activeFetches[t.resource].push(t);else{let n=`${t.resource}_${t.cmd}`.toUpperCase();this.store.dispatch(Pe({type:n},t))}}receive(t,n){let l=`${t}_RECEIVE`.toUpperCase();this.store.dispatch({type:l,cmd:"receive",resource:t,data:n});let d=this.activeFetches[t];delete this.activeFetches[t],d.forEach(m=>this.onMessage(m)),Object.keys(this.activeFetches).length===0&&this.store.dispatch(mk())}onClose(t){this.store.dispatch(vk(`Connection closed at ${new Date().toUTCString()} with error code ${t.code}.`)),console.error("websocket connection closed",t)}onError(t){console.error("websocket connection errored",arguments)}};o(sm,"WebsocketBackend");var lm=class{constructor(t){this.store=t,this.onOpen()}onOpen(){this.fetchData("flows"),this.fetchData("options")}fetchData(t){Ot(`./${t}`).then(n=>n.json()).then(n=>{this.receive(t,n)})}receive(t,n){let l=`${t}_RECEIVE`.toUpperCase();this.store.dispatch({type:l,cmd:"receive",resource:t,data:n})}};o(lm,"StaticBackend");jS(gp);window.MITMWEB_STATIC?window.backend=new lm(gp):window.backend=new sm(gp);window.addEventListener("error",e=>{gp.dispatch(sk(`${e.message} +${e.error.stack}`))});document.addEventListener("DOMContentLoaded",()=>{(0,GO.render)(qS.createElement(tx,{store:gp},qS.createElement(KO,null)),document.getElementById("mitmproxy"))});})(); /* object-assign (c) Sindre Sorhus diff --git a/mitmproxy/tools/web/static/images/resourceDnsIcon.png b/mitmproxy/tools/web/static/images/resourceDnsIcon.png new file mode 100644 index 0000000000..31dee2765e Binary files /dev/null and b/mitmproxy/tools/web/static/images/resourceDnsIcon.png differ diff --git a/mitmproxy/tools/web/static_viewer.py b/mitmproxy/tools/web/static_viewer.py index 7fa87e10ac..2d7eb8817f 100644 --- a/mitmproxy/tools/web/static_viewer.py +++ b/mitmproxy/tools/web/static_viewer.py @@ -3,7 +3,8 @@ import pathlib import shutil import time -import typing +from collections.abc import Iterable +from typing import Optional from mitmproxy import contentviews, http from mitmproxy import ctx @@ -23,65 +24,62 @@ def save_static(path: pathlib.Path) -> None: if (path / "static").exists(): shutil.rmtree(str(path / "static")) shutil.copytree(str(web_dir / "static"), str(path / "static")) - shutil.copyfile(str(web_dir / 'templates' / 'index.html'), str(path / "index.html")) + shutil.copyfile(str(web_dir / "templates" / "index.html"), str(path / "index.html")) with open(str(path / "static" / "static.js"), "w") as f: f.write("MITMWEB_STATIC = true;") def save_filter_help(path: pathlib.Path) -> None: - with open(str(path / 'filter-help.json'), 'w') as f: + with open(str(path / "filter-help.json"), "w") as f: json.dump(dict(commands=flowfilter.help), f) def save_settings(path: pathlib.Path) -> None: - with open(str(path / 'settings.json'), 'w') as f: + with open(str(path / "settings.json"), "w") as f: json.dump(dict(version=version.VERSION), f) -def save_flows(path: pathlib.Path, flows: typing.Iterable[flow.Flow]) -> None: - with open(str(path / 'flows.json'), 'w') as f: - json.dump( - [flow_to_json(f) for f in flows], - f - ) +def save_flows(path: pathlib.Path, flows: Iterable[flow.Flow]) -> None: + with open(str(path / "flows.json"), "w") as f: + json.dump([flow_to_json(f) for f in flows], f) -def save_flows_content(path: pathlib.Path, flows: typing.Iterable[flow.Flow]) -> None: +def save_flows_content(path: pathlib.Path, flows: Iterable[flow.Flow]) -> None: for f in flows: assert isinstance(f, http.HTTPFlow) - for m in ('request', 'response'): + for m in ("request", "response"): message = getattr(f, m) message_path = path / "flows" / f.id / m os.makedirs(str(message_path / "content"), exist_ok=True) - with open(str(message_path / 'content.data'), 'wb') as content_file: + with open(str(message_path / "content.data"), "wb") as content_file: # don't use raw_content here as this is served with a default content type if message: content_file.write(message.content) else: - content_file.write(b'No content.') + content_file.write(b"No content.") # content_view t = time.time() if message: description, lines, error = contentviews.get_message_content_view( - 'Auto', message, f + "Auto", message, f ) else: - description, lines = 'No content.', [] + description, lines = "No content.", [] if time.time() - t > 0.1: ctx.log( "Slow content view: {} took {}s".format( - description.strip(), - round(time.time() - t, 1) + description.strip(), round(time.time() - t, 1) ), - "info" + "info", ) - with open(str(message_path / "content" / "Auto.json"), "w") as content_view_file: + with open( + str(message_path / "content" / "Auto.json"), "w" + ) as content_view_file: json.dump( - dict(lines=list(lines), description=description), - content_view_file + dict(lines=list(lines), description=description), content_view_file ) @@ -89,8 +87,10 @@ class StaticViewer: # TODO: make this a command at some point. def load(self, loader): loader.add_option( - "web_static_viewer", typing.Optional[str], "", - "The path to output a static viewer." + "web_static_viewer", + Optional[str], + "", + "The path to output a static viewer.", ) def configure(self, updated): @@ -99,7 +99,7 @@ def configure(self, updated): p = pathlib.Path(ctx.options.web_static_viewer).expanduser() self.export(p, flows) - def export(self, path: pathlib.Path, flows: typing.Iterable[flow.Flow]) -> None: + def export(self, path: pathlib.Path, flows: Iterable[flow.Flow]) -> None: save_static(path) save_filter_help(path) save_flows(path, flows) diff --git a/mitmproxy/tools/web/webaddons.py b/mitmproxy/tools/web/webaddons.py index 99485dbc1d..265cc83f28 100644 --- a/mitmproxy/tools/web/webaddons.py +++ b/mitmproxy/tools/web/webaddons.py @@ -1,30 +1,20 @@ import webbrowser +from collections.abc import Sequence from mitmproxy import ctx -from typing import Sequence class WebAddon: def load(self, loader): + loader.add_option("web_open_browser", bool, True, "Start a browser.") + loader.add_option("web_debug", bool, False, "Enable mitmweb debugging.") + loader.add_option("web_port", int, 8081, "Web UI port.") + loader.add_option("web_host", str, "127.0.0.1", "Web UI host.") loader.add_option( - "web_open_browser", bool, True, - "Start a browser." - ) - loader.add_option( - "web_debug", bool, False, - "Enable mitmweb debugging." - ) - loader.add_option( - "web_port", int, 8081, - "Web UI port." - ) - loader.add_option( - "web_host", str, "127.0.0.1", - "Web UI host." - ) - loader.add_option( - "web_columns", Sequence[str], ["tls", "icon", "path", "method", "status", "size", "time"], - "Columns to show in the flow list" + "web_columns", + Sequence[str], + ["tls", "icon", "path", "method", "status", "size", "time"], + "Columns to show in the flow list", ) def running(self): @@ -49,11 +39,19 @@ def open_browser(url: str) -> bool: False, if no suitable browser has been found. """ browsers = ( - "windows-default", "macosx", + "windows-default", + "macosx", "wslview %s", - "x-www-browser %s", "gnome-open %s", "xdg-open", - "google-chrome", "chrome", "chromium", "chromium-browser", - "firefox", "opera", "safari", + "x-www-browser %s", + "gnome-open %s", + "xdg-open", + "google-chrome", + "chrome", + "chromium", + "chromium-browser", + "firefox", + "opera", + "safari", ) for browser in browsers: try: diff --git a/mitmproxy/types.py b/mitmproxy/types.py index 01d7ac47f8..868dd42a21 100644 --- a/mitmproxy/types.py +++ b/mitmproxy/types.py @@ -2,13 +2,14 @@ import os import glob import re -import typing +from collections.abc import Sequence +from typing import Any, Optional, TYPE_CHECKING, Union from mitmproxy import exceptions from mitmproxy import flow from mitmproxy.utils import emoji, strutils -if typing.TYPE_CHECKING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from mitmproxy.command import CommandManager @@ -32,11 +33,11 @@ class Space(str): pass -class CutSpec(typing.Sequence[str]): +class CutSpec(Sequence[str]): pass -class Data(typing.Sequence[typing.Sequence[typing.Union[str, bytes]]]): +class Data(Sequence[Sequence[Union[str, bytes]]]): pass @@ -55,28 +56,28 @@ def __instancecheck__(self, instance): # pragma: no cover class _BaseType: - typ: typing.Type = object + typ: type = object display: str = "" - def completion(self, manager: "CommandManager", t: typing.Any, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: Any, s: str) -> Sequence[str]: """ - Returns a list of completion strings for a given prefix. The strings - returned don't necessarily need to be suffixes of the prefix, since - completers will do prefix filtering themselves.. + Returns a list of completion strings for a given prefix. The strings + returned don't necessarily need to be suffixes of the prefix, since + completers will do prefix filtering themselves.. """ raise NotImplementedError - def parse(self, manager: "CommandManager", typ: typing.Any, s: str) -> typing.Any: + def parse(self, manager: "CommandManager", typ: Any, s: str) -> Any: """ - Parse a string, given the specific type instance (to allow rich type annotations like Choice) and a string. + Parse a string, given the specific type instance (to allow rich type annotations like Choice) and a string. - Raises exceptions.TypeError if the value is invalid. + Raises exceptions.TypeError if the value is invalid. """ raise NotImplementedError - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: """ - Check if data is valid for this type. + Check if data is valid for this type. """ raise NotImplementedError @@ -85,7 +86,7 @@ class _BoolType(_BaseType): typ = bool display = "bool" - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return ["false", "true"] def parse(self, manager: "CommandManager", t: type, s: str) -> bool: @@ -94,11 +95,9 @@ def parse(self, manager: "CommandManager", t: type, s: str) -> bool: elif s == "false": return False else: - raise exceptions.TypeError( - "Booleans are 'true' or 'false', got %s" % s - ) + raise exceptions.TypeError("Booleans are 'true' or 'false', got %s" % s) - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return val in [True, False] @@ -107,7 +106,8 @@ class _StrType(_BaseType): display = "str" # https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals - escape_sequences = re.compile(r""" + escape_sequences = re.compile( + r""" \\ ( [\\'"abfnrtv] # Standard C escape sequence | [0-7]{1,3} # Character with octal value @@ -116,13 +116,15 @@ class _StrType(_BaseType): | u.... # Character with 16-bit hex value | U........ # Character with 32-bit hex value ) - """, re.VERBOSE) + """, + re.VERBOSE, + ) @staticmethod def _unescape(match: re.Match) -> str: return codecs.decode(match.group(0), "unicode-escape") # type: ignore - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return [] def parse(self, manager: "CommandManager", t: type, s: str) -> str: @@ -131,7 +133,7 @@ def parse(self, manager: "CommandManager", t: type, s: str) -> str: except ValueError as e: raise exceptions.TypeError(f"Invalid str: {e}") from e - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return isinstance(val, str) @@ -139,7 +141,7 @@ class _BytesType(_BaseType): typ = bytes display = "bytes" - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return [] def parse(self, manager: "CommandManager", t: type, s: str) -> bytes: @@ -148,7 +150,7 @@ def parse(self, manager: "CommandManager", t: type, s: str) -> bytes: except ValueError as e: raise exceptions.TypeError(str(e)) - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return isinstance(val, bytes) @@ -156,13 +158,13 @@ class _UnknownType(_BaseType): typ = Unknown display = "unknown" - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return [] def parse(self, manager: "CommandManager", t: type, s: str) -> str: return s - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return False @@ -170,7 +172,7 @@ class _IntType(_BaseType): typ = int display = "int" - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return [] def parse(self, manager: "CommandManager", t: type, s: str) -> int: @@ -179,7 +181,7 @@ def parse(self, manager: "CommandManager", t: type, s: str) -> int: except ValueError as e: raise exceptions.TypeError(str(e)) from e - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return isinstance(val, int) @@ -187,7 +189,9 @@ class _PathType(_BaseType): typ = Path display = "path" - def completion(self, manager: "CommandManager", t: type, start: str) -> typing.Sequence[str]: + def completion( + self, manager: "CommandManager", t: type, start: str + ) -> Sequence[str]: if not start: start = "./" path = os.path.expanduser(start) @@ -212,7 +216,7 @@ def completion(self, manager: "CommandManager", t: type, start: str) -> typing.S def parse(self, manager: "CommandManager", t: type, s: str) -> str: return os.path.expanduser(s) - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return isinstance(val, str) @@ -220,7 +224,7 @@ class _CmdType(_BaseType): typ = Cmd display = "cmd" - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return list(manager.commands.keys()) def parse(self, manager: "CommandManager", t: type, s: str) -> str: @@ -228,7 +232,7 @@ def parse(self, manager: "CommandManager", t: type, s: str) -> str: raise exceptions.TypeError("Unknown command: %s" % s) return s - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return val in manager.commands @@ -236,27 +240,27 @@ class _ArgType(_BaseType): typ = CmdArgs display = "arg" - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return [] def parse(self, manager: "CommandManager", t: type, s: str) -> str: return s - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return isinstance(val, str) class _StrSeqType(_BaseType): - typ = typing.Sequence[str] + typ = Sequence[str] display = "str[]" - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return [] - def parse(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def parse(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return [x.strip() for x in s.split(",")] - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: if isinstance(val, str) or isinstance(val, bytes): return False try: @@ -285,7 +289,6 @@ class _CutSpecType(_BaseType): "request.timestamp_start", "request.timestamp_end", "request.header[", - "response.status_code", "response.reason", "response.text", @@ -294,13 +297,11 @@ class _CutSpecType(_BaseType): "response.timestamp_end", "response.raw_content", "response.header[", - "client_conn.peername.port", "client_conn.peername.host", "client_conn.tls_version", "client_conn.sni", "client_conn.tls_established", - "server_conn.address.port", "server_conn.address.host", "server_conn.ip_address.host", @@ -309,7 +310,7 @@ class _CutSpecType(_BaseType): "server_conn.tls_established", ] - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: spec = s.split(",") opts = [] for pref in self.valid_prefixes: @@ -318,10 +319,10 @@ def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Seque return opts def parse(self, manager: "CommandManager", t: type, s: str) -> CutSpec: - parts: typing.Any = s.split(",") + parts: Any = s.split(",") return parts - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: if not isinstance(val, str): return False parts = [x.strip() for x in val.split(",")] @@ -359,7 +360,7 @@ class _BaseFlowType(_BaseType): "~c", ] - def completion(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: type, s: str) -> Sequence[str]: return self.valid_prefixes @@ -369,7 +370,7 @@ class _FlowType(_BaseFlowType): def parse(self, manager: "CommandManager", t: type, s: str) -> flow.Flow: try: - flows = manager.execute("view.flows.resolve %s" % (s)) + flows = manager.call_strings("view.flows.resolve", [s]) except exceptions.CommandError as e: raise exceptions.TypeError(str(e)) from e if len(flows) != 1: @@ -378,21 +379,21 @@ def parse(self, manager: "CommandManager", t: type, s: str) -> flow.Flow: ) return flows[0] - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: return isinstance(val, flow.Flow) class _FlowsType(_BaseFlowType): - typ = typing.Sequence[flow.Flow] + typ = Sequence[flow.Flow] display = "flow[]" - def parse(self, manager: "CommandManager", t: type, s: str) -> typing.Sequence[flow.Flow]: + def parse(self, manager: "CommandManager", t: type, s: str) -> Sequence[flow.Flow]: try: - return manager.execute("view.flows.resolve %s" % (s)) + return manager.call_strings("view.flows.resolve", [s]) except exceptions.CommandError as e: raise exceptions.TypeError(str(e)) from e - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: try: for v in val: if not isinstance(v, flow.Flow): @@ -408,15 +409,15 @@ class _DataType(_BaseType): def completion( self, manager: "CommandManager", t: type, s: str - ) -> typing.Sequence[str]: # pragma: no cover + ) -> Sequence[str]: # pragma: no cover raise exceptions.TypeError("data cannot be passed as argument") def parse( self, manager: "CommandManager", t: type, s: str - ) -> typing.Any: # pragma: no cover + ) -> Any: # pragma: no cover raise exceptions.TypeError("data cannot be passed as argument") - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: # FIXME: validate that all rows have equal length, and all columns have equal types try: for row in val: @@ -432,7 +433,7 @@ class _ChoiceType(_BaseType): typ = Choice display = "choice" - def completion(self, manager: "CommandManager", t: Choice, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: Choice, s: str) -> Sequence[str]: return manager.execute(t.options_command) def parse(self, manager: "CommandManager", t: Choice, s: str) -> str: @@ -441,7 +442,7 @@ def parse(self, manager: "CommandManager", t: Choice, s: str) -> str: raise exceptions.TypeError("Invalid choice.") return s - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: Any) -> bool: try: opts = manager.execute(typ.options_command) except exceptions.CommandError: @@ -449,26 +450,26 @@ def is_valid(self, manager: "CommandManager", typ: typing.Any, val: typing.Any) return val in opts -ALL_MARKERS = ['true', 'false'] + list(emoji.emoji) +ALL_MARKERS = ["true", "false"] + list(emoji.emoji) class _MarkerType(_BaseType): typ = Marker display = "marker" - def completion(self, manager: "CommandManager", t: Choice, s: str) -> typing.Sequence[str]: + def completion(self, manager: "CommandManager", t: Choice, s: str) -> Sequence[str]: return ALL_MARKERS def parse(self, manager: "CommandManager", t: Choice, s: str) -> str: if s not in ALL_MARKERS: raise exceptions.TypeError("Invalid choice.") - if s == 'true': + if s == "true": return ":default:" - elif s == 'false': + elif s == "false": return "" return s - def is_valid(self, manager: "CommandManager", typ: typing.Any, val: str) -> bool: + def is_valid(self, manager: "CommandManager", typ: Any, val: str) -> bool: return val in ALL_MARKERS @@ -478,7 +479,7 @@ def __init__(self, *types): for t in types: self.typemap[t.typ] = t() - def get(self, t: typing.Optional[typing.Type], default=None) -> typing.Optional[_BaseType]: + def get(self, t: Optional[type], default=None) -> Optional[_BaseType]: if type(t) in self.typemap: return self.typemap[type(t)] return self.typemap.get(t, default) diff --git a/mitmproxy/utils/arg_check.py b/mitmproxy/utils/arg_check.py index 4bb1f18122..b6736e3ab2 100644 --- a/mitmproxy/utils/arg_check.py +++ b/mitmproxy/utils/arg_check.py @@ -16,7 +16,6 @@ --no-http2-priority --no-websocket --websocket ---spoof-source-address --upstream-bind-address --ciphers-client --ciphers-server @@ -102,7 +101,7 @@ "-i": "--intercept", "-f": "--view-filter", "--filter": "--view-filter", - "--socks": "--mode socks5" + "--socks": "--mode socks5", } @@ -122,12 +121,13 @@ def check(): for option in ("--nonanonymous", "--singleuser", "--htpasswd"): if option in args: print( - '{} is deprecated.\n' - 'Please use `--proxyauth SPEC` instead.\n' + "{} is deprecated.\n" + "Please use `--proxyauth SPEC` instead.\n" 'SPEC Format: "username:pass", "any" to accept any user/pass combination,\n' '"@path" to use an Apache htpasswd file, or\n' '"ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" ' - 'for LDAP authentication.'.format(option)) + "for LDAP authentication.".format(option) + ) for option in REPLACED.splitlines(): if option in args: @@ -137,10 +137,7 @@ def check(): new_options = [REPLACEMENTS.get(option)] print( "{} is deprecated.\n" - "Please use `{}` instead.".format( - option, - "` or `".join(new_options) - ) + "Please use `{}` instead.".format(option, "` or `".join(new_options)) ) for option in DEPRECATED.splitlines(): @@ -150,15 +147,17 @@ def check(): "Please use `--set {}=value` instead.\n" "To show all options and their default values use --options".format( option, - REPLACEMENTS.get(option, None) or option.lstrip("-").replace("-", "_") + REPLACEMENTS.get(option, None) + or option.lstrip("-").replace("-", "_"), ) ) # Check for underscores in the options. Options always follow '--'. for argument in args: - underscoreParam = re.search(r'[-]{2}((.*?_)(.*?(\s|$)))+', argument) + underscoreParam = re.search(r"[-]{2}((.*?_)(.*?(\s|$)))+", argument) if underscoreParam is not None: - print("{} uses underscores, please use hyphens {}".format( - argument, - argument.replace('_', '-')) + print( + "{} uses underscores, please use hyphens {}".format( + argument, argument.replace("_", "-") + ) ) diff --git a/mitmproxy/utils/asyncio_utils.py b/mitmproxy/utils/asyncio_utils.py index 2feaaaeed6..44ec46077f 100644 --- a/mitmproxy/utils/asyncio_utils.py +++ b/mitmproxy/utils/asyncio_utils.py @@ -1,5 +1,4 @@ import asyncio -import sys import time from collections.abc import Coroutine from typing import Optional @@ -7,44 +6,25 @@ from mitmproxy.utils import human -def cancel_task(task: asyncio.Task, message: str) -> None: - """Like task.cancel(), but optionally with a message if the Python version supports it.""" - if sys.version_info >= (3, 9): - task.cancel(message) # type: ignore - else: # pragma: no cover - task.cancel() - - def create_task( - coro: Coroutine, *, - name: str, - client: Optional[tuple] = None, - ignore_closed_loop: bool = True, -) -> Optional[asyncio.Task]: + coro: Coroutine, + *, + name: str, + client: Optional[tuple] = None, +) -> asyncio.Task: """ Like asyncio.create_task, but also store some debug info on the task object. - - If ignore_closed_loop is True, the task will be silently discarded if the event loop is closed. - This is currently useful during shutdown where no new tasks can be spawned. - Ideally we stop closing the event loop during shutdown and then remove this parameter. """ - try: - t = asyncio.create_task(coro, name=name) - except RuntimeError: - if ignore_closed_loop: - coro.close() - return None - else: - raise + t = asyncio.create_task(coro) set_task_debug_info(t, name=name, client=client) return t def set_task_debug_info( - task: asyncio.Task, - *, - name: str, - client: Optional[tuple] = None, + task: asyncio.Task, + *, + name: str, + client: Optional[tuple] = None, ) -> None: """Set debug info for an externally-spawned task.""" task.created = time.time() # type: ignore @@ -53,12 +33,25 @@ def set_task_debug_info( task.client = client # type: ignore +def set_current_task_debug_info( + *, + name: str, + client: Optional[tuple] = None, +) -> None: + """Set debug info for the current task.""" + task = asyncio.current_task() + assert task + set_task_debug_info(task, name=name, client=client) + + def task_repr(task: asyncio.Task) -> str: """Get a task representation with debug info.""" name = task.get_name() - age = getattr(task, "created", "") - if age: - age = f" (age: {time.time() - age:.0f}s)" + a: float = getattr(task, "created", 0) + if a: + age = f" (age: {time.time() - a:.0f}s)" + else: + age = "" client = getattr(task, "client", "") if client: client = f"{human.format_address(client)}: " diff --git a/mitmproxy/utils/bits.py b/mitmproxy/utils/bits.py index 2c89a999b2..4aa49036fd 100644 --- a/mitmproxy/utils/bits.py +++ b/mitmproxy/utils/bits.py @@ -1,6 +1,6 @@ def setbit(byte, offset, value): """ - Set a bit in a byte to 1 if value is truthy, 0 if not. + Set a bit in a byte to 1 if value is truthy, 0 if not. """ if value: return byte | (1 << offset) diff --git a/mitmproxy/utils/data.py b/mitmproxy/utils/data.py index 5a175fcef8..b715072178 100644 --- a/mitmproxy/utils/data.py +++ b/mitmproxy/utils/data.py @@ -4,7 +4,6 @@ class Data: - def __init__(self, name): self.name = name m = importlib.import_module(name) @@ -13,7 +12,7 @@ def __init__(self, name): def push(self, subpath): """ - Change the data object to a path relative to the module. + Change the data object to a path relative to the module. """ dirname = os.path.normpath(os.path.join(self.dirname, subpath)) ret = Data(self.name) @@ -22,10 +21,10 @@ def push(self, subpath): def path(self, path): """ - Returns a path to the package data housed at 'path' under this - module.Path can be a path to a file, or to a directory. + Returns a path to the package data housed at 'path' under this + module.Path can be a path to a file, or to a directory. - This function will raise ValueError if the path does not exist. + This function will raise ValueError if the path does not exist. """ fullpath = os.path.normpath(os.path.join(self.dirname, path)) if not os.path.exists(fullpath): diff --git a/mitmproxy/utils/debug.py b/mitmproxy/utils/debug.py index 986372efa2..a522d46a6f 100644 --- a/mitmproxy/utils/debug.py +++ b/mitmproxy/utils/debug.py @@ -7,9 +7,11 @@ import sys import threading import traceback +from collections import Counter from contextlib import redirect_stdout from OpenSSL import SSL + from mitmproxy import version from mitmproxy.utils import asyncio_utils @@ -20,13 +22,13 @@ def dump_system_info(): data = [ f"Mitmproxy: {mitmproxy_version}", f"Python: {platform.python_version()}", - "OpenSSL: {}".format(SSL.SSLeay_version(SSL.SSLEAY_VERSION).decode()), + f"OpenSSL: {SSL.SSLeay_version(SSL.SSLEAY_VERSION).decode()}", f"Platform: {platform.platform()}", ] return "\n".join(data) -def dump_info(signal=None, frame=None, file=sys.stdout, testing=False): # pragma: no cover +def dump_info(signal=None, frame=None, file=sys.stdout): # pragma: no cover with redirect_stdout(file): print("****************************************************") print("Summary") @@ -70,17 +72,19 @@ def dump_info(signal=None, frame=None, file=sys.stdout, testing=False): # pragm print() print("Memory") - print("=======") + print("======") gc.collect() - d = {} - for i in gc.get_objects(): - t = str(type(i)) - if "mitmproxy" in t: - d[t] = d.setdefault(t, 0) + 1 - itms = list(d.items()) - itms.sort(key=lambda x: x[1]) - for i in itms[-20:]: - print(i[1], i[0]) + objs = Counter(str(type(i)) for i in gc.get_objects()) + + for cls, count in objs.most_common(20): + print(f"{count} {cls}") + + print() + print("Memory (mitmproxy only)") + print("=======================") + mitm_objs = Counter({k: v for k, v in objs.items() if "mitmproxy" in k}) + for cls, count in mitm_objs.most_common(20): + print(f"{count} {cls}") try: asyncio.get_running_loop() @@ -92,32 +96,29 @@ def dump_info(signal=None, frame=None, file=sys.stdout, testing=False): # pragm print("=======") for task in asyncio.all_tasks(): f = task.get_stack(limit=1)[0] - line = linecache.getline(f.f_code.co_filename, f.f_lineno, f.f_globals).strip() + line = linecache.getline( + f.f_code.co_filename, f.f_lineno, f.f_globals + ).strip() line = f"{line} # at {os.path.basename(f.f_code.co_filename)}:{f.f_lineno}" - print(f"{asyncio_utils.task_repr(task)}\n" - f" {line}") + print(f"{asyncio_utils.task_repr(task)}\n" f" {line}") print("****************************************************") - if not testing: + if os.getenv("MITMPROXY_DEBUG_EXIT"): # pragma: no cover sys.exit(1) -def dump_stacks(signal=None, frame=None, file=sys.stdout, testing=False): +def dump_stacks(signal=None, frame=None, file=sys.stdout): id2name = {th.ident: th.name for th in threading.enumerate()} code = [] for threadId, stack in sys._current_frames().items(): - code.append( - "\n# Thread: %s(%d)" % ( - id2name.get(threadId, ""), threadId - ) - ) + code.append("\n# Thread: %s(%d)" % (id2name.get(threadId, ""), threadId)) for filename, lineno, name, line in traceback.extract_stack(stack): code.append('File: "%s", line %d, in %s' % (filename, lineno, name)) if line: code.append(" %s" % (line.strip())) print("\n".join(code), file=file) - if not testing: # pragma: no cover + if os.getenv("MITMPROXY_DEBUG_EXIT"): # pragma: no cover sys.exit(1) diff --git a/mitmproxy/utils/emoji.py b/mitmproxy/utils/emoji.py index 35832b85d8..2bb31432ea 100644 --- a/mitmproxy/utils/emoji.py +++ b/mitmproxy/utils/emoji.py @@ -1882,6 +1882,11 @@ print(CHAR_SRC.format(name=c, emoji_val=c), file=out) Path(__file__).write_text( - re.sub(r"(?<={\n)[\s\S]*(?=}\n)", lambda x: out.getvalue(), Path(__file__).read_text("utf8"), 1), - "utf8" - ) \ No newline at end of file + re.sub( + r"(?<={\n)[\s\S]*(?=}\n)", + lambda x: out.getvalue(), + Path(__file__).read_text("utf8"), + 1, + ), + "utf8", + ) diff --git a/mitmproxy/utils/human.py b/mitmproxy/utils/human.py index 1c9654b5b8..ddb336340b 100644 --- a/mitmproxy/utils/human.py +++ b/mitmproxy/utils/human.py @@ -2,8 +2,7 @@ import functools import ipaddress import time -import typing - +from typing import Optional SIZE_UNITS = { "b": 1024 ** 0, @@ -31,8 +30,8 @@ def pretty_size(size: int) -> str: raise AssertionError -@functools.lru_cache() -def parse_size(s: typing.Optional[str]) -> typing.Optional[int]: +@functools.lru_cache +def parse_size(s: Optional[str]) -> Optional[int]: """ Parse a size with an optional k/m/... suffix. Invalid values raise a ValueError. For added convenience, passing `None` returns `None`. @@ -52,7 +51,7 @@ def parse_size(s: typing.Optional[str]) -> typing.Optional[int]: raise ValueError("Invalid size specification.") -def pretty_duration(secs: typing.Optional[float]) -> str: +def pretty_duration(secs: Optional[float]) -> str: formatters = [ (100, "{:.0f}s"), (10, "{:2.1f}s"), @@ -65,7 +64,7 @@ def pretty_duration(secs: typing.Optional[float]) -> str: if secs >= limit: return formatter.format(secs) # less than 1 sec - return "{:.0f}ms".format(secs * 1000) + return f"{secs * 1000:.0f}ms" def format_timestamp(s): @@ -79,8 +78,8 @@ def format_timestamp_with_milli(s): return d.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3] -@functools.lru_cache() -def format_address(address: typing.Optional[tuple]) -> str: +@functools.lru_cache +def format_address(address: Optional[tuple]) -> str: """ This function accepts IPv4/IPv6 tuples and returns the formatted address string with port number @@ -90,12 +89,12 @@ def format_address(address: typing.Optional[tuple]) -> str: try: host = ipaddress.ip_address(address[0]) if host.is_unspecified: - return "*:{}".format(address[1]) + return f"*:{address[1]}" if isinstance(host, ipaddress.IPv4Address): - return "{}:{}".format(str(host), address[1]) + return f"{str(host)}:{address[1]}" # If IPv6 is mapped to IPv4 elif host.ipv4_mapped: - return "{}:{}".format(str(host.ipv4_mapped), address[1]) - return "[{}]:{}".format(str(host), address[1]) + return f"{str(host.ipv4_mapped)}:{address[1]}" + return f"[{str(host)}]:{address[1]}" except ValueError: - return "{}:{}".format(address[0], address[1]) + return f"{address[0]}:{address[1]}" diff --git a/mitmproxy/utils/pyinstaller/__init__.py b/mitmproxy/utils/pyinstaller/__init__.py new file mode 100644 index 0000000000..0a9bb3903c --- /dev/null +++ b/mitmproxy/utils/pyinstaller/__init__.py @@ -0,0 +1,7 @@ +from pathlib import Path + +here = Path(__file__).parent.absolute() + + +def hook_dirs() -> list[str]: + return [str(here)] diff --git a/mitmproxy/utils/pyinstaller/hook-mitmproxy.addons.onboardingapp.py b/mitmproxy/utils/pyinstaller/hook-mitmproxy.addons.onboardingapp.py new file mode 100644 index 0000000000..8269d39509 --- /dev/null +++ b/mitmproxy/utils/pyinstaller/hook-mitmproxy.addons.onboardingapp.py @@ -0,0 +1,3 @@ +from PyInstaller.utils.hooks import collect_data_files + +datas = collect_data_files("mitmproxy.addons.onboardingapp") diff --git a/release/hooks/hook-mitmproxy.py b/mitmproxy/utils/pyinstaller/hook-mitmproxy.py similarity index 100% rename from release/hooks/hook-mitmproxy.py rename to mitmproxy/utils/pyinstaller/hook-mitmproxy.py diff --git a/release/hooks/hook-mitmproxy.tools.web.py b/mitmproxy/utils/pyinstaller/hook-mitmproxy.tools.web.py similarity index 52% rename from release/hooks/hook-mitmproxy.tools.web.py rename to mitmproxy/utils/pyinstaller/hook-mitmproxy.tools.web.py index 519c4c00e9..4078ded4d0 100644 --- a/release/hooks/hook-mitmproxy.tools.web.py +++ b/mitmproxy/utils/pyinstaller/hook-mitmproxy.tools.web.py @@ -1,3 +1,3 @@ from PyInstaller.utils.hooks import collect_data_files -datas = collect_data_files('mitmproxy.tools.web') +datas = collect_data_files("mitmproxy.tools.web") diff --git a/mitmproxy/utils/sliding_window.py b/mitmproxy/utils/sliding_window.py index cb31756dbe..dca71cbd6c 100644 --- a/mitmproxy/utils/sliding_window.py +++ b/mitmproxy/utils/sliding_window.py @@ -1,10 +1,12 @@ import itertools -from typing import TypeVar, Iterable, Iterator, Tuple, Optional, List +from typing import Iterable, Iterator, Optional, TypeVar -T = TypeVar('T') +T = TypeVar("T") -def window(iterator: Iterable[T], behind: int = 0, ahead: int = 0) -> Iterator[Tuple[Optional[T], ...]]: +def window( + iterator: Iterable[T], behind: int = 0, ahead: int = 0 +) -> Iterator[tuple[Optional[T], ...]]: """ Sliding window for an iterator. @@ -18,13 +20,13 @@ def window(iterator: Iterable[T], behind: int = 0, ahead: int = 0) -> Iterator[T 2 3 None """ # TODO: move into utils - iters: List[Iterator[Optional[T]]] = list(itertools.tee(iterator, behind + 1 + ahead)) + iters: list[Iterator[Optional[T]]] = list( + itertools.tee(iterator, behind + 1 + ahead) + ) for i in range(behind): iters[i] = itertools.chain((behind - i) * [None], iters[i]) for i in range(ahead): iters[-1 - i] = itertools.islice( - itertools.chain(iters[-1 - i], (ahead - i) * [None]), - (ahead - i), - None + itertools.chain(iters[-1 - i], (ahead - i) * [None]), (ahead - i), None ) return zip(*iters) diff --git a/mitmproxy/utils/spec.py b/mitmproxy/utils/spec.py index 1298685b91..3194012256 100644 --- a/mitmproxy/utils/spec.py +++ b/mitmproxy/utils/spec.py @@ -1,8 +1,7 @@ -import typing from mitmproxy import flowfilter -def parse_spec(option: str) -> typing.Tuple[flowfilter.TFilter, str, str]: +def parse_spec(option: str) -> tuple[flowfilter.TFilter, str, str]: """ Parse strings in the following format: diff --git a/mitmproxy/utils/strutils.py b/mitmproxy/utils/strutils.py index 0622b737dc..6f61ff54de 100644 --- a/mitmproxy/utils/strutils.py +++ b/mitmproxy/utils/strutils.py @@ -6,6 +6,7 @@ # https://mypy.readthedocs.io/en/stable/more_types.html#function-overloading + @overload def always_bytes(str_or_bytes: None, *encode_args) -> None: ... @@ -16,13 +17,17 @@ def always_bytes(str_or_bytes: Union[str, bytes], *encode_args) -> bytes: ... -def always_bytes(str_or_bytes: Union[None, str, bytes], *encode_args) -> Union[None, bytes]: +def always_bytes( + str_or_bytes: Union[None, str, bytes], *encode_args +) -> Union[None, bytes]: if str_or_bytes is None or isinstance(str_or_bytes, bytes): return str_or_bytes elif isinstance(str_or_bytes, str): return str_or_bytes.encode(*encode_args) else: - raise TypeError("Expected str or bytes, but got {}.".format(type(str_or_bytes).__name__)) + raise TypeError( + f"Expected str or bytes, but got {type(str_or_bytes).__name__}." + ) @overload @@ -45,7 +50,9 @@ def always_str(str_or_bytes: Union[None, str, bytes], *decode_args) -> Union[Non elif isinstance(str_or_bytes, bytes): return str_or_bytes.decode(*decode_args) else: - raise TypeError("Expected str or bytes, but got {}.".format(type(str_or_bytes).__name__)) + raise TypeError( + f"Expected str or bytes, but got {type(str_or_bytes).__name__}." + ) # Translate control characters to "safe" characters. This implementation @@ -53,8 +60,7 @@ def always_str(str_or_bytes: Union[None, str, bytes], *decode_args) -> Union[Non # (http://unicode.org/charts/PDF/U2400.pdf), but that turned out to render badly # with monospace fonts. We are back to "." therefore. _control_char_trans = { - x: ord(".") # x + 0x2400 for unicode control group pictures - for x in range(32) + x: ord(".") for x in range(32) # x + 0x2400 for unicode control group pictures } _control_char_trans[127] = ord(".") # 0x2421 _control_char_trans_newline = _control_char_trans.copy() @@ -73,13 +79,15 @@ def escape_control_characters(text: str, keep_spacing=True) -> str: keep_spacing: If True, tabs and newlines will not be replaced. """ if not isinstance(text, str): - raise ValueError("text type must be unicode but is {}".format(type(text).__name__)) + raise ValueError(f"text type must be unicode but is {type(text).__name__}") trans = _control_char_trans_newline if keep_spacing else _control_char_trans return text.translate(trans) -def bytes_to_escaped_str(data: bytes, keep_spacing: bool = False, escape_single_quotes: bool = False) -> str: +def bytes_to_escaped_str( + data: bytes, keep_spacing: bool = False, escape_single_quotes: bool = False +) -> str: """ Take bytes and return a safe string that can be displayed to the user. @@ -102,7 +110,7 @@ def bytes_to_escaped_str(data: bytes, keep_spacing: bool = False, escape_single_ ret = re.sub( r"(? bool: if not s or len(s) == 0: return False - return sum( - i < 9 or 13 < i < 32 or 126 < i - for i in s[:100] - ) / len(s[:100]) > 0.3 + return sum(i < 9 or 13 < i < 32 or 126 < i for i in s[:100]) / len(s[:100]) > 0.3 def is_xml(s: bytes) -> bool: @@ -142,10 +147,10 @@ def is_xml(s: bytes) -> bool: def clean_hanging_newline(t): """ - Many editors will silently add a newline to the final line of a - document (I'm looking at you, Vim). This function fixes this common - problem at the risk of removing a hanging newline in the rare cases - where the user actually intends it. + Many editors will silently add a newline to the final line of a + document (I'm looking at you, Vim). This function fixes this common + problem at the risk of removing a hanging newline in the rare cases + where the user actually intends it. """ if t and t[-1] == "\n": return t[:-1] @@ -154,18 +159,19 @@ def clean_hanging_newline(t): def hexdump(s): """ - Returns: - A generator of (offset, hex, str) tuples + Returns: + A generator of (offset, hex, str) tuples """ for i in range(0, len(s), 16): offset = f"{i:0=10x}" - part = s[i:i + 16] + part = s[i : i + 16] x = " ".join(f"{i:0=2x}" for i in part) x = x.ljust(47) # 16*2 + 15 - part_repr = always_str(escape_control_characters( - part.decode("ascii", "replace").replace("\ufffd", "."), - False - )) + part_repr = always_str( + escape_control_characters( + part.decode("ascii", "replace").replace("\ufffd", "."), False + ) + ) yield (offset, x, part_repr) @@ -184,8 +190,8 @@ def _restore_from_private_code_plane(matchobj): def split_special_areas( - data: str, - area_delimiter: Iterable[str], + data: str, + area_delimiter: Iterable[str], ): """ Split a string of code into a [code, special area, code, special area, ..., code] list. @@ -199,17 +205,13 @@ def split_special_areas( "".join(split_special_areas(x, ...)) == x always holds true. """ - return re.split( - "({})".format("|".join(area_delimiter)), - data, - flags=re.MULTILINE - ) + return re.split("({})".format("|".join(area_delimiter)), data, flags=re.MULTILINE) def escape_special_areas( - data: str, - area_delimiter: Iterable[str], - control_characters, + data: str, + area_delimiter: Iterable[str], + control_characters, ): """ Escape all control characters present in special areas with UTF8 symbols diff --git a/mitmproxy/utils/typecheck.py b/mitmproxy/utils/typecheck.py index 501fcb078e..49ce3b90fe 100644 --- a/mitmproxy/utils/typecheck.py +++ b/mitmproxy/utils/typecheck.py @@ -1,46 +1,28 @@ import typing +from collections import abc + +try: + from types import UnionType +except ImportError: + UnionType = object() # type: ignore Type = typing.Union[ typing.Any # anything more elaborate really fails with mypy at the moment. ] -def sequence_type(typeinfo: typing.Type[typing.List]) -> Type: - """Return the type of a sequence, e.g. typing.List""" - return typeinfo.__args__[0] # type: ignore - - -def tuple_types(typeinfo: typing.Type[typing.Tuple]) -> typing.Sequence[Type]: - """Return the types of a typing.Tuple""" - return typeinfo.__args__ # type: ignore - - -def union_types(typeinfo: typing.Type[typing.Tuple]) -> typing.Sequence[Type]: - """return the types of a typing.Union""" - return typeinfo.__args__ # type: ignore - - -def mapping_types(typeinfo: typing.Type[typing.Mapping]) -> typing.Tuple[Type, Type]: - """return the types of a mapping, e.g. typing.Dict""" - return typeinfo.__args__ # type: ignore - - def check_option_type(name: str, value: typing.Any, typeinfo: Type) -> None: """ Check if the provided value is an instance of typeinfo and raises a TypeError otherwise. This function supports only those types required for options. """ - e = TypeError("Expected {} for {}, but got {}.".format( - typeinfo, - name, - type(value) - )) + e = TypeError("Expected {} for {}, but got {}.".format(typeinfo, name, type(value))) - typename = str(typeinfo) + origin = typing.get_origin(typeinfo) - if typename.startswith("typing.Union") or typename.startswith("typing.Optional"): - for T in union_types(typeinfo): + if origin is typing.Union or origin is UnionType: + for T in typing.get_args(typeinfo): try: check_option_type(name, value, T) except TypeError: @@ -48,8 +30,8 @@ def check_option_type(name: str, value: typing.Any, typeinfo: Type) -> None: else: return raise e - elif typename.startswith("typing.Tuple"): - types = tuple_types(typeinfo) + elif origin is tuple: + types = typing.get_args(typeinfo) if not isinstance(value, (tuple, list)): raise e if len(types) != len(value): @@ -57,18 +39,18 @@ def check_option_type(name: str, value: typing.Any, typeinfo: Type) -> None: for i, (x, T) in enumerate(zip(value, types)): check_option_type(f"{name}[{i}]", x, T) return - elif typename.startswith("typing.Sequence"): - T = sequence_type(typeinfo) + elif origin is abc.Sequence: + T = typing.get_args(typeinfo)[0] if not isinstance(value, (tuple, list)): raise e for v in value: check_option_type(name, v, T) - elif typename.startswith("typing.IO"): + elif origin is typing.IO or typeinfo in (typing.TextIO, typing.BinaryIO): if hasattr(value, "read"): return else: raise e - elif typename.startswith("typing.Any"): + elif typeinfo is typing.Any: return elif not isinstance(value, typeinfo): if typeinfo is float and isinstance(value, int): @@ -80,11 +62,11 @@ def typespec_to_str(typespec: typing.Any) -> str: if typespec in (str, int, float, bool): t = typespec.__name__ elif typespec == typing.Optional[str]: - t = 'optional str' - elif typespec == typing.Sequence[str]: - t = 'sequence of str' + t = "optional str" + elif typespec in (typing.Sequence[str], abc.Sequence[str]): + t = "sequence of str" elif typespec == typing.Optional[int]: - t = 'optional int' + t = "optional int" else: raise NotImplementedError return t diff --git a/mitmproxy/utils/vt_codes.py b/mitmproxy/utils/vt_codes.py new file mode 100644 index 0000000000..e33a8f2492 --- /dev/null +++ b/mitmproxy/utils/vt_codes.py @@ -0,0 +1,56 @@ +""" +This module provides a method to detect if a given file object supports virtual terminal escape codes. +""" +import os +import sys +from typing import IO + +if os.name == "nt": + from ctypes import byref, windll # type: ignore + from ctypes.wintypes import BOOL, DWORD, HANDLE, LPDWORD + + ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + STD_OUTPUT_HANDLE = -11 + STD_ERROR_HANDLE = -12 + + # https://docs.microsoft.com/de-de/windows/console/getstdhandle + GetStdHandle = windll.kernel32.GetStdHandle + GetStdHandle.argtypes = [DWORD] + GetStdHandle.restype = HANDLE + + # https://docs.microsoft.com/de-de/windows/console/getconsolemode + GetConsoleMode = windll.kernel32.GetConsoleMode + GetConsoleMode.argtypes = [HANDLE, LPDWORD] + GetConsoleMode.restype = BOOL + + # https://docs.microsoft.com/de-de/windows/console/setconsolemode + SetConsoleMode = windll.kernel32.SetConsoleMode + SetConsoleMode.argtypes = [HANDLE, DWORD] + SetConsoleMode.restype = BOOL + + def ensure_supported(f: IO[str]) -> bool: + if not f.isatty(): + return False + if f == sys.stdout: + h = STD_OUTPUT_HANDLE + elif f == sys.stderr: + h = STD_ERROR_HANDLE + else: + return False + + handle = GetStdHandle(h) + console_mode = DWORD() + ok = GetConsoleMode(handle, byref(console_mode)) + if not ok: + return False + + ok = SetConsoleMode( + handle, console_mode.value | ENABLE_VIRTUAL_TERMINAL_PROCESSING + ) + return ok + + +else: + + def ensure_supported(f: IO[str]) -> bool: + return f.isatty() diff --git a/mitmproxy/version.py b/mitmproxy/version.py index 67fe27b306..a047d86f61 100644 --- a/mitmproxy/version.py +++ b/mitmproxy/version.py @@ -2,12 +2,12 @@ import subprocess import sys -VERSION = "8.0.0.dev" +VERSION = "8.1.1" MITMPROXY = "mitmproxy " + VERSION # Serialization format version. This is displayed nowhere, it just needs to be incremented by one # for each change in the file format. -FLOW_FORMAT_VERSION = 14 +FLOW_FORMAT_VERSION = 17 def get_dev_version() -> str: @@ -22,13 +22,14 @@ def get_dev_version() -> str: # Check that we're in the mitmproxy repository: https://github.com/mitmproxy/mitmproxy/issues/3987 # cb0e3287090786fad566feb67ac07b8ef361b2c3 is the first mitmproxy commit. subprocess.run( - ['git', 'cat-file', '-e', 'cb0e3287090786fad566feb67ac07b8ef361b2c3'], + ["git", "cat-file", "-e", "cb0e3287090786fad566feb67ac07b8ef361b2c3"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, cwd=here, - check=True) + check=True, + ) git_describe = subprocess.check_output( - ['git', 'describe', '--tags', '--long'], + ["git", "describe", "--tags", "--long"], stderr=subprocess.STDOUT, cwd=here, ) @@ -43,7 +44,7 @@ def get_dev_version() -> str: mitmproxy_version += f" (+{tag_dist}, commit {commit})" # PyInstaller build indicator, if using precompiled binary - if getattr(sys, 'frozen', False): + if getattr(sys, "frozen", False): mitmproxy_version += " binary" return mitmproxy_version diff --git a/mitmproxy/websocket.py b/mitmproxy/websocket.py index 0269d888d2..7cac0a712c 100644 --- a/mitmproxy/websocket.py +++ b/mitmproxy/websocket.py @@ -7,14 +7,14 @@ """ import time import warnings -from typing import List, Tuple, Union +from typing import Union from typing import Optional from mitmproxy import stateobject from mitmproxy.coretypes import serializable from wsproto.frame_protocol import Opcode -WebSocketMessageState = Tuple[int, bool, bytes, float, bool] +WebSocketMessageState = tuple[int, bool, bytes, float, bool, bool] class WebSocketMessage(serializable.Serializable): @@ -47,6 +47,8 @@ class WebSocketMessage(serializable.Serializable): """Timestamp of when this message was received or created.""" dropped: bool """True if the message has not been forwarded by mitmproxy, False otherwise.""" + injected: bool + """True if the message was injected and did not originate from a client/server, False otherwise""" def __init__( self, @@ -54,23 +56,39 @@ def __init__( from_client: bool, content: bytes, timestamp: Optional[float] = None, - killed: bool = False, + dropped: bool = False, + injected: bool = False, ) -> None: self.from_client = from_client self.type = Opcode(type) self.content = content self.timestamp: float = timestamp or time.time() - self.dropped = killed + self.dropped = dropped + self.injected = injected @classmethod def from_state(cls, state: WebSocketMessageState): return cls(*state) def get_state(self) -> WebSocketMessageState: - return int(self.type), self.from_client, self.content, self.timestamp, self.dropped + return ( + int(self.type), + self.from_client, + self.content, + self.timestamp, + self.dropped, + self.injected, + ) def set_state(self, state: WebSocketMessageState) -> None: - typ, self.from_client, self.content, self.timestamp, self.dropped = state + ( + typ, + self.from_client, + self.content, + self.timestamp, + self.dropped, + self.injected, + ) = state self.type = Opcode(typ) def __repr__(self): @@ -93,7 +111,11 @@ def drop(self): def kill(self): # pragma: no cover """A deprecated alias for `.drop()`.""" - warnings.warn("WebSocketMessage.kill() is deprecated, use .drop() instead.", DeprecationWarning, stacklevel=2) + warnings.warn( + "WebSocketMessage.kill() is deprecated, use .drop() instead.", + DeprecationWarning, + stacklevel=2, + ) self.drop() @property @@ -106,14 +128,18 @@ def text(self) -> str: *See also:* `WebSocketMessage.content` """ if self.type != Opcode.TEXT: - raise AttributeError(f"{self.type.name.title()} WebSocket frames do not have a 'text' attribute.") + raise AttributeError( + f"{self.type.name.title()} WebSocket frames do not have a 'text' attribute." + ) return self.content.decode() @text.setter def text(self, value: str) -> None: if self.type != Opcode.TEXT: - raise AttributeError(f"{self.type.name.title()} WebSocket frames do not have a 'text' attribute.") + raise AttributeError( + f"{self.type.name.title()} WebSocket frames do not have a 'text' attribute." + ) self.content = value.encode() @@ -124,7 +150,7 @@ class WebSocketData(stateobject.StateObject): This is typically accessed as `mitmproxy.http.HTTPFlow.websocket`. """ - messages: List[WebSocketMessage] + messages: list[WebSocketMessage] """All `WebSocketMessage`s transferred over this connection.""" closed_by_client: Optional[bool] = None @@ -142,7 +168,7 @@ class WebSocketData(stateobject.StateObject): """*Timestamp:* WebSocket connection closed.""" _stateobject_attributes = dict( - messages=List[WebSocketMessage], + messages=list[WebSocketMessage], closed_by_client=bool, close_code=int, close_reason=str, diff --git a/release/README.md b/release/README.md index fbb6820d6c..b14737122a 100644 --- a/release/README.md +++ b/release/README.md @@ -3,7 +3,7 @@ These steps assume you are on the correct branch and have a git remote called `origin` that points to the `mitmproxy/mitmproxy` repo. If necessary, create a major version branch starting off the release tag (e.g. `git checkout -b v4.x v4.0.0`) first. - Update CHANGELOG. -- Verify that the compiled mitmweb assets are up-to-date. +- Verify that the compiled mitmweb assets are up-to-date (`npm start prod`). - Verify that all CI tests pass. - Verify that `mitmproxy/version.py` is correct. Remove `.dev` suffix if it exists. - Tag the release and push to GitHub. diff --git a/release/cibuild.py b/release/cibuild.py index 318623e827..a1c6ac5e6f 100755 --- a/release/cibuild.py +++ b/release/cibuild.py @@ -85,7 +85,9 @@ def from_env(cls) -> "BuildEnviron": if ref.startswith("refs/tags/"): tag = ref.replace("refs/tags/", "") - is_pull_request = os.environ.get("GITHUB_EVENT_NAME", "pull_request") == "pull_request" + is_pull_request = ( + os.environ.get("GITHUB_EVENT_NAME", "pull_request") == "pull_request" + ) return cls( system=platform.system(), @@ -98,7 +100,8 @@ def from_env(cls) -> "BuildEnviron": should_build_wininstaller=bool_from_env("CI_BUILD_WININSTALLER"), should_build_docker=bool_from_env("CI_BUILD_DOCKER"), has_aws_creds=bool_from_env("AWS_ACCESS_KEY_ID"), - has_twine_creds=bool_from_env("TWINE_USERNAME") and bool_from_env("TWINE_PASSWORD"), + has_twine_creds=bool_from_env("TWINE_USERNAME") + and bool_from_env("TWINE_PASSWORD"), docker_username=os.environ.get("DOCKER_USERNAME", None), docker_password=os.environ.get("DOCKER_PASSWORD", None), build_key=os.environ.get("CI_BUILD_KEY", None), @@ -169,7 +172,9 @@ def check_version(self) -> None: if self.is_prod_release: # For production releases, we require strict version equality if self.version != version: - raise ValueError(f"Tag is {self.tag}, but mitmproxy/version.py is {version}.") + raise ValueError( + f"Tag is {self.tag}, but mitmproxy/version.py is {version}." + ) elif not self.is_maintenance_branch: # Commits on maintenance branches don't need the dev suffix. This # allows us to incorporate and test commits between tagged releases. @@ -177,12 +182,14 @@ def check_version(self) -> None: # dev release. version_info = parver.Version.parse(version) if not version_info.is_devrelease: - raise ValueError(f"Non-production releases must have dev suffix: {version}") + raise ValueError( + f"Non-production releases must have dev suffix: {version}" + ) @property def is_maintenance_branch(self) -> bool: """ - Is this an untagged commit on a maintenance branch? + Is this an untagged commit on a maintenance branch? """ if not self.tag and self.branch and re.match(r"v\d+\.x", self.branch): return True @@ -214,26 +221,36 @@ def release_dir(self) -> Path: @property def should_upload_docker(self) -> bool: - return all([ - (self.is_prod_release or self.branch in ["main", "dockertest"]), - self.should_build_docker, - self.has_docker_creds, - ]) + return all( + [ + (self.is_prod_release or self.branch in ["main", "dockertest"]), + self.should_build_docker, + self.has_docker_creds, + ] + ) @property def should_upload_aws(self) -> bool: - return all([ - self.has_aws_creds, - (self.should_build_wheel or self.should_build_pyinstaller or self.should_build_wininstaller), - ]) + return all( + [ + self.has_aws_creds, + ( + self.should_build_wheel + or self.should_build_pyinstaller + or self.should_build_wininstaller + ), + ] + ) @property def should_upload_pypi(self) -> bool: - return all([ - self.is_prod_release, - self.should_build_wheel, - self.has_twine_creds, - ]) + return all( + [ + self.is_prod_release, + self.should_build_wheel, + self.has_twine_creds, + ] + ) @property def upload_dir(self) -> str: @@ -255,19 +272,24 @@ def version(self) -> str: elif self.branch: return self.branch else: - raise BuildError("We're on neither a tag nor a branch - could not establish version") + raise BuildError( + "We're on neither a tag nor a branch - could not establish version" + ) def build_wheel(be: BuildEnviron) -> None: # pragma: no cover click.echo("Building wheel...") - subprocess.check_call([ - "python", - "setup.py", - "-q", - "bdist_wheel", - "--dist-dir", be.dist_dir, - ]) - whl, = be.dist_dir.glob('mitmproxy-*-py3-none-any.whl') + subprocess.check_call( + [ + "python", + "setup.py", + "-q", + "bdist_wheel", + "--dist-dir", + be.dist_dir, + ] + ) + (whl,) = be.dist_dir.glob("mitmproxy-*-py3-none-any.whl") click.echo(f"Found wheel package: {whl}") subprocess.check_call(["tox", "-e", "wheeltest", "--", whl]) @@ -278,36 +300,55 @@ def build_wheel(be: BuildEnviron) -> None: # pragma: no cover def build_docker_image(be: BuildEnviron) -> None: # pragma: no cover click.echo("Building Docker images...") - whl, = be.dist_dir.glob('mitmproxy-*-py3-none-any.whl') + (whl,) = be.dist_dir.glob("mitmproxy-*-py3-none-any.whl") docker_build_dir = be.release_dir / "docker" shutil.copy(whl, docker_build_dir / whl.name) - subprocess.check_call([ - "docker", "buildx", "build", - "--tag", be.docker_tag, - "--platform", DOCKER_PLATFORMS, - "--build-arg", f"MITMPROXY_WHEEL={whl.name}", - "." - ], cwd=docker_build_dir) + subprocess.check_call( + [ + "docker", + "buildx", + "build", + "--tag", + be.docker_tag, + "--platform", + DOCKER_PLATFORMS, + "--build-arg", + f"MITMPROXY_WHEEL={whl.name}", + ".", + ], + cwd=docker_build_dir, + ) # smoke-test the newly built docker image # build again without --platform but with --load to make the tag available, # see https://github.com/docker/buildx/issues/59#issuecomment-616050491 - subprocess.check_call([ - "docker", "buildx", "build", - "--tag", be.docker_tag, - "--load", - "--build-arg", f"MITMPROXY_WHEEL={whl.name}", - "." - ], cwd=docker_build_dir) - r = subprocess.run([ - "docker", - "run", - "--rm", - be.docker_tag, - "mitmdump", - "--version", - ], check=True, capture_output=True) + subprocess.check_call( + [ + "docker", + "buildx", + "build", + "--tag", + be.docker_tag, + "--load", + "--build-arg", + f"MITMPROXY_WHEEL={whl.name}", + ".", + ], + cwd=docker_build_dir, + ) + r = subprocess.run( + [ + "docker", + "run", + "--rm", + be.docker_tag, + "mitmdump", + "--version", + ], + check=True, + capture_output=True, + ) print(r.stdout.decode()) assert "Mitmproxy: " in r.stdout.decode() @@ -316,7 +357,6 @@ def build_pyinstaller(be: BuildEnviron) -> None: # pragma: no cover click.echo("Building pyinstaller package...") PYINSTALLER_SPEC = be.release_dir / "specs" - PYINSTALLER_HOOKS = be.release_dir / "hooks" PYINSTALLER_TEMP = be.build_dir / "pyinstaller" PYINSTALLER_DIST = be.build_dir / "binaries" / be.platform_tag @@ -332,9 +372,11 @@ def build_pyinstaller(be: BuildEnviron) -> None: # pragma: no cover [ "pyinstaller", "--clean", - "--workpath", PYINSTALLER_TEMP, - "--distpath", PYINSTALLER_DIST, - "./windows-dir.spec" + "--workpath", + PYINSTALLER_TEMP, + "--distpath", + PYINSTALLER_DIST, + "./windows-dir.spec", ] ) for tool in ["mitmproxy", "mitmdump", "mitmweb"]: @@ -357,15 +399,17 @@ def build_pyinstaller(be: BuildEnviron) -> None: # pragma: no cover excludes.append("mitmproxy.tools.console") subprocess.check_call( - [ # type: ignore + [ # type: ignore "pyinstaller", "--clean", - "--workpath", PYINSTALLER_TEMP, - "--distpath", PYINSTALLER_DIST, - "--additional-hooks-dir", PYINSTALLER_HOOKS, + "--workpath", + PYINSTALLER_TEMP, + "--distpath", + PYINSTALLER_DIST, "--onefile", "--console", - "--icon", "icon.ico", + "--icon", + "icon.ico", ] + [x for e in excludes for x in ["--exclude-module", e]] + [tool] @@ -388,7 +432,7 @@ def build_pyinstaller(be: BuildEnviron) -> None: # pragma: no cover click.echo(subprocess.check_output([executable, "--version"]).decode()) archive.add(str(executable), str(executable.name)) - click.echo("Packed {}.".format(be.archive_path.name)) + click.echo(f"Packed {be.archive_path.name}.") def build_wininstaller(be: BuildEnviron) -> None: # pragma: no cover @@ -398,7 +442,9 @@ def build_wininstaller(be: BuildEnviron) -> None: # pragma: no cover IB_SETUP_SHA256 = "2bc9f9945cb727ad176aa31fa2fa5a8c57a975bad879c169b93e312af9d05814" IB_DIR = be.release_dir / "installbuilder" IB_SETUP = IB_DIR / "setup" / f"{IB_VERSION}-installer.exe" - IB_CLI = Path(fr"C:\Program Files\VMware InstallBuilder Enterprise {IB_VERSION}\bin\builder-cli.exe") + IB_CLI = Path( + fr"C:\Program Files\VMware InstallBuilder Enterprise {IB_VERSION}\bin\builder-cli.exe" + ) IB_LICENSE = IB_DIR / "license.xml" if not IB_LICENSE.exists() and not be.build_key: @@ -418,7 +464,7 @@ def report(block, blocksize, total): urllib.request.urlretrieve( f"https://clients.bitrock.com/installbuilder/installbuilder-enterprise-{IB_VERSION}-windows-x64-installer.exe", tmp, - reporthook=report + reporthook=report, ) tmp.rename(IB_SETUP) @@ -433,28 +479,36 @@ def report(block, blocksize, total): raise RuntimeError("InstallBuilder hashes don't match.") click.echo("Install InstallBuilder...") - subprocess.run([IB_SETUP, "--mode", "unattended", "--unattendedmodeui", "none"], check=True) + subprocess.run( + [IB_SETUP, "--mode", "unattended", "--unattendedmodeui", "none"], check=True + ) assert IB_CLI.is_file() if not IB_LICENSE.exists(): assert be.build_key click.echo("Decrypt InstallBuilder license...") f = cryptography.fernet.Fernet(be.build_key.encode()) - with open(IB_LICENSE.with_suffix(".xml.enc"), "rb") as infile, \ - open(IB_LICENSE, "wb") as outfile: + with open(IB_LICENSE.with_suffix(".xml.enc"), "rb") as infile, open( + IB_LICENSE, "wb" + ) as outfile: outfile.write(f.decrypt(infile.read())) click.echo("Run InstallBuilder...") - subprocess.run([ - IB_CLI, - "build", - str(IB_DIR / "mitmproxy.xml"), - "windows", - "--license", str(IB_LICENSE), - "--setvars", f"project.version={be.version}", - "--verbose" - ], check=True) - assert (be.dist_dir / f"mitmproxy-{be.version}-windows-installer.exe").exists() + subprocess.run( + [ + IB_CLI, + "build", + str(IB_DIR / "mitmproxy.xml"), + "windows-x64", + "--license", + str(IB_LICENSE), + "--setvars", + f"project.version={be.version}", + "--verbose", + ], + check=True, + ) + assert (be.dist_dir / f"mitmproxy-{be.version}-windows-x64-installer.exe").exists() @click.group(chain=True) @@ -462,13 +516,12 @@ def cli(): # pragma: no cover """ mitmproxy build tool """ - pass @cli.command("build") def build(): # pragma: no cover """ - Build a binary distribution + Build a binary distribution """ be = BuildEnviron.from_env() be.dump_info() @@ -489,11 +542,11 @@ def build(): # pragma: no cover @cli.command("upload") def upload(): # pragma: no cover """ - Upload build artifacts + Upload build artifacts - Uploads the wheels package to PyPi. - Uploads the Pyinstaller and wheels packages to the snapshot server. - Pushes the Docker image to Docker Hub. + Uploads the wheels package to PyPi. + Uploads the Pyinstaller and wheels packages to the snapshot server. + Pushes the Docker image to Docker Hub. """ be = BuildEnviron.from_env() be.dump_info() @@ -505,50 +558,75 @@ def upload(): # pragma: no cover if be.should_upload_aws: num_files = len([name for name in be.dist_dir.iterdir() if name.is_file()]) click.echo(f"Uploading {num_files} files to AWS dir {be.upload_dir}...") - subprocess.check_call([ - "aws", "s3", "cp", - "--acl", "public-read", - f"{be.dist_dir}/", - f"s3://snapshots.mitmproxy.org/{be.upload_dir}/", - "--recursive", - ]) + subprocess.check_call( + [ + "aws", + "s3", + "cp", + "--acl", + "public-read", + f"{be.dist_dir}/", + f"s3://snapshots.mitmproxy.org/{be.upload_dir}/", + "--recursive", + ] + ) if be.should_upload_pypi: - whl, = be.dist_dir.glob('mitmproxy-*-py3-none-any.whl') + (whl,) = be.dist_dir.glob("mitmproxy-*-py3-none-any.whl") click.echo(f"Uploading {whl} to PyPi...") subprocess.check_call(["twine", "upload", whl]) if be.should_upload_docker: click.echo(f"Uploading Docker image to tag={be.docker_tag}...") - subprocess.check_call([ - "docker", - "login", - "-u", be.docker_username, - "-p", be.docker_password, - ]) + subprocess.check_call( + [ + "docker", + "login", + "-u", + be.docker_username, + "-p", + be.docker_password, + ] + ) - whl, = be.dist_dir.glob('mitmproxy-*-py3-none-any.whl') + (whl,) = be.dist_dir.glob("mitmproxy-*-py3-none-any.whl") docker_build_dir = be.release_dir / "docker" shutil.copy(whl, docker_build_dir / whl.name) # buildx is a bit weird in that we need to reinvoke build, but oh well. - subprocess.check_call([ - "docker", "buildx", "build", - "--tag", be.docker_tag, - "--push", - "--platform", DOCKER_PLATFORMS, - "--build-arg", f"MITMPROXY_WHEEL={whl.name}", - "." - ], cwd=docker_build_dir) + subprocess.check_call( + [ + "docker", + "buildx", + "build", + "--tag", + be.docker_tag, + "--push", + "--platform", + DOCKER_PLATFORMS, + "--build-arg", + f"MITMPROXY_WHEEL={whl.name}", + ".", + ], + cwd=docker_build_dir, + ) if be.is_prod_release: - subprocess.check_call([ - "docker", "buildx", "build", - "--tag", "mitmproxy/mitmproxy:latest", - "--push", - "--platform", DOCKER_PLATFORMS, - "--build-arg", f"MITMPROXY_WHEEL={whl.name}", - "." - ], cwd=docker_build_dir) + subprocess.check_call( + [ + "docker", + "buildx", + "build", + "--tag", + "mitmproxy/mitmproxy:latest", + "--push", + "--platform", + DOCKER_PLATFORMS, + "--build-arg", + f"MITMPROXY_WHEEL={whl.name}", + ".", + ], + cwd=docker_build_dir, + ) if __name__ == "__main__": # pragma: no cover diff --git a/release/deploy.py b/release/deploy.py index 9363109dd5..3c4d4aaaac 100755 --- a/release/deploy.py +++ b/release/deploy.py @@ -4,6 +4,7 @@ import subprocess from pathlib import Path from typing import Optional + # Security: No third-party dependencies here! if __name__ == "__main__": @@ -23,36 +24,47 @@ upload_dir = re.sub(r"^v([\d.]+)$", r"\1", tag) else: upload_dir = f"branches/{branch}" - subprocess.check_call([ - "aws", "s3", "cp", - "--acl", "public-read", - f"./release/dist/", - f"s3://snapshots.mitmproxy.org/{upload_dir}/", - "--recursive", - ]) + subprocess.check_call( + [ + "aws", + "s3", + "cp", + "--acl", + "public-read", + f"./release/dist/", + f"s3://snapshots.mitmproxy.org/{upload_dir}/", + "--recursive", + ] + ) # Upload releases to PyPI if tag: - whl, = Path("release/dist/").glob('mitmproxy-*-py3-none-any.whl') + (whl,) = Path("release/dist/").glob("mitmproxy-*-py3-none-any.whl") subprocess.check_call(["twine", "upload", whl]) # Upload dev docs if branch == "main" or branch == "actions-hardening": # FIXME remove - subprocess.check_call([ - "aws", "configure", - "set", "preview.cloudfront", "true" - ]) - subprocess.check_call([ - "aws", "s3", - "sync", - "--delete", - "--acl", "public-read", - "docs/public", - "s3://docs.mitmproxy.org/dev" - ]) - subprocess.check_call([ - "aws", "cloudfront", - "create-invalidation", - "--distribution-id", "E1TH3USJHFQZ5Q", - "--paths", "/dev/*" - ]) + subprocess.check_call(["aws", "configure", "set", "preview.cloudfront", "true"]) + subprocess.check_call( + [ + "aws", + "s3", + "sync", + "--delete", + "--acl", + "public-read", + "docs/public", + "s3://docs.mitmproxy.org/dev", + ] + ) + subprocess.check_call( + [ + "aws", + "cloudfront", + "create-invalidation", + "--distribution-id", + "E1TH3USJHFQZ5Q", + "--paths", + "/dev/*", + ] + ) diff --git a/release/docker/Dockerfile b/release/docker/Dockerfile index 0f3d4ec036..0ebf71ec07 100644 --- a/release/docker/Dockerfile +++ b/release/docker/Dockerfile @@ -1,10 +1,10 @@ -FROM python:3.9-bullseye as wheelbuilder +FROM python:3.10-bullseye as wheelbuilder ARG MITMPROXY_WHEEL COPY $MITMPROXY_WHEEL /wheels/ RUN pip install wheel && pip wheel --wheel-dir /wheels /wheels/${MITMPROXY_WHEEL} -FROM python:3.9-bullseye +FROM python:3.10-slim-bullseye RUN useradd -mU mitmproxy RUN apt-get update \ diff --git a/release/hooks/hook-mitmproxy.addons.onboardingapp.py b/release/hooks/hook-mitmproxy.addons.onboardingapp.py deleted file mode 100644 index 2b2fe06bac..0000000000 --- a/release/hooks/hook-mitmproxy.addons.onboardingapp.py +++ /dev/null @@ -1,3 +0,0 @@ -from PyInstaller.utils.hooks import collect_data_files - -datas = collect_data_files('mitmproxy.addons.onboardingapp') diff --git a/release/specs/windows-dir.spec b/release/specs/windows-dir.spec index 83e2fc8ef1..da6041f89d 100644 --- a/release/specs/windows-dir.spec +++ b/release/specs/windows-dir.spec @@ -3,8 +3,6 @@ from pathlib import Path from PyInstaller.building.api import PYZ, EXE, COLLECT from PyInstaller.building.build_main import Analysis -assert SPECPATH == "." - here = Path(r".") tools = ["mitmproxy", "mitmdump", "mitmweb"] @@ -12,7 +10,6 @@ analysis = Analysis( tools, excludes=["tcl", "tk", "tkinter"], pathex=[str(here)], - hookspath=[str(here / ".." / "hooks")], ) pyz = PYZ(analysis.pure, analysis.zipped_data) diff --git a/setup.cfg b/setup.cfg index 7112b48924..660b9a2641 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,16 @@ [flake8] max-line-length = 140 max-complexity = 25 -ignore = E251,E252,C901,W292,W503,W504,W605,E722,E741,E126,F541 +ignore = E203,E251,E252,C901,W292,W503,W504,W605,E722,E741,E126,F541 exclude = mitmproxy/contrib/*,test/mitmproxy/data/*,release/build/* addons = file,open,basestring,xrange,unicode,long,cmp [tool:pytest] +asyncio_mode = auto testpaths = test addopts = --capture=no --color=yes +filterwarnings = + ignore::DeprecationWarning:tornado.*: [coverage:run] branch = False @@ -48,11 +51,9 @@ exclude = [tool:individual_coverage] exclude = mitmproxy/addons/onboarding.py - mitmproxy/addons/termlog.py mitmproxy/connections.py mitmproxy/contentviews/base.py mitmproxy/contentviews/grpc.py - mitmproxy/controller.py mitmproxy/ctx.py mitmproxy/exceptions.py mitmproxy/flow.py @@ -66,9 +67,11 @@ exclude = mitmproxy/net/http/multipart.py mitmproxy/net/tcp.py mitmproxy/net/tls.py + mitmproxy/net/udp.py mitmproxy/options.py mitmproxy/proxy/config.py mitmproxy/proxy/server.py mitmproxy/proxy/layers/tls.py mitmproxy/utils/bits.py - release/hooks + mitmproxy/utils/vt_codes.py + mitmproxy/utils/pyinstaller diff --git a/setup.py b/setup.py index dc91eef264..4e149c4d87 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ here = os.path.abspath(os.path.dirname(__file__)) -with open(os.path.join(here, 'README.md'), encoding='utf-8') as f: +with open(os.path.join(here, "README.md"), encoding="utf-8") as f: long_description = f.read() long_description_content_type = "text/markdown" @@ -36,7 +36,6 @@ "Operating System :: POSIX", "Operating System :: Microsoft :: Windows", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: Implementation :: CPython", @@ -48,69 +47,74 @@ "Typing :: Typed", ], project_urls={ - 'Documentation': 'https://docs.mitmproxy.org/stable/', - 'Source': 'https://github.com/mitmproxy/mitmproxy/', - 'Tracker': 'https://github.com/mitmproxy/mitmproxy/issues', + "Documentation": "https://docs.mitmproxy.org/stable/", + "Source": "https://github.com/mitmproxy/mitmproxy/", + "Tracker": "https://github.com/mitmproxy/mitmproxy/issues", }, - packages=find_packages(include=[ - "mitmproxy", "mitmproxy.*", - ]), + packages=find_packages( + include=[ + "mitmproxy", + "mitmproxy.*", + ] + ), include_package_data=True, entry_points={ - 'console_scripts': [ + "console_scripts": [ "mitmproxy = mitmproxy.tools.main:mitmproxy", "mitmdump = mitmproxy.tools.main:mitmdump", "mitmweb = mitmproxy.tools.main:mitmweb", + ], + "pyinstaller40": [ + "hook-dirs = mitmproxy.utils.pyinstaller:hook_dirs", ] }, - python_requires='>=3.8', - # https://packaging.python.org/en/latest/requirements/#install-requires + python_requires=">=3.9", + # https://packaging.python.org/en/latest/discussions/install-requires-vs-requirements/#install-requires # It is not considered best practice to use install_requires to pin dependencies to specific versions. install_requires=[ - "asgiref>=3.2.10,<3.5", + "asgiref>=3.2.10,<3.6", "blinker>=1.4, <1.5", "Brotli>=1.0,<1.1", "certifi>=2019.9.11", # no semver here - this should always be on the last release! - "click>=7.0,<8.1", - "cryptography>=3.3,<3.5", - "flask>=1.1.1,<2.1", - "h11>=0.11,<0.13", + "cryptography>=36,<38", + "flask>=1.1.1,<2.2", + "h11>=0.11,<0.14", "h2>=4.1,<5", "hyperframe>=6.0,<7", "kaitaistruct>=0.7,<0.10", "ldap3>=2.8,<2.10", "msgpack>=1.0.0, <1.1.0", "passlib>=1.6.5, <1.8", - "protobuf>=3.14,<=3.19", - "pyOpenSSL>=20.0,<21.1", - "pyparsing>=2.4.2,<2.5", + "protobuf>=3.14,<5", + "pyOpenSSL>=21.0,<22.1", + "pyparsing>=2.4.2,<3.1", "pyperclip>=1.6.0,<1.9", - "ruamel.yaml>=0.16,<0.17.17", + "ruamel.yaml>=0.16,<0.18", "sortedcontainers>=2.3,<2.5", "tornado>=6.1,<7", "urwid>=2.1.1,<2.2", - "wsproto>=1.0,<1.1", + "wsproto>=1.0,<1.2", "publicsuffix2>=2.20190812,<3", - "zstandard>=0.11,<0.16", + "zstandard>=0.11,<0.19", ], extras_require={ ':sys_platform == "win32"': [ "pydivert>=2.0.3,<2.2", ], - 'dev': [ + "dev": [ + "click>=7.0,<8.2", "hypothesis>=5.8,<7", "parver>=0.1,<2.0", "pdoc>=4.0.0", - "pyinstaller==4.5.1", - "pytest-asyncio>=0.10.0,<0.16,!=0.14", - "pytest-cov>=2.7.1,<3", - "pytest-timeout>=1.3.3,<2", + "pyinstaller==5.1", + "pytest-asyncio>=0.17.0,<0.19", + "pytest-cov>=2.7.1,<3.1", + "pytest-timeout>=1.3.3,<2.2", "pytest-xdist>=2.1.0,<3", - "pytest>=6.1.0,<7", + "pytest>=6.1.0,<8", "requests>=2.9.1,<3", "tox>=3.5,<4", "wheel>=0.36.2,<0.38", - "coverage==5.5", # workaround issue with import errors introduced in 5.6b1/6.0 ], - } + }, ) diff --git a/test/bench/benchmark.py b/test/bench/benchmark.py index 96daf16cc9..9f733dcd21 100644 --- a/test/bench/benchmark.py +++ b/test/bench/benchmark.py @@ -5,8 +5,9 @@ class Benchmark: """ - A simple profiler addon. + A simple profiler addon. """ + def __init__(self): self.pr = cProfile.Profile() self.started = False @@ -28,7 +29,7 @@ async def procs(self): "-c50", "-d5s", "http://localhost:%s/benchmark.py" % ctx.master.server.address[1], - stdout=asyncio.subprocess.PIPE + stdout=asyncio.subprocess.PIPE, ) stdout, _ = await traf.communicate() with open(ctx.options.benchmark_save_path + ".bench", mode="wb") as f: @@ -43,7 +44,7 @@ def load(self, loader): "benchmark_save_path", str, "/tmp/profile", - "Destination for the .prof and and .bench result files" + "Destination for the .prof and and .bench result files", ) ctx.options.update( mode="reverse:http://devd.io:10001", @@ -53,10 +54,10 @@ def load(self, loader): def running(self): if not self.started: self.started = True - asyncio.get_event_loop().create_task(self.procs()) + asyncio.get_running_loop().create_task(self.procs()) def done(self): self.pr.dump_stats(ctx.options.benchmark_save_path + ".prof") -addons = [Benchmark()] \ No newline at end of file +addons = [Benchmark()] diff --git a/test/conftest.py b/test/conftest.py index 27ab2f2bfb..9df569b1a2 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -5,16 +5,12 @@ import pytest -pytest_plugins = ('test.full_coverage_plugin',) +pytest_plugins = ("test.full_coverage_plugin",) -skip_windows = pytest.mark.skipif( - os.name == "nt", - reason='Skipping due to Windows' -) +skip_windows = pytest.mark.skipif(os.name == "nt", reason="Skipping due to Windows") skip_not_windows = pytest.mark.skipif( - os.name != "nt", - reason='Skipping due to not Windows' + os.name != "nt", reason="Skipping due to not Windows" ) try: @@ -26,10 +22,7 @@ else: no_ipv6 = False -skip_no_ipv6 = pytest.mark.skipif( - no_ipv6, - reason='Host has no IPv6 support' -) +skip_no_ipv6 = pytest.mark.skipif(no_ipv6, reason="Host has no IPv6 support") @pytest.fixture() diff --git a/test/examples/test_examples.py b/test/examples/test_examples.py index 4a9606186c..50b08a1962 100644 --- a/test/examples/test_examples.py +++ b/test/examples/test_examples.py @@ -4,10 +4,8 @@ from mitmproxy.test import taddons from mitmproxy.http import Headers -from ..mitmproxy import tservers - -class TestScripts(tservers.MasterTest): +class TestScripts: def test_add_header(self, tdata): with taddons.context() as tctx: a = tctx.script(tdata.path("../examples/addons/anatomy2.py")) @@ -20,7 +18,7 @@ def test_custom_contentviews(self, tdata): tctx.script(tdata.path("../examples/addons/contentview.py")) swapcase = contentviews.get("swapcase") _, fmt = swapcase(b"Test!") - assert any(b'tEST!' in val[0][1] for val in fmt) + assert any(b"tEST!" in val[0][1] for val in fmt) def test_custom_grpc_contentview(self, tdata): with taddons.context() as tctx: @@ -32,28 +30,55 @@ def test_custom_grpc_contentview(self, tdata): raw = f.read() sim_msg_req = tutils.treq( - port=443, - host="example.com", - path="/ReverseGeocode" + port=443, host="example.com", path="/ReverseGeocode" ) sim_msg_resp = tutils.tresp() - sim_flow = tflow.tflow( - req=sim_msg_req, - resp=sim_msg_resp - ) + sim_flow = tflow.tflow(req=sim_msg_req, resp=sim_msg_resp) - view_text, output = v(raw, flow=sim_flow, http_message=sim_flow.request) # simulate request message + view_text, output = v( + raw, flow=sim_flow, http_message=sim_flow.request + ) # simulate request message assert view_text == "Protobuf (flattened) (addon with custom rules)" output = list(output) # assure list conversion if generator assert output == [ - [('text', '[message] '), ('text', 'position '), ('text', '1 '), ('text', ' ')], - [('text', '[double] '), ('text', 'latitude '), ('text', '1.1 '), ('text', '38.89816675798073 ')], - [('text', '[double] '), ('text', 'longitude '), ('text', '1.2 '), ('text', '-77.03829828366696 ')], - [('text', '[string] '), ('text', 'country '), ('text', '3 '), ('text', 'de_DE ')], - [('text', '[uint32] '), ('text', ' '), ('text', '6 '), ('text', '1 ')], - [('text', '[string] '), ('text', 'app '), ('text', '7 '), ('text', 'de.mcdonalds.mcdonaldsinfoapp ')] + [ + ("text", "[message] "), + ("text", "position "), + ("text", "1 "), + ("text", " "), + ], + [ + ("text", "[double] "), + ("text", "latitude "), + ("text", "1.1 "), + ("text", "38.89816675798073 "), + ], + [ + ("text", "[double] "), + ("text", "longitude "), + ("text", "1.2 "), + ("text", "-77.03829828366696 "), + ], + [ + ("text", "[string] "), + ("text", "country "), + ("text", "3 "), + ("text", "de_DE "), + ], + [ + ("text", "[uint32] "), + ("text", " "), + ("text", "6 "), + ("text", "1 "), + ], + [ + ("text", "[string] "), + ("text", "app "), + ("text", "7 "), + ("text", "de.mcdonalds.mcdonaldsinfoapp "), + ], ] def test_modify_form(self, tdata): @@ -72,7 +97,9 @@ def test_modify_form(self, tdata): def test_modify_querystring(self, tdata): with taddons.context() as tctx: - sc = tctx.script(tdata.path("../examples/addons/http-modify-query-string.py")) + sc = tctx.script( + tdata.path("../examples/addons/http-modify-query-string.py") + ) f = tflow.tflow(req=tutils.treq(path="/search?q=term")) sc.request(f) diff --git a/test/filename_matching.py b/test/filename_matching.py index 021155507f..3e9878f020 100755 --- a/test/filename_matching.py +++ b/test/filename_matching.py @@ -10,16 +10,19 @@ def check_src_files_have_test(): missing_test_files = [] excluded = [ - 'mitmproxy/contrib/', - 'mitmproxy/io/proto/', - 'mitmproxy/proxy/layers/http', - 'mitmproxy/test/', - 'mitmproxy/tools/', - 'mitmproxy/platform/', + "mitmproxy/contrib/", + "mitmproxy/io/proto/", + "mitmproxy/proxy/layers/http", + "mitmproxy/test/", + "mitmproxy/tools/", + "mitmproxy/platform/", + "mitmproxy/utils/pyinstaller/", + ] + src_files = glob.glob("mitmproxy/**/*.py", recursive=True) + src_files = [f for f in src_files if os.path.basename(f) != "__init__.py"] + src_files = [ + f for f in src_files if not any(os.path.normpath(p) in f for p in excluded) ] - src_files = glob.glob('mitmproxy/**/*.py', recursive=True) - src_files = [f for f in src_files if os.path.basename(f) != '__init__.py'] - src_files = [f for f in src_files if not any(os.path.normpath(p) in f for p in excluded)] for f in src_files: p = os.path.join("test", os.path.dirname(f), "test_" + os.path.basename(f)) if not os.path.isfile(p): @@ -32,16 +35,21 @@ def check_test_files_have_src(): unknown_test_files = [] excluded = [ - 'test/mitmproxy/data/', - 'test/mitmproxy/net/data/', - '/tservers.py', - '/conftest.py', + "test/mitmproxy/data/", + "test/mitmproxy/net/data/", + "/tservers.py", + "/conftest.py", + ] + test_files = glob.glob("test/mitmproxy/**/*.py", recursive=True) + test_files = [f for f in test_files if os.path.basename(f) != "__init__.py"] + test_files = [ + f for f in test_files if not any(os.path.normpath(p) in f for p in excluded) ] - test_files = glob.glob('test/mitmproxy/**/*.py', recursive=True) - test_files = [f for f in test_files if os.path.basename(f) != '__init__.py'] - test_files = [f for f in test_files if not any(os.path.normpath(p) in f for p in excluded)] for f in test_files: - p = os.path.join(re.sub('^test/', '', os.path.dirname(f)), re.sub('^test_', '', os.path.basename(f))) + p = os.path.join( + re.sub("^test/", "", os.path.dirname(f)), + re.sub("^test_", "", os.path.basename(f)), + ) if not os.path.isfile(p): unknown_test_files.append((f, p)) @@ -67,5 +75,5 @@ def main(): sys.exit(exitcode) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/test/full_coverage_plugin.py b/test/full_coverage_plugin.py index c7b05cf68f..3d1b8b6787 100644 --- a/test/full_coverage_plugin.py +++ b/test/full_coverage_plugin.py @@ -13,17 +13,21 @@ def pytest_addoption(parser): - parser.addoption('--full-cov', - action='append', - dest='full_cov', - default=[], - help="Require full test coverage of 100%% for this module/path/filename (multi-allowed). Default: none") + parser.addoption( + "--full-cov", + action="append", + dest="full_cov", + default=[], + help="Require full test coverage of 100%% for this module/path/filename (multi-allowed). Default: none", + ) - parser.addoption('--no-full-cov', - action='append', - dest='no_full_cov', - default=[], - help="Exclude file from a parent 100%% coverage requirement (multi-allowed). Default: none") + parser.addoption( + "--no-full-cov", + action="append", + dest="no_full_cov", + default=[], + help="Exclude file from a parent 100%% coverage requirement (multi-allowed). Default: none", + ) def pytest_configure(config): @@ -31,16 +35,18 @@ def pytest_configure(config): global no_full_cov enable_coverage = ( - config.getoption('file_or_dir') and len(config.getoption('file_or_dir')) == 0 and - config.getoption('full_cov') and len(config.getoption('full_cov')) > 0 and - config.pluginmanager.getplugin("_cov") is not None and - config.pluginmanager.getplugin("_cov").cov_controller is not None and - config.pluginmanager.getplugin("_cov").cov_controller.cov is not None + config.getoption("file_or_dir") + and len(config.getoption("file_or_dir")) == 0 + and config.getoption("full_cov") + and len(config.getoption("full_cov")) > 0 + and config.pluginmanager.getplugin("_cov") is not None + and config.pluginmanager.getplugin("_cov").cov_controller is not None + and config.pluginmanager.getplugin("_cov").cov_controller.cov is not None ) c = configparser.ConfigParser() c.read(os.path.join(here, "..", "setup.cfg")) - fs = c['tool:full_coverage']['exclude'].split('\n') + fs = c["tool:full_coverage"]["exclude"].split("\n") no_full_cov = config.option.no_full_cov + [f.strip() for f in fs] @@ -57,14 +63,14 @@ def pytest_runtestloop(session): cov = session.config.pluginmanager.getplugin("_cov").cov_controller.cov - if os.name == 'nt': - cov.exclude('pragma: windows no cover') + if os.name == "nt": + cov.exclude("pragma: windows no cover") - if sys.platform == 'darwin': - cov.exclude('pragma: osx no cover') + if sys.platform == "darwin": + cov.exclude("pragma: osx no cover") if os.environ.get("OPENSSL") == "old": - cov.exclude('pragma: openssl-old no cover') + cov.exclude("pragma: openssl-old no cover") yield @@ -73,15 +79,24 @@ def pytest_runtestloop(session): prefix = os.getcwd() excluded_files = [os.path.normpath(f) for f in no_full_cov] - measured_files = [os.path.normpath(os.path.relpath(f, prefix)) for f in cov.get_data().measured_files()] - measured_files = [f for f in measured_files if not any(f.startswith(excluded_f) for excluded_f in excluded_files)] + measured_files = [ + os.path.normpath(os.path.relpath(f, prefix)) + for f in cov.get_data().measured_files() + ] + measured_files = [ + f + for f in measured_files + if not any(f.startswith(excluded_f) for excluded_f in excluded_files) + ] for name in coverage_values.keys(): files = [f for f in measured_files if f.startswith(os.path.normpath(name))] try: - with open(os.devnull, 'w') as null: + with open(os.devnull, "w") as null: overall = cov.report(files, ignore_errors=True, file=null) - singles = [(s, cov.report(s, ignore_errors=True, file=null)) for s in files] + singles = [ + (s, cov.report(s, ignore_errors=True, file=null)) for s in files + ] coverage_values[name] = (overall, singles) except: pass @@ -101,28 +116,28 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): if not enable_coverage: return - terminalreporter.write('\n') + terminalreporter.write("\n") if not coverage_passed: - markup = {'red': True, 'bold': True} + markup = {"red": True, "bold": True} msg = "FAIL: Full test coverage not reached!\n" terminalreporter.write(msg, **markup) for name in sorted(coverage_values.keys()): - msg = 'Coverage for {}: {:.2f}%\n'.format(name, coverage_values[name][0]) + msg = f"Coverage for {name}: {coverage_values[name][0]:.2f}%\n" if coverage_values[name][0] < 100: - markup = {'red': True, 'bold': True} + markup = {"red": True, "bold": True} for s, v in sorted(coverage_values[name][1]): if v < 100: - msg += f' {s}: {v:.2f}%\n' + msg += f" {s}: {v:.2f}%\n" else: - markup = {'green': True} + markup = {"green": True} terminalreporter.write(msg, **markup) else: - msg = 'SUCCESS: Full test coverage reached in modules and files:\n' - msg += '{}\n\n'.format('\n'.join(config.option.full_cov)) + msg = "SUCCESS: Full test coverage reached in modules and files:\n" + msg += "{}\n\n".format("\n".join(config.option.full_cov)) terminalreporter.write(msg, green=True) - msg = '\nExcluded files:\n' + msg = "\nExcluded files:\n" for s in sorted(no_full_cov): msg += f" {s}\n" terminalreporter.write(msg) diff --git a/test/helper_tools/dumperview.py b/test/helper_tools/dumperview.py index b672ebaf91..fd0fbc9b0c 100755 --- a/test/helper_tools/dumperview.py +++ b/test/helper_tools/dumperview.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import asyncio import click from mitmproxy.addons import dumper @@ -6,12 +7,24 @@ from mitmproxy.test import taddons +def run_async(coro): + """ + Run the given async function in a new event loop. + This allows async functions to be called synchronously. + """ + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + loop.close() + + def show(flow_detail, flows): d = dumper.Dumper() with taddons.context() as ctx: ctx.configure(d, flow_detail=flow_detail) for f in flows: - ctx.cycle(d, f) + run_async(ctx.cycle(d, f)) @click.group() @@ -20,35 +33,36 @@ def cli(): @cli.command() -@click.option('--level', default=1, help='Detail level') +@click.option("--level", default=1, help="Detail level") def tcp(level): - f1 = tflow.ttcpflow(client_conn=True, server_conn=True) + f1 = tflow.ttcpflow() show(level, [f1]) @cli.command() -@click.option('--level', default=1, help='Detail level') +@click.option("--level", default=1, help="Detail level") def large(level): - f1 = tflow.tflow(client_conn=True, server_conn=True, resp=True) + f1 = tflow.tflow(resp=True) f1.response.headers["content-type"] = "text/html" f1.response.content = b"foo bar voing\n" * 100 show(level, [f1]) @cli.command() -@click.option('--level', default=1, help='Detail level') +@click.option("--level", default=1, help="Detail level") def small(level): - f1 = tflow.tflow(client_conn=True, server_conn=True, resp=True) + f1 = tflow.tflow(resp=True) f1.response.headers["content-type"] = "text/html" f1.response.content = b"Hello!" - f2 = tflow.tflow(client_conn=True, server_conn=True, err=True) + f2 = tflow.tflow(err=True) show( level, [ - f1, f2, - ] + f1, + f2, + ], ) diff --git a/test/helper_tools/hunt_memory_leaks.py b/test/helper_tools/hunt_memory_leaks.py index 6a563af4d7..838cfafb60 100644 --- a/test/helper_tools/hunt_memory_leaks.py +++ b/test/helper_tools/hunt_memory_leaks.py @@ -36,7 +36,9 @@ def debug1(*_): print() print("Memory") print("=======") - for t, count in collections.Counter([str(type(o)) for o in gc.get_objects()]).most_common(50): + for t, count in collections.Counter( + [str(type(o)) for o in gc.get_objects()] + ).most_common(50): print(count, t) @@ -69,7 +71,12 @@ def print_refs(x, ignore: set, seen: set, depth: int = 0, max_depth: int = 10): return if id(x) in seen: - print(" " * depth + "↖ " + repr(str(x))[1:60] + f" (\x1b[31mseen\x1b[0m: {id(x):x})") + print( + " " * depth + + "↖ " + + repr(str(x))[1:60] + + f" (\x1b[31mseen\x1b[0m: {id(x):x})" + ) return else: if depth == 0: diff --git a/test/helper_tools/inspect_dumpfile.py b/test/helper_tools/inspect_dumpfile.py index 77006a7a94..eea1a2b4f7 100644 --- a/test/helper_tools/inspect_dumpfile.py +++ b/test/helper_tools/inspect_dumpfile.py @@ -18,7 +18,7 @@ def read_tnetstring(input): @click.command() -@click.argument("input", type=click.File('rb')) +@click.argument("input", type=click.File("rb")) def inspect(input): """ pretty-print a dumpfile diff --git a/test/helper_tools/linkify-changelog.py b/test/helper_tools/linkify-changelog.py new file mode 100644 index 0000000000..f0db26175e --- /dev/null +++ b/test/helper_tools/linkify-changelog.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +from pathlib import Path +import re + +changelog = Path(__file__).parent / "../../CHANGELOG.md" + +text = changelog.read_text(encoding="utf8") +text, n = re.subn( + r"\s*\(([^)]+)#(\d+)\)", + "\n (\\1[#\\2](https://github.com/mitmproxy/mitmproxy/issues/\\2))", + text, +) +changelog.write_text(text, encoding="utf8") +print(f"Linkified {n} issues and users.") diff --git a/test/helper_tools/memoryleak.py b/test/helper_tools/memoryleak.py index 9d5429b419..d02482353b 100644 --- a/test/helper_tools/memoryleak.py +++ b/test/helper_tools/memoryleak.py @@ -2,6 +2,7 @@ import threading from pympler import muppy, refbrowser from OpenSSL import SSL + # import os # os.environ["TK_LIBRARY"] = r"C:\Python27\tcl\tcl8.5" # os.environ["TCL_LIBRARY"] = r"C:\Python27\tcl\tcl8.5" @@ -18,7 +19,11 @@ def str_fun(obj): return "(-locals-)" if "self" in obj and isinstance(obj["self"], refbrowser.InteractiveBrowser): return "(-browser-)" - return str(id(obj)) + ": " + str(obj)[:100].replace("\r\n", "\\r\\n").replace("\n", "\\n") + return ( + str(id(obj)) + + ": " + + str(obj)[:100].replace("\r\n", "\\r\\n").replace("\n", "\\n") + ) def request(ctx, flow): diff --git a/test/helper_tools/memoryleak2.py b/test/helper_tools/memoryleak2.py index 26fa742d9a..1401c3cd4c 100644 --- a/test/helper_tools/memoryleak2.py +++ b/test/helper_tools/memoryleak2.py @@ -6,16 +6,22 @@ from mitmproxy import certs if __name__ == "__main__": - store = certs.CertStore.from_store(path=Path("~/.mitmproxy/").expanduser(), basename="mitmproxy", key_size=2048) + store = certs.CertStore.from_store( + path=Path("~/.mitmproxy/").expanduser(), basename="mitmproxy", key_size=2048 + ) store.STORE_CAP = 5 for _ in range(5): - store.get_cert(commonname=secrets.token_hex(16).encode(), sans=[], organization=None) + store.get_cert( + commonname=secrets.token_hex(16).encode(), sans=[], organization=None + ) objgraph.show_growth() for _ in range(20): - store.get_cert(commonname=secrets.token_hex(16).encode(), sans=[], organization=None) + store.get_cert( + commonname=secrets.token_hex(16).encode(), sans=[], organization=None + ) print("====") objgraph.show_growth() diff --git a/test/helper_tools/passive_close.py b/test/helper_tools/passive_close.py index 6f97ea4fb9..c204c8d684 100644 --- a/test/helper_tools/passive_close.py +++ b/test/helper_tools/passive_close.py @@ -3,13 +3,13 @@ class service(socketserver.BaseRequestHandler): - def handle(self): - data = 'dummy' + data = "dummy" print("Client connected with ", self.client_address) while True: self.request.send( - "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\ncontent") + "HTTP/1.1 200 OK\r\nConnection: close\r\nContent-Length: 7\r\n\r\ncontent" + ) data = self.request.recv(1024) if not len(data): print("Connection closed by remote: ", self.client_address) @@ -20,5 +20,5 @@ class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): pass -server = ThreadedTCPServer(('', 1520), service) +server = ThreadedTCPServer(("", 1520), service) server.serve_forever() diff --git a/test/individual_coverage.py b/test/individual_coverage.py index c969b4b820..7c989c3852 100755 --- a/test/individual_coverage.py +++ b/test/individual_coverage.py @@ -16,19 +16,29 @@ def run_tests(src, test, fail): stdout = io.StringIO() with contextlib.redirect_stderr(stderr): with contextlib.redirect_stdout(stdout): - e = pytest.main([ - '-qq', - '--disable-pytest-warnings', - '--cov', src.replace('.py', '').replace('/', '.'), - '--cov-fail-under', '100', - '--cov-report', 'term-missing:skip-covered', - '-o', 'faulthandler_timeout=0', - test - ]) + e = pytest.main( + [ + "-qq", + "--disable-pytest-warnings", + "--cov", + src.replace(".py", "").replace("/", "."), + "--cov-fail-under", + "100", + "--cov-report", + "term-missing:skip-covered", + "-o", + "faulthandler_timeout=0", + test, + ] + ) if e == 0: if fail: - print("FAIL DUE TO UNEXPECTED SUCCESS:", src, "Please remove this file from setup.cfg tool:individual_coverage/exclude.") + print( + "FAIL DUE TO UNEXPECTED SUCCESS:", + src, + "Please remove this file from setup.cfg tool:individual_coverage/exclude.", + ) e = 42 else: print(".") @@ -37,7 +47,11 @@ def run_tests(src, test, fail): print("Ignoring allowed fail:", src) e = 0 else: - cov = [l for l in stdout.getvalue().split("\n") if (src in l) or ("was never imported" in l)] + cov = [ + l + for l in stdout.getvalue().split("\n") + if (src in l) or ("was never imported" in l) + ] if len(cov) == 1: print("FAIL:", cov[0]) else: @@ -58,18 +72,27 @@ def start_pytest(src, test, fail): def main(): c = configparser.ConfigParser() - c.read('setup.cfg') - fs = c['tool:individual_coverage']['exclude'].strip().split('\n') + c.read("setup.cfg") + fs = c["tool:individual_coverage"]["exclude"].strip().split("\n") no_individual_cov = [f.strip() for f in fs] - excluded = ['mitmproxy/contrib/', 'mitmproxy/test/', 'mitmproxy/tools/', 'mitmproxy/platform/'] - src_files = glob.glob('mitmproxy/**/*.py', recursive=True) - src_files = [f for f in src_files if os.path.basename(f) != '__init__.py'] - src_files = [f for f in src_files if not any(os.path.normpath(p) in f for p in excluded)] + excluded = [ + "mitmproxy/contrib/", + "mitmproxy/test/", + "mitmproxy/tools/", + "mitmproxy/platform/", + ] + src_files = glob.glob("mitmproxy/**/*.py", recursive=True) + src_files = [f for f in src_files if os.path.basename(f) != "__init__.py"] + src_files = [ + f for f in src_files if not any(os.path.normpath(p) in f for p in excluded) + ] ps = [] for src in sorted(src_files): - test = os.path.join("test", os.path.dirname(src), "test_" + os.path.basename(src)) + test = os.path.join( + "test", os.path.dirname(src), "test_" + os.path.basename(src) + ) if os.path.isfile(test): ps.append((src, test, src in no_individual_cov)) @@ -77,8 +100,7 @@ def main(): if any(e != 0 for _, _, e in result): sys.exit(1) - pass -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/test/mitmproxy/__init__.py b/test/mitmproxy/__init__.py index 6f114e1815..29a0fe16c3 100644 --- a/test/mitmproxy/__init__.py +++ b/test/mitmproxy/__init__.py @@ -1,5 +1,6 @@ # Silence third-party modules import logging + logging.getLogger("hyper").setLevel(logging.WARNING) logging.getLogger("requests").setLevel(logging.WARNING) logging.getLogger("passlib").setLevel(logging.WARNING) diff --git a/test/mitmproxy/addons/test_anticache.py b/test/mitmproxy/addons/test_anticache.py index d1765fe075..b3eb00d332 100644 --- a/test/mitmproxy/addons/test_anticache.py +++ b/test/mitmproxy/addons/test_anticache.py @@ -16,7 +16,7 @@ def test_simple(self): assert "if-modified-since" in f.request.headers assert "if-none-match" in f.request.headers - tctx.configure(sa, anticache = True) + tctx.configure(sa, anticache=True) sa.request(f) assert "if-modified-since" not in f.request.headers assert "if-none-match" not in f.request.headers diff --git a/test/mitmproxy/addons/test_asgiapp.py b/test/mitmproxy/addons/test_asgiapp.py index e6b0415163..d282b33368 100644 --- a/test/mitmproxy/addons/test_asgiapp.py +++ b/test/mitmproxy/addons/test_asgiapp.py @@ -2,7 +2,6 @@ import json import flask -import pytest from flask import request from mitmproxy.addons import asgiapp @@ -45,7 +44,6 @@ async def noresponseapp(scope, receive, send): return -@pytest.mark.asyncio async def test_asgi_full(): ps = Proxyserver() addons = [ @@ -56,9 +54,9 @@ async def test_asgi_full(): with taddons.context(ps, *addons) as tctx: tctx.master.addons.add(next_layer.NextLayer()) tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) - ps.running() + await ps.running() await tctx.master.await_log("Proxy server listening", level="info") - proxy_addr = ps.server.sockets[0].getsockname()[:2] + proxy_addr = ps.tcp_server.sockets[0].getsockname()[:2] reader, writer = await asyncio.open_connection(*proxy_addr) req = f"GET http://testapp:80/ HTTP/1.1\r\n\r\n" diff --git a/test/mitmproxy/addons/test_block.py b/test/mitmproxy/addons/test_block.py index ea147a4ca3..3a7d0d4837 100644 --- a/test/mitmproxy/addons/test_block.py +++ b/test/mitmproxy/addons/test_block.py @@ -5,52 +5,53 @@ from mitmproxy.test import taddons -@pytest.mark.parametrize("block_global, block_private, should_be_killed, address", [ - # block_global: loopback - (True, False, False, ("127.0.0.1",)), - (True, False, False, ("::1",)), - # block_global: private - (True, False, False, ("10.0.0.1",)), - (True, False, False, ("172.20.0.1",)), - (True, False, False, ("192.168.1.1",)), - (True, False, False, ("::ffff:10.0.0.1",)), - (True, False, False, ("::ffff:172.20.0.1",)), - (True, False, False, ("::ffff:192.168.1.1",)), - (True, False, False, ("fe80::",)), - (True, False, False, (r"::ffff:192.168.1.1%scope",)), - # block_global: global - (True, False, True, ("1.1.1.1",)), - (True, False, True, ("8.8.8.8",)), - (True, False, True, ("216.58.207.174",)), - (True, False, True, ("::ffff:1.1.1.1",)), - (True, False, True, ("::ffff:8.8.8.8",)), - (True, False, True, ("::ffff:216.58.207.174",)), - (True, False, True, ("2001:4860:4860::8888",)), - (True, False, True, (r"2001:4860:4860::8888%scope",)), - - # block_private: loopback - (False, True, False, ("127.0.0.1",)), - (False, True, False, ("::1",)), - # block_private: private - (False, True, True, ("10.0.0.1",)), - (False, True, True, ("172.20.0.1",)), - (False, True, True, ("192.168.1.1",)), - (False, True, True, ("::ffff:10.0.0.1",)), - (False, True, True, ("::ffff:172.20.0.1",)), - (False, True, True, ("::ffff:192.168.1.1",)), - (False, True, True, (r"::ffff:192.168.1.1%scope",)), - (False, True, True, ("fe80::",)), - # block_private: global - (False, True, False, ("1.1.1.1",)), - (False, True, False, ("8.8.8.8",)), - (False, True, False, ("216.58.207.174",)), - (False, True, False, ("::ffff:1.1.1.1",)), - (False, True, False, ("::ffff:8.8.8.8",)), - (False, True, False, ("::ffff:216.58.207.174",)), - (False, True, False, (r"::ffff:216.58.207.174%scope",)), - (False, True, False, ("2001:4860:4860::8888",)), -]) -@pytest.mark.asyncio +@pytest.mark.parametrize( + "block_global, block_private, should_be_killed, address", + [ + # block_global: loopback + (True, False, False, ("127.0.0.1",)), + (True, False, False, ("::1",)), + # block_global: private + (True, False, False, ("10.0.0.1",)), + (True, False, False, ("172.20.0.1",)), + (True, False, False, ("192.168.1.1",)), + (True, False, False, ("::ffff:10.0.0.1",)), + (True, False, False, ("::ffff:172.20.0.1",)), + (True, False, False, ("::ffff:192.168.1.1",)), + (True, False, False, ("fe80::",)), + (True, False, False, (r"::ffff:192.168.1.1%scope",)), + # block_global: global + (True, False, True, ("1.1.1.1",)), + (True, False, True, ("8.8.8.8",)), + (True, False, True, ("216.58.207.174",)), + (True, False, True, ("::ffff:1.1.1.1",)), + (True, False, True, ("::ffff:8.8.8.8",)), + (True, False, True, ("::ffff:216.58.207.174",)), + (True, False, True, ("2001:4860:4860::8888",)), + (True, False, True, (r"2001:4860:4860::8888%scope",)), + # block_private: loopback + (False, True, False, ("127.0.0.1",)), + (False, True, False, ("::1",)), + # block_private: private + (False, True, True, ("10.0.0.1",)), + (False, True, True, ("172.20.0.1",)), + (False, True, True, ("192.168.1.1",)), + (False, True, True, ("::ffff:10.0.0.1",)), + (False, True, True, ("::ffff:172.20.0.1",)), + (False, True, True, ("::ffff:192.168.1.1",)), + (False, True, True, (r"::ffff:192.168.1.1%scope",)), + (False, True, True, ("fe80::",)), + # block_private: global + (False, True, False, ("1.1.1.1",)), + (False, True, False, ("8.8.8.8",)), + (False, True, False, ("216.58.207.174",)), + (False, True, False, ("::ffff:1.1.1.1",)), + (False, True, False, ("::ffff:8.8.8.8",)), + (False, True, False, ("::ffff:216.58.207.174",)), + (False, True, False, (r"::ffff:216.58.207.174%scope",)), + (False, True, False, ("2001:4860:4860::8888",)), + ], +) async def test_block_global(block_global, block_private, should_be_killed, address): ar = block.Block() with taddons.context(ar) as tctx: diff --git a/test/mitmproxy/addons/test_blocklist.py b/test/mitmproxy/addons/test_blocklist.py index 32a96c4311..9187443b28 100644 --- a/test/mitmproxy/addons/test_blocklist.py +++ b/test/mitmproxy/addons/test_blocklist.py @@ -6,25 +6,30 @@ from mitmproxy.test import tflow -@pytest.mark.parametrize("filter,err", [ - ("/~u index.html/TOOMANY/300", "Invalid number of parameters"), - (":~d ~d ~d:200", "Invalid filter"), - ("/~u index.html/999", "Invalid HTTP status code"), - ("/~u index.html/abc", "Invalid HTTP status code"), -]) +@pytest.mark.parametrize( + "filter,err", + [ + ("/~u index.html/TOOMANY/300", "Invalid number of parameters"), + (":~d ~d ~d:200", "Invalid filter"), + ("/~u index.html/999", "Invalid HTTP status code"), + ("/~u index.html/abc", "Invalid HTTP status code"), + ], +) def test_parse_spec_err(filter, err): with pytest.raises(ValueError, match=err): blocklist.parse_spec(filter) class TestBlockList: - @pytest.mark.parametrize("filter,status_code", [ - (":~u example.org:404", 404), - (":~u example.com:404", None), - ("/!jpg/418", None), - ("/!png/418", 418), - - ]) + @pytest.mark.parametrize( + "filter,status_code", + [ + (":~u example.org:404", 404), + (":~u example.com:404", None), + ("/!jpg/418", None), + ("/!png/418", 418), + ], + ) def test_block(self, filter, status_code): bl = blocklist.BlockList() with taddons.context(bl) as tctx: @@ -34,19 +39,19 @@ def test_block(self, filter, status_code): bl.request(f) if status_code is not None: assert f.response.status_code == status_code - assert f.metadata['blocklisted'] + assert f.metadata["blocklisted"] else: assert not f.response def test_special_kill_status_closes_connection(self): bl = blocklist.BlockList() with taddons.context(bl) as tctx: - tctx.configure(bl, block_list=[':.*:444']) + tctx.configure(bl, block_list=[":.*:444"]) f = tflow.tflow() bl.request(f) assert f.error.msg == f.error.KILLED_MESSAGE assert f.response is None - assert f.metadata['blocklisted'] is True + assert f.metadata["blocklisted"] is True def test_already_handled(self): """Test that we don't interfere if another addon already killed this request.""" diff --git a/test/mitmproxy/addons/test_browser.py b/test/mitmproxy/addons/test_browser.py index ac009e4d96..0beb2071fc 100644 --- a/test/mitmproxy/addons/test_browser.py +++ b/test/mitmproxy/addons/test_browser.py @@ -1,11 +1,9 @@ from unittest import mock -import pytest from mitmproxy.addons import browser from mitmproxy.test import taddons -@pytest.mark.asyncio async def test_browser(): with mock.patch("subprocess.Popen") as po, mock.patch("shutil.which") as which: which.return_value = "chrome" @@ -21,7 +19,6 @@ async def test_browser(): assert not b.browser -@pytest.mark.asyncio async def test_no_browser(): with mock.patch("shutil.which") as which: which.return_value = False @@ -30,3 +27,42 @@ async def test_no_browser(): with taddons.context() as tctx: b.start() await tctx.master.await_log("platform is not supported") + + +async def test_get_browser_cmd_executable(): + with mock.patch("shutil.which") as which: + which.side_effect = lambda cmd: cmd == "chrome" + assert browser.get_browser_cmd() == ["chrome"] + + +async def test_get_browser_cmd_no_executable(): + with mock.patch("shutil.which") as which: + which.return_value = False + assert browser.get_browser_cmd() is None + + +async def test_get_browser_cmd_flatpak(): + def subprocess_run_mock(cmd, **kwargs): + returncode = 0 if cmd == ["flatpak", "info", "com.google.Chrome"] else 1 + return mock.Mock(returncode=returncode) + + with mock.patch("shutil.which") as which, mock.patch( + "subprocess.run" + ) as subprocess_run: + which.side_effect = lambda cmd: cmd == "flatpak" + subprocess_run.side_effect = subprocess_run_mock + assert browser.get_browser_cmd() == [ + "flatpak", + "run", + "-p", + "com.google.Chrome", + ] + + +async def test_get_browser_cmd_no_flatpak(): + with mock.patch("shutil.which") as which, mock.patch( + "subprocess.run" + ) as subprocess_run: + which.side_effect = lambda cmd: cmd == "flatpak" + subprocess_run.return_value = mock.Mock(returncode=1) + assert browser.get_browser_cmd() is None diff --git a/test/mitmproxy/addons/test_clientplayback.py b/test/mitmproxy/addons/test_clientplayback.py index 4de48ffe5b..2b93d7aff7 100644 --- a/test/mitmproxy/addons/test_clientplayback.py +++ b/test/mitmproxy/addons/test_clientplayback.py @@ -12,7 +12,7 @@ @asynccontextmanager async def tcp_server(handle_conn) -> Address: - server = await asyncio.start_server(handle_conn, '127.0.0.1', 0) + server = await asyncio.start_server(handle_conn, "127.0.0.1", 0) await server.start_serving() try: yield server.sockets[0].getsockname() @@ -20,7 +20,6 @@ async def tcp_server(handle_conn) -> Address: server.close() -@pytest.mark.asyncio @pytest.mark.parametrize("mode", ["regular", "upstream", "err"]) @pytest.mark.parametrize("concurrency", [-1, 1]) async def test_playback(mode, concurrency): @@ -33,16 +32,11 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): return req = await reader.readline() if mode == "upstream": - assert req == b'GET http://address:22/path HTTP/1.1\r\n' + assert req == b"GET http://address:22/path HTTP/1.1\r\n" else: - assert req == b'GET /path HTTP/1.1\r\n' + assert req == b"GET /path HTTP/1.1\r\n" req = await reader.readuntil(b"data") - assert req == ( - b'header: qvalue\r\n' - b'content-length: 4\r\n' - b'\r\n' - b'data' - ) + assert req == (b"header: qvalue\r\n" b"content-length: 4\r\n" b"\r\n" b"data") writer.write(b"HTTP/1.1 204 No Content\r\n\r\n") await writer.drain() assert not await reader.read() @@ -55,12 +49,12 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): async with tcp_server(handler) as addr: cp.running() - flow = tflow.tflow() + flow = tflow.tflow(live=False) flow.request.content = b"data" if mode == "upstream": tctx.options.mode = f"upstream:http://{addr[0]}:{addr[1]}" flow.request.authority = f"{addr[0]}:{addr[1]}" - flow.request.host, flow.request.port = 'address', 22 + flow.request.host, flow.request.port = "address", 22 else: flow.request.host, flow.request.port = addr cp.start_replay([flow]) @@ -72,13 +66,12 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): assert flow.response.status_code == 204 -@pytest.mark.asyncio async def test_playback_https_upstream(): handler_ok = asyncio.Event() async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): conn_req = await reader.readuntil(b"\r\n\r\n") - assert conn_req == b'CONNECT address:22 HTTP/1.1\r\n\r\n' + assert conn_req == b"CONNECT address:22 HTTP/1.1\r\n\r\n" writer.write(b"HTTP/1.1 502 Bad Gateway\r\n\r\n") await writer.drain() assert not await reader.read() @@ -90,7 +83,7 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): tctx.configure(cp) async with tcp_server(handler) as addr: cp.running() - flow = tflow.tflow() + flow = tflow.tflow(live=False) flow.request.scheme = b"https" flow.request.content = b"data" tctx.options.mode = f"upstream:http://{addr[0]}:{addr[1]}" @@ -100,10 +93,12 @@ async def handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): await asyncio.wait_for(handler_ok.wait(), 5) cp.done() assert flow.response is None - assert str(flow.error) == f'Upstream proxy {addr[0]}:{addr[1]} refused HTTP CONNECT request: 502 Bad Gateway' + assert ( + str(flow.error) + == f"Upstream proxy {addr[0]}:{addr[1]} refused HTTP CONNECT request: 502 Bad Gateway" + ) -@pytest.mark.asyncio async def test_playback_crash(monkeypatch): async def raise_err(): raise ValueError("oops") @@ -112,7 +107,7 @@ async def raise_err(): cp = ClientPlayback() with taddons.context(cp) as tctx: cp.running() - cp.start_replay([tflow.tflow()]) + cp.start_replay([tflow.tflow(live=False)]) await tctx.master.await_log("Client replay has crashed!", level="error") assert cp.count() == 0 cp.done() @@ -124,30 +119,32 @@ def test_check(): f.live = True assert "live flow" in cp.check(f) - f = tflow.tflow(resp=True) + f = tflow.tflow(resp=True, live=False) f.intercepted = True assert "intercepted flow" in cp.check(f) - f = tflow.tflow(resp=True) + f = tflow.tflow(resp=True, live=False) f.request = None assert "missing request" in cp.check(f) - f = tflow.tflow(resp=True) + f = tflow.tflow(resp=True, live=False) f.request.raw_content = None assert "missing content" in cp.check(f) f = tflow.ttcpflow() + f.live = False assert "Can only replay HTTP" in cp.check(f) -@pytest.mark.asyncio async def test_start_stop(tdata): cp = ClientPlayback() with taddons.context(cp) as tctx: - cp.start_replay([tflow.tflow()]) + cp.start_replay([tflow.tflow(live=False)]) assert cp.count() == 1 - cp.start_replay([tflow.twebsocketflow()]) + ws_flow = tflow.twebsocketflow() + ws_flow.live = False + cp.start_replay([ws_flow]) await tctx.master.await_log("Can't replay WebSocket flows.", level="warn") assert cp.count() == 1 @@ -170,7 +167,9 @@ def test_configure(tdata): cp = ClientPlayback() with taddons.context(cp) as tctx: assert cp.count() == 0 - tctx.configure(cp, client_replay=[tdata.path("mitmproxy/data/dumpfile-018.mitm")]) + tctx.configure( + cp, client_replay=[tdata.path("mitmproxy/data/dumpfile-018.mitm")] + ) assert cp.count() == 1 tctx.configure(cp, client_replay=[]) with pytest.raises(OptionsError): diff --git a/test/mitmproxy/addons/test_command_history.py b/test/mitmproxy/addons/test_command_history.py index 3d9a6e8eeb..8a8a8a5234 100644 --- a/test/mitmproxy/addons/test_command_history.py +++ b/test/mitmproxy/addons/test_command_history.py @@ -2,17 +2,15 @@ from unittest.mock import patch from pathlib import Path -import pytest - from mitmproxy.addons import command_history from mitmproxy.test import taddons class TestCommandHistory: def test_load_and_save(self, tmpdir): - history_file = tmpdir.join('command_history') + history_file = tmpdir.join("command_history") commands = ["cmd1", "cmd2", "cmd3"] - with open(history_file, 'w') as f: + with open(history_file, "w") as f: f.write("\n".join(commands)) ch = command_history.CommandHistory() @@ -26,34 +24,32 @@ def test_load_and_save(self, tmpdir): with open(history_file) as f: assert f.read() == "cmd3\ncmd4\n" - @pytest.mark.asyncio async def test_done_writing_failed(self): ch = command_history.CommandHistory() ch.VACUUM_SIZE = 1 with taddons.context(ch) as tctx: - ch.history.append('cmd1') - ch.history.append('cmd2') - ch.history.append('cmd3') - tctx.options.confdir = '/non/existent/path/foobar1234/' + ch.history.append("cmd1") + ch.history.append("cmd2") + ch.history.append("cmd3") + tctx.options.confdir = "/non/existent/path/foobar1234/" ch.done() await tctx.master.await_log(f"Failed writing to {ch.history_file}") def test_add_command(self): ch = command_history.CommandHistory() with taddons.context(ch): - ch.add_command('cmd1') - ch.add_command('cmd2') - assert ch.history == ['cmd1', 'cmd2'] + ch.add_command("cmd1") + ch.add_command("cmd2") + assert ch.history == ["cmd1", "cmd2"] - ch.add_command('') - assert ch.history == ['cmd1', 'cmd2'] + ch.add_command("") + assert ch.history == ["cmd1", "cmd2"] - @pytest.mark.asyncio async def test_add_command_failed(self): ch = command_history.CommandHistory() with taddons.context(ch) as tctx: - tctx.options.confdir = '/non/existent/path/foobar1234/' - ch.add_command('cmd1') + tctx.options.confdir = "/non/existent/path/foobar1234/" + ch.add_command("cmd1") await tctx.master.await_log(f"Failed writing to {ch.history_file}") def test_get_next_and_prev(self, tmpdir): @@ -62,78 +58,78 @@ def test_get_next_and_prev(self, tmpdir): with taddons.context(ch) as tctx: tctx.options.confdir = str(tmpdir) - ch.add_command('cmd1') - - assert ch.get_next() == '' - assert ch.get_next() == '' - assert ch.get_prev() == 'cmd1' - assert ch.get_prev() == 'cmd1' - assert ch.get_prev() == 'cmd1' - assert ch.get_next() == '' - assert ch.get_next() == '' - - ch.add_command('cmd2') - - assert ch.get_next() == '' - assert ch.get_next() == '' - assert ch.get_prev() == 'cmd2' - assert ch.get_prev() == 'cmd1' - assert ch.get_prev() == 'cmd1' - assert ch.get_next() == 'cmd2' - assert ch.get_next() == '' - assert ch.get_next() == '' - - ch.add_command('cmd3') - - assert ch.get_next() == '' - assert ch.get_next() == '' - assert ch.get_prev() == 'cmd3' - assert ch.get_prev() == 'cmd2' - assert ch.get_prev() == 'cmd1' - assert ch.get_prev() == 'cmd1' - assert ch.get_next() == 'cmd2' - assert ch.get_next() == 'cmd3' - assert ch.get_next() == '' - assert ch.get_next() == '' - assert ch.get_prev() == 'cmd3' - assert ch.get_prev() == 'cmd2' - - ch.add_command('cmd4') - - assert ch.get_prev() == 'cmd4' - assert ch.get_prev() == 'cmd3' - assert ch.get_prev() == 'cmd2' - assert ch.get_prev() == 'cmd1' - assert ch.get_prev() == 'cmd1' - assert ch.get_next() == 'cmd2' - assert ch.get_next() == 'cmd3' - assert ch.get_next() == 'cmd4' - assert ch.get_next() == '' - assert ch.get_next() == '' - - ch.add_command('cmd5') - ch.add_command('cmd6') - - assert ch.get_next() == '' - assert ch.get_prev() == 'cmd6' - assert ch.get_prev() == 'cmd5' - assert ch.get_prev() == 'cmd4' - assert ch.get_next() == 'cmd5' - assert ch.get_prev() == 'cmd4' - assert ch.get_prev() == 'cmd3' - assert ch.get_prev() == 'cmd2' - assert ch.get_next() == 'cmd3' - assert ch.get_prev() == 'cmd2' - assert ch.get_prev() == 'cmd1' - assert ch.get_prev() == 'cmd1' - assert ch.get_prev() == 'cmd1' - assert ch.get_next() == 'cmd2' - assert ch.get_next() == 'cmd3' - assert ch.get_next() == 'cmd4' - assert ch.get_next() == 'cmd5' - assert ch.get_next() == 'cmd6' - assert ch.get_next() == '' - assert ch.get_next() == '' + ch.add_command("cmd1") + + assert ch.get_next() == "" + assert ch.get_next() == "" + assert ch.get_prev() == "cmd1" + assert ch.get_prev() == "cmd1" + assert ch.get_prev() == "cmd1" + assert ch.get_next() == "" + assert ch.get_next() == "" + + ch.add_command("cmd2") + + assert ch.get_next() == "" + assert ch.get_next() == "" + assert ch.get_prev() == "cmd2" + assert ch.get_prev() == "cmd1" + assert ch.get_prev() == "cmd1" + assert ch.get_next() == "cmd2" + assert ch.get_next() == "" + assert ch.get_next() == "" + + ch.add_command("cmd3") + + assert ch.get_next() == "" + assert ch.get_next() == "" + assert ch.get_prev() == "cmd3" + assert ch.get_prev() == "cmd2" + assert ch.get_prev() == "cmd1" + assert ch.get_prev() == "cmd1" + assert ch.get_next() == "cmd2" + assert ch.get_next() == "cmd3" + assert ch.get_next() == "" + assert ch.get_next() == "" + assert ch.get_prev() == "cmd3" + assert ch.get_prev() == "cmd2" + + ch.add_command("cmd4") + + assert ch.get_prev() == "cmd4" + assert ch.get_prev() == "cmd3" + assert ch.get_prev() == "cmd2" + assert ch.get_prev() == "cmd1" + assert ch.get_prev() == "cmd1" + assert ch.get_next() == "cmd2" + assert ch.get_next() == "cmd3" + assert ch.get_next() == "cmd4" + assert ch.get_next() == "" + assert ch.get_next() == "" + + ch.add_command("cmd5") + ch.add_command("cmd6") + + assert ch.get_next() == "" + assert ch.get_prev() == "cmd6" + assert ch.get_prev() == "cmd5" + assert ch.get_prev() == "cmd4" + assert ch.get_next() == "cmd5" + assert ch.get_prev() == "cmd4" + assert ch.get_prev() == "cmd3" + assert ch.get_prev() == "cmd2" + assert ch.get_next() == "cmd3" + assert ch.get_prev() == "cmd2" + assert ch.get_prev() == "cmd1" + assert ch.get_prev() == "cmd1" + assert ch.get_prev() == "cmd1" + assert ch.get_next() == "cmd2" + assert ch.get_next() == "cmd3" + assert ch.get_next() == "cmd4" + assert ch.get_next() == "cmd5" + assert ch.get_next() == "cmd6" + assert ch.get_next() == "" + assert ch.get_next() == "" ch.clear_history() @@ -142,30 +138,29 @@ def test_clear(self, tmpdir): with taddons.context(ch) as tctx: tctx.options.confdir = str(tmpdir) - ch.add_command('cmd1') - ch.add_command('cmd2') + ch.add_command("cmd1") + ch.add_command("cmd2") ch.clear_history() saved_commands = ch.get_history() assert saved_commands == [] - assert ch.get_next() == '' - assert ch.get_next() == '' - assert ch.get_prev() == '' - assert ch.get_prev() == '' + assert ch.get_next() == "" + assert ch.get_next() == "" + assert ch.get_prev() == "" + assert ch.get_prev() == "" ch.clear_history() - @pytest.mark.asyncio async def test_clear_failed(self, monkeypatch): ch = command_history.CommandHistory() with taddons.context(ch) as tctx: - tctx.options.confdir = '/non/existent/path/foobar1234/' + tctx.options.confdir = "/non/existent/path/foobar1234/" - with patch.object(Path, 'exists') as mock_exists: + with patch.object(Path, "exists") as mock_exists: mock_exists.return_value = True - with patch.object(Path, 'unlink') as mock_unlink: + with patch.object(Path, "unlink") as mock_unlink: mock_unlink.side_effect = IOError() ch.clear_history() await tctx.master.await_log(f"Failed deleting {ch.history_file}") @@ -176,32 +171,32 @@ def test_filter(self, tmpdir): with taddons.context(ch) as tctx: tctx.options.confdir = str(tmpdir) - ch.add_command('cmd1') - ch.add_command('cmd2') - ch.add_command('abc') - ch.set_filter('c') - - assert ch.get_next() == 'c' - assert ch.get_next() == 'c' - assert ch.get_prev() == 'cmd2' - assert ch.get_prev() == 'cmd1' - assert ch.get_prev() == 'cmd1' - assert ch.get_next() == 'cmd2' - assert ch.get_next() == 'c' - assert ch.get_next() == 'c' - - ch.set_filter('') - - assert ch.get_next() == '' - assert ch.get_next() == '' - assert ch.get_prev() == 'abc' - assert ch.get_prev() == 'cmd2' - assert ch.get_prev() == 'cmd1' - assert ch.get_prev() == 'cmd1' - assert ch.get_next() == 'cmd2' - assert ch.get_next() == 'abc' - assert ch.get_next() == '' - assert ch.get_next() == '' + ch.add_command("cmd1") + ch.add_command("cmd2") + ch.add_command("abc") + ch.set_filter("c") + + assert ch.get_next() == "c" + assert ch.get_next() == "c" + assert ch.get_prev() == "cmd2" + assert ch.get_prev() == "cmd1" + assert ch.get_prev() == "cmd1" + assert ch.get_next() == "cmd2" + assert ch.get_next() == "c" + assert ch.get_next() == "c" + + ch.set_filter("") + + assert ch.get_next() == "" + assert ch.get_next() == "" + assert ch.get_prev() == "abc" + assert ch.get_prev() == "cmd2" + assert ch.get_prev() == "cmd1" + assert ch.get_prev() == "cmd1" + assert ch.get_next() == "cmd2" + assert ch.get_next() == "abc" + assert ch.get_next() == "" + assert ch.get_next() == "" ch.clear_history() @@ -213,17 +208,17 @@ def test_multiple_instances(self, tmpdir): instances = [ command_history.CommandHistory(), command_history.CommandHistory(), - command_history.CommandHistory() + command_history.CommandHistory(), ] for i in instances: - i.configure('command_history') + i.configure("command_history") saved_commands = i.get_history() assert saved_commands == [] - instances[0].add_command('cmd1') + instances[0].add_command("cmd1") saved_commands = instances[0].get_history() - assert saved_commands == ['cmd1'] + assert saved_commands == ["cmd1"] # These instances haven't yet added a new command, so they haven't # yet reloaded their commands from the command file. @@ -237,73 +232,77 @@ def test_multiple_instances(self, tmpdir): # Since the second instanced added a new command, its list of # saved commands has been updated to have the commands from the # first instance + its own commands - instances[1].add_command('cmd2') + instances[1].add_command("cmd2") saved_commands = instances[1].get_history() - assert saved_commands == ['cmd2'] + assert saved_commands == ["cmd2"] saved_commands = instances[0].get_history() - assert saved_commands == ['cmd1'] + assert saved_commands == ["cmd1"] # Third instance is still empty as it has not yet ran any command saved_commands = instances[2].get_history() assert saved_commands == [] - instances[2].add_command('cmd3') + instances[2].add_command("cmd3") saved_commands = instances[2].get_history() - assert saved_commands == ['cmd3'] + assert saved_commands == ["cmd3"] - instances[0].add_command('cmd4') + instances[0].add_command("cmd4") saved_commands = instances[0].get_history() - assert saved_commands == ['cmd1', 'cmd4'] + assert saved_commands == ["cmd1", "cmd4"] instances.append(command_history.CommandHistory()) - instances[3].configure('command_history') + instances[3].configure("command_history") saved_commands = instances[3].get_history() - assert saved_commands == ['cmd1', 'cmd2', 'cmd3', 'cmd4'] + assert saved_commands == ["cmd1", "cmd2", "cmd3", "cmd4"] - instances[0].add_command('cmd_before_close') + instances[0].add_command("cmd_before_close") instances.pop(0).done() saved_commands = instances[0].get_history() - assert saved_commands == ['cmd2'] + assert saved_commands == ["cmd2"] - instances[0].add_command('new_cmd') + instances[0].add_command("new_cmd") saved_commands = instances[0].get_history() - assert saved_commands == ['cmd2', 'new_cmd'] + assert saved_commands == ["cmd2", "new_cmd"] instances.pop(0).done() instances.pop(0).done() instances.pop(0).done() - _path = os.path.join(tctx.options.confdir, 'command_history') + _path = os.path.join(tctx.options.confdir, "command_history") lines = open(_path).readlines() saved_commands = [cmd.strip() for cmd in lines] - assert saved_commands == ['cmd1', 'cmd2', 'cmd3', 'cmd4', 'cmd_before_close', 'new_cmd'] - - instances = [ - command_history.CommandHistory(), - command_history.CommandHistory() + assert saved_commands == [ + "cmd1", + "cmd2", + "cmd3", + "cmd4", + "cmd_before_close", + "new_cmd", ] + instances = [command_history.CommandHistory(), command_history.CommandHistory()] + for i in instances: - i.configure('command_history') + i.configure("command_history") i.clear_history() saved_commands = i.get_history() assert saved_commands == [] - instances[0].add_command('cmd1') - instances[0].add_command('cmd2') - instances[1].add_command('cmd3') - instances[1].add_command('cmd4') - instances[1].add_command('cmd5') + instances[0].add_command("cmd1") + instances[0].add_command("cmd2") + instances[1].add_command("cmd3") + instances[1].add_command("cmd4") + instances[1].add_command("cmd5") saved_commands = instances[1].get_history() - assert saved_commands == ['cmd3', 'cmd4', 'cmd5'] + assert saved_commands == ["cmd3", "cmd4", "cmd5"] instances.pop().done() instances.pop().done() - _path = os.path.join(tctx.options.confdir, 'command_history') + _path = os.path.join(tctx.options.confdir, "command_history") lines = open(_path).readlines() saved_commands = [cmd.strip() for cmd in lines] - assert saved_commands == ['cmd1', 'cmd2', 'cmd3', 'cmd4', 'cmd5'] + assert saved_commands == ["cmd1", "cmd2", "cmd3", "cmd4", "cmd5"] diff --git a/test/mitmproxy/addons/test_core.py b/test/mitmproxy/addons/test_core.py index ebdfbab607..707d775f29 100644 --- a/test/mitmproxy/addons/test_core.py +++ b/test/mitmproxy/addons/test_core.py @@ -25,7 +25,7 @@ def test_resume(): assert not sa.resume([f]) f.intercept() sa.resume([f]) - assert not f.reply.state == "taken" + assert not f.intercepted def test_mark(): @@ -151,7 +151,7 @@ def test_options(tmpdir): sa.options_load("/nonexistent") - with open(p, 'a') as f: + with open(p, "a") as f: f.write("'''") with pytest.raises(exceptions.CommandError): sa.options_load(p) @@ -160,17 +160,15 @@ def test_options(tmpdir): def test_validation_simple(): sa = core.Core() with taddons.context() as tctx: - with pytest.raises(exceptions.OptionsError, match="requires the upstream_cert option to be enabled"): + with pytest.raises( + exceptions.OptionsError, + match="requires the upstream_cert option to be enabled", + ): tctx.configure( - sa, - add_upstream_certs_to_client_chain = True, - upstream_cert = False + sa, add_upstream_certs_to_client_chain=True, upstream_cert=False ) with pytest.raises(exceptions.OptionsError, match="Invalid mode"): - tctx.configure( - sa, - mode = "Flibble" - ) + tctx.configure(sa, mode="Flibble") @mock.patch("mitmproxy.platform.original_addr", None) @@ -178,25 +176,29 @@ def test_validation_no_transparent(): sa = core.Core() with taddons.context() as tctx: with pytest.raises(Exception, match="Transparent mode not supported"): - tctx.configure(sa, mode = "transparent") + tctx.configure(sa, mode="transparent") @mock.patch("mitmproxy.platform.original_addr") def test_validation_modes(m): sa = core.Core() with taddons.context() as tctx: - tctx.configure(sa, mode = "reverse:http://localhost") + tctx.configure(sa, mode="reverse:http://localhost") with pytest.raises(Exception, match="Invalid server specification"): - tctx.configure(sa, mode = "reverse:") + tctx.configure(sa, mode="reverse:") def test_client_certs(tdata): sa = core.Core() with taddons.context() as tctx: # Folders should work. - tctx.configure(sa, client_certs = tdata.path("mitmproxy/data/clientcert")) + tctx.configure(sa, client_certs=tdata.path("mitmproxy/data/clientcert")) # Files, too. - tctx.configure(sa, client_certs = tdata.path("mitmproxy/data/clientcert/client.pem")) - - with pytest.raises(exceptions.OptionsError, match="certificate path does not exist"): - tctx.configure(sa, client_certs = "invalid") + tctx.configure( + sa, client_certs=tdata.path("mitmproxy/data/clientcert/client.pem") + ) + + with pytest.raises( + exceptions.OptionsError, match="certificate path does not exist" + ): + tctx.configure(sa, client_certs="invalid") diff --git a/test/mitmproxy/addons/test_cut.py b/test/mitmproxy/addons/test_cut.py index b964abbb9c..b17cf8155e 100644 --- a/test/mitmproxy/addons/test_cut.py +++ b/test/mitmproxy/addons/test_cut.py @@ -25,7 +25,6 @@ def test_extract(tdata): ["request.timestamp_start", "946681200"], ["request.timestamp_end", "946681201"], ["request.header[header]", "qvalue"], - ["response.status_code", "200"], ["response.reason", "OK"], ["response.text", "message"], @@ -34,13 +33,11 @@ def test_extract(tdata): ["response.header[header-response]", "svalue"], ["response.timestamp_start", "946681202"], ["response.timestamp_end", "946681203"], - ["client_conn.peername.port", "22"], ["client_conn.peername.host", "127.0.0.1"], ["client_conn.tls_version", "TLSv1.2"], ["client_conn.sni", "address"], ["client_conn.tls_established", "true"], - ["server_conn.address.port", "22"], ["server_conn.address.host", "address"], ["server_conn.peername.host", "192.168.0.1"], @@ -59,6 +56,12 @@ def test_extract(tdata): assert "CERTIFICATE" in cut.extract("server_conn.certificate_list", tf) +def test_extract_str(): + tf = tflow.tflow() + tf.request.raw_content = b"\xFF" + assert cut.extract_str("request.raw_content", tf) == r"b'\xff'" + + def test_headername(): with pytest.raises(exceptions.CommandError): cut.headername("header[foo.") @@ -69,7 +72,6 @@ def qr(f): return fp.read() -@pytest.mark.asyncio async def test_cut_clip(): v = view.View() c = cut.Cut() @@ -77,21 +79,22 @@ async def test_cut_clip(): tctx.master.addons.add(v, c) v.add([tflow.tflow(resp=True)]) - with mock.patch('pyperclip.copy') as pc: + with mock.patch("pyperclip.copy") as pc: tctx.command(c.clip, "@all", "request.method") assert pc.called - with mock.patch('pyperclip.copy') as pc: + with mock.patch("pyperclip.copy") as pc: tctx.command(c.clip, "@all", "request.content") assert pc.called - with mock.patch('pyperclip.copy') as pc: + with mock.patch("pyperclip.copy") as pc: tctx.command(c.clip, "@all", "request.method,request.content") assert pc.called - with mock.patch('pyperclip.copy') as pc: - log_message = "Pyperclip could not find a " \ - "copy/paste mechanism for your system." + with mock.patch("pyperclip.copy") as pc: + log_message = ( + "Pyperclip could not find a " "copy/paste mechanism for your system." + ) pc.side_effect = pyperclip.PyperclipException(log_message) tctx.command(c.clip, "@all", "request.method") await tctx.master.await_log(log_message, level="error") @@ -116,15 +119,17 @@ def test_cut_save(tmpdir): tctx.command(c.save, "@all", "request.method", f) assert qr(f).splitlines() == [b"GET", b"GET"] tctx.command(c.save, "@all", "request.method,request.content", f) - assert qr(f).splitlines() == [b"GET,content", b"GET,content"] + assert qr(f).splitlines() == [b"GET,b'content'", b"GET,b'content'"] -@pytest.mark.parametrize("exception, log_message", [ - (PermissionError, "Permission denied"), - (IsADirectoryError, "Is a directory"), - (FileNotFoundError, "No such file or directory") -]) -@pytest.mark.asyncio +@pytest.mark.parametrize( + "exception, log_message", + [ + (PermissionError, "Permission denied"), + (IsADirectoryError, "Is a directory"), + (FileNotFoundError, "No such file or directory"), + ], +) async def test_cut_save_open(exception, log_message, tmpdir): f = str(tmpdir.join("path")) v = view.View() diff --git a/test/mitmproxy/addons/test_disable_h2c.py b/test/mitmproxy/addons/test_disable_h2c.py index fd3efef3b9..98ec0e3dda 100644 --- a/test/mitmproxy/addons/test_disable_h2c.py +++ b/test/mitmproxy/addons/test_disable_h2c.py @@ -11,14 +11,14 @@ def test_upgrade(self): tctx.configure(a) f = tflow.tflow() - f.request.headers['upgrade'] = 'h2c' - f.request.headers['connection'] = 'foo' - f.request.headers['http2-settings'] = 'bar' + f.request.headers["upgrade"] = "h2c" + f.request.headers["connection"] = "foo" + f.request.headers["http2-settings"] = "bar" a.request(f) - assert 'upgrade' not in f.request.headers - assert 'connection' not in f.request.headers - assert 'http2-settings' not in f.request.headers + assert "upgrade" not in f.request.headers + assert "connection" not in f.request.headers + assert "http2-settings" not in f.request.headers def test_prior_knowledge(self): with taddons.context() as tctx: diff --git a/test/mitmproxy/addons/test_dns_resolver.py b/test/mitmproxy/addons/test_dns_resolver.py new file mode 100644 index 0000000000..a4b959eb9c --- /dev/null +++ b/test/mitmproxy/addons/test_dns_resolver.py @@ -0,0 +1,134 @@ +import asyncio +import ipaddress +import socket +from typing import Callable + +import pytest + +from mitmproxy import dns +from mitmproxy.addons import dns_resolver, proxyserver +from mitmproxy.connection import Address +from mitmproxy.test import taddons, tflow, tutils + + +async def test_simple(monkeypatch): + monkeypatch.setattr( + dns_resolver, "resolve_message", lambda _, __: asyncio.sleep(0, "resp") + ) + + dr = dns_resolver.DnsResolver() + with taddons.context(dr, proxyserver.Proxyserver()) as tctx: + f = tflow.tdnsflow() + await dr.dns_request(f) + assert f.response + + tctx.options.dns_mode = "reverse:8.8.8.8" + f = tflow.tdnsflow() + await dr.dns_request(f) + assert not f.response + + +class DummyLoop: + async def getnameinfo(self, socketaddr: Address, flags: int = 0): + assert flags == socket.NI_NAMEREQD + if socketaddr[0] in ("8.8.8.8", "2001:4860:4860::8888"): + return ("dns.google", "") + e = socket.gaierror() + e.errno = socket.EAI_NONAME + raise e + + async def getaddrinfo(self, host: str, port: int, *, family: int): + e = socket.gaierror() + e.errno = socket.EAI_NONAME + if family == socket.AF_INET: + if host == "dns.google": + return [(socket.AF_INET, None, None, None, ("8.8.8.8", port))] + elif family == socket.AF_INET6: + if host == "dns.google": + return [ + ( + socket.AF_INET6, + None, + None, + None, + ("2001:4860:4860::8888", port, None, None), + ) + ] + else: + e.errno = socket.EAI_FAMILY + raise e + + +async def test_resolve(): + async def fail_with(question: dns.Question, code: int): + with pytest.raises(dns_resolver.ResolveError) as ex: + await dns_resolver.resolve_question(question, DummyLoop()) + assert ex.value.response_code == code + + async def succeed_with( + question: dns.Question, check: Callable[[dns.ResourceRecord], bool] + ): + assert any( + map(check, await dns_resolver.resolve_question(question, DummyLoop())) + ) + + await fail_with( + dns.Question("dns.google", dns.types.A, dns.classes.CH), + dns.response_codes.NOTIMP, + ) + await fail_with( + dns.Question("not.exists", dns.types.A, dns.classes.IN), + dns.response_codes.NXDOMAIN, + ) + await fail_with( + dns.Question("dns.google", dns.types.SOA, dns.classes.IN), + dns.response_codes.NOTIMP, + ) + await fail_with( + dns.Question("totally.invalid", dns.types.PTR, dns.classes.IN), + dns.response_codes.FORMERR, + ) + await fail_with( + dns.Question("invalid.in-addr.arpa", dns.types.PTR, dns.classes.IN), + dns.response_codes.FORMERR, + ) + await fail_with( + dns.Question("0.0.0.1.in-addr.arpa", dns.types.PTR, dns.classes.IN), + dns.response_codes.NXDOMAIN, + ) + + await succeed_with( + dns.Question("dns.google", dns.types.A, dns.classes.IN), + lambda rr: rr.ipv4_address == ipaddress.IPv4Address("8.8.8.8"), + ) + await succeed_with( + dns.Question("dns.google", dns.types.AAAA, dns.classes.IN), + lambda rr: rr.ipv6_address == ipaddress.IPv6Address("2001:4860:4860::8888"), + ) + await succeed_with( + dns.Question("8.8.8.8.in-addr.arpa", dns.types.PTR, dns.classes.IN), + lambda rr: rr.domain_name == "dns.google", + ) + await succeed_with( + dns.Question( + "8.8.8.8.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.6.8.4.0.6.8.4.1.0.0.2.ip6.arpa", + dns.types.PTR, + dns.classes.IN, + ), + lambda rr: rr.domain_name == "dns.google", + ) + + req = tutils.tdnsreq() + req.query = False + assert ( + await dns_resolver.resolve_message(req, DummyLoop()) + ).response_code == dns.response_codes.REFUSED + req.query = True + req.op_code = dns.op_codes.IQUERY + assert ( + await dns_resolver.resolve_message(req, DummyLoop()) + ).response_code == dns.response_codes.NOTIMP + req.op_code = dns.op_codes.QUERY + resp = await dns_resolver.resolve_message(req, DummyLoop()) + assert resp.response_code == dns.response_codes.NOERROR + assert filter(lambda rr: str(rr.ipv4_address) == "8.8.8.8", resp.answers) diff --git a/test/mitmproxy/addons/test_dumper.py b/test/mitmproxy/addons/test_dumper.py index 5e9ed7639e..e1041226ba 100644 --- a/test/mitmproxy/addons/test_dumper.py +++ b/test/mitmproxy/addons/test_dumper.py @@ -118,12 +118,16 @@ def test_echo_trailer(): f.request.headers["transfer-encoding"] = "chunked" f.request.headers["trailer"] = "my-little-request-trailer" f.request.content = b"some request content\n" * 100 - f.request.trailers = Headers([(b"my-little-request-trailer", b"foobar-request-trailer")]) + f.request.trailers = Headers( + [(b"my-little-request-trailer", b"foobar-request-trailer")] + ) f.response.headers["transfer-encoding"] = "chunked" f.response.headers["trailer"] = "my-little-response-trailer" f.response.content = b"some response content\n" * 100 - f.response.trailers = Headers([(b"my-little-response-trailer", b"foobar-response-trailer")]) + f.response.trailers = Headers( + [(b"my-little-response-trailer", b"foobar-response-trailer")] + ) d.echo_flow(f) t = sio.getvalue() @@ -158,26 +162,26 @@ def test_echo_request_line(): assert "nonstandard" in sio.getvalue() sio.truncate(0) - ctx.configure(d, flow_detail=0, showhost=True) + ctx.configure(d, flow_detail=1, showhost=True) f = tflow.tflow(resp=True) terminalWidth = max(shutil.get_terminal_size()[0] - 25, 50) - f.request.url = "http://address:22/" + ("x" * terminalWidth) + "textToBeTruncated" + f.request.url = ( + "http://address:22/" + ("x" * terminalWidth) + "textToBeTruncated" + ) d._echo_request_line(f) assert "textToBeTruncated" not in sio.getvalue() sio.truncate(0) -class TestContentView: - @pytest.mark.asyncio - async def test_contentview(self): - with mock.patch("mitmproxy.contentviews.auto.ViewAuto.__call__") as va: - va.side_effect = ValueError("") - sio = io.StringIO() - d = dumper.Dumper(sio) - with taddons.context(d) as tctx: - tctx.configure(d, flow_detail=4) - d.response(tflow.tflow()) - await tctx.master.await_log("content viewer failed") +async def test_contentview(): + with mock.patch("mitmproxy.contentviews.auto.ViewAuto.__call__") as va: + va.side_effect = ValueError("") + sio = io.StringIO() + d = dumper.Dumper(sio) + with taddons.context(d) as tctx: + tctx.configure(d, flow_detail=4) + d.response(tflow.tflow()) + await tctx.master.await_log("content viewer failed") def test_tcp(): @@ -195,6 +199,22 @@ def test_tcp(): assert "Error in TCP" in sio.getvalue() +def test_dns(): + sio = io.StringIO() + d = dumper.Dumper(sio) + with taddons.context(d) as ctx: + ctx.configure(d, flow_detail=3, showhost=True) + + f = tflow.tdnsflow(resp=True) + d.dns_response(f) + assert "8.8.8.8" in sio.getvalue() + sio.truncate(0) + + f = tflow.tdnsflow(err=True) + d.dns_error(f) + assert "error" in sio.getvalue() + + def test_websocket(): sio = io.StringIO() d = dumper.Dumper(sio) @@ -215,7 +235,7 @@ def test_websocket(): assert "(reason:" not in sio.getvalue() sio.truncate(0) - f = tflow.twebsocketflow(err=True, close_reason='Some lame excuse') + f = tflow.twebsocketflow(err=True, close_reason="Some lame excuse") d.websocket_end(f) assert "Error in WebSocket" in sio.getvalue() assert "(reason: Some lame excuse)" in sio.getvalue() @@ -227,7 +247,7 @@ def test_websocket(): assert "(reason:" not in sio.getvalue() sio.truncate(0) - f = tflow.twebsocketflow(close_code=4000, close_reason='I swear I had a reason') + f = tflow.twebsocketflow(close_code=4000, close_reason="I swear I had a reason") d.websocket_end(f) assert "UNKNOWN_ERROR=4000" in sio.getvalue() assert "(reason: I swear I had a reason)" in sio.getvalue() @@ -241,3 +261,13 @@ def test_http2(): f.response.http_version = b"HTTP/2.0" d.response(f) assert "HTTP/2.0 200 OK" in sio.getvalue() + + +def test_styling(): + sio = io.StringIO() + + d = dumper.Dumper(sio) + d.out_has_vt_codes = True + with taddons.context(d): + d.response(tflow.tflow(resp=True)) + assert "\x1b[" in sio.getvalue() diff --git a/test/mitmproxy/addons/test_errorcheck.py b/test/mitmproxy/addons/test_errorcheck.py new file mode 100644 index 0000000000..17d3f12e0b --- /dev/null +++ b/test/mitmproxy/addons/test_errorcheck.py @@ -0,0 +1,29 @@ +import asyncio + +import pytest + +from mitmproxy import log +from mitmproxy.addons.errorcheck import ErrorCheck + + +@pytest.mark.parametrize("do_log", [True, False]) +def test_errorcheck(capsys, do_log): + async def run(): + # suppress error that task exception was not retrieved. + asyncio.get_running_loop().set_exception_handler(lambda *_: 0) + e = ErrorCheck(do_log) + e.add_log(log.LogEntry("fatal", "error")) + await e.running() + await asyncio.sleep(0) + + with pytest.raises(SystemExit): + asyncio.run(run()) + + if do_log: + assert capsys.readouterr().err == "Error on startup: fatal\n" + + +async def test_no_error(): + e = ErrorCheck() + await e.running() + await asyncio.sleep(0) diff --git a/test/mitmproxy/addons/test_export.py b/test/mitmproxy/addons/test_export.py index f90756b4e0..f8a24bf54f 100644 --- a/test/mitmproxy/addons/test_export.py +++ b/test/mitmproxy/addons/test_export.py @@ -15,36 +15,36 @@ @pytest.fixture def get_request(): return tflow.tflow( - req=tutils.treq(method=b'GET', content=b'', path=b"/path?a=foo&a=bar&b=baz")) + req=tutils.treq(method=b"GET", content=b"", path=b"/path?a=foo&a=bar&b=baz") + ) @pytest.fixture def get_response(): return tflow.tflow( - resp=tutils.tresp(status_code=404, content=b"Test Response Body")) + resp=tutils.tresp(status_code=404, content=b"Test Response Body") + ) @pytest.fixture def get_flow(): return tflow.tflow( - req=tutils.treq(method=b'GET', content=b'', path=b"/path?a=foo&a=bar&b=baz"), - resp=tutils.tresp(status_code=404, content=b"Test Response Body")) + req=tutils.treq(method=b"GET", content=b"", path=b"/path?a=foo&a=bar&b=baz"), + resp=tutils.tresp(status_code=404, content=b"Test Response Body"), + ) @pytest.fixture def post_request(): return tflow.tflow( - req=tutils.treq(method=b'POST', headers=(), content=bytes(range(256)))) + req=tutils.treq(method=b"POST", headers=(), content=bytes(range(256))) + ) @pytest.fixture def patch_request(): return tflow.tflow( - req=tutils.treq( - method=b'PATCH', - content=b'content', - path=b"/path?query=param" - ) + req=tutils.treq(method=b"PATCH", content=b"content", path=b"/path?query=param") ) @@ -63,11 +63,13 @@ def export_curl(): class TestExportCurlCommand: def test_get(self, export_curl, get_request): - result = """curl -H 'header: qvalue' 'http://address:22/path?a=foo&a=bar&b=baz'""" + result = ( + """curl -H 'header: qvalue' 'http://address:22/path?a=foo&a=bar&b=baz'""" + ) assert export_curl(get_request) == result def test_post(self, export_curl, post_request): - post_request.request.content = b'nobinarysupport' + post_request.request.content = b"nobinarysupport" result = "curl -X POST http://address:22/path -d nobinarysupport" assert export_curl(post_request) == result @@ -88,14 +90,10 @@ def test_tcp(self, export_curl, tcp_flow): def test_escape_single_quotes_in_body(self, export_curl): request = tflow.tflow( - req=tutils.treq( - method=b'POST', - headers=(), - content=b"'&#" - ) + req=tutils.treq(method=b"POST", headers=(), content=b"'&#") ) command = export_curl(request) - assert shlex.split(command)[-2] == '-d' + assert shlex.split(command)[-2] == "-d" assert shlex.split(command)[-1] == "'&#" def test_strip_unnecessary(self, export_curl, get_request): @@ -121,18 +119,22 @@ def test_correct_host_used(self, get_request): assert export.curl_command(get_request) == result tctx.options.export_preserve_original_ip = True - result = """curl --resolve 'domain:22:[192.168.0.1]' -H 'header: qvalue' -H 'host: domain:22' """ \ - """'http://domain:22/path?a=foo&a=bar&b=baz'""" + result = ( + """curl --resolve 'domain:22:[192.168.0.1]' -H 'header: qvalue' -H 'host: domain:22' """ + """'http://domain:22/path?a=foo&a=bar&b=baz'""" + ) assert export.curl_command(get_request) == result class TestExportHttpieCommand: def test_get(self, get_request): - result = """http GET 'http://address:22/path?a=foo&a=bar&b=baz' 'header: qvalue'""" + result = ( + """http GET 'http://address:22/path?a=foo&a=bar&b=baz' 'header: qvalue'""" + ) assert export.httpie_command(get_request) == result def test_post(self, post_request): - post_request.request.content = b'nobinarysupport' + post_request.request.content = b"nobinarysupport" result = "http POST http://address:22/path <<< nobinarysupport" assert export.httpie_command(post_request) == result @@ -153,14 +155,10 @@ def test_tcp(self, tcp_flow): def test_escape_single_quotes_in_body(self): request = tflow.tflow( - req=tutils.treq( - method=b'POST', - headers=(), - content=b"'&#" - ) + req=tutils.treq(method=b"POST", headers=(), content=b"'&#") ) command = export.httpie_command(request) - assert shlex.split(command)[-2] == '<<<' + assert shlex.split(command)[-2] == "<<<" assert shlex.split(command)[-1] == "'&#" # See comment in `TestExportCurlCommand.test_correct_host_used`. httpie @@ -172,8 +170,10 @@ def test_escape_single_quotes_in_body(self): def test_correct_host_used(self, get_request): get_request.request.headers["host"] = "domain:22" - result = """http GET 'http://domain:22/path?a=foo&a=bar&b=baz' """ \ - """'header: qvalue' 'host: domain:22'""" + result = ( + """http GET 'http://domain:22/path?a=foo&a=bar&b=baz' """ + """'header: qvalue' 'host: domain:22'""" + ) assert export.httpie_command(get_request) == result @@ -191,7 +191,10 @@ def test_get_response_present(self, get_response): assert b"header-response: svalue" in export.raw(get_response) def test_tcp(self, tcp_flow): - with pytest.raises(exceptions.CommandError, match="Can't export flow with no request or response"): + with pytest.raises( + exceptions.CommandError, + match="Can't export flow with no request or response", + ): export.raw(tcp_flow) @@ -256,12 +259,14 @@ def test_export(tmp_path) -> None: os.unlink(f) -@pytest.mark.parametrize("exception, log_message", [ - (PermissionError, "Permission denied"), - (IsADirectoryError, "Is a directory"), - (FileNotFoundError, "No such file or directory") -]) -@pytest.mark.asyncio +@pytest.mark.parametrize( + "exception, log_message", + [ + (PermissionError, "Permission denied"), + (IsADirectoryError, "Is a directory"), + (FileNotFoundError, "No such file or directory"), + ], +) async def test_export_open(exception, log_message, tmpdir): f = str(tmpdir.join("path")) e = export.Export() @@ -272,7 +277,6 @@ async def test_export_open(exception, log_message, tmpdir): await tctx.master.await_log(log_message, level="error") -@pytest.mark.asyncio async def test_clip(tmpdir): e = export.Export() with taddons.context() as tctx: @@ -281,25 +285,26 @@ async def test_clip(tmpdir): with pytest.raises(exceptions.CommandError): e.clip("nonexistent", tflow.tflow(resp=True)) - with mock.patch('pyperclip.copy') as pc: + with mock.patch("pyperclip.copy") as pc: e.clip("raw_request", tflow.tflow(resp=True)) assert pc.called - with mock.patch('pyperclip.copy') as pc: + with mock.patch("pyperclip.copy") as pc: e.clip("raw_response", tflow.tflow(resp=True)) assert pc.called - with mock.patch('pyperclip.copy') as pc: + with mock.patch("pyperclip.copy") as pc: e.clip("curl", tflow.tflow(resp=True)) assert pc.called - with mock.patch('pyperclip.copy') as pc: + with mock.patch("pyperclip.copy") as pc: e.clip("httpie", tflow.tflow(resp=True)) assert pc.called - with mock.patch('pyperclip.copy') as pc: - log_message = "Pyperclip could not find a " \ - "copy/paste mechanism for your system." + with mock.patch("pyperclip.copy") as pc: + log_message = ( + "Pyperclip could not find a " "copy/paste mechanism for your system." + ) pc.side_effect = pyperclip.PyperclipException(log_message) e.clip("raw_request", tflow.tflow(resp=True)) await tctx.master.await_log(log_message, level="error") diff --git a/test/mitmproxy/addons/test_intercept.py b/test/mitmproxy/addons/test_intercept.py index aad4e2c6f2..60008ddf9a 100644 --- a/test/mitmproxy/addons/test_intercept.py +++ b/test/mitmproxy/addons/test_intercept.py @@ -2,12 +2,11 @@ from mitmproxy.addons import intercept from mitmproxy import exceptions -from mitmproxy.proxy import layers from mitmproxy.test import taddons from mitmproxy.test import tflow -def test_simple(): +async def test_simple(): r = intercept.Intercept() with taddons.context(r) as tctx: assert not r.filt @@ -23,11 +22,11 @@ def test_simple(): tctx.configure(r, intercept="~s") f = tflow.tflow(resp=True) - tctx.cycle(r, f) + await tctx.cycle(r, f) assert f.intercepted f = tflow.tflow(resp=False) - tctx.cycle(r, f) + await tctx.cycle(r, f) assert not f.intercepted f = tflow.tflow(resp=True) @@ -36,39 +35,43 @@ def test_simple(): tctx.configure(r, intercept_active=False) f = tflow.tflow(resp=True) - tctx.cycle(r, f) + await tctx.cycle(r, f) assert not f.intercepted tctx.configure(r, intercept_active=True) f = tflow.tflow(resp=True) - tctx.cycle(r, f) + await tctx.cycle(r, f) assert f.intercepted -def test_tcp(): +async def test_dns(): r = intercept.Intercept() with taddons.context(r) as tctx: - tctx.configure(r, intercept="~tcp") - f = tflow.ttcpflow() - tctx.cycle(r, f) + tctx.configure(r, intercept="~s ~dns") + + f = tflow.tdnsflow(resp=True) + await tctx.cycle(r, f) assert f.intercepted + f = tflow.tdnsflow(resp=False) + await tctx.cycle(r, f) + assert not f.intercepted + tctx.configure(r, intercept_active=False) - f = tflow.ttcpflow() - tctx.cycle(r, f) + f = tflow.tdnsflow(resp=True) + await tctx.cycle(r, f) assert not f.intercepted -def test_already_taken(): +async def test_tcp(): r = intercept.Intercept() with taddons.context(r) as tctx: - tctx.configure(r, intercept="~q") - - f = tflow.tflow() - tctx.invoke(r, layers.http.HttpRequestHook(f)) + tctx.configure(r, intercept="~tcp") + f = tflow.ttcpflow() + await tctx.cycle(r, f) assert f.intercepted - f = tflow.tflow() - f.reply.take() - tctx.invoke(r, layers.http.HttpRequestHook(f)) + tctx.configure(r, intercept_active=False) + f = tflow.ttcpflow() + await tctx.cycle(r, f) assert not f.intercepted diff --git a/test/mitmproxy/addons/test_keepserving.py b/test/mitmproxy/addons/test_keepserving.py index 01b0d09c27..99459bf37b 100644 --- a/test/mitmproxy/addons/test_keepserving.py +++ b/test/mitmproxy/addons/test_keepserving.py @@ -1,5 +1,4 @@ import asyncio -import pytest from mitmproxy.addons import keepserving from mitmproxy.test import taddons @@ -35,7 +34,6 @@ def shutdown(self): self.is_shutdown = True -@pytest.mark.asyncio async def test_keepserving(): ks = TKS() d = Dummy(True) diff --git a/test/mitmproxy/addons/test_maplocal.py b/test/mitmproxy/addons/test_maplocal.py index 7e773a3214..fd0de25140 100644 --- a/test/mitmproxy/addons/test_maplocal.py +++ b/test/mitmproxy/addons/test_maplocal.py @@ -16,62 +16,109 @@ ("https://example.com/foo", ":example.com/foo:/tmp", ["/tmp/index.html"]), ("https://example.com/foo/", ":example.com/foo:/tmp", ["/tmp/index.html"]), ("https://example.com/foo", ":example.com/foo:/tmp/", ["/tmp/index.html"]), - ] + [ + ] + + [ # simple prefixes - ("http://example.com/foo/bar.jpg", ":example.com/foo:/tmp", ["/tmp/bar.jpg", "/tmp/bar.jpg/index.html"]), - ("https://example.com/foo/bar.jpg", ":example.com/foo:/tmp", ["/tmp/bar.jpg", "/tmp/bar.jpg/index.html"]), - ("https://example.com/foo/bar.jpg?query", ":example.com/foo:/tmp", ["/tmp/bar.jpg", "/tmp/bar.jpg/index.html"]), - ("https://example.com/foo/bar/baz.jpg", ":example.com/foo:/tmp", - ["/tmp/bar/baz.jpg", "/tmp/bar/baz.jpg/index.html"]), + ( + "http://example.com/foo/bar.jpg", + ":example.com/foo:/tmp", + ["/tmp/bar.jpg", "/tmp/bar.jpg/index.html"], + ), + ( + "https://example.com/foo/bar.jpg", + ":example.com/foo:/tmp", + ["/tmp/bar.jpg", "/tmp/bar.jpg/index.html"], + ), + ( + "https://example.com/foo/bar.jpg?query", + ":example.com/foo:/tmp", + ["/tmp/bar.jpg", "/tmp/bar.jpg/index.html"], + ), + ( + "https://example.com/foo/bar/baz.jpg", + ":example.com/foo:/tmp", + ["/tmp/bar/baz.jpg", "/tmp/bar/baz.jpg/index.html"], + ), ("https://example.com/foo/bar.jpg", ":/foo/bar.jpg:/tmp", ["/tmp/index.html"]), - ] + [ + ] + + [ # URL decode and special characters - ("http://example.com/foo%20bar.jpg", ":example.com:/tmp", [ - "/tmp/foo bar.jpg", - "/tmp/foo bar.jpg/index.html", - "/tmp/foo_bar.jpg", - "/tmp/foo_bar.jpg/index.html" - ]), - ("http://example.com/fóobår.jpg", ":example.com:/tmp", [ - "/tmp/fóobår.jpg", - "/tmp/fóobår.jpg/index.html", - "/tmp/f_ob_r.jpg", - "/tmp/f_ob_r.jpg/index.html" - ]), - ] + [ + ( + "http://example.com/foo%20bar.jpg", + ":example.com:/tmp", + [ + "/tmp/foo bar.jpg", + "/tmp/foo bar.jpg/index.html", + "/tmp/foo_bar.jpg", + "/tmp/foo_bar.jpg/index.html", + ], + ), + ( + "http://example.com/fóobår.jpg", + ":example.com:/tmp", + [ + "/tmp/fóobår.jpg", + "/tmp/fóobår.jpg/index.html", + "/tmp/f_ob_r.jpg", + "/tmp/f_ob_r.jpg/index.html", + ], + ), + ] + + [ # index.html ("https://example.com/foo", ":example.com/foo:/tmp", ["/tmp/index.html"]), ("https://example.com/foo/", ":example.com/foo:/tmp", ["/tmp/index.html"]), - ("https://example.com/foo/bar", ":example.com/foo:/tmp", ["/tmp/bar", "/tmp/bar/index.html"]), - ("https://example.com/foo/bar/", ":example.com/foo:/tmp", ["/tmp/bar", "/tmp/bar/index.html"]), - ] + [ + ( + "https://example.com/foo/bar", + ":example.com/foo:/tmp", + ["/tmp/bar", "/tmp/bar/index.html"], + ), + ( + "https://example.com/foo/bar/", + ":example.com/foo:/tmp", + ["/tmp/bar", "/tmp/bar/index.html"], + ), + ] + + [ # regex ( - "https://example/view.php?f=foo.jpg", - ":example/view.php\\?f=(.+):/tmp", - ["/tmp/foo.jpg", "/tmp/foo.jpg/index.html"] - ), ( - "https://example/results?id=1&foo=2", - ":example/(results\\?id=.+):/tmp", - [ - "/tmp/results?id=1&foo=2", - "/tmp/results?id=1&foo=2/index.html", - "/tmp/results_id=1_foo=2", - "/tmp/results_id=1_foo=2/index.html" - ] + "https://example/view.php?f=foo.jpg", + ":example/view.php\\?f=(.+):/tmp", + ["/tmp/foo.jpg", "/tmp/foo.jpg/index.html"], ), - ] + [ + ( + "https://example/results?id=1&foo=2", + ":example/(results\\?id=.+):/tmp", + [ + "/tmp/results?id=1&foo=2", + "/tmp/results?id=1&foo=2/index.html", + "/tmp/results_id=1_foo=2", + "/tmp/results_id=1_foo=2/index.html", + ], + ), + ] + + [ # test directory traversal detection ("https://example.com/../../../../../../etc/passwd", ":example.com:/tmp", []), # this is slightly hacky, but werkzeug's behavior differs per system. - ("https://example.com/C:\\foo.txt", ":example.com:/tmp", [] if sys.platform == "win32" else [ - "/tmp/C:\\foo.txt", - "/tmp/C:\\foo.txt/index.html", - "/tmp/C__foo.txt", - "/tmp/C__foo.txt/index.html" - ]), - ("https://example.com//etc/passwd", ":example.com:/tmp", ["/tmp/etc/passwd", "/tmp/etc/passwd/index.html"]), - ] + ( + "https://example.com/C:\\foo.txt", + ":example.com:/tmp", + [] + if sys.platform == "win32" + else [ + "/tmp/C:\\foo.txt", + "/tmp/C:\\foo.txt/index.html", + "/tmp/C__foo.txt", + "/tmp/C__foo.txt/index.html", + ], + ), + ( + "https://example.com//etc/passwd", + ":example.com:/tmp", + ["/tmp/etc/passwd", "/tmp/etc/passwd/index.html"], + ), + ], ) def test_file_candidates(url, spec, expected_candidates): # we circumvent the path existence checks here to simplify testing @@ -83,7 +130,6 @@ def test_file_candidates(url, spec, expected_candidates): class TestMapLocal: - def test_configure(self, tmpdir): ml = MapLocal() with taddons.context(ml) as tctx: @@ -99,12 +145,7 @@ def test_simple(self, tmpdir): with taddons.context(ml) as tctx: tmpfile = tmpdir.join("foo.jpg") tmpfile.write("foo") - tctx.configure( - ml, - map_local=[ - "|//example.org/images|" + str(tmpdir) - ] - ) + tctx.configure(ml, map_local=["|//example.org/images|" + str(tmpdir)]) f = tflow.tflow() f.request.url = b"https://example.org/images/foo.jpg" ml.request(f) @@ -112,12 +153,7 @@ def test_simple(self, tmpdir): tmpfile = tmpdir.join("images", "bar.jpg") tmpfile.write("bar", ensure=True) - tctx.configure( - ml, - map_local=[ - "|//example.org|" + str(tmpdir) - ] - ) + tctx.configure(ml, map_local=["|//example.org|" + str(tmpdir)]) f = tflow.tflow() f.request.url = b"https://example.org/images/bar.jpg" ml.request(f) @@ -126,27 +162,18 @@ def test_simple(self, tmpdir): tmpfile = tmpdir.join("foofoobar.jpg") tmpfile.write("foofoobar", ensure=True) tctx.configure( - ml, - map_local=[ - "|example.org/foo/foo/bar.jpg|" + str(tmpfile) - ] + ml, map_local=["|example.org/foo/foo/bar.jpg|" + str(tmpfile)] ) f = tflow.tflow() f.request.url = b"https://example.org/foo/foo/bar.jpg" ml.request(f) assert f.response.content == b"foofoobar" - @pytest.mark.asyncio async def test_nonexistent_files(self, tmpdir, monkeypatch): ml = MapLocal() with taddons.context(ml) as tctx: - tctx.configure( - ml, - map_local=[ - "|example.org/css|" + str(tmpdir) - ] - ) + tctx.configure(ml, map_local=["|example.org/css|" + str(tmpdir)]) f = tflow.tflow() f.request.url = b"https://example.org/css/nonexistent" ml.request(f) @@ -155,12 +182,7 @@ async def test_nonexistent_files(self, tmpdir, monkeypatch): tmpfile = tmpdir.join("foo.jpg") tmpfile.write("foo") - tctx.configure( - ml, - map_local=[ - "|//example.org/images|" + str(tmpfile) - ] - ) + tctx.configure(ml, map_local=["|//example.org/images|" + str(tmpfile)]) tmpfile.remove() monkeypatch.setattr(Path, "is_file", lambda x: True) f = tflow.tflow() @@ -168,19 +190,14 @@ async def test_nonexistent_files(self, tmpdir, monkeypatch): ml.request(f) await tctx.master.await_log("could not read file") - def test_has_reply(self, tmpdir): + def test_is_killed(self, tmpdir): ml = MapLocal() with taddons.context(ml) as tctx: tmpfile = tmpdir.join("foo.jpg") tmpfile.write("foo") - tctx.configure( - ml, - map_local=[ - "|//example.org/images|" + str(tmpfile) - ] - ) + tctx.configure(ml, map_local=["|//example.org/images|" + str(tmpfile)]) f = tflow.tflow() f.request.url = b"https://example.org/images/foo.jpg" - f.reply.take() + f.kill() ml.request(f) assert not f.response diff --git a/test/mitmproxy/addons/test_mapremote.py b/test/mitmproxy/addons/test_mapremote.py index d3f4beb32a..2aff3468d9 100644 --- a/test/mitmproxy/addons/test_mapremote.py +++ b/test/mitmproxy/addons/test_mapremote.py @@ -6,7 +6,6 @@ class TestMapRemote: - def test_configure(self): mr = mapremote.MapRemote() with taddons.context(mr) as tctx: @@ -21,19 +20,19 @@ def test_simple(self): mr, map_remote=[ ":example.org/images/:mitmproxy.org/img/", - ] + ], ) f = tflow.tflow() f.request.url = b"https://example.org/images/test.jpg" mr.request(f) assert f.request.url == "https://mitmproxy.org/img/test.jpg" - def test_has_reply(self): + def test_is_killed(self): mr = mapremote.MapRemote() with taddons.context(mr) as tctx: tctx.configure(mr, map_remote=[":example.org:mitmproxy.org"]) f = tflow.tflow() f.request.url = b"https://example.org/images/test.jpg" - f.reply.take() + f.kill() mr.request(f) assert f.request.url == "https://example.org/images/test.jpg" diff --git a/test/mitmproxy/addons/test_modifybody.py b/test/mitmproxy/addons/test_modifybody.py index e82bb1e98c..05388e1d5c 100644 --- a/test/mitmproxy/addons/test_modifybody.py +++ b/test/mitmproxy/addons/test_modifybody.py @@ -3,6 +3,7 @@ from mitmproxy.addons import modifybody from mitmproxy.test import taddons from mitmproxy.test import tflow +from mitmproxy.test.tutils import tresp class TestModifyBody: @@ -21,7 +22,7 @@ def test_simple(self): modify_body=[ "/~q/foo/bar", "/~s/foo/bar", - ] + ], ) f = tflow.tflow() f.request.content = b"foo" @@ -41,14 +42,14 @@ def test_taken(self, take): f = tflow.tflow() f.request.content = b"foo" if take: - f.reply.take() + f.response = tresp() mb.request(f) assert (f.request.content == b"bar") ^ take f = tflow.tflow(resp=True) f.response.content = b"foo" if take: - f.reply.take() + f.kill() mb.response(f) assert (f.response.content == b"bar") ^ take @@ -62,7 +63,7 @@ def test_order(self): "/bar/baz", "/foo/oh noes!", "/bar/oh noes!", - ] + ], ) f = tflow.tflow() f.request.content = b"foo" @@ -76,31 +77,21 @@ def test_simple(self, tmpdir): with taddons.context(mb) as tctx: tmpfile = tmpdir.join("replacement") tmpfile.write("bar") - tctx.configure( - mb, - modify_body=["/~q/foo/@" + str(tmpfile)] - ) + tctx.configure(mb, modify_body=["/~q/foo/@" + str(tmpfile)]) f = tflow.tflow() f.request.content = b"foo" mb.request(f) assert f.request.content == b"bar" - @pytest.mark.asyncio async def test_nonexistent(self, tmpdir): mb = modifybody.ModifyBody() with taddons.context(mb) as tctx: with pytest.raises(Exception, match="Invalid file path"): - tctx.configure( - mb, - modify_body=["/~q/foo/@nonexistent"] - ) + tctx.configure(mb, modify_body=["/~q/foo/@nonexistent"]) tmpfile = tmpdir.join("replacement") tmpfile.write("bar") - tctx.configure( - mb, - modify_body=["/~q/foo/@" + str(tmpfile)] - ) + tctx.configure(mb, modify_body=["/~q/foo/@" + str(tmpfile)]) tmpfile.remove() f = tflow.tflow() f.request.content = b"foo" diff --git a/test/mitmproxy/addons/test_modifyheaders.py b/test/mitmproxy/addons/test_modifyheaders.py index 8718510ae0..6a078488a6 100644 --- a/test/mitmproxy/addons/test_modifyheaders.py +++ b/test/mitmproxy/addons/test_modifyheaders.py @@ -3,6 +3,7 @@ from mitmproxy.addons.modifyheaders import parse_modify_spec, ModifyHeaders from mitmproxy.test import taddons from mitmproxy.test import tflow +from mitmproxy.test.tutils import tresp def test_parse_modify_spec(): @@ -26,7 +27,6 @@ def test_parse_modify_spec(): class TestModifyHeaders: - def test_configure(self): mh = ModifyHeaders() with taddons.context(mh) as tctx: @@ -37,13 +37,7 @@ def test_configure(self): def test_modify_headers(self): mh = ModifyHeaders() with taddons.context(mh) as tctx: - tctx.configure( - mh, - modify_headers=[ - "/~q/one/two", - "/~s/one/three" - ] - ) + tctx.configure(mh, modify_headers=["/~q/one/two", "/~s/one/three"]) f = tflow.tflow() f.request.headers["one"] = "xxx" mh.request(f) @@ -54,39 +48,21 @@ def test_modify_headers(self): mh.response(f) assert f.response.headers["one"] == "three" - tctx.configure( - mh, - modify_headers=[ - "/~s/one/two", - "/~s/one/three" - ] - ) + tctx.configure(mh, modify_headers=["/~s/one/two", "/~s/one/three"]) f = tflow.tflow(resp=True) f.request.headers["one"] = "xxx" f.response.headers["one"] = "xxx" mh.response(f) assert f.response.headers.get_all("one") == ["two", "three"] - tctx.configure( - mh, - modify_headers=[ - "/~q/one/two", - "/~q/one/three" - ] - ) + tctx.configure(mh, modify_headers=["/~q/one/two", "/~q/one/three"]) f = tflow.tflow() f.request.headers["one"] = "xxx" mh.request(f) assert f.request.headers.get_all("one") == ["two", "three"] # test removal of existing headers - tctx.configure( - mh, - modify_headers=[ - "/~q/one/", - "/~s/one/" - ] - ) + tctx.configure(mh, modify_headers=["/~q/one/", "/~s/one/"]) f = tflow.tflow() f.request.headers["one"] = "xxx" mh.request(f) @@ -97,12 +73,7 @@ def test_modify_headers(self): mh.response(f) assert "one" not in f.response.headers - tctx.configure( - mh, - modify_headers=[ - "/one/" - ] - ) + tctx.configure(mh, modify_headers=["/one/"]) f = tflow.tflow() f.request.headers["one"] = "xxx" mh.request(f) @@ -119,7 +90,7 @@ def test_modify_headers(self): mh, modify_headers=[ "/~hq ^user-agent:.+Mozilla.+$/user-agent/Definitely not Mozilla ;)" - ] + ], ) f = tflow.tflow() f.request.headers["user-agent"] = "Hello, it's me, Mozilla" @@ -133,13 +104,13 @@ def test_taken(self, take): tctx.configure(mh, modify_headers=["/content-length/42"]) f = tflow.tflow() if take: - f.reply.take() + f.response = tresp() mh.request(f) assert (f.request.headers["content-length"] == "42") ^ take f = tflow.tflow(resp=True) if take: - f.reply.take() + f.kill() mh.response(f) assert (f.response.headers["content-length"] == "42") ^ take @@ -150,31 +121,23 @@ def test_simple(self, tmpdir): with taddons.context(mh) as tctx: tmpfile = tmpdir.join("replacement") tmpfile.write("two") - tctx.configure( - mh, - modify_headers=["/~q/one/@" + str(tmpfile)] - ) + tctx.configure(mh, modify_headers=["/~q/one/@" + str(tmpfile)]) f = tflow.tflow() f.request.headers["one"] = "xxx" mh.request(f) assert f.request.headers["one"] == "two" - @pytest.mark.asyncio async def test_nonexistent(self, tmpdir): mh = ModifyHeaders() with taddons.context(mh) as tctx: - with pytest.raises(Exception, match="Cannot parse modify_headers .* Invalid file path"): - tctx.configure( - mh, - modify_headers=["/~q/foo/@nonexistent"] - ) + with pytest.raises( + Exception, match="Cannot parse modify_headers .* Invalid file path" + ): + tctx.configure(mh, modify_headers=["/~q/foo/@nonexistent"]) tmpfile = tmpdir.join("replacement") tmpfile.write("bar") - tctx.configure( - mh, - modify_headers=["/~q/foo/@" + str(tmpfile)] - ) + tctx.configure(mh, modify_headers=["/~q/foo/@" + str(tmpfile)]) tmpfile.remove() f = tflow.tflow() f.request.content = b"foo" diff --git a/test/mitmproxy/addons/test_next_layer.py b/test/mitmproxy/addons/test_next_layer.py index 2999d4a890..534fbd6fd6 100644 --- a/test/mitmproxy/addons/test_next_layer.py +++ b/test/mitmproxy/addons/test_next_layer.py @@ -11,7 +11,10 @@ @pytest.fixture def tctx(): - context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) + context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) client_hello_no_extensions = bytes.fromhex( @@ -33,12 +36,13 @@ def tctx(): class TestNextLayer: - def test_configure(self): nl = NextLayer() with taddons.context(nl) as tctx: with pytest.raises(Exception, match="mutually exclusive"): - tctx.configure(nl, allow_hosts=["example.org"], ignore_hosts=["example.com"]) + tctx.configure( + nl, allow_hosts=["example.org"], ignore_hosts=["example.com"] + ) def test_ignore_connection(self): nl = NextLayer() @@ -54,7 +58,12 @@ def test_ignore_connection(self): assert nl.ignore_connection(None, client_hello_with_extensions) assert nl.ignore_connection(None, client_hello_with_extensions[:-5]) is None # invalid clienthello - assert nl.ignore_connection(None, client_hello_no_extensions[:9] + b"\x00" * 200) is False + assert ( + nl.ignore_connection( + None, client_hello_no_extensions[:9] + b"\x00" * 200 + ) + is False + ) # different server name and SNI assert nl.ignore_connection(("decoy", 1234), client_hello_with_extensions) @@ -62,7 +71,10 @@ def test_ignore_connection(self): assert nl.ignore_connection(("example.com", 443), b"") is False assert nl.ignore_connection(("example.org", 443), b"") # different server name and SNI - assert nl.ignore_connection(("decoy", 1234), client_hello_with_extensions) is False + assert ( + nl.ignore_connection(("decoy", 1234), client_hello_with_extensions) + is False + ) def test_make_top_layer(self): nl = NextLayer() @@ -96,22 +108,34 @@ def test_next_layer(self): assert nl._next_layer(ctx, client_hello_no_extensions[:10], b"") is None tctx.configure(nl, ignore_hosts=[]) - assert isinstance(nl._next_layer(ctx, client_hello_no_extensions, b""), layers.ServerTLSLayer) + assert isinstance( + nl._next_layer(ctx, client_hello_no_extensions, b""), + layers.ServerTLSLayer, + ) assert isinstance(ctx.layers[-1], layers.ClientTLSLayer) ctx.layers = [] assert isinstance(nl._next_layer(ctx, b"", b""), layers.modes.HttpProxy) - assert isinstance(nl._next_layer(ctx, client_hello_no_extensions, b""), layers.ClientTLSLayer) + assert isinstance( + nl._next_layer(ctx, client_hello_no_extensions, b""), + layers.ClientTLSLayer, + ) ctx.layers = [] assert isinstance(nl._next_layer(ctx, b"", b""), layers.modes.HttpProxy) - assert isinstance(nl._next_layer(ctx, b"GET http://example.com/ HTTP/1.1\r\n", b""), layers.HttpLayer) + assert isinstance( + nl._next_layer(ctx, b"GET http://example.com/ HTTP/1.1\r\n", b""), + layers.HttpLayer, + ) assert ctx.layers[-1].mode == HTTPMode.regular ctx.layers = [] tctx.configure(nl, mode="upstream:http://localhost:8081") assert isinstance(nl._next_layer(ctx, b"", b""), layers.modes.HttpProxy) - assert isinstance(nl._next_layer(ctx, b"GET http://example.com/ HTTP/1.1\r\n", b""), layers.HttpLayer) + assert isinstance( + nl._next_layer(ctx, b"GET http://example.com/ HTTP/1.1\r\n", b""), + layers.HttpLayer, + ) assert ctx.layers[-1].mode == HTTPMode.upstream tctx.configure(nl, tcp_hosts=["example.com"]) diff --git a/test/mitmproxy/addons/test_onboarding.py b/test/mitmproxy/addons/test_onboarding.py index 7a309700d6..dc90e4073c 100644 --- a/test/mitmproxy/addons/test_onboarding.py +++ b/test/mitmproxy/addons/test_onboarding.py @@ -14,7 +14,6 @@ class TestApp: def addons(self): return [onboarding.Onboarding()] - @pytest.mark.asyncio async def test_basic(self, client): ob = onboarding.Onboarding() with taddons.context(ob) as tctx: @@ -22,7 +21,6 @@ async def test_basic(self, client): assert client.get("/").status_code == 200 @pytest.mark.parametrize("ext", ["pem", "p12", "cer"]) - @pytest.mark.asyncio async def test_cert(self, client, ext, tdata): ob = onboarding.Onboarding() with taddons.context(ob) as tctx: @@ -32,7 +30,6 @@ async def test_cert(self, client, ext, tdata): assert resp.data @pytest.mark.parametrize("ext", ["pem", "p12", "cer"]) - @pytest.mark.asyncio async def test_head(self, client, ext, tdata): ob = onboarding.Onboarding() with taddons.context(ob) as tctx: diff --git a/test/mitmproxy/addons/test_proxyauth.py b/test/mitmproxy/addons/test_proxyauth.py index 6182cdf997..a27ccd4671 100644 --- a/test/mitmproxy/addons/test_proxyauth.py +++ b/test/mitmproxy/addons/test_proxyauth.py @@ -13,24 +13,33 @@ class TestMkauth: def test_mkauth_scheme(self): - assert proxyauth.mkauth('username', 'password') == 'basic dXNlcm5hbWU6cGFzc3dvcmQ=\n' - - @pytest.mark.parametrize('scheme, expected', [ - ('', ' dXNlcm5hbWU6cGFzc3dvcmQ=\n'), - ('basic', 'basic dXNlcm5hbWU6cGFzc3dvcmQ=\n'), - ('foobar', 'foobar dXNlcm5hbWU6cGFzc3dvcmQ=\n'), - ]) + assert ( + proxyauth.mkauth("username", "password") + == "basic dXNlcm5hbWU6cGFzc3dvcmQ=\n" + ) + + @pytest.mark.parametrize( + "scheme, expected", + [ + ("", " dXNlcm5hbWU6cGFzc3dvcmQ=\n"), + ("basic", "basic dXNlcm5hbWU6cGFzc3dvcmQ=\n"), + ("foobar", "foobar dXNlcm5hbWU6cGFzc3dvcmQ=\n"), + ], + ) def test_mkauth(self, scheme, expected): - assert proxyauth.mkauth('username', 'password', scheme) == expected + assert proxyauth.mkauth("username", "password", scheme) == expected class TestParseHttpBasicAuth: - @pytest.mark.parametrize('input', [ - '', - 'foo bar', - 'basic abc', - 'basic ' + binascii.b2a_base64(b"foo").decode("ascii"), - ]) + @pytest.mark.parametrize( + "input", + [ + "", + "foo bar", + "basic abc", + "basic " + binascii.b2a_base64(b"foo").decode("ascii"), + ], + ) def test_parse_http_basic_auth_error(self, input): with pytest.raises(ValueError): proxyauth.parse_http_basic_auth(input) @@ -41,35 +50,50 @@ def test_parse_http_basic_auth(self): class TestProxyAuth: - @pytest.mark.parametrize('mode, expected', [ - ('', False), - ('foobar', False), - ('regular', True), - ('upstream:', True), - ('upstream:foobar', True), - ]) + @pytest.mark.parametrize( + "mode, expected", + [ + ("", False), + ("foobar", False), + ("regular", True), + ("upstream:", True), + ("upstream:foobar", True), + ], + ) def test_is_http_proxy(self, mode, expected): up = proxyauth.ProxyAuth() with taddons.context(up, loadcore=False) as ctx: ctx.options.mode = mode assert up.is_http_proxy is expected - @pytest.mark.parametrize('is_http_proxy, expected', [ - (True, 'Proxy-Authorization'), - (False, 'Authorization'), - ]) + @pytest.mark.parametrize( + "is_http_proxy, expected", + [ + (True, "Proxy-Authorization"), + (False, "Authorization"), + ], + ) def test_which_auth_header(self, is_http_proxy, expected): up = proxyauth.ProxyAuth() - with mock.patch('mitmproxy.addons.proxyauth.ProxyAuth.is_http_proxy', new=is_http_proxy): + with mock.patch( + "mitmproxy.addons.proxyauth.ProxyAuth.is_http_proxy", new=is_http_proxy + ): assert up.http_auth_header == expected - @pytest.mark.parametrize('is_http_proxy, expected_status_code, expected_header', [ - (True, 407, 'Proxy-Authenticate'), - (False, 401, 'WWW-Authenticate'), - ]) - def test_auth_required_response(self, is_http_proxy, expected_status_code, expected_header): + @pytest.mark.parametrize( + "is_http_proxy, expected_status_code, expected_header", + [ + (True, 407, "Proxy-Authenticate"), + (False, 401, "WWW-Authenticate"), + ], + ) + def test_auth_required_response( + self, is_http_proxy, expected_status_code, expected_header + ): up = proxyauth.ProxyAuth() - with mock.patch('mitmproxy.addons.proxyauth.ProxyAuth.is_http_proxy', new=is_http_proxy): + with mock.patch( + "mitmproxy.addons.proxyauth.ProxyAuth.is_http_proxy", new=is_http_proxy + ): resp = up.make_auth_required_response() assert resp.status_code == expected_status_code assert expected_header in resp.headers.keys() @@ -96,9 +120,7 @@ def test_authenticate(self): assert f.response.status_code == 407 f = tflow.tflow() - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "test" - ) + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth("test", "test") up.authenticate_http(f) assert not f.response assert not f.request.headers.get("Proxy-Authorization") @@ -110,9 +132,7 @@ def test_authenticate(self): assert f.response.status_code == 401 f = tflow.tflow() - f.request.headers["Authorization"] = proxyauth.mkauth( - "test", "test" - ) + f.request.headers["Authorization"] = proxyauth.mkauth("test", "test") up.authenticate_http(f) assert not f.response assert not f.request.headers.get("Authorization") @@ -123,7 +143,9 @@ def test_configure(self, monkeypatch, tdata): pa = proxyauth.ProxyAuth() with taddons.context(pa) as ctx: - with pytest.raises(exceptions.OptionsError, match="Invalid proxyauth specification"): + with pytest.raises( + exceptions.OptionsError, match="Invalid proxyauth specification" + ): ctx.configure(pa, proxyauth="foo") ctx.configure(pa, proxyauth="foo:bar") @@ -131,7 +153,9 @@ def test_configure(self, monkeypatch, tdata): assert pa.validator("foo", "bar") assert not pa.validator("foo", "baz") - with pytest.raises(exceptions.OptionsError, match="Invalid single-user auth specification."): + with pytest.raises( + exceptions.OptionsError, match="Invalid single-user auth specification." + ): ctx.configure(pa, proxyauth="foo:bar:baz") ctx.configure(pa, proxyauth="any") @@ -143,22 +167,42 @@ def test_configure(self, monkeypatch, tdata): ctx.configure( pa, - proxyauth="ldap:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com" + proxyauth="ldap:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com", ) assert isinstance(pa.validator, proxyauth.Ldap) - with pytest.raises(exceptions.OptionsError, match="Invalid ldap specification"): + ctx.configure( + pa, + proxyauth="ldap:localhost:1234:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com", + ) + assert isinstance(pa.validator, proxyauth.Ldap) + + with pytest.raises( + exceptions.OptionsError, match="Invalid LDAP specification" + ): ctx.configure(pa, proxyauth="ldap:test:test:test") - with pytest.raises(exceptions.OptionsError, match="Invalid ldap specification"): - ctx.configure(pa, proxyauth="ldap:fake_serveruid=?dc=example,dc=com:person") + with pytest.raises( + exceptions.OptionsError, match="Invalid LDAP specification" + ): + ctx.configure( + pa, proxyauth="ldap:fake_serveruid=?dc=example,dc=com:person" + ) - with pytest.raises(exceptions.OptionsError, match="Invalid ldap specification"): + with pytest.raises( + exceptions.OptionsError, match="Invalid LDAP specification" + ): ctx.configure(pa, proxyauth="ldapssssssss:fake_server:dn:password:tree") - with pytest.raises(exceptions.OptionsError, match="Could not open htpasswd file"): - ctx.configure(pa, proxyauth="@" + tdata.path("mitmproxy/net/data/server.crt")) - with pytest.raises(exceptions.OptionsError, match="Could not open htpasswd file"): + with pytest.raises( + exceptions.OptionsError, match="Could not open htpasswd file" + ): + ctx.configure( + pa, proxyauth="@" + tdata.path("mitmproxy/net/data/server.crt") + ) + with pytest.raises( + exceptions.OptionsError, match="Could not open htpasswd file" + ): ctx.configure(pa, proxyauth="@nonexistent") ctx.configure(pa, proxyauth="@" + tdata.path("mitmproxy/net/data/htpasswd")) @@ -166,8 +210,10 @@ def test_configure(self, monkeypatch, tdata): assert pa.validator("test", "test") assert not pa.validator("test", "foo") - with pytest.raises(exceptions.OptionsError, - match="Proxy Authentication not supported in transparent mode."): + with pytest.raises( + exceptions.OptionsError, + match="Proxy Authentication not supported in transparent mode.", + ): ctx.configure(pa, proxyauth="any", mode="transparent") def test_handlers(self): @@ -188,23 +234,28 @@ def test_handlers(self): f = tflow.tflow() f.request.method = "CONNECT" - f.request.headers["Proxy-Authorization"] = proxyauth.mkauth( - "test", "test" - ) + f.request.headers["Proxy-Authorization"] = proxyauth.mkauth("test", "test") up.http_connect(f) assert not f.response f2 = tflow.tflow(client_conn=f.client_conn) up.requestheaders(f2) assert not f2.response - assert f2.metadata["proxyauth"] == ('test', 'test') + assert f2.metadata["proxyauth"] == ("test", "test") -def test_ldap(monkeypatch): +@pytest.mark.parametrize( + "spec", + [ + "ldaps:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com", + "ldap:localhost:1234:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com", + ], +) +def test_ldap(monkeypatch, spec): monkeypatch.setattr(ldap3, "Server", mock.MagicMock()) monkeypatch.setattr(ldap3, "Connection", mock.MagicMock()) - validator = proxyauth.Ldap("ldaps:localhost:cn=default,dc=cdhdt,dc=com:password:ou=application,dc=cdhdt,dc=com") + validator = proxyauth.Ldap(spec) assert not validator("", "") assert validator("foo", "bar") validator.conn.response = False diff --git a/test/mitmproxy/addons/test_proxyserver.py b/test/mitmproxy/addons/test_proxyserver.py index be644a9634..9abeb983ec 100644 --- a/test/mitmproxy/addons/test_proxyserver.py +++ b/test/mitmproxy/addons/test_proxyserver.py @@ -1,15 +1,19 @@ import asyncio from contextlib import asynccontextmanager +import socket import pytest -from mitmproxy import exceptions +from mitmproxy import dns, exceptions +from mitmproxy.addons import dns_resolver from mitmproxy.addons.proxyserver import Proxyserver from mitmproxy.connection import Address +from mitmproxy.net import udp from mitmproxy.proxy import layers, server_hooks from mitmproxy.proxy.layers.http import HTTPMode from mitmproxy.test import taddons, tflow from mitmproxy.test.tflow import tclient_conn, tserver_conn +from mitmproxy.test.tutils import tdnsreq class HelperAddon: @@ -33,7 +37,7 @@ def next_layer(self, nl): @asynccontextmanager async def tcp_server(handle_conn) -> Address: - server = await asyncio.start_server(handle_conn, '127.0.0.1', 0) + server = await asyncio.start_server(handle_conn, "127.0.0.1", 0) await server.start_serving() try: yield server.sockets[0].getsockname() @@ -41,9 +45,10 @@ async def tcp_server(handle_conn) -> Address: server.close() -@pytest.mark.asyncio async def test_start_stop(): - async def server_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + async def server_handler( + reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ): assert await reader.readuntil(b"\r\n\r\n") == b"GET /hello HTTP/1.1\r\n\r\n" writer.write(b"HTTP/1.1 204 No Content\r\n\r\n") await writer.drain() @@ -55,21 +60,24 @@ async def server_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWri tctx.master.addons.add(state) async with tcp_server(server_handler) as addr: tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) - assert not ps.server - ps.running() + assert not ps.tcp_server + await ps.running() await tctx.master.await_log("Proxy server listening", level="info") - assert ps.server + assert ps.tcp_server - proxy_addr = ps.server.sockets[0].getsockname()[:2] + proxy_addr = ps.tcp_server.sockets[0].getsockname()[:2] reader, writer = await asyncio.open_connection(*proxy_addr) req = f"GET http://{addr[0]}:{addr[1]}/hello HTTP/1.1\r\n\r\n" writer.write(req.encode()) - assert await reader.readuntil(b"\r\n\r\n") == b"HTTP/1.1 204 No Content\r\n\r\n" + assert ( + await reader.readuntil(b"\r\n\r\n") + == b"HTTP/1.1 204 No Content\r\n\r\n" + ) assert repr(ps) == "ProxyServer(running, 1 active conns)" tctx.configure(ps, server=False) - await tctx.master.await_log("Stopping server", level="info") - assert not ps.server + await tctx.master.await_log("Stopping Proxy server", level="info") + assert not ps.tcp_server assert state.flows assert state.flows[0].request.path == "/hello" assert state.flows[0].response.status_code == 204 @@ -89,9 +97,10 @@ async def server_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWri assert repr(ps) == "ProxyServer(stopped, 0 active conns)" -@pytest.mark.asyncio async def test_inject() -> None: - async def server_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWriter): + async def server_handler( + reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ): while s := await reader.read(1): writer.write(s.upper()) @@ -101,14 +110,17 @@ async def server_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWri tctx.master.addons.add(state) async with tcp_server(server_handler) as addr: tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) - ps.running() + await ps.running() await tctx.master.await_log("Proxy server listening", level="info") - proxy_addr = ps.server.sockets[0].getsockname()[:2] + proxy_addr = ps.tcp_server.sockets[0].getsockname()[:2] reader, writer = await asyncio.open_connection(*proxy_addr) req = f"CONNECT {addr[0]}:{addr[1]} HTTP/1.1\r\n\r\n" writer.write(req.encode()) - assert await reader.readuntil(b"\r\n\r\n") == b"HTTP/1.1 200 Connection established\r\n\r\n" + assert ( + await reader.readuntil(b"\r\n\r\n") + == b"HTTP/1.1 200 Connection established\r\n\r\n" + ) writer.write(b"a") assert await reader.read(1) == b"A" @@ -118,38 +130,24 @@ async def server_handler(reader: asyncio.StreamReader, writer: asyncio.StreamWri assert await reader.read(1) == b"c" -@pytest.mark.asyncio async def test_inject_fail() -> None: ps = Proxyserver() with taddons.context(ps) as tctx: - ps.inject_websocket( - tflow.tflow(), - True, - b"test" + ps.inject_websocket(tflow.tflow(), True, b"test") + await tctx.master.await_log( + "Cannot inject WebSocket messages into non-WebSocket flows.", level="warn" ) - await tctx.master.await_log("Cannot inject WebSocket messages into non-WebSocket flows.", level="warn") - ps.inject_tcp( - tflow.tflow(), - True, - b"test" + ps.inject_tcp(tflow.tflow(), True, b"test") + await tctx.master.await_log( + "Cannot inject TCP messages into non-TCP flows.", level="warn" ) - await tctx.master.await_log("Cannot inject TCP messages into non-TCP flows.", level="warn") - ps.inject_websocket( - tflow.twebsocketflow(), - True, - b"test" - ) + ps.inject_websocket(tflow.twebsocketflow(), True, b"test") await tctx.master.await_log("Flow is not from a live connection.", level="warn") - ps.inject_websocket( - tflow.ttcpflow(), - True, - b"test" - ) + ps.inject_websocket(tflow.ttcpflow(), True, b"test") await tctx.master.await_log("Flow is not from a live connection.", level="warn") -@pytest.mark.asyncio async def test_warn_no_nextlayer(): """ Test that we log an error if the proxy server is started without NextLayer addon. @@ -158,13 +156,15 @@ async def test_warn_no_nextlayer(): ps = Proxyserver() with taddons.context(ps) as tctx: tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) - ps.running() + await ps.running() await tctx.master.await_log("Proxy server listening at", level="info") - assert tctx.master.has_log("Warning: Running proxyserver without nextlayer addon!", level="warn") + assert tctx.master.has_log( + "Warning: Running proxyserver without nextlayer addon!", level="warn" + ) await ps.shutdown_server() -def test_self_connect(): +async def test_self_connect(): server = tserver_conn() client = tclient_conn() server.address = ("localhost", 8080) @@ -172,10 +172,8 @@ def test_self_connect(): with taddons.context(ps) as tctx: # not calling .running() here to avoid unnecessary socket ps.options = tctx.options - ps.server_connect( - server_hooks.ServerConnectionHookData(server, client) - ) - assert server.error == "Stopped mitmproxy from recursively connecting to itself." + ps.server_connect(server_hooks.ServerConnectionHookData(server, client)) + assert "Request destination unknown" in server.error def test_options(): @@ -188,3 +186,97 @@ def test_options(): with pytest.raises(exceptions.OptionsError): tctx.configure(ps, stream_large_bodies="invalid") tctx.configure(ps, stream_large_bodies="1m") + with pytest.raises(exceptions.OptionsError): + tctx.configure(ps, dns_mode="invalid") + tctx.configure(ps, dns_mode="regular") + + with pytest.raises(exceptions.OptionsError): + tctx.configure(ps, dns_mode="reverse") + tctx.configure(ps, dns_mode="reverse:8.8.8.8") + assert ps.dns_reverse_addr == ("8.8.8.8", 53) + + with pytest.raises(exceptions.OptionsError): + tctx.configure(ps, dns_mode="reverse:invalid:53") + tctx.configure(ps, dns_mode="reverse:8.8.8.8:53") + assert ps.dns_reverse_addr == ("8.8.8.8", 53) + + with pytest.raises(exceptions.OptionsError): + tctx.configure(ps, connect_addr="invalid") + tctx.configure(ps, connect_addr="1.2.3.4") + assert ps.connect_addr == ("1.2.3.4", 0) + + +async def test_startup_err(monkeypatch) -> None: + async def _raise(*_): + raise OSError("cannot bind") + + monkeypatch.setattr(asyncio, "start_server", _raise) + + ps = Proxyserver() + with taddons.context(ps) as tctx: + await ps.running() + await tctx.master.await_log("cannot bind", level="error") + + +async def test_shutdown_err() -> None: + def _raise(*_): + raise OSError("cannot close") + + ps = Proxyserver() + with taddons.context(ps) as tctx: + tctx.configure(ps, listen_host="127.0.0.1", listen_port=0) + await ps.running() + assert ps.running_servers + for server in ps.running_servers: + setattr(server, "close", _raise) + await ps.shutdown_server() + await tctx.master.await_log("cannot close", level="error") + assert ps.running_servers + + +class DummyResolver: + async def dns_request(self, flow: dns.DNSFlow) -> None: + flow.response = await dns_resolver.resolve_message(flow.request, self) + + async def getaddrinfo(self, host: str, port: int, *, family: int): + if family == socket.AF_INET and host == "dns.google": + return [(socket.AF_INET, None, None, None, ("8.8.8.8", port))] + e = socket.gaierror() + e.errno = socket.EAI_NONAME + raise e + + +async def test_dns() -> None: + ps = Proxyserver() + with taddons.context(ps, DummyResolver()) as tctx: + tctx.configure( + ps, + server=False, + dns_server=True, + dns_listen_host="127.0.0.1", + dns_listen_port=0, + dns_mode="regular", + ) + await ps.running() + await tctx.master.await_log("DNS server listening at", level="info") + assert ps.dns_server + dns_addr = ps.dns_server.sockets[0].getsockname()[:2] + r, w = await udp.open_connection(*dns_addr) + w.write(b"\x00") + await tctx.master.await_log("Invalid DNS datagram received", level="info") + req = tdnsreq() + w.write(req.packed) + resp = dns.Message.unpack(await r.read(udp.MAX_DATAGRAM_SIZE)) + assert req.id == resp.id and "8.8.8.8" in str(resp) + assert len(ps._connections) == 1 + w.write(req.packed) + resp = dns.Message.unpack(await r.read(udp.MAX_DATAGRAM_SIZE)) + assert req.id == resp.id and "8.8.8.8" in str(resp) + assert len(ps._connections) == 1 + req.id = req.id + 1 + w.write(req.packed) + resp = dns.Message.unpack(await r.read(udp.MAX_DATAGRAM_SIZE)) + assert req.id == resp.id and "8.8.8.8" in str(resp) + assert len(ps._connections) == 2 + await ps.shutdown_server() + await tctx.master.await_log("Stopping DNS server", level="info") diff --git a/test/mitmproxy/addons/test_readfile.py b/test/mitmproxy/addons/test_readfile.py index 42a1a1768d..20e7721afc 100644 --- a/test/mitmproxy/addons/test_readfile.py +++ b/test/mitmproxy/addons/test_readfile.py @@ -20,7 +20,7 @@ def data(): tflow.tflow(resp=True), tflow.tflow(err=True), tflow.ttcpflow(), - tflow.ttcpflow(err=True) + tflow.ttcpflow(err=True), ] for flow in flows: w.add(flow) @@ -47,7 +47,6 @@ def test_configure(self): tctx.configure(rf, readfile_filter="~~") tctx.configure(rf, readfile_filter="") - @pytest.mark.asyncio async def test_read(self, tmpdir, data, corrupt_data): rf = readfile.ReadFile() with taddons.context(rf) as tctx: @@ -55,13 +54,9 @@ async def test_read(self, tmpdir, data, corrupt_data): tf = tmpdir.join("tfile") - with mock.patch('mitmproxy.master.Master.load_flow') as mck: + with mock.patch("mitmproxy.master.Master.load_flow") as mck: tf.write(data.getvalue()) - tctx.configure( - rf, - rfile = str(tf), - readfile_filter = ".*" - ) + tctx.configure(rf, rfile=str(tf), readfile_filter=".*") mck.assert_not_awaited() rf.running() await asyncio.sleep(0) @@ -72,7 +67,6 @@ async def test_read(self, tmpdir, data, corrupt_data): rf.running() await tctx.master.await_log("corrupted") - @pytest.mark.asyncio async def test_corrupt(self, corrupt_data): rf = readfile.ReadFile() with taddons.context(rf) as tctx: @@ -84,7 +78,6 @@ async def test_corrupt(self, corrupt_data): await rf.load_flows(corrupt_data) await tctx.master.await_log("file corrupted") - @pytest.mark.asyncio async def test_nonexistent_file(self): rf = readfile.ReadFile() with taddons.context(rf) as tctx: @@ -94,12 +87,11 @@ async def test_nonexistent_file(self): class TestReadFileStdin: - @mock.patch('sys.stdin') - @pytest.mark.asyncio + @mock.patch("sys.stdin") async def test_stdin(self, stdin, data, corrupt_data): rf = readfile.ReadFileStdin() with taddons.context(rf): - with mock.patch('mitmproxy.master.Master.load_flow') as mck: + with mock.patch("mitmproxy.master.Master.load_flow") as mck: stdin.buffer = data mck.assert_not_awaited() await rf.load_flows(stdin.buffer) @@ -109,12 +101,11 @@ async def test_stdin(self, stdin, data, corrupt_data): with pytest.raises(exceptions.FlowReadException): await rf.load_flows(stdin.buffer) - @pytest.mark.asyncio async def test_normal(self, tmpdir, data): rf = readfile.ReadFileStdin() with taddons.context(rf) as tctx: tf = tmpdir.join("tfile") - with mock.patch('mitmproxy.master.Master.load_flow') as mck: + with mock.patch("mitmproxy.master.Master.load_flow") as mck: tf.write(data.getvalue()) tctx.configure(rf, rfile=str(tf)) mck.assert_not_awaited() diff --git a/test/mitmproxy/addons/test_save.py b/test/mitmproxy/addons/test_save.py index 5067562cd8..48af437deb 100644 --- a/test/mitmproxy/addons/test_save.py +++ b/test/mitmproxy/addons/test_save.py @@ -1,22 +1,21 @@ import pytest -from mitmproxy.test import taddons -from mitmproxy.test import tflow - -from mitmproxy import io from mitmproxy import exceptions +from mitmproxy import io from mitmproxy.addons import save from mitmproxy.addons import view +from mitmproxy.test import taddons +from mitmproxy.test import tflow -def test_configure(tmpdir): +def test_configure(tmp_path): sa = save.Save() with taddons.context(sa) as tctx: with pytest.raises(exceptions.OptionsError): - tctx.configure(sa, save_stream_file=str(tmpdir)) + tctx.configure(sa, save_stream_file=str(tmp_path)) with pytest.raises(Exception, match="Invalid filter"): tctx.configure( - sa, save_stream_file=str(tmpdir.join("foo")), save_stream_filter="~~" + sa, save_stream_file=str(tmp_path / "foo"), save_stream_filter="~~" ) tctx.configure(sa, save_stream_filter="foo") assert sa.filt @@ -30,10 +29,10 @@ def rd(p): return list(x.stream()) -def test_tcp(tmpdir): +def test_tcp(tmp_path): sa = save.Save() with taddons.context(sa) as tctx: - p = str(tmpdir.join("foo")) + p = str(tmp_path / "foo") tctx.configure(sa, save_stream_file=p) tt = tflow.ttcpflow() @@ -48,10 +47,40 @@ def test_tcp(tmpdir): assert len(rd(p)) == 2 -def test_websocket(tmpdir): +def test_dns(tmp_path): + sa = save.Save() + with taddons.context(sa) as tctx: + p = str(tmp_path / "foo") + tctx.configure(sa, save_stream_file=p) + + f = tflow.tdnsflow(resp=True) + sa.dns_request(f) + sa.dns_response(f) + tctx.configure(sa, save_stream_file=None) + assert rd(p)[0].response + + tctx.configure(sa, save_stream_file="+" + p) + f = tflow.tdnsflow(err=True) + sa.dns_request(f) + sa.dns_error(f) + tctx.configure(sa, save_stream_file=None) + assert rd(p)[1].error + + tctx.configure(sa, save_stream_file="+" + p) + f = tflow.tdnsflow() + sa.dns_request(f) + tctx.configure(sa, save_stream_file=None) + assert not rd(p)[2].response + + f = tflow.tdnsflow() + sa.dns_response(f) + assert len(rd(p)) == 3 + + +def test_websocket(tmp_path): sa = save.Save() with taddons.context(sa) as tctx: - p = str(tmpdir.join("foo")) + p = str(tmp_path / "foo") tctx.configure(sa, save_stream_file=p) f = tflow.twebsocketflow() @@ -66,10 +95,10 @@ def test_websocket(tmpdir): assert len(rd(p)) == 2 -def test_save_command(tmpdir): +def test_save_command(tmp_path): sa = save.Save() with taddons.context() as tctx: - p = str(tmpdir.join("foo")) + p = str(tmp_path / "foo") sa.save([tflow.tflow(resp=True)], p) assert len(rd(p)) == 1 sa.save([tflow.tflow(resp=True)], p) @@ -78,7 +107,7 @@ def test_save_command(tmpdir): assert len(rd(p)) == 2 with pytest.raises(exceptions.CommandError): - sa.save([tflow.tflow(resp=True)], str(tmpdir)) + sa.save([tflow.tflow(resp=True)], str(tmp_path)) v = view.View() tctx.master.addons.add(v) @@ -86,10 +115,10 @@ def test_save_command(tmpdir): tctx.master.commands.execute("save.file @shown %s" % p) -def test_simple(tmpdir): +def test_simple(tmp_path): sa = save.Save() with taddons.context(sa) as tctx: - p = str(tmpdir.join("foo")) + p = str(tmp_path / "foo") tctx.configure(sa, save_stream_file=p) @@ -111,3 +140,42 @@ def test_simple(tmpdir): sa.request(f) tctx.configure(sa, save_stream_file=None) assert not rd(p)[2].response + + f = tflow.tflow() + sa.response(f) + assert len(rd(p)) == 3 + + +def test_rotate_stream(tmp_path): + sa = save.Save() + with taddons.context(sa) as tctx: + tctx.configure(sa, save_stream_file=str(tmp_path / "a.txt")) + f1 = tflow.tflow(resp=True) + f2 = tflow.tflow(resp=True) + sa.request(f1) + sa.response(f1) + sa.request(f2) # second request already started. + tctx.configure(sa, save_stream_file=str(tmp_path / "b.txt")) + sa.response(f2) + sa.done() + + assert len(rd(tmp_path / "a.txt")) == 1 + assert len(rd(tmp_path / "b.txt")) == 1 + + +def test_disk_full(tmp_path, monkeypatch, capsys): + sa = save.Save() + with taddons.context(sa) as tctx: + tctx.configure(sa, save_stream_file=str(tmp_path / "foo.txt")) + + def _raise(*_): + raise OSError("wat") + + monkeypatch.setattr(sa, "maybe_rotate_to_new_file", _raise) + + f = tflow.tflow(resp=True) + sa.request(f) + with pytest.raises(SystemExit): + sa.response(f) + + assert "Error while writing" in capsys.readouterr().err diff --git a/test/mitmproxy/addons/test_script.py b/test/mitmproxy/addons/test_script.py index 12ee070c60..071911628b 100644 --- a/test/mitmproxy/addons/test_script.py +++ b/test/mitmproxy/addons/test_script.py @@ -1,5 +1,6 @@ import asyncio import os +import re import sys import traceback @@ -11,32 +12,24 @@ from mitmproxy.proxy.layers.http import HttpRequestHook from mitmproxy.test import taddons from mitmproxy.test import tflow +from mitmproxy.tools import main # We want this to be speedy for testing script.ReloadInterval = 0.1 -@pytest.mark.asyncio async def test_load_script(tdata): with taddons.context() as tctx: ns = script.load_script( - tdata.path( - "mitmproxy/data/addonscripts/recorder/recorder.py" - ) + tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py") ) assert ns.addons - script.load_script( - "nonexistent" - ) + script.load_script("nonexistent") await tctx.master.await_log("No such file or directory") - script.load_script( - tdata.path( - "mitmproxy/data/addonscripts/recorder/error.py" - ) - ) + script.load_script(tdata.path("mitmproxy/data/addonscripts/recorder/error.py")) await tctx.master.await_log("invalid syntax") @@ -46,16 +39,10 @@ def test_load_fullname(tdata): This only succeeds if they get assigned different basenames. """ - ns = script.load_script( - tdata.path( - "mitmproxy/data/addonscripts/addon.py" - ) - ) + ns = script.load_script(tdata.path("mitmproxy/data/addonscripts/addon.py")) assert ns.addons ns2 = script.load_script( - tdata.path( - "mitmproxy/data/addonscripts/same_filename/addon.py" - ) + tdata.path("mitmproxy/data/addonscripts/same_filename/addon.py") ) assert ns.name != ns2.name assert not hasattr(ns2, "addons") @@ -74,23 +61,17 @@ def test_quotes_around_filename(self, tdata): """ path = tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py") - s = script.Script( - f'"{path}"', - False - ) + s = script.Script(f'"{path}"', False) assert '"' not in s.fullpath - @pytest.mark.asyncio async def test_simple(self, tdata): sc = script.Script( - tdata.path( - "mitmproxy/data/addonscripts/recorder/recorder.py" - ), + tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py"), True, ) with taddons.context(sc) as tctx: tctx.configure(sc) - await tctx.master.await_log("recorder running") + await tctx.master.await_log("recorder configure") rec = tctx.master.addons.get("recorder") assert rec.call_log[0][0:2] == ("recorder", "load") @@ -101,7 +82,6 @@ async def test_simple(self, tdata): assert rec.call_log[0][1] == "request" - @pytest.mark.asyncio async def test_reload(self, tmpdir): with taddons.context() as tctx: f = tmpdir.join("foo.py") @@ -120,7 +100,6 @@ async def test_reload(self, tmpdir): else: raise AssertionError("No reload seen") - @pytest.mark.asyncio async def test_exception(self, tdata): with taddons.context() as tctx: sc = script.Script( @@ -128,7 +107,7 @@ async def test_exception(self, tdata): True, ) tctx.master.addons.add(sc) - await tctx.master.await_log("error running") + await tctx.master.await_log("error load") tctx.configure(sc) f = tflow.tflow(resp=True) @@ -137,7 +116,6 @@ async def test_exception(self, tdata): await tctx.master.await_log("ValueError: Error!") await tctx.master.await_log("error.py") - @pytest.mark.asyncio async def test_optionexceptions(self, tdata): with taddons.context() as tctx: sc = script.Script( @@ -148,19 +126,16 @@ async def test_optionexceptions(self, tdata): tctx.configure(sc) await tctx.master.await_log("Options Error") - @pytest.mark.asyncio async def test_addon(self, tdata): with taddons.context() as tctx: - sc = script.Script( - tdata.path( - "mitmproxy/data/addonscripts/addon.py" - ), - True - ) + sc = script.Script(tdata.path("mitmproxy/data/addonscripts/addon.py"), True) tctx.master.addons.add(sc) await tctx.master.await_log("addon running") assert sc.ns.event_log == [ - 'scriptload', 'addonload', 'scriptconfigure', 'addonconfigure' + "scriptload", + "addonload", + "scriptconfigure", + "addonconfigure", ] @@ -183,7 +158,6 @@ def test_simple(self): class TestScriptLoader: - @pytest.mark.asyncio async def test_script_run(self, tdata): rp = tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py") sc = script.ScriptLoader() @@ -192,19 +166,20 @@ async def test_script_run(self, tdata): await tctx.master.await_log("recorder response") debug = [i.msg for i in tctx.master.logs if i.level == "debug"] assert debug == [ - 'recorder running', 'recorder configure', - 'recorder requestheaders', 'recorder request', - 'recorder responseheaders', 'recorder response' + "recorder configure", + "recorder running", + "recorder requestheaders", + "recorder request", + "recorder responseheaders", + "recorder response", ] - @pytest.mark.asyncio async def test_script_run_nonexistent(self): sc = script.ScriptLoader() with taddons.context(sc) as tctx: sc.script_run([tflow.tflow(resp=True)], "/") await tctx.master.await_log("No such script") - @pytest.mark.asyncio async def test_simple(self, tdata): sc = script.ScriptLoader() with taddons.context(loadcore=False) as tctx: @@ -212,15 +187,11 @@ async def test_simple(self, tdata): sc.running() assert len(tctx.master.addons) == 1 tctx.master.options.update( - scripts = [ - tdata.path( - "mitmproxy/data/addonscripts/recorder/recorder.py" - ) - ] + scripts=[tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py")] ) assert len(tctx.master.addons) == 1 assert len(sc.addons) == 1 - tctx.master.options.update(scripts = []) + tctx.master.options.update(scripts=[]) assert len(tctx.master.addons) == 1 assert len(sc.addons) == 0 @@ -228,21 +199,19 @@ def test_dupes(self): sc = script.ScriptLoader() with taddons.context(sc) as tctx: with pytest.raises(exceptions.OptionsError): - tctx.configure( - sc, - scripts = ["one", "one"] - ) + tctx.configure(sc, scripts=["one", "one"]) - @pytest.mark.asyncio async def test_script_deletion(self, tdata): tdir = tdata.path("mitmproxy/data/addonscripts/") - with open(tdir + "/dummy.py", 'w') as f: + with open(tdir + "/dummy.py", "w") as f: f.write("\n") with taddons.context() as tctx: sl = script.ScriptLoader() tctx.master.addons.add(sl) - tctx.configure(sl, scripts=[tdata.path("mitmproxy/data/addonscripts/dummy.py")]) + tctx.configure( + sl, scripts=[tdata.path("mitmproxy/data/addonscripts/dummy.py")] + ) await tctx.master.await_log("Loading") os.remove(tdata.path("mitmproxy/data/addonscripts/dummy.py")) @@ -252,7 +221,6 @@ async def test_script_deletion(self, tdata): assert not tctx.options.scripts assert not sl.addons - @pytest.mark.asyncio async def test_script_error_handler(self): path = "/sample/path/example.py" exc = SyntaxError @@ -265,7 +233,6 @@ async def test_script_error_handler(self): await tctx.master.await_log("lineno") await tctx.master.await_log("NoneType") - @pytest.mark.asyncio async def test_order(self, tdata): rec = tdata.path("mitmproxy/data/addonscripts/recorder") sc = script.ScriptLoader() @@ -273,61 +240,84 @@ async def test_order(self, tdata): with taddons.context(sc) as tctx: tctx.configure( sc, - scripts = [ + scripts=[ "%s/a.py" % rec, "%s/b.py" % rec, "%s/c.py" % rec, - ] + ], ) await tctx.master.await_log("configure") debug = [i.msg for i in tctx.master.logs if i.level == "debug"] assert debug == [ - 'a load', - 'a running', - 'a configure', - - 'b load', - 'b running', - 'b configure', - - 'c load', - 'c running', - 'c configure', + "a load", + "a configure", + "a running", + "b load", + "b configure", + "b running", + "c load", + "c configure", + "c running", ] tctx.master.clear() tctx.configure( sc, - scripts = [ + scripts=[ "%s/c.py" % rec, "%s/a.py" % rec, "%s/b.py" % rec, - ] + ], ) await tctx.master.await_log("b configure") debug = [i.msg for i in tctx.master.logs if i.level == "debug"] assert debug == [ - 'c configure', - 'a configure', - 'b configure', + "c configure", + "a configure", + "b configure", ] tctx.master.clear() tctx.configure( sc, - scripts = [ + scripts=[ "%s/e.py" % rec, "%s/a.py" % rec, - ] + ], ) await tctx.master.await_log("e configure") debug = [i.msg for i in tctx.master.logs if i.level == "debug"] assert debug == [ - 'c done', - 'b done', - 'a configure', - 'e load', - 'e running', - 'e configure', + "c done", + "b done", + "a configure", + "e load", + "e configure", + "e running", ] + + # stop reload tasks + tctx.configure(sc, scripts=[]) + + +def test_order(tdata, capsys): + """Integration test: Make sure that the runtime hooks are triggered on startup in the correct order.""" + main.mitmdump( + [ + "-n", + "-s", + tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py"), + "-s", + tdata.path("mitmproxy/data/addonscripts/shutdown.py"), + ] + ) + assert re.match( + r"Loading script.+recorder.py\n" + r"\('recorder', 'load', .+\n" + r"\('recorder', 'configure', .+\n" + r"Loading script.+shutdown.py\n" + r"\('recorder', 'running', .+\n" + r"\('recorder', 'done', .+\n$", + capsys.readouterr().out, + ) diff --git a/test/mitmproxy/addons/test_server_side_events.py b/test/mitmproxy/addons/test_server_side_events.py new file mode 100644 index 0000000000..548afeabf5 --- /dev/null +++ b/test/mitmproxy/addons/test_server_side_events.py @@ -0,0 +1,14 @@ +from mitmproxy.addons.server_side_events import ServerSideEvents +from mitmproxy.test import taddons +from mitmproxy.test.tflow import tflow + + +async def test_simple(): + s = ServerSideEvents() + with taddons.context() as tctx: + f = tflow(resp=True) + f.response.headers["content-type"] = "text/event-stream" + s.response(f) + await tctx.master.await_log( + "mitmproxy currently does not support server side events." + ) diff --git a/test/mitmproxy/addons/test_serverplayback.py b/test/mitmproxy/addons/test_serverplayback.py index 9e0ca15660..77d4f28367 100644 --- a/test/mitmproxy/addons/test_serverplayback.py +++ b/test/mitmproxy/addons/test_serverplayback.py @@ -107,9 +107,7 @@ def test_ignore_content_wins_over_params(): tctx.configure( s, server_replay_ignore_content=True, - server_replay_ignore_payload_params=[ - "param1", "param2" - ] + server_replay_ignore_payload_params=["param1", "param2"], ) # NOTE: parameters are mutually exclusive in options @@ -131,9 +129,7 @@ def test_ignore_payload_params_other_content_type(): tctx.configure( s, server_replay_ignore_content=False, - server_replay_ignore_payload_params=[ - "param1", "param2" - ] + server_replay_ignore_payload_params=["param1", "param2"], ) r = tflow.tflow(resp=True) @@ -236,10 +232,7 @@ def test_load_with_server_replay_nopop(): def test_ignore_params(): s = serverplayback.ServerPlayback() with taddons.context(s) as tctx: - tctx.configure( - s, - server_replay_ignore_params=["param1", "param2"] - ) + tctx.configure(s, server_replay_ignore_params=["param1", "param2"]) r = tflow.tflow(resp=True) r.request.path = "/test?param1=1" @@ -258,10 +251,7 @@ def thash(r, r2, setter): s = serverplayback.ServerPlayback() with taddons.context(s) as tctx: s = serverplayback.ServerPlayback() - tctx.configure( - s, - server_replay_ignore_payload_params=["param1", "param2"] - ) + tctx.configure(s, server_replay_ignore_payload_params=["param1", "param2"]) setter(r, paramx="x", param1="1") @@ -296,20 +286,18 @@ def urlencode_setter(r, **kwargs): r2.request.headers["Content-Type"] = "application/x-www-form-urlencoded" thash(r, r2, urlencode_setter) - boundary = 'somefancyboundary' + boundary = "somefancyboundary" def multipart_setter(r, **kwargs): b = f"--{boundary}\n" parts = [] for k, v in kwargs.items(): parts.append( - "Content-Disposition: form-data; name=\"%s\"\n\n" - "%s\n" % (k, v) + 'Content-Disposition: form-data; name="%s"\n\n' "%s\n" % (k, v) ) c = b + b.join(parts) + b r.request.content = c.encode() - r.request.headers["content-type"] = 'multipart/form-data; boundary=' +\ - boundary + r.request.headers["content-type"] = "multipart/form-data; boundary=" + boundary r = tflow.tflow(resp=True) r2 = tflow.tflow(resp=True) @@ -340,14 +328,10 @@ def test_server_playback_full(): assert not tf.response -def test_server_playback_kill(): +async def test_server_playback_kill(): s = serverplayback.ServerPlayback() with taddons.context(s) as tctx: - tctx.configure( - s, - server_replay_refresh=True, - server_replay_kill_extra=True - ) + tctx.configure(s, server_replay_refresh=True, server_replay_kill_extra=True) f = tflow.tflow() f.response = mitmproxy.test.tutils.tresp(content=f.request.content) @@ -355,7 +339,7 @@ def test_server_playback_kill(): f = tflow.tflow() f.request.host = "nonexistent" - tctx.cycle(s, f) + await tctx.cycle(s, f) assert f.error diff --git a/test/mitmproxy/addons/test_stickycookie.py b/test/mitmproxy/addons/test_stickycookie.py index 25fc085ab9..d3edbbdb76 100644 --- a/test/mitmproxy/addons/test_stickycookie.py +++ b/test/mitmproxy/addons/test_stickycookie.py @@ -32,7 +32,6 @@ def test_simple(self): f.response.headers["set-cookie"] = "foo=bar" sc.request(f) - f.reply.acked = False sc.response(f) assert sc.jar @@ -53,8 +52,10 @@ def test_response(self): with taddons.context(sc) as tctx: tctx.configure(sc, stickycookie=".*") - c = "SSID=mooo; domain=.google.com, FOO=bar; Domain=.google.com; Path=/; " \ + c = ( + "SSID=mooo; domain=.google.com, FOO=bar; Domain=.google.com; Path=/; " "Expires=Wed, 13-Jan-2021 22:23:01 GMT; Secure; " + ) self._response(sc, c, "host") assert not sc.jar.keys() @@ -64,7 +65,7 @@ def test_response(self): sc.jar.clear() self._response(sc, "SSID=mooo", "www.google.com") - assert list(sc.jar.keys())[0] == ('www.google.com', 80, '/') + assert list(sc.jar.keys())[0] == ("www.google.com", 80, "/") def test_response_multiple(self): sc = stickycookie.StickyCookie() @@ -121,7 +122,9 @@ def test_response_delete(self): # Test that a cookie is be deleted # by setting the expire time in the past f = self._response(sc, "duffer=zafar; Path=/", "www.google.com") - f.response.headers["Set-Cookie"] = "duffer=; Expires=Thu, 01-Jan-1970 00:00:00 GMT" + f.response.headers[ + "Set-Cookie" + ] = "duffer=; Expires=Thu, 01-Jan-1970 00:00:00 GMT" sc.response(f) assert not sc.jar.keys() diff --git a/test/mitmproxy/addons/test_termlog.py b/test/mitmproxy/addons/test_termlog.py index 89aea0aab7..e6ea67eff7 100644 --- a/test/mitmproxy/addons/test_termlog.py +++ b/test/mitmproxy/addons/test_termlog.py @@ -1,27 +1,30 @@ -import sys -import pytest +import io -from mitmproxy.addons import termlog from mitmproxy import log +from mitmproxy.addons import termlog from mitmproxy.test import taddons -class TestTermLog: - @pytest.mark.usefixtures('capfd') - @pytest.mark.parametrize('outfile, expected_out, expected_err', [ - (None, ['one', 'three'], ['four']), - (sys.stdout, ['one', 'three', 'four'], []), - (sys.stderr, [], ['one', 'three', 'four']), - ]) - def test_output(self, outfile, expected_out, expected_err, capfd): - t = termlog.TermLog(outfile=outfile) - with taddons.context(t) as tctx: - tctx.options.termlog_verbosity = "info" - tctx.configure(t) - t.add_log(log.LogEntry("one", "info")) - t.add_log(log.LogEntry("two", "debug")) - t.add_log(log.LogEntry("three", "warn")) - t.add_log(log.LogEntry("four", "error")) - out, err = capfd.readouterr() - assert out.strip().splitlines() == expected_out - assert err.strip().splitlines() == expected_err +def test_output(capsys): + t = termlog.TermLog() + with taddons.context(t) as tctx: + tctx.options.termlog_verbosity = "info" + tctx.configure(t) + t.add_log(log.LogEntry("one", "info")) + t.add_log(log.LogEntry("two", "debug")) + t.add_log(log.LogEntry("three", "warn")) + t.add_log(log.LogEntry("four", "error")) + out, err = capsys.readouterr() + assert out.strip().splitlines() == ["one", "three"] + assert err.strip().splitlines() == ["four"] + + +def test_styling(monkeypatch) -> None: + f = io.StringIO() + t = termlog.TermLog(out=f) + t.out_has_vt_codes = True + with taddons.context(t) as tctx: + tctx.configure(t) + t.add_log(log.LogEntry("hello world", "info")) + + assert f.getvalue() == "\x1b[22mhello world\x1b[0m\n" diff --git a/test/mitmproxy/addons/test_tlsconfig.py b/test/mitmproxy/addons/test_tlsconfig.py index cf407c9ed3..a4bb9697ab 100644 --- a/test/mitmproxy/addons/test_tlsconfig.py +++ b/test/mitmproxy/addons/test_tlsconfig.py @@ -6,10 +6,10 @@ import pytest from OpenSSL import SSL -from mitmproxy import certs, connection +from mitmproxy import certs, connection, tls from mitmproxy.addons import tlsconfig from mitmproxy.proxy import context -from mitmproxy.proxy.layers import modes, tls +from mitmproxy.proxy.layers import modes, tls as proxy_tls from mitmproxy.test import taddons from test.mitmproxy.proxy.layers import test_tls @@ -19,26 +19,42 @@ def test_alpn_select_callback(): conn = SSL.Connection(ctx) # Test that we respect addons setting `client.alpn`. - conn.set_app_data(tlsconfig.AppData(server_alpn=b"h2", http2=True, client_alpn=b"qux")) + conn.set_app_data( + tlsconfig.AppData(server_alpn=b"h2", http2=True, client_alpn=b"qux") + ) assert tlsconfig.alpn_select_callback(conn, [b"http/1.1", b"qux", b"h2"]) == b"qux" conn.set_app_data(tlsconfig.AppData(server_alpn=b"h2", http2=True, client_alpn=b"")) - assert tlsconfig.alpn_select_callback(conn, [b"http/1.1", b"qux", b"h2"]) == SSL.NO_OVERLAPPING_PROTOCOLS + assert ( + tlsconfig.alpn_select_callback(conn, [b"http/1.1", b"qux", b"h2"]) + == SSL.NO_OVERLAPPING_PROTOCOLS + ) # Test that we try to mirror the server connection's ALPN - conn.set_app_data(tlsconfig.AppData(server_alpn=b"h2", http2=True, client_alpn=None)) + conn.set_app_data( + tlsconfig.AppData(server_alpn=b"h2", http2=True, client_alpn=None) + ) assert tlsconfig.alpn_select_callback(conn, [b"http/1.1", b"qux", b"h2"]) == b"h2" # Test that we respect the client's preferred HTTP ALPN. conn.set_app_data(tlsconfig.AppData(server_alpn=None, http2=True, client_alpn=None)) - assert tlsconfig.alpn_select_callback(conn, [b"qux", b"http/1.1", b"h2"]) == b"http/1.1" + assert ( + tlsconfig.alpn_select_callback(conn, [b"qux", b"http/1.1", b"h2"]) + == b"http/1.1" + ) assert tlsconfig.alpn_select_callback(conn, [b"qux", b"h2", b"http/1.1"]) == b"h2" # Test no overlap - assert tlsconfig.alpn_select_callback(conn, [b"qux", b"quux"]) == SSL.NO_OVERLAPPING_PROTOCOLS + assert ( + tlsconfig.alpn_select_callback(conn, [b"qux", b"quux"]) + == SSL.NO_OVERLAPPING_PROTOCOLS + ) # Test that we don't select an ALPN if the server refused to select one. conn.set_app_data(tlsconfig.AppData(server_alpn=b"", http2=True, client_alpn=None)) - assert tlsconfig.alpn_select_callback(conn, [b"http/1.1"]) == SSL.NO_OVERLAPPING_PROTOCOLS + assert ( + tlsconfig.alpn_select_callback(conn, [b"http/1.1"]) + == SSL.NO_OVERLAPPING_PROTOCOLS + ) here = Path(__file__).parent @@ -52,10 +68,22 @@ def test_configure(self, tdata): tctx.configure(ta, certs=["*=nonexistent"]) with pytest.raises(Exception, match="Invalid certificate format"): - tctx.configure(ta, certs=[tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.key")]) + tctx.configure( + ta, + certs=[ + tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-leaf.key" + ) + ], + ) assert not ta.certstore.certs - tctx.configure(ta, certs=[tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem")]) + tctx.configure( + ta, + certs=[ + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem") + ], + ) assert ta.certstore.certs def test_get_cert(self, tdata): @@ -64,7 +92,10 @@ def test_get_cert(self, tdata): with taddons.context(ta) as tctx: ta.configure(["confdir"]) - ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) # Edge case first: We don't have _any_ idea about the server nor is there a SNI, # so we just return our local IP as subject. @@ -73,11 +104,17 @@ def test_get_cert(self, tdata): # Here we have an existing server connection... ctx.server.address = ("server-address.example", 443) - with open(tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.crt"), "rb") as f: + with open( + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.crt"), + "rb", + ) as f: ctx.server.certificate_list = [certs.Cert.from_pem(f.read())] entry = ta.get_cert(ctx) assert entry.cert.cn == "example.mitmproxy.org" - assert entry.cert.altnames == ["example.mitmproxy.org", "server-address.example"] + assert entry.cert.altnames == [ + "example.mitmproxy.org", + "server-address.example", + ] # And now we also incorporate SNI. ctx.client.sni = "sni.example" @@ -86,21 +123,27 @@ def test_get_cert(self, tdata): with open(tdata.path("mitmproxy/data/invalid-subject.pem"), "rb") as f: ctx.server.certificate_list = [certs.Cert.from_pem(f.read())] - assert ta.get_cert(ctx) # does not raise + with pytest.warns( + UserWarning, match="Country names should be two characters" + ): + assert ta.get_cert(ctx) # does not raise def test_tls_clienthello(self): # only really testing for coverage here, there's no point in mirroring the individual conditions ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) ch = tls.ClientHelloData(ctx, None) # type: ignore ta.tls_clienthello(ch) assert not ch.establish_server_tls_first def do_handshake( - self, - tssl_client: Union[test_tls.SSLTest, SSL.Connection], - tssl_server: Union[test_tls.SSLTest, SSL.Connection] + self, + tssl_client: Union[test_tls.SSLTest, SSL.Connection], + tssl_server: Union[test_tls.SSLTest, SSL.Connection], ) -> bool: # ClientHello with pytest.raises((ssl.SSLWantReadError, SSL.WantReadError)): @@ -125,51 +168,96 @@ def test_tls_start_client(self, tdata): ta.configure(["confdir"]) tctx.configure( ta, - certs=[tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem")], + certs=[ + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem") + ], ciphers_client="ECDHE-ECDSA-AES128-GCM-SHA256", ) - ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) - tls_start = tls.TlsStartData(ctx.client, context=ctx) + tls_start = tls.TlsData(ctx.client, context=ctx) ta.tls_start_client(tls_start) tssl_server = tls_start.ssl_conn + + # assert that a preexisting ssl_conn is not overwritten + ta.tls_start_client(tls_start) + assert tssl_server is tls_start.ssl_conn + tssl_client = test_tls.SSLTest() assert self.do_handshake(tssl_client, tssl_server) - assert tssl_client.obj.getpeercert()["subjectAltName"] == (("DNS", "example.mitmproxy.org"),) + assert tssl_client.obj.getpeercert()["subjectAltName"] == ( + ("DNS", "example.mitmproxy.org"), + ) + + def test_tls_start_server_cannot_verify(self): + ta = tlsconfig.TlsConfig() + with taddons.context(ta) as tctx: + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) + ctx.server.address = ("example.mitmproxy.org", 443) + ctx.server.sni = "" # explicitly opt out of using the address. + + tls_start = tls.TlsData(ctx.server, context=ctx) + with pytest.raises(ValueError, match="Cannot validate certificate hostname without SNI"): + ta.tls_start_server(tls_start) def test_tls_start_server_verify_failed(self): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) ctx.client.alpn_offers = [b"h2"] ctx.client.cipher_list = ["TLS_AES_256_GCM_SHA384", "ECDHE-RSA-AES128-SHA"] ctx.server.address = ("example.mitmproxy.org", 443) - tls_start = tls.TlsStartData(ctx.server, context=ctx) + tls_start = tls.TlsData(ctx.server, context=ctx) ta.tls_start_server(tls_start) tssl_client = tls_start.ssl_conn tssl_server = test_tls.SSLTest(server_side=True) with pytest.raises(SSL.Error, match="certificate verify failed"): assert self.do_handshake(tssl_client, tssl_server) - def test_tls_start_server_verify_ok(self, tdata): + @pytest.mark.parametrize("hostname", ["example.mitmproxy.org", "192.0.2.42"]) + def test_tls_start_server_verify_ok(self, hostname, tdata): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) - ctx.server.address = ("example.mitmproxy.org", 443) - tctx.configure(ta, ssl_verify_upstream_trusted_ca=tdata.path( - "mitmproxy/net/data/verificationcerts/trusted-root.crt")) + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) + ctx.server.address = (hostname, 443) + tctx.configure( + ta, + ssl_verify_upstream_trusted_ca=tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-root.crt" + ), + ) - tls_start = tls.TlsStartData(ctx.server, context=ctx) + tls_start = tls.TlsData(ctx.server, context=ctx) ta.tls_start_server(tls_start) tssl_client = tls_start.ssl_conn - tssl_server = test_tls.SSLTest(server_side=True) + + # assert that a preexisting ssl_conn is not overwritten + ta.tls_start_server(tls_start) + assert tssl_client is tls_start.ssl_conn + + tssl_server = test_tls.SSLTest(server_side=True, sni=hostname.encode()) assert self.do_handshake(tssl_client, tssl_server) def test_tls_start_server_insecure(self): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) ctx.server.address = ("example.mitmproxy.org", 443) tctx.configure( @@ -177,9 +265,9 @@ def test_tls_start_server_insecure(self): ssl_verify_upstream_trusted_ca=None, ssl_insecure=True, http2=False, - ciphers_server="ALL" + ciphers_server="ALL", ) - tls_start = tls.TlsStartData(ctx.server, context=ctx) + tls_start = tls.TlsData(ctx.server, context=ctx) ta.tls_start_server(tls_start) tssl_client = tls_start.ssl_conn tssl_server = test_tls.SSLTest(server_side=True) @@ -188,19 +276,29 @@ def test_tls_start_server_insecure(self): def test_alpn_selection(self): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) ctx.server.address = ("example.mitmproxy.org", 443) - tls_start = tls.TlsStartData(ctx.server, context=ctx) + tls_start = tls.TlsData(ctx.server, context=ctx) def assert_alpn(http2, client_offers, expected): tctx.configure(ta, http2=http2) ctx.client.alpn_offers = client_offers ctx.server.alpn_offers = None + tls_start.ssl_conn = None ta.tls_start_server(tls_start) assert ctx.server.alpn_offers == expected - assert_alpn(True, tls.HTTP_ALPNS + (b"foo",), tls.HTTP_ALPNS + (b"foo",)) - assert_alpn(False, tls.HTTP_ALPNS + (b"foo",), tls.HTTP1_ALPNS + (b"foo",)) + assert_alpn( + True, proxy_tls.HTTP_ALPNS + (b"foo",), proxy_tls.HTTP_ALPNS + (b"foo",) + ) + assert_alpn( + False, + proxy_tls.HTTP_ALPNS + (b"foo",), + proxy_tls.HTTP1_ALPNS + (b"foo",), + ) assert_alpn(True, [], []) assert_alpn(False, [], []) ctx.client.timestamp_tls_setup = time.time() @@ -214,15 +312,20 @@ def test_no_h2_proxy(self, tdata): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - tctx.configure(ta, certs=[tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem")]) + tctx.configure( + ta, + certs=[ + tdata.path("mitmproxy/net/data/verificationcerts/trusted-leaf.pem") + ], + ) - ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) # mock up something that looks like a secure web proxy. - ctx.layers = [ - modes.HttpProxy(ctx), - 123 - ] - tls_start = tls.TlsStartData(ctx.client, context=ctx) + ctx.layers = [modes.HttpProxy(ctx), 123] + tls_start = tls.TlsData(ctx.client, context=ctx) ta.tls_start_client(tls_start) assert tls_start.ssl_conn.get_app_data()["client_alpn"] == b"http/1.1" @@ -236,15 +339,20 @@ def test_no_h2_proxy(self, tdata): def test_client_cert_file(self, tdata, client_certs): ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: - ctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), tctx.options) + ctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + tctx.options, + ) ctx.server.address = ("example.mitmproxy.org", 443) tctx.configure( ta, client_certs=tdata.path(client_certs), - ssl_verify_upstream_trusted_ca=tdata.path("mitmproxy/net/data/verificationcerts/trusted-root.crt"), + ssl_verify_upstream_trusted_ca=tdata.path( + "mitmproxy/net/data/verificationcerts/trusted-root.crt" + ), ) - tls_start = tls.TlsStartData(ctx.server, context=ctx) + tls_start = tls.TlsData(ctx.server, context=ctx) ta.tls_start_server(tls_start) tssl_client = tls_start.ssl_conn tssl_server = test_tls.SSLTest(server_side=True) @@ -252,10 +360,11 @@ def test_client_cert_file(self, tdata, client_certs): assert self.do_handshake(tssl_client, tssl_server) assert tssl_server.obj.getpeercert() - @pytest.mark.asyncio async def test_ca_expired(self, monkeypatch): monkeypatch.setattr(certs.Cert, "has_expired", lambda self: True) ta = tlsconfig.TlsConfig() with taddons.context(ta) as tctx: ta.configure(["confdir"]) - await tctx.master.await_log("The mitmproxy certificate authority has expired", "warn") + await tctx.master.await_log( + "The mitmproxy certificate authority has expired", "warn" + ) diff --git a/test/mitmproxy/addons/test_view.py b/test/mitmproxy/addons/test_view.py index 0032a7823a..be0a10022f 100644 --- a/test/mitmproxy/addons/test_view.py +++ b/test/mitmproxy/addons/test_view.py @@ -14,7 +14,7 @@ def tft(*, method="get", start=0): f = tflow.tflow() f.request.method = method - f.request.timestamp_start = start + f.timestamp_created = start return f @@ -31,7 +31,7 @@ def save(*args, **kwargs): with taddons.context() as tctx: tctx.configure(v, view_order="time") v.add([tf]) - tf.request.timestamp_start = 10 + tf.timestamp_created = 10 assert not sargs v.update([tf]) assert sargs @@ -54,6 +54,26 @@ def test_order_generators_http(): assert sz.generate(tf) == len(tf.request.raw_content) + len(tf.response.raw_content) +def test_order_generators_dns(): + v = view.View() + tf = tflow.tdnsflow(resp=True) + + rs = view.OrderRequestStart(v) + assert rs.generate(tf) == 946681200 + + rm = view.OrderRequestMethod(v) + assert rm.generate(tf) == "QUERY" + + ru = view.OrderRequestURL(v) + assert ru.generate(tf) == "dns.google" + + sz = view.OrderKeySize(v) + assert sz.generate(tf) == tf.response.size + + tf = tflow.tdnsflow(resp=False) + assert sz.generate(tf) == 0 + + def test_order_generators_tcp(): v = view.View() tf = tflow.ttcpflow() @@ -138,13 +158,27 @@ def test_simple_tcp(): assert list(v) == [f] +def test_simple_dns(): + v = view.View() + f = tflow.tdnsflow(resp=True, err=True) + assert v.store_count() == 0 + v.dns_request(f) + assert list(v) == [f] + + # These all just call update + v.dns_request(f) + v.dns_response(f) + v.dns_error(f) + assert list(v) == [f] + + def test_filter(): v = view.View() v.requestheaders(tft(method="get")) v.requestheaders(tft(method="put")) v.requestheaders(tft(method="get")) v.requestheaders(tft(method="put")) - assert(len(v)) == 4 + assert (len(v)) == 4 v.set_filter_cmd("~m get") assert [i.request.method for i in v] == ["GET", "GET"] assert len(v._store) == 4 @@ -194,19 +228,12 @@ def test_orders(): assert v.order_options() -@pytest.mark.asyncio async def test_load(tmpdir): path = str(tmpdir.join("path")) v = view.View() with taddons.context() as tctx: tctx.master.addons.add(v) - tdump( - path, - [ - tflow.tflow(resp=True), - tflow.tflow(resp=True) - ] - ) + tdump(path, [tflow.tflow(resp=True), tflow.tflow(resp=True)]) v.load_file(path) assert len(v) == 2 v.load_file(path) @@ -276,13 +303,15 @@ def test_movement(): v = view.View() with taddons.context(): v.go(0) - v.add([ - tflow.tflow(), - tflow.tflow(), - tflow.tflow(), - tflow.tflow(), - tflow.tflow(), - ]) + v.add( + [ + tflow.tflow(), + tflow.tflow(), + tflow.tflow(), + tflow.tflow(), + tflow.tflow(), + ] + ) assert v.focus.index == 0 v.go(-1) assert v.focus.index == 4 @@ -346,7 +375,7 @@ def test_order(): v.requestheaders(tft(method="put", start=2)) v.requestheaders(tft(method="get", start=3)) v.requestheaders(tft(method="put", start=4)) - assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] + assert [i.timestamp_created for i in v] == [1, 2, 3, 4] v.set_order("method") assert v.get_order() == "method" @@ -356,10 +385,10 @@ def test_order(): v.set_order("time") assert v.get_order() == "time" - assert [i.request.timestamp_start for i in v] == [4, 3, 2, 1] + assert [i.timestamp_created for i in v] == [4, 3, 2, 1] v.set_reversed(False) - assert [i.request.timestamp_start for i in v] == [1, 2, 3, 4] + assert [i.timestamp_created for i in v] == [1, 2, 3, 4] with pytest.raises(exceptions.CommandError): v.set_order("not_an_order") @@ -371,9 +400,9 @@ def test_reversed(): v.requestheaders(tft(start=3)) v.set_reversed(True) - assert v[0].request.timestamp_start == 3 - assert v[-1].request.timestamp_start == 1 - assert v[2].request.timestamp_start == 1 + assert v[0].timestamp_created == 3 + assert v[-1].timestamp_created == 1 + assert v[2].timestamp_created == 1 with pytest.raises(IndexError): v[5] with pytest.raises(IndexError): @@ -486,21 +515,21 @@ def test_focus_follow(): v.add([tft(start=4)]) assert v.focus.index == 0 - assert v.focus.flow.request.timestamp_start == 4 + assert v.focus.flow.timestamp_created == 4 v.add([tft(start=7)]) assert v.focus.index == 2 - assert v.focus.flow.request.timestamp_start == 7 + assert v.focus.flow.timestamp_created == 7 mod = tft(method="put", start=6) v.add([mod]) assert v.focus.index == 2 - assert v.focus.flow.request.timestamp_start == 7 + assert v.focus.flow.timestamp_created == 7 mod.request.method = "GET" v.update([mod]) assert v.focus.index == 2 - assert v.focus.flow.request.timestamp_start == 6 + assert v.focus.flow.timestamp_created == 6 def test_focus(): @@ -552,12 +581,14 @@ def test_focus(): assert f.index is None assert f.flow is None - v.add([ - tft(method="get", start=0), - tft(method="get", start=1), - tft(method="put", start=2), - tft(method="get", start=3), - ]) + v.add( + [ + tft(method="get", start=0), + tft(method="get", start=1), + tft(method="put", start=2), + tft(method="get", start=3), + ] + ) f.flow = v[2] assert f.flow.request.method == "PUT" @@ -621,11 +652,15 @@ def test_configure(): assert v.focus_follow -@pytest.mark.parametrize("marker, expected", [ - [":default:", SYMBOL_MARK], - ["X", "X"], - [":grapes:", "\N{grapes}"], - [":not valid:", SYMBOL_MARK], [":weird", SYMBOL_MARK] -]) +@pytest.mark.parametrize( + "marker, expected", + [ + [":default:", SYMBOL_MARK], + ["X", "X"], + [":grapes:", "\N{grapes}"], + [":not valid:", SYMBOL_MARK], + [":weird", SYMBOL_MARK], + ], +) def test_marker(marker, expected): assert render_marker(marker) == expected diff --git a/test/mitmproxy/contentviews/image/test_image_parser.py b/test/mitmproxy/contentviews/image/test_image_parser.py index 481d821f40..cd97d53390 100644 --- a/test/mitmproxy/contentviews/image/test_image_parser.py +++ b/test/mitmproxy/contentviews/image/test_image_parser.py @@ -3,189 +3,219 @@ from mitmproxy.contentviews.image import image_parser -@pytest.mark.parametrize("filename, metadata", { - # no textual data - "mitmproxy/data/image_parser/ct0n0g04.png": [ - ('Format', 'Portable network graphics'), - ('Size', '32 x 32 px'), - ('gamma', '1.0') - ], - # with textual data - "mitmproxy/data/image_parser/ct1n0g04.png": [ - ('Format', 'Portable network graphics'), - ('Size', '32 x 32 px'), - ('gamma', '1.0'), - ('Title', 'PngSuite'), - ('Author', 'Willem A.J. van Schaik\n(willem@schaik.com)'), - ('Copyright', 'Copyright Willem van Schaik, Singapore 1995-96'), - ('Description', 'A compilation of a set of images created to test the\n' - 'various color-types of the PNG format. Included are\nblack&white, color,' - ' paletted, with alpha channel, with\ntransparency formats. All bit-depths' - ' allowed according\nto the spec are present.'), - ('Software', 'Created on a NeXTstation color using "pnmtopng".'), - ('Disclaimer', 'Freeware.') - ], - # with compressed textual data - "mitmproxy/data/image_parser/ctzn0g04.png": [ - ('Format', 'Portable network graphics'), - ('Size', '32 x 32 px'), - ('gamma', '1.0'), - ('Title', 'PngSuite'), - ('Author', 'Willem A.J. van Schaik\n(willem@schaik.com)'), - ('Copyright', 'Copyright Willem van Schaik, Singapore 1995-96'), - ('Description', 'A compilation of a set of images created to test the\n' - 'various color-types of the PNG format. Included are\nblack&white, color,' - ' paletted, with alpha channel, with\ntransparency formats. All bit-depths' - ' allowed according\nto the spec are present.'), - ('Software', 'Created on a NeXTstation color using "pnmtopng".'), - ('Disclaimer', 'Freeware.') - ], - # UTF-8 international text - english - "mitmproxy/data/image_parser/cten0g04.png": [ - ('Format', 'Portable network graphics'), - ('Size', '32 x 32 px'), - ('gamma', '1.0'), - ('Title', 'PngSuite'), - ('Author', 'Willem van Schaik (willem@schaik.com)'), - ('Copyright', 'Copyright Willem van Schaik, Canada 2011'), - ('Description', 'A compilation of a set of images created to test the ' - 'various color-types of the PNG format. Included are black&white, color,' - ' paletted, with alpha channel, with transparency formats. All bit-depths' - ' allowed according to the spec are present.'), - ('Software', 'Created on a NeXTstation color using "pnmtopng".'), - ('Disclaimer', 'Freeware.') - ], - # check gamma value - "mitmproxy/data/image_parser/g07n0g16.png": [ - ('Format', 'Portable network graphics'), - ('Size', '32 x 32 px'), - ('gamma', '0.7') - ], - # check aspect value - "mitmproxy/data/image_parser/aspect.png": [ - ('Format', 'Portable network graphics'), - ('Size', '1280 x 798 px'), - ('aspect', '72 x 72'), - ('date:create', '2012-07-11T14:04:52-07:00'), - ('date:modify', '2012-07-11T14:04:52-07:00') - ], -}.items()) +@pytest.mark.parametrize( + "filename, metadata", + { + # no textual data + "mitmproxy/data/image_parser/ct0n0g04.png": [ + ("Format", "Portable network graphics"), + ("Size", "32 x 32 px"), + ("gamma", "1.0"), + ], + # with textual data + "mitmproxy/data/image_parser/ct1n0g04.png": [ + ("Format", "Portable network graphics"), + ("Size", "32 x 32 px"), + ("gamma", "1.0"), + ("Title", "PngSuite"), + ("Author", "Willem A.J. van Schaik\n(willem@schaik.com)"), + ("Copyright", "Copyright Willem van Schaik, Singapore 1995-96"), + ( + "Description", + "A compilation of a set of images created to test the\n" + "various color-types of the PNG format. Included are\nblack&white, color," + " paletted, with alpha channel, with\ntransparency formats. All bit-depths" + " allowed according\nto the spec are present.", + ), + ("Software", 'Created on a NeXTstation color using "pnmtopng".'), + ("Disclaimer", "Freeware."), + ], + # with compressed textual data + "mitmproxy/data/image_parser/ctzn0g04.png": [ + ("Format", "Portable network graphics"), + ("Size", "32 x 32 px"), + ("gamma", "1.0"), + ("Title", "PngSuite"), + ("Author", "Willem A.J. van Schaik\n(willem@schaik.com)"), + ("Copyright", "Copyright Willem van Schaik, Singapore 1995-96"), + ( + "Description", + "A compilation of a set of images created to test the\n" + "various color-types of the PNG format. Included are\nblack&white, color," + " paletted, with alpha channel, with\ntransparency formats. All bit-depths" + " allowed according\nto the spec are present.", + ), + ("Software", 'Created on a NeXTstation color using "pnmtopng".'), + ("Disclaimer", "Freeware."), + ], + # UTF-8 international text - english + "mitmproxy/data/image_parser/cten0g04.png": [ + ("Format", "Portable network graphics"), + ("Size", "32 x 32 px"), + ("gamma", "1.0"), + ("Title", "PngSuite"), + ("Author", "Willem van Schaik (willem@schaik.com)"), + ("Copyright", "Copyright Willem van Schaik, Canada 2011"), + ( + "Description", + "A compilation of a set of images created to test the " + "various color-types of the PNG format. Included are black&white, color," + " paletted, with alpha channel, with transparency formats. All bit-depths" + " allowed according to the spec are present.", + ), + ("Software", 'Created on a NeXTstation color using "pnmtopng".'), + ("Disclaimer", "Freeware."), + ], + # check gamma value + "mitmproxy/data/image_parser/g07n0g16.png": [ + ("Format", "Portable network graphics"), + ("Size", "32 x 32 px"), + ("gamma", "0.7"), + ], + # check aspect value + "mitmproxy/data/image_parser/aspect.png": [ + ("Format", "Portable network graphics"), + ("Size", "1280 x 798 px"), + ("aspect", "72 x 72"), + ("date:create", "2012-07-11T14:04:52-07:00"), + ("date:modify", "2012-07-11T14:04:52-07:00"), + ], + }.items(), +) def test_parse_png(filename, metadata, tdata): with open(tdata.path(filename), "rb") as f: assert metadata == image_parser.parse_png(f.read()) -@pytest.mark.parametrize("filename, metadata", { - # check comment - "mitmproxy/data/image_parser/hopper.gif": [ - ('Format', 'Compuserve GIF'), - ('Version', 'GIF89a'), - ('Size', '128 x 128 px'), - ('background', '0'), - ('comment', "b'File written by Adobe Photoshop\\xa8 4.0'") - ], - # check background - "mitmproxy/data/image_parser/chi.gif": [ - ('Format', 'Compuserve GIF'), - ('Version', 'GIF89a'), - ('Size', '320 x 240 px'), - ('background', '248'), - ('comment', "b'Created with GIMP'") - ], - # check working with color table - "mitmproxy/data/image_parser/iss634.gif": [ - ('Format', 'Compuserve GIF'), - ('Version', 'GIF89a'), - ('Size', '245 x 245 px'), - ('background', '0') - ], -}.items()) +@pytest.mark.parametrize( + "filename, metadata", + { + # check comment + "mitmproxy/data/image_parser/hopper.gif": [ + ("Format", "Compuserve GIF"), + ("Version", "GIF89a"), + ("Size", "128 x 128 px"), + ("background", "0"), + ("comment", "b'File written by Adobe Photoshop\\xa8 4.0'"), + ], + # check background + "mitmproxy/data/image_parser/chi.gif": [ + ("Format", "Compuserve GIF"), + ("Version", "GIF89a"), + ("Size", "320 x 240 px"), + ("background", "248"), + ("comment", "b'Created with GIMP'"), + ], + # check working with color table + "mitmproxy/data/image_parser/iss634.gif": [ + ("Format", "Compuserve GIF"), + ("Version", "GIF89a"), + ("Size", "245 x 245 px"), + ("background", "0"), + ], + }.items(), +) def test_parse_gif(filename, metadata, tdata): - with open(tdata.path(filename), 'rb') as f: + with open(tdata.path(filename), "rb") as f: assert metadata == image_parser.parse_gif(f.read()) -@pytest.mark.parametrize("filename, metadata", { - # check app0 - "mitmproxy/data/image_parser/example.jpg": [ - ('Format', 'JPEG (ISO 10918)'), - ('jfif_version', '(1, 1)'), - ('jfif_density', '(96, 96)'), - ('jfif_unit', '1'), - ('Size', '256 x 256 px') - ], - # check com - "mitmproxy/data/image_parser/comment.jpg": [ - ('Format', 'JPEG (ISO 10918)'), - ('jfif_version', '(1, 1)'), - ('jfif_density', '(96, 96)'), - ('jfif_unit', '1'), - ('comment', "b'mitmproxy test image'"), - ('Size', '256 x 256 px') - ], - # check app1 - "mitmproxy/data/image_parser/app1.jpeg": [ - ('Format', 'JPEG (ISO 10918)'), - ('jfif_version', '(1, 1)'), - ('jfif_density', '(72, 72)'), - ('jfif_unit', '1'), - ('make', 'Canon'), - ('model', 'Canon PowerShot A60'), - ('modify_date', '2004:07:16 18:46:04'), - ('Size', '717 x 558 px') - ], - # check multiple segments - "mitmproxy/data/image_parser/all.jpeg": [ - ('Format', 'JPEG (ISO 10918)'), - ('jfif_version', '(1, 1)'), - ('jfif_density', '(300, 300)'), - ('jfif_unit', '1'), - ('comment', 'b\'BARTOLOMEO DI FRUOSINO\\r\\n(b. ca. 1366, Firenze, d. 1441, ' - 'Firenze)\\r\\n\\r\\nInferno, from the Divine Comedy by Dante (Folio 1v)' - '\\r\\n1430-35\\r\\nTempera, gold, and silver on parchment, 365 x 265 mm' - '\\r\\nBiblioth\\xe8que Nationale, Paris\\r\\n\\r\\nThe codex in Paris ' - 'contains the text of the Inferno, the first of three books of the Divine ' - 'Comedy, the masterpiece of the Florentine poet Dante Alighieri (1265-1321).' - ' The codex begins with two full-page illuminations. On folio 1v Dante and ' - 'Virgil stand within the doorway of Hell at the upper left and observe its ' - 'nine different zones. Dante and Virgil are to wade through successive ' - 'circles teeming with images of the damned. The gates of Hell appear in ' - 'the middle, a scarlet row of open sarcophagi before them. Devils orchestrate' - ' the movements of the wretched souls.\\r\\n\\r\\nThe vision of the fiery ' - 'inferno follows a convention established by ' - 'Nardo di Cione\\\'s fresco in the church of Santa Maria Novella, Florence.' - ' Of remarkable vivacity and intensity of expression, the illumination is ' - 'executed in Bartolomeo\\\'s late style.\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n' - '--- Keywords: --------------\\r\\n\\r\\nAuthor: BARTOLOMEO DI FRUOSINO' - '\\r\\nTitle: Inferno, from the Divine Comedy by Dante (Folio 1v)\\r\\nTime-line:' - ' 1401-1450\\r\\nSchool: Italian\\r\\nForm: illumination\\r\\nType: other\\r\\n\''), - ('Size', '750 x 1055 px') - ], -}.items()) +@pytest.mark.parametrize( + "filename, metadata", + { + # check app0 + "mitmproxy/data/image_parser/example.jpg": [ + ("Format", "JPEG (ISO 10918)"), + ("jfif_version", "(1, 1)"), + ("jfif_density", "(96, 96)"), + ("jfif_unit", "1"), + ("Size", "256 x 256 px"), + ], + # check com + "mitmproxy/data/image_parser/comment.jpg": [ + ("Format", "JPEG (ISO 10918)"), + ("jfif_version", "(1, 1)"), + ("jfif_density", "(96, 96)"), + ("jfif_unit", "1"), + ("comment", "b'mitmproxy test image'"), + ("Size", "256 x 256 px"), + ], + # check app1 + "mitmproxy/data/image_parser/app1.jpeg": [ + ("Format", "JPEG (ISO 10918)"), + ("jfif_version", "(1, 1)"), + ("jfif_density", "(72, 72)"), + ("jfif_unit", "1"), + ("make", "Canon"), + ("model", "Canon PowerShot A60"), + ("modify_date", "2004:07:16 18:46:04"), + ("Size", "717 x 558 px"), + ], + # check multiple segments + "mitmproxy/data/image_parser/all.jpeg": [ + ("Format", "JPEG (ISO 10918)"), + ("jfif_version", "(1, 1)"), + ("jfif_density", "(300, 300)"), + ("jfif_unit", "1"), + ( + "comment", + "b'BARTOLOMEO DI FRUOSINO\\r\\n(b. ca. 1366, Firenze, d. 1441, " + "Firenze)\\r\\n\\r\\nInferno, from the Divine Comedy by Dante (Folio 1v)" + "\\r\\n1430-35\\r\\nTempera, gold, and silver on parchment, 365 x 265 mm" + "\\r\\nBiblioth\\xe8que Nationale, Paris\\r\\n\\r\\nThe codex in Paris " + "contains the text of the Inferno, the first of three books of the Divine " + "Comedy, the masterpiece of the Florentine poet Dante Alighieri (1265-1321)." + " The codex begins with two full-page illuminations. On folio 1v Dante and " + "Virgil stand within the doorway of Hell at the upper left and observe its " + "nine different zones. Dante and Virgil are to wade through successive " + "circles teeming with images of the damned. The gates of Hell appear in " + "the middle, a scarlet row of open sarcophagi before them. Devils orchestrate" + " the movements of the wretched souls.\\r\\n\\r\\nThe vision of the fiery " + 'inferno follows a convention established by ' + "Nardo di Cione\\'s fresco in the church of Santa Maria Novella, Florence." + " Of remarkable vivacity and intensity of expression, the illumination is " + "executed in Bartolomeo\\'s late style.\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n" + "--- Keywords: --------------\\r\\n\\r\\nAuthor: BARTOLOMEO DI FRUOSINO" + "\\r\\nTitle: Inferno, from the Divine Comedy by Dante (Folio 1v)\\r\\nTime-line:" + " 1401-1450\\r\\nSchool: Italian\\r\\nForm: illumination\\r\\nType: other\\r\\n'", + ), + ("Size", "750 x 1055 px"), + ], + }.items(), +) def test_parse_jpeg(filename, metadata, tdata): - with open(tdata.path(filename), 'rb') as f: + with open(tdata.path(filename), "rb") as f: assert metadata == image_parser.parse_jpeg(f.read()) -@pytest.mark.parametrize("filename, metadata", { - "mitmproxy/data/image.ico": [ - ('Format', 'ICO'), - ('Number of images', '3'), - ('Image 1', "Size: {} x {}\n" - "{: >18}Bits per pixel: {}\n" - "{: >18}PNG: {}".format(48, 48, '', 24, '', False) - ), - ('Image 2', "Size: {} x {}\n" - "{: >18}Bits per pixel: {}\n" - "{: >18}PNG: {}".format(32, 32, '', 24, '', False) - ), - ('Image 3', "Size: {} x {}\n" - "{: >18}Bits per pixel: {}\n" - "{: >18}PNG: {}".format(16, 16, '', 24, '', False) - ) - ] -}.items()) +@pytest.mark.parametrize( + "filename, metadata", + { + "mitmproxy/data/image.ico": [ + ("Format", "ICO"), + ("Number of images", "3"), + ( + "Image 1", + "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(48, 48, "", 24, "", False), + ), + ( + "Image 2", + "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(32, 32, "", 24, "", False), + ), + ( + "Image 3", + "Size: {} x {}\n" + "{: >18}Bits per pixel: {}\n" + "{: >18}PNG: {}".format(16, 16, "", 24, "", False), + ), + ] + }.items(), +) def test_ico(filename, metadata, tdata): - with open(tdata.path(filename), 'rb') as f: + with open(tdata.path(filename), "rb") as f: assert metadata == image_parser.parse_ico(f.read()) diff --git a/test/mitmproxy/contentviews/image/test_view.py b/test/mitmproxy/contentviews/image/test_view.py index 7abfeb3f49..67c4b81b4e 100644 --- a/test/mitmproxy/contentviews/image/test_view.py +++ b/test/mitmproxy/contentviews/image/test_view.py @@ -14,7 +14,10 @@ def test_view_image(tdata): viewname, lines = v(f.read()) assert img.split(".")[-1].upper() in viewname - assert v(b"flibble") == ('Unknown Image', [[('header', 'Image Format: '), ('text', 'unknown')]]) + assert v(b"flibble") == ( + "Unknown Image", + [[("header", "Image Format: "), ("text", "unknown")]], + ) def test_render_priority(): diff --git a/test/mitmproxy/contentviews/test_css.py b/test/mitmproxy/contentviews/test_css.py index c69a7e80ba..7474a6b36f 100644 --- a/test/mitmproxy/contentviews/test_css.py +++ b/test/mitmproxy/contentviews/test_css.py @@ -4,18 +4,21 @@ from . import full_eval -@pytest.mark.parametrize("filename", [ - "animation-keyframe.css", - "blank-lines-and-spaces.css", - "block-comment.css", - "empty-rule.css", - "import-directive.css", - "indentation.css", - "media-directive.css", - "quoted-string.css", - "selectors.css", - "simple.css", -]) +@pytest.mark.parametrize( + "filename", + [ + "animation-keyframe.css", + "blank-lines-and-spaces.css", + "block-comment.css", + "empty-rule.css", + "import-directive.css", + "indentation.css", + "media-directive.css", + "quoted-string.css", + "selectors.css", + "simple.css", + ], +) def test_beautify(filename, tdata): path = tdata.path("mitmproxy/contentviews/test_css_data/" + filename) with open(path) as f: @@ -28,14 +31,14 @@ def test_beautify(filename, tdata): def test_simple(): v = full_eval(css.ViewCSS()) - assert v(b"#foo{color:red}") == ('CSS', [ - [('text', '#foo {')], - [('text', ' color: red')], - [('text', '}')] - ]) - assert v(b"") == ('CSS', [[('text', '')]]) + assert v(b"#foo{color:red}") == ( + "CSS", + [[("text", "#foo {")], [("text", " color: red")], [("text", "}")]], + ) + assert v(b"") == ("CSS", [[("text", "")]]) assert v(b"console.log('not really css')") == ( - 'CSS', [[('text', "console.log('not really css')")]] + "CSS", + [[("text", "console.log('not really css')")]], ) diff --git a/test/mitmproxy/contentviews/test_graphql.py b/test/mitmproxy/contentviews/test_graphql.py index 668737695e..a38eedea00 100644 --- a/test/mitmproxy/contentviews/test_graphql.py +++ b/test/mitmproxy/contentviews/test_graphql.py @@ -7,10 +7,18 @@ def test_render_priority(): v = graphql.ViewGraphQL() - assert 2 == v.render_priority(b"""{"query": "query P { \\n }"}""", content_type="application/json") - assert 2 == v.render_priority(b"""[{"query": "query P { \\n }"}]""", content_type="application/json") - assert 0 == v.render_priority(b"""[{"query": "query P { \\n }"}]""", content_type="text/html") - assert 0 == v.render_priority(b"""[{"xquery": "query P { \\n }"}]""", content_type="application/json") + assert 2 == v.render_priority( + b"""{"query": "query P { \\n }"}""", content_type="application/json" + ) + assert 2 == v.render_priority( + b"""[{"query": "query P { \\n }"}]""", content_type="application/json" + ) + assert 0 == v.render_priority( + b"""[{"query": "query P { \\n }"}]""", content_type="text/html" + ) + assert 0 == v.render_priority( + b"""[{"xquery": "query P { \\n }"}]""", content_type="application/json" + ) assert 0 == v.render_priority(b"}", content_type="application/json") diff --git a/test/mitmproxy/contentviews/test_grpc.py b/test/mitmproxy/contentviews/test_grpc.py index cde774a7f3..83de5000d9 100644 --- a/test/mitmproxy/contentviews/test_grpc.py +++ b/test/mitmproxy/contentviews/test_grpc.py @@ -1,8 +1,12 @@ import pytest -from typing import List from mitmproxy.contentviews import grpc -from mitmproxy.contentviews.grpc import ViewGrpcProtobuf, ViewConfig, ProtoParser, parse_grpc_messages +from mitmproxy.contentviews.grpc import ( + ViewGrpcProtobuf, + ViewConfig, + ProtoParser, + parse_grpc_messages, +) from mitmproxy.net.encoding import encode from mitmproxy.test import tflow, tutils import struct @@ -14,10 +18,11 @@ def helper_pack_grpc_message(data: bytes, compress=False, encoding="gzip") -> bytes: if compress: data = encode(data, encoding) - header = struct.pack('!?i', compress, len(data)) + header = struct.pack("!?i", compress, len(data)) return header + data +# fmt: off custom_parser_rules = [ ProtoParser.ParserRuleRequest( name = "Geo coordinate lookup request", @@ -371,7 +376,7 @@ def helper_gen_lendel_msg_field(f_idx: int, f_val: bytes): return msg -def helper_gen_bits64_msg_field_packed(f_idx: int, values: List[int]): +def helper_gen_bits64_msg_field_packed(f_idx: int, values: list[int]): # manual encoding of protobuf data msg_inner = b"" for f_val in values: @@ -379,7 +384,7 @@ def helper_gen_bits64_msg_field_packed(f_idx: int, values: List[int]): return helper_gen_lendel_msg_field(f_idx, msg_inner) -def helper_gen_bits32_msg_field_packed(f_idx: int, values: List[int]): +def helper_gen_bits32_msg_field_packed(f_idx: int, values: list[int]): # manual encoding of protobuf data msg_inner = b"" for f_val in values: @@ -387,7 +392,7 @@ def helper_gen_bits32_msg_field_packed(f_idx: int, values: List[int]): return helper_gen_lendel_msg_field(f_idx, msg_inner) -def helper_gen_varint_msg_field_packed(f_idx: int, values: List[int]): +def helper_gen_varint_msg_field_packed(f_idx: int, values: list[int]): # manual encoding of protobuf data msg_inner = b"" for f_val in values: @@ -395,7 +400,7 @@ def helper_gen_varint_msg_field_packed(f_idx: int, values: List[int]): return helper_gen_lendel_msg_field(f_idx, msg_inner) -def helper_gen_lendel_msg_field_packed(f_idx: int, values: List[bytes]): +def helper_gen_lendel_msg_field_packed(f_idx: int, values: list[bytes]): # manual encoding of protobuf data msg_inner = b"" for f_val in values: diff --git a/test/mitmproxy/contentviews/test_javascript.py b/test/mitmproxy/contentviews/test_javascript.py index e16f9f3760..c050adee4f 100644 --- a/test/mitmproxy/contentviews/test_javascript.py +++ b/test/mitmproxy/contentviews/test_javascript.py @@ -8,17 +8,19 @@ def test_view_javascript(): v = full_eval(javascript.ViewJavaScript()) assert v(b"[1, 2, 3]") assert v(b"[1, 2, 3") - assert v(b"function(a){[1, 2, 3]}") == ("JavaScript", [ - [('text', 'function(a) {')], - [('text', ' [1, 2, 3]')], - [('text', '}')] - ]) + assert v(b"function(a){[1, 2, 3]}") == ( + "JavaScript", + [[("text", "function(a) {")], [("text", " [1, 2, 3]")], [("text", "}")]], + ) assert v(b"\xfe") # invalid utf-8 -@pytest.mark.parametrize("filename", [ - "simple.js", -]) +@pytest.mark.parametrize( + "filename", + [ + "simple.js", + ], +) def test_format_xml(filename, tdata): path = tdata.path("mitmproxy/contentviews/test_js_data/" + filename) with open(path) as f: diff --git a/test/mitmproxy/contentviews/test_json.py b/test/mitmproxy/contentviews/test_json.py index 5c28eb03b9..43565b2302 100644 --- a/test/mitmproxy/contentviews/test_json.py +++ b/test/mitmproxy/contentviews/test_json.py @@ -7,24 +7,16 @@ def test_parse_json(): assert json.parse_json(b'{"foo": 1}') - assert json.parse_json(b'null') is None + assert json.parse_json(b"null") is None assert json.parse_json(b"moo") is json.PARSE_ERROR - assert json.parse_json(b'{"foo" : "\xe4\xb8\x96\xe7\x95\x8c"}') # utf8 with chinese characters + assert json.parse_json( + b'{"foo" : "\xe4\xb8\x96\xe7\x95\x8c"}' + ) # utf8 with chinese characters assert json.parse_json(b'{"foo" : "\xFF"}') is json.PARSE_ERROR def test_format_json(): - assert list(json.format_json({ - "data": [ - "str", - 42, - True, - False, - None, - {}, - [] - ] - })) + assert list(json.format_json({"data": ["str", 42, True, False, None, {}, []]})) def test_view_json(): diff --git a/test/mitmproxy/contentviews/test_msgpack.py b/test/mitmproxy/contentviews/test_msgpack.py index ac97cd241f..458dac810c 100644 --- a/test/mitmproxy/contentviews/test_msgpack.py +++ b/test/mitmproxy/contentviews/test_msgpack.py @@ -18,17 +18,9 @@ def test_parse_msgpack(): def test_format_msgpack(): - assert list(msgpack.format_msgpack({ - "data": [ - "str", - 42, - True, - False, - None, - {}, - [] - ] - })) + assert list( + msgpack.format_msgpack({"data": ["str", 42, True, False, None, {}, []]}) + ) def test_view_msgpack(): diff --git a/test/mitmproxy/contentviews/test_protobuf.py b/test/mitmproxy/contentviews/test_protobuf.py index 9624854963..5f8d84d2e8 100644 --- a/test/mitmproxy/contentviews/test_protobuf.py +++ b/test/mitmproxy/contentviews/test_protobuf.py @@ -14,9 +14,9 @@ def test_view_protobuf_request(tdata): raw = f.read() content_type, output = v(raw) assert content_type == "Protobuf" - assert output == [[('text', '1: 3bbc333c-e61c-433b-819a-0b9a8cc103b8')]] + assert output == [[("text", "1: 3bbc333c-e61c-433b-819a-0b9a8cc103b8")]] with pytest.raises(ValueError, match="Failed to parse input."): - v(b'foobar') + v(b"foobar") @pytest.mark.parametrize("filename", ["protobuf02.bin", "protobuf03.bin"]) diff --git a/test/mitmproxy/contentviews/test_query.py b/test/mitmproxy/contentviews/test_query.py index 606300bca0..af47a02f81 100644 --- a/test/mitmproxy/contentviews/test_query.py +++ b/test/mitmproxy/contentviews/test_query.py @@ -10,7 +10,10 @@ def test_view_query(): req.query = [("foo", "bar"), ("foo", "baz")] f = v(d, http_message=req) assert f[0] == "Query" - assert f[1] == [[("header", "foo: "), ("text", "bar")], [("header", "foo: "), ("text", "baz")]] + assert f[1] == [ + [("header", "foo: "), ("text", "bar")], + [("header", "foo: "), ("text", "baz")], + ] assert v(d) == ("Query", []) diff --git a/test/mitmproxy/contentviews/test_wbxml.py b/test/mitmproxy/contentviews/test_wbxml.py index f9a411145d..e37f0da21f 100644 --- a/test/mitmproxy/contentviews/test_wbxml.py +++ b/test/mitmproxy/contentviews/test_wbxml.py @@ -7,11 +7,13 @@ def test_wbxml(tdata): v = full_eval(wbxml.ViewWBXML()) - assert v(b'\x03\x01\x6A\x00') == ('WBXML', [[('text', '')]]) - assert v(b'foo') is None + assert v(b"\x03\x01\x6A\x00") == ("WBXML", [[("text", '')]]) + assert v(b"foo") is None - path = tdata.path(datadir + "data.wbxml") # File taken from https://github.com/davidpshaw/PyWBXMLDecoder/tree/master/wbxml_samples - with open(path, 'rb') as f: + path = tdata.path( + datadir + "data.wbxml" + ) # File taken from https://github.com/davidpshaw/PyWBXMLDecoder/tree/master/wbxml_samples + with open(path, "rb") as f: input = f.read() with open("-formatted.".join(path.rsplit(".", 1))) as f: expected = f.read() diff --git a/test/mitmproxy/contentviews/test_xml_html.py b/test/mitmproxy/contentviews/test_xml_html.py index fc1bac5c8e..4bb007972e 100644 --- a/test/mitmproxy/contentviews/test_xml_html.py +++ b/test/mitmproxy/contentviews/test_xml_html.py @@ -8,10 +8,10 @@ def test_simple(tdata): v = full_eval(xml_html.ViewXmlHtml()) - assert v(b"foo") == ('XML', [[('text', 'foo')]]) - assert v(b"") == ('HTML', [[('text', '')]]) - assert v(b"<>") == ('XML', [[('text', '<>')]]) - assert v(b"") == ("HTML", [[("text", "")]]) + assert v(b"<>") == ("XML", [[("text", "<>")]]) + assert v(b")" -@pytest.mark.parametrize("filename", [ - "simple.html", - "cdata.xml", - "comment.xml", - "inline.html", - "test.html" -]) +@pytest.mark.parametrize( + "filename", ["simple.html", "cdata.xml", "comment.xml", "inline.html", "test.html"] +) def test_format_xml(filename, tdata): path = tdata.path(datadir + filename) with open(path) as f: diff --git a/test/mitmproxy/coretypes/test_basethread.py b/test/mitmproxy/coretypes/test_basethread.py index 6b0ae154a5..59e28bb0e0 100644 --- a/test/mitmproxy/coretypes/test_basethread.py +++ b/test/mitmproxy/coretypes/test_basethread.py @@ -3,5 +3,5 @@ def test_basethread(): - t = basethread.BaseThread('foobar') - assert re.match(r'foobar - age: \d+s', t._threadinfo()) + t = basethread.BaseThread("foobar") + assert re.match(r"foobar - age: \d+s", t._threadinfo()) diff --git a/test/mitmproxy/coretypes/test_multidict.py b/test/mitmproxy/coretypes/test_multidict.py index 273d8ca24b..1fcb10c9dd 100644 --- a/test/mitmproxy/coretypes/test_multidict.py +++ b/test/mitmproxy/coretypes/test_multidict.py @@ -16,11 +16,7 @@ class TMultiDict(_TMulti, multidict.MultiDict): class TestMultiDict: @staticmethod def _multi(): - return TMultiDict(( - ("foo", "bar"), - ("bar", "baz"), - ("Bar", "bam") - )) + return TMultiDict((("foo", "bar"), ("bar", "baz"), ("Bar", "bam"))) def test_init(self): md = TMultiDict() @@ -44,9 +40,7 @@ def test_getitem(self): with pytest.raises(KeyError): assert md["bar"] - md_multi = TMultiDict( - [("foo", "a"), ("foo", "b")] - ) + md_multi = TMultiDict([("foo", "a"), ("foo", "b")]) assert md_multi["foo"] == "a" def test_setitem(self): @@ -113,13 +107,15 @@ def test_set_all(self): md.set_all("foo", ["bar", "baz"]) assert md.fields == (("foo", "bar"), ("foo", "baz")) - md = TMultiDict(( - ("a", "b"), - ("x", "x"), - ("c", "d"), - ("X", "X"), - ("e", "f"), - )) + md = TMultiDict( + ( + ("a", "b"), + ("x", "x"), + ("c", "d"), + ("X", "X"), + ("e", "f"), + ) + ) md.set_all("x", ["1", "2", "3"]) assert md.fields == ( ("a", "b"), @@ -144,7 +140,7 @@ def test_add(self): ("foo", "bar"), ("bar", "baz"), ("Bar", "bam"), - ("foo", "foo") + ("foo", "foo"), ) def test_insert(self): @@ -166,7 +162,11 @@ def test_values(self): def test_items(self): md = self._multi() assert list(md.items()) == [("foo", "bar"), ("bar", "baz")] - assert list(md.items(multi=True)) == [("foo", "bar"), ("bar", "baz"), ("Bar", "bam")] + assert list(md.items(multi=True)) == [ + ("foo", "bar"), + ("bar", "baz"), + ("Bar", "bam"), + ] def test_state(self): md = self._multi() diff --git a/test/mitmproxy/coretypes/test_serializable.py b/test/mitmproxy/coretypes/test_serializable.py index a316f876f5..8617a75ee6 100644 --- a/test/mitmproxy/coretypes/test_serializable.py +++ b/test/mitmproxy/coretypes/test_serializable.py @@ -30,10 +30,7 @@ def test_copy(self): assert b.i == 42 def test_copy_id(self): - a = SerializableDummy({ - "id": "foo", - "foo": 42 - }) + a = SerializableDummy({"id": "foo", "foo": 42}) b = a.copy() assert a.get_state()["id"] != b.get_state()["id"] assert a.get_state()["foo"] == b.get_state()["foo"] diff --git a/test/mitmproxy/data/addonscripts/addon.py b/test/mitmproxy/data/addonscripts/addon.py index c6b540d46a..0acb97e00b 100644 --- a/test/mitmproxy/data/addonscripts/addon.py +++ b/test/mitmproxy/data/addonscripts/addon.py @@ -1,4 +1,5 @@ from mitmproxy import ctx + event_log = [] diff --git a/test/mitmproxy/data/addonscripts/concurrent_decorator.py b/test/mitmproxy/data/addonscripts/concurrent_decorator.py index d1ab6c6c19..bf2628958a 100644 --- a/test/mitmproxy/data/addonscripts/concurrent_decorator.py +++ b/test/mitmproxy/data/addonscripts/concurrent_decorator.py @@ -1,8 +1,12 @@ import time -import sys from mitmproxy.script import concurrent @concurrent def request(flow): - time.sleep(0.1) + time.sleep(0.25) + + +@concurrent +async def requestheaders(flow): + time.sleep(0.25) diff --git a/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py b/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py index b52f55c563..b4ef75292c 100644 --- a/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py +++ b/test/mitmproxy/data/addonscripts/concurrent_decorator_class.py @@ -3,10 +3,13 @@ class ConcurrentClass: - @concurrent def request(self, flow): - time.sleep(0.1) + time.sleep(0.25) + + @concurrent + async def requestheaders(self, flow): + time.sleep(0.25) addons = [ConcurrentClass()] diff --git a/test/mitmproxy/data/addonscripts/configure.py b/test/mitmproxy/data/addonscripts/configure.py index 6f6ac06a75..3b7ad654cf 100644 --- a/test/mitmproxy/data/addonscripts/configure.py +++ b/test/mitmproxy/data/addonscripts/configure.py @@ -1,4 +1,4 @@ -import typing +from typing import Optional from mitmproxy import exceptions @@ -6,16 +6,14 @@ class OptionAddon: def load(self, loader): loader.add_option( - name = "optionaddon", - typespec = typing.Optional[int], - default = None, - help = "Option Addon", + name="optionaddon", + typespec=Optional[int], + default=None, + help="Option Addon", ) def configure(self, updates): raise exceptions.OptionsError("Options Error") -addons = [ - OptionAddon() -] +addons = [OptionAddon()] diff --git a/test/mitmproxy/data/addonscripts/error.py b/test/mitmproxy/data/addonscripts/error.py index 2f0c1755c2..8dcd404d8b 100644 --- a/test/mitmproxy/data/addonscripts/error.py +++ b/test/mitmproxy/data/addonscripts/error.py @@ -1,8 +1,8 @@ from mitmproxy import ctx -def running(): - ctx.log.info("error running") +def load(loader): + ctx.log.info("error load") def request(flow): diff --git a/test/mitmproxy/data/addonscripts/recorder/recorder.py b/test/mitmproxy/data/addonscripts/recorder/recorder.py index 71f1b4ae32..3af4e5303e 100644 --- a/test/mitmproxy/data/addonscripts/recorder/recorder.py +++ b/test/mitmproxy/data/addonscripts/recorder/recorder.py @@ -10,6 +10,7 @@ def __init__(self, name="recorder"): def __getattr__(self, attr): if attr in hooks.all_hooks: + def prox(*args, **kwargs): lg = (self.name, attr, args, kwargs) if attr != "add_log": diff --git a/test/mitmproxy/data/addonscripts/shutdown.py b/test/mitmproxy/data/addonscripts/shutdown.py index f4a8f55d5c..51a99b5c88 100644 --- a/test/mitmproxy/data/addonscripts/shutdown.py +++ b/test/mitmproxy/data/addonscripts/shutdown.py @@ -2,4 +2,4 @@ def running(): - ctx.master.shutdown() \ No newline at end of file + ctx.master.shutdown() diff --git a/test/mitmproxy/data/addonscripts/stream_modify.py b/test/mitmproxy/data/addonscripts/stream_modify.py index 4ebcc3e94f..7941320542 100644 --- a/test/mitmproxy/data/addonscripts/stream_modify.py +++ b/test/mitmproxy/data/addonscripts/stream_modify.py @@ -1,5 +1,6 @@ from mitmproxy import ctx + def modify(chunks): for chunk in chunks: yield chunk.replace(b"foo", b"bar") diff --git a/test/mitmproxy/data/servercert/generate.py b/test/mitmproxy/data/servercert/generate.py index 604912679f..b88dcc751c 100644 --- a/test/mitmproxy/data/servercert/generate.py +++ b/test/mitmproxy/data/servercert/generate.py @@ -8,6 +8,5 @@ for x in ["self-signed", "trusted-leaf", "trusted-root"]: (here / f"{x}.pem").write_text( - (src / f"{x}.crt").read_text() + - (src / f"{x}.key").read_text() + (src / f"{x}.crt").read_text() + (src / f"{x}.key").read_text() ) diff --git a/test/mitmproxy/io/test_compat.py b/test/mitmproxy/io/test_compat.py index da96c38886..e19c8909b8 100644 --- a/test/mitmproxy/io/test_compat.py +++ b/test/mitmproxy/io/test_compat.py @@ -4,13 +4,16 @@ from mitmproxy import exceptions -@pytest.mark.parametrize("dumpfile, url, count", [ - ["dumpfile-011.mitm", "https://example.com/", 1], - ["dumpfile-018.mitm", "https://www.example.com/", 1], - ["dumpfile-019.mitm", "https://webrv.rtb-seller.com/", 1], - ["dumpfile-7-websocket.mitm", "https://echo.websocket.org/", 6], - ["dumpfile-10.mitm", "https://example.com/", 1] -]) +@pytest.mark.parametrize( + "dumpfile, url, count", + [ + ["dumpfile-011.mitm", "https://example.com/", 1], + ["dumpfile-018.mitm", "https://www.example.com/", 1], + ["dumpfile-019.mitm", "https://webrv.rtb-seller.com/", 1], + ["dumpfile-7-websocket.mitm", "https://echo.websocket.org/", 6], + ["dumpfile-10.mitm", "https://example.com/", 1], + ], +) def test_load(tdata, dumpfile, url, count): with open(tdata.path("mitmproxy/data/" + dumpfile), "rb") as f: flow_reader = io.FlowReader(f) diff --git a/test/mitmproxy/io/test_io.py b/test/mitmproxy/io/test_io.py index 8d37dc8f71..9d7ad80809 100644 --- a/test/mitmproxy/io/test_io.py +++ b/test/mitmproxy/io/test_io.py @@ -10,8 +10,8 @@ class TestFlowReader: @given(binary()) - @example(b'51:11:12345678901#4:this,8:true!0:~,4:true!0:]4:\\x00,~}') - @example(b'0:') + @example(b"51:11:12345678901#4:this,8:true!0:~,4:true!0:]4:\\x00,~}") + @example(b"0:") def test_fuzz(self, data): f = io.BytesIO(data) reader = FlowReader(f) @@ -26,11 +26,16 @@ def test_empty(self): def test_unknown_type(self): with pytest.raises(exceptions.FlowReadException, match="Unknown flow type"): - weird_flow = tnetstring.dumps({"type": "unknown", "version": version.FLOW_FORMAT_VERSION}) + weird_flow = tnetstring.dumps( + {"type": "unknown", "version": version.FLOW_FORMAT_VERSION} + ) for _ in FlowReader(io.BytesIO(weird_flow)).stream(): pass def test_cannot_migrate(self): - with pytest.raises(exceptions.FlowReadException, match="cannot read files with flow format version 0"): + with pytest.raises( + exceptions.FlowReadException, + match="cannot read files with flow format version 0", + ): for _ in FlowReader(io.BytesIO(b"14:7:version;1:0#}")).stream(): pass diff --git a/test/mitmproxy/io/test_tnetstring.py b/test/mitmproxy/io/test_tnetstring.py index 3f5469e5b7..caf37fe5ab 100644 --- a/test/mitmproxy/io/test_tnetstring.py +++ b/test/mitmproxy/io/test_tnetstring.py @@ -6,8 +6,9 @@ from mitmproxy.io import tnetstring -MAXINT = 2 ** (struct.Struct('i').size * 8 - 1) - 1 +MAXINT = 2 ** (struct.Struct("i").size * 8 - 1) - 1 +# fmt: off FORMAT_EXAMPLES = { b'0:}': {}, b'0:]': [], @@ -26,6 +27,7 @@ b'18:3:0.1^3:0.2^3:0.3^]': [0.1, 0.2, 0.3], b'243:238:233:228:223:218:213:208:203:198:193:188:183:178:173:168:163:158:153:148:143:138:133:128:123:118:113:108:103:99:95:91:87:83:79:75:71:67:63:59:55:51:47:43:39:35:31:27:23:19:15:11:hello-there,]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]': [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[b'hello-there']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]] # noqa } +# fmt: on def get_random_object(random=random, depth=0): @@ -62,17 +64,15 @@ def get_random_object(random=random, depth=0): else: return -1 * random.randint(0, MAXINT) n = random.randint(0, 100) - return bytes([random.randint(32, 126) for _ in range(n)]) + return bytes(random.randint(32, 126) for _ in range(n)) class Test_Format(unittest.TestCase): - def test_roundtrip_format_examples(self): for data, expect in FORMAT_EXAMPLES.items(): self.assertEqual(expect, tnetstring.loads(data)) - self.assertEqual( - expect, tnetstring.loads(tnetstring.dumps(expect))) - self.assertEqual((expect, b''), tnetstring.pop(data)) + self.assertEqual(expect, tnetstring.loads(tnetstring.dumps(expect))) + self.assertEqual((expect, b""), tnetstring.pop(data)) def test_roundtrip_format_random(self): for _ in range(10): @@ -84,7 +84,7 @@ def test_roundtrip_format_unicode(self): for _ in range(10): v = get_random_object() self.assertEqual(v, tnetstring.loads(tnetstring.dumps(v))) - self.assertEqual((v, b''), tnetstring.pop(tnetstring.dumps(v))) + self.assertEqual((v, b""), tnetstring.pop(tnetstring.dumps(v))) def test_roundtrip_big_integer(self): i1 = math.factorial(30000) @@ -94,39 +94,38 @@ def test_roundtrip_big_integer(self): class Test_FileLoading(unittest.TestCase): - def test_roundtrip_file_examples(self): for data, expect in FORMAT_EXAMPLES.items(): s = io.BytesIO() s.write(data) - s.write(b'OK') + s.write(b"OK") s.seek(0) self.assertEqual(expect, tnetstring.load(s)) - self.assertEqual(b'OK', s.read()) + self.assertEqual(b"OK", s.read()) s = io.BytesIO() tnetstring.dump(expect, s) - s.write(b'OK') + s.write(b"OK") s.seek(0) self.assertEqual(expect, tnetstring.load(s)) - self.assertEqual(b'OK', s.read()) + self.assertEqual(b"OK", s.read()) def test_roundtrip_file_random(self): for _ in range(10): v = get_random_object() s = io.BytesIO() tnetstring.dump(v, s) - s.write(b'OK') + s.write(b"OK") s.seek(0) self.assertEqual(v, tnetstring.load(s)) - self.assertEqual(b'OK', s.read()) + self.assertEqual(b"OK", s.read()) def test_error_on_absurd_lengths(self): s = io.BytesIO() - s.write(b'1000000000:pwned!,') + s.write(b"1000000000000:pwned!,") s.seek(0) with self.assertRaises(ValueError): tnetstring.load(s) - self.assertEqual(s.read(1), b':') + self.assertEqual(s.read(1), b":") def suite(): diff --git a/test/mitmproxy/net/data/verificationcerts/generate.py b/test/mitmproxy/net/data/verificationcerts/generate.py index 829e1a7cbe..599e2c0dd5 100644 --- a/test/mitmproxy/net/data/verificationcerts/generate.py +++ b/test/mitmproxy/net/data/verificationcerts/generate.py @@ -22,61 +22,70 @@ def genrsa(cert: str): do(f"openssl genrsa -out {cert}.key 2048") -def sign(cert: str, subject: str): +def sign(cert: str, subject: str, ip: bool): with open(f"openssl-{cert}.conf", "w") as f: - f.write(textwrap.dedent(f""" + f.write( + textwrap.dedent( + f""" authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, keyEncipherment - subjectAltName = DNS:{subject} - """)) - do(f"openssl x509 -req -in {cert}.csr " - f"-CA {ROOT_CA}.crt " - f"-CAkey {ROOT_CA}.key " - f"-CAcreateserial " - f"-days 7300 " - f"-sha256 " - f"-extfile \"openssl-{cert}.conf\" " - f"-out {cert}.crt" - ) + subjectAltName = {"IP" if ip else "DNS" }:{subject} + """ + ) + ) + do( + f"openssl x509 -req -in {cert}.csr " + f"-CA {ROOT_CA}.crt " + f"-CAkey {ROOT_CA}.key " + f"-CAcreateserial " + f"-days 7300 " + f"-sha256 " + f'-extfile "openssl-{cert}.conf" ' + f"-out {cert}.crt" + ) os.remove(f"openssl-{cert}.conf") -def mkcert(cert, subject): +def mkcert(cert, subject, ip: bool): genrsa(cert) - do(f"openssl req -new -nodes -batch " - f"-key {cert}.key " - f"-subj /CN={subject}/O=mitmproxy " - f"-addext \"subjectAltName = DNS:{subject}\" " - f"-out {cert}.csr" - ) - sign(cert, subject) + do( + f"openssl req -new -nodes -batch " + f"-key {cert}.key " + f"-subj /CN={subject}/O=mitmproxy " + f'-addext "subjectAltName = {"IP" if ip else "DNS" }:{subject}" ' + f"-out {cert}.csr" + ) + sign(cert, subject, ip) os.remove(f"{cert}.csr") # create trusted root CA genrsa("trusted-root") -do("openssl req -x509 -new -nodes -batch " - "-key trusted-root.key " - "-days 7300 " - "-out trusted-root.crt" - ) +do( + "openssl req -x509 -new -nodes -batch " + "-key trusted-root.key " + "-days 7300 " + "-out trusted-root.crt" +) h = do("openssl x509 -hash -noout -in trusted-root.crt").decode("ascii").strip() shutil.copyfile("trusted-root.crt", f"{h}.0") # create trusted leaf cert. -mkcert("trusted-leaf", SUBJECT) +mkcert("trusted-leaf", SUBJECT, False) +mkcert("trusted-leaf-ip", "192.0.2.42", True) # create self-signed cert genrsa("self-signed") -do("openssl req -x509 -new -nodes -batch " - "-key self-signed.key " - f'-addext "subjectAltName = DNS:{SUBJECT}" ' - "-days 7300 " - "-out self-signed.crt" - ) +do( + "openssl req -x509 -new -nodes -batch " + "-key self-signed.key " + f'-addext "subjectAltName = DNS:{SUBJECT}" ' + "-days 7300 " + "-out self-signed.crt" +) -for x in ["self-signed", "trusted-leaf", "trusted-root"]: +for x in ["self-signed", "trusted-leaf", "trusted-leaf-ip", "trusted-root"]: with open(f"{x}.crt") as crt, open(f"{x}.key") as key, open(f"{x}.pem", "w") as pem: pem.write(crt.read()) pem.write(key.read()) diff --git a/test/mitmproxy/net/data/verificationcerts/trusted-leaf-ip.crt b/test/mitmproxy/net/data/verificationcerts/trusted-leaf-ip.crt new file mode 100644 index 0000000000..7cd2705e52 --- /dev/null +++ b/test/mitmproxy/net/data/verificationcerts/trusted-leaf-ip.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDSDCCAjCgAwIBAgIUSWoLwl7Rc//TKPwKwv25Vhb6uFIwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA1MTExNDMwMzRaFw00MjA1 +MDYxNDMwMzRaMCkxEzARBgNVBAMMCjE5Mi4wLjIuNDIxEjAQBgNVBAoMCW1pdG1w +cm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALjlCKOWasirms/w +rzZugsh9BTrGU3XijxnmoSbRrju3BdKR2R+wZ/i/43gZYXkCaYNDXUCfTxzFVG1A +cWTFKuFFWx03JfsKV8Hx7p2mhNbMrQ0ZfTBMLKgWSN9J1G7qZTg7Q2J8jGfcoiBu +4wqpX6eWUBC42Rt8lD/FWHlprEuLLAzGB8sWbz3N3Db/UIlBnvSlFMH1XU0BEe+5 +/SfcW1l2T93HIUFJT903xdpw8h9ZiO0DdlSZabL+bEyXbtM2ucWuFTpcJknqQieS +ctbGcfkzrHU1pJV6WSk8ObL99McYnUjf2DJKC7KplSAkddC7OAG1Oqz3/jX3uIlO +sww7iZ8CAwEAAaNMMEowHwYDVR0jBBgwFoAU5A4+ulpmn/WD9GDKhihudT3o8Vsw +CQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwDwYDVR0RBAgwBocEwAACKjANBgkqhkiG +9w0BAQsFAAOCAQEAbywFzAgUiGfJbUZ8Iqft+CS39mlgaOY6DniArKo95FSSWMME +x/cGrmYSF8KWfBJacumWxgdozwOeCw8EyaYoVarMOz/OOWtlJwHue0vXlyPg2Wob +LPeaCpO1QYnSAAgtm/ANraXeOKh4nKw1A1VYJogCJkEZ6Y+Ib82PLbNjqzgTRvLB +FvCUZeS6MCyFz77SbGEBJFtLRI2jB9ViytV72sWYTd4H9BAhiZSkOtsY9Szhtdma +w5g7Rc1cTi4+eAz6aM/e4rvqkBBS2eignC+QcpX6ZKA7P2+rWiVc+K9kuQEVcWyY +Sa0GEB3q6GtRlPwoz4lY/xa2uvSwF0VgZxxbog== +-----END CERTIFICATE----- diff --git a/test/mitmproxy/net/data/verificationcerts/trusted-leaf-ip.key b/test/mitmproxy/net/data/verificationcerts/trusted-leaf-ip.key new file mode 100644 index 0000000000..d51aeea19e --- /dev/null +++ b/test/mitmproxy/net/data/verificationcerts/trusted-leaf-ip.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuOUIo5ZqyKuaz/CvNm6CyH0FOsZTdeKPGeahJtGuO7cF0pHZ +H7Bn+L/jeBlheQJpg0NdQJ9PHMVUbUBxZMUq4UVbHTcl+wpXwfHunaaE1sytDRl9 +MEwsqBZI30nUbuplODtDYnyMZ9yiIG7jCqlfp5ZQELjZG3yUP8VYeWmsS4ssDMYH +yxZvPc3cNv9QiUGe9KUUwfVdTQER77n9J9xbWXZP3cchQUlP3TfF2nDyH1mI7QN2 +VJlpsv5sTJdu0za5xa4VOlwmSepCJ5Jy1sZx+TOsdTWklXpZKTw5sv30xxidSN/Y +MkoLsqmVICR10Ls4AbU6rPf+Nfe4iU6zDDuJnwIDAQABAoIBADqvLzPE7TWuCeAQ +E3yiTM5XqA5Enn7fHu5oniOVD8kSST3RXunI8ucn+InI/IAM/PJVskZtig2msCpQ +9uy2C+seOVIni92HJd1/7W2KScVnh1GOEob+1nmvQQfmBhACQ4g6fyPGRkY86BSF +PXjH0318nwL/uKEZxHANMgyvNqlvA7s8PffNQo1medsRzwkHwnvACf5l0CUV+eQk +KmGSkeXNH8V0HOwPvaDsfgpxfV89FNVBczvTJKKKlO8PFESkTcwiZpnmsYiba1op +vW4L0k0OMyQ+l8GxWQZV6QnjH9go/gGVNGWDW0vQFcjo1d2NeVxgM8fWaQ6KzAgE +WCHqo3ECgYEA2r9nSJn2JTwYDMaMC3ft2cKVlZGaY6kKo7ZYfmeuRp/IIjSjgwe5 +Zv1lVJZ/mxnD4tmKj6ebfiWtSXFpilgOYx6Jpybw2u1xFAqvein0V1sYgOuaSEz/ +LvciXdqyiPXjoiknJZE/TYiXRXQpZDiQ/EWcTUQQwpSnA5+jZidQKOkCgYEA2GHD +Z3Q747n/EkKV+Xq9dRUVS+/Ol/whEy/kMKpV434KN1O0ZOiV6DOYUQJgU9/h4Mbb +adRCWrBjtsq8oFDDzwX0Ki+pCKQHBRxYDcjN0RvamsakHF5sYzGcCeaLUquZNTsg +dY1qSQLOo7FD4Vur4J1lgzYY4Y4X5wFFAH0QCUcCgYEAxpIhzAIXM83Ndyt1TaPc +wmSlLVUzdWyqP9rzkivERFAfeQ2XsQZ+A0PbjGHiDIXjEDayVZ2sxWKmX5kYWYF9 +7fR2uMncsqAAmlTo3ljfeb00DTPSpfdfXt7wz4oLr9CmhzocUzn64QMxbtb4DAZd +duQp8unq3PfcdKmhxsXBOqECgYEAuVKyCy8QBDDO95Kz5GJtVZPjE5Cl/qHgqhBA +fjXFLfxLP6ufOzXA/okCEY/ZdLyxNtTaIz+6PPYJ0Qq+lwfVTMAqqN79BPuHT6dA ++z1amZgjmKA8+lccubBJlmkwNnPl2iNz33po53NSC/zMyHy9LrlfsgtpL/WFH0KF +GLAERg0CgYA71sy1rn8oolWTsc/jD7jXfabqWZ7nrJoK1mx0JNWjbCwQsEgZ2Lrn +BVoaHpmNlAIfQEJNl3rIZHbfI6z6c1n3I8WNzGkfgt8/X+xylh8fs/ThxrWP9qyQ +SO+SlDTLhP9ovoIJbUZeiXj150v/ULQb67J9wrqEPA+sdZDOE+cmRQ== +-----END RSA PRIVATE KEY----- diff --git a/test/mitmproxy/net/data/verificationcerts/trusted-leaf-ip.pem b/test/mitmproxy/net/data/verificationcerts/trusted-leaf-ip.pem new file mode 100644 index 0000000000..718c74242c --- /dev/null +++ b/test/mitmproxy/net/data/verificationcerts/trusted-leaf-ip.pem @@ -0,0 +1,47 @@ +-----BEGIN CERTIFICATE----- +MIIDSDCCAjCgAwIBAgIUSWoLwl7Rc//TKPwKwv25Vhb6uFIwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA1MTExNDMwMzRaFw00MjA1 +MDYxNDMwMzRaMCkxEzARBgNVBAMMCjE5Mi4wLjIuNDIxEjAQBgNVBAoMCW1pdG1w +cm94eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALjlCKOWasirms/w +rzZugsh9BTrGU3XijxnmoSbRrju3BdKR2R+wZ/i/43gZYXkCaYNDXUCfTxzFVG1A +cWTFKuFFWx03JfsKV8Hx7p2mhNbMrQ0ZfTBMLKgWSN9J1G7qZTg7Q2J8jGfcoiBu +4wqpX6eWUBC42Rt8lD/FWHlprEuLLAzGB8sWbz3N3Db/UIlBnvSlFMH1XU0BEe+5 +/SfcW1l2T93HIUFJT903xdpw8h9ZiO0DdlSZabL+bEyXbtM2ucWuFTpcJknqQieS +ctbGcfkzrHU1pJV6WSk8ObL99McYnUjf2DJKC7KplSAkddC7OAG1Oqz3/jX3uIlO +sww7iZ8CAwEAAaNMMEowHwYDVR0jBBgwFoAU5A4+ulpmn/WD9GDKhihudT3o8Vsw +CQYDVR0TBAIwADALBgNVHQ8EBAMCBaAwDwYDVR0RBAgwBocEwAACKjANBgkqhkiG +9w0BAQsFAAOCAQEAbywFzAgUiGfJbUZ8Iqft+CS39mlgaOY6DniArKo95FSSWMME +x/cGrmYSF8KWfBJacumWxgdozwOeCw8EyaYoVarMOz/OOWtlJwHue0vXlyPg2Wob +LPeaCpO1QYnSAAgtm/ANraXeOKh4nKw1A1VYJogCJkEZ6Y+Ib82PLbNjqzgTRvLB +FvCUZeS6MCyFz77SbGEBJFtLRI2jB9ViytV72sWYTd4H9BAhiZSkOtsY9Szhtdma +w5g7Rc1cTi4+eAz6aM/e4rvqkBBS2eignC+QcpX6ZKA7P2+rWiVc+K9kuQEVcWyY +Sa0GEB3q6GtRlPwoz4lY/xa2uvSwF0VgZxxbog== +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAuOUIo5ZqyKuaz/CvNm6CyH0FOsZTdeKPGeahJtGuO7cF0pHZ +H7Bn+L/jeBlheQJpg0NdQJ9PHMVUbUBxZMUq4UVbHTcl+wpXwfHunaaE1sytDRl9 +MEwsqBZI30nUbuplODtDYnyMZ9yiIG7jCqlfp5ZQELjZG3yUP8VYeWmsS4ssDMYH +yxZvPc3cNv9QiUGe9KUUwfVdTQER77n9J9xbWXZP3cchQUlP3TfF2nDyH1mI7QN2 +VJlpsv5sTJdu0za5xa4VOlwmSepCJ5Jy1sZx+TOsdTWklXpZKTw5sv30xxidSN/Y +MkoLsqmVICR10Ls4AbU6rPf+Nfe4iU6zDDuJnwIDAQABAoIBADqvLzPE7TWuCeAQ +E3yiTM5XqA5Enn7fHu5oniOVD8kSST3RXunI8ucn+InI/IAM/PJVskZtig2msCpQ +9uy2C+seOVIni92HJd1/7W2KScVnh1GOEob+1nmvQQfmBhACQ4g6fyPGRkY86BSF +PXjH0318nwL/uKEZxHANMgyvNqlvA7s8PffNQo1medsRzwkHwnvACf5l0CUV+eQk +KmGSkeXNH8V0HOwPvaDsfgpxfV89FNVBczvTJKKKlO8PFESkTcwiZpnmsYiba1op +vW4L0k0OMyQ+l8GxWQZV6QnjH9go/gGVNGWDW0vQFcjo1d2NeVxgM8fWaQ6KzAgE +WCHqo3ECgYEA2r9nSJn2JTwYDMaMC3ft2cKVlZGaY6kKo7ZYfmeuRp/IIjSjgwe5 +Zv1lVJZ/mxnD4tmKj6ebfiWtSXFpilgOYx6Jpybw2u1xFAqvein0V1sYgOuaSEz/ +LvciXdqyiPXjoiknJZE/TYiXRXQpZDiQ/EWcTUQQwpSnA5+jZidQKOkCgYEA2GHD +Z3Q747n/EkKV+Xq9dRUVS+/Ol/whEy/kMKpV434KN1O0ZOiV6DOYUQJgU9/h4Mbb +adRCWrBjtsq8oFDDzwX0Ki+pCKQHBRxYDcjN0RvamsakHF5sYzGcCeaLUquZNTsg +dY1qSQLOo7FD4Vur4J1lgzYY4Y4X5wFFAH0QCUcCgYEAxpIhzAIXM83Ndyt1TaPc +wmSlLVUzdWyqP9rzkivERFAfeQ2XsQZ+A0PbjGHiDIXjEDayVZ2sxWKmX5kYWYF9 +7fR2uMncsqAAmlTo3ljfeb00DTPSpfdfXt7wz4oLr9CmhzocUzn64QMxbtb4DAZd +duQp8unq3PfcdKmhxsXBOqECgYEAuVKyCy8QBDDO95Kz5GJtVZPjE5Cl/qHgqhBA +fjXFLfxLP6ufOzXA/okCEY/ZdLyxNtTaIz+6PPYJ0Qq+lwfVTMAqqN79BPuHT6dA ++z1amZgjmKA8+lccubBJlmkwNnPl2iNz33po53NSC/zMyHy9LrlfsgtpL/WFH0KF +GLAERg0CgYA71sy1rn8oolWTsc/jD7jXfabqWZ7nrJoK1mx0JNWjbCwQsEgZ2Lrn +BVoaHpmNlAIfQEJNl3rIZHbfI6z6c1n3I8WNzGkfgt8/X+xylh8fs/ThxrWP9qyQ +SO+SlDTLhP9ovoIJbUZeiXj150v/ULQb67J9wrqEPA+sdZDOE+cmRQ== +-----END RSA PRIVATE KEY----- diff --git a/test/mitmproxy/net/data/verificationcerts/trusted-root.srl b/test/mitmproxy/net/data/verificationcerts/trusted-root.srl index cf0f9c442b..fb3552b371 100644 --- a/test/mitmproxy/net/data/verificationcerts/trusted-root.srl +++ b/test/mitmproxy/net/data/verificationcerts/trusted-root.srl @@ -1 +1 @@ -496A0BC25ED173FFD328FC0AC2FDB95616FAB850 +496A0BC25ED173FFD328FC0AC2FDB95616FAB852 diff --git a/test/mitmproxy/net/dns/__init__.py b/test/mitmproxy/net/dns/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/mitmproxy/net/dns/test_classes.py b/test/mitmproxy/net/dns/test_classes.py new file mode 100644 index 0000000000..81c4e10ea0 --- /dev/null +++ b/test/mitmproxy/net/dns/test_classes.py @@ -0,0 +1,7 @@ +from mitmproxy.net.dns import classes + + +def test_simple(): + assert classes.IN == 1 + assert classes.to_str(classes.IN) == "IN" + assert classes.to_str(0) == "CLASS(0)" diff --git a/test/mitmproxy/net/dns/test_domain_names.py b/test/mitmproxy/net/dns/test_domain_names.py new file mode 100644 index 0000000000..72e6e5391b --- /dev/null +++ b/test/mitmproxy/net/dns/test_domain_names.py @@ -0,0 +1,69 @@ +import re +import struct +import pytest + +from mitmproxy.net.dns import domain_names + + +def test_unpack_from_with_compression(): + assert domain_names.unpack_from_with_compression( + b"\xFF\x03www\x07example\x03org\x00", 1, domain_names.cache() + ) == ("www.example.org", 17) + with pytest.raises( + struct.error, match=re.escape("unpack encountered domain name loop") + ): + domain_names.unpack_from_with_compression( + b"\x03www\xc0\x00", 0, domain_names.cache() + ) + assert ( + domain_names.unpack_from_with_compression( + b"\xFF\xFF\xFF\x07example\x03org\x00\xFF\xFF\xFF\x03www\xc0\x03", + 19, + domain_names.cache(), + ) + == ("www.example.org", 6) + ) + + +def test_unpack(): + assert domain_names.unpack(b"\x03www\x07example\x03org\x00") == "www.example.org" + with pytest.raises( + struct.error, match=re.escape("unpack requires a buffer of 17 bytes") + ): + domain_names.unpack(b"\x03www\x07example\x03org\x00\xFF") + with pytest.raises( + struct.error, + match=re.escape("unpack encountered a pointer which is not supported in RDATA"), + ): + domain_names.unpack(b"\x03www\x07example\x03org\xc0\x00") + with pytest.raises( + struct.error, match=re.escape("unpack requires a label buffer of 10 bytes") + ): + domain_names.unpack(b"\x0a") + with pytest.raises( + struct.error, match=re.escape("unpack encountered a label of length 64") + ): + domain_names.unpack(b"\x40" + (b"a" * 64) + b"\x00") + with pytest.raises( + struct.error, + match=re.escape("unpack encountered a illegal characters at offset 1"), + ): + domain_names.unpack(b"\x03\xff\xff\xff\00") + + +def test_pack(): + assert domain_names.pack("") == b"\x00" + with pytest.raises( + ValueError, match=re.escape("domain name 'hello..world' contains empty labels") + ): + domain_names.pack("hello..world") + label = "a" * 64 + name = f"www.{label}.com" + with pytest.raises( + ValueError, + match=re.escape( + "encoding with 'idna' codec failed (UnicodeError: label too long)" + ), + ): + domain_names.pack(name) + assert domain_names.pack("www.example.org") == b"\x03www\x07example\x03org\x00" diff --git a/test/mitmproxy/net/dns/test_op_codes.py b/test/mitmproxy/net/dns/test_op_codes.py new file mode 100644 index 0000000000..f2641a58a6 --- /dev/null +++ b/test/mitmproxy/net/dns/test_op_codes.py @@ -0,0 +1,7 @@ +from mitmproxy.net.dns import op_codes + + +def test_simple(): + assert op_codes.QUERY == 0 + assert op_codes.to_str(op_codes.QUERY) == "QUERY" + assert op_codes.to_str(100) == "OPCODE(100)" diff --git a/test/mitmproxy/net/dns/test_response_codes.py b/test/mitmproxy/net/dns/test_response_codes.py new file mode 100644 index 0000000000..28eef77bb5 --- /dev/null +++ b/test/mitmproxy/net/dns/test_response_codes.py @@ -0,0 +1,8 @@ +from mitmproxy.net.dns import response_codes + + +def test_simple(): + assert response_codes.NOERROR == 0 + assert response_codes.to_str(response_codes.NOERROR) == "NOERROR" + assert response_codes.to_str(100) == "RCODE(100)" + assert response_codes.http_equiv_status_code(response_codes.NOERROR) == 200 diff --git a/test/mitmproxy/net/dns/test_types.py b/test/mitmproxy/net/dns/test_types.py new file mode 100644 index 0000000000..21e65417a3 --- /dev/null +++ b/test/mitmproxy/net/dns/test_types.py @@ -0,0 +1,7 @@ +from mitmproxy.net.dns import types + + +def test_simple(): + assert types.A == 1 + assert types.to_str(types.A) == "A" + assert types.to_str(0) == "TYPE(0)" diff --git a/test/mitmproxy/net/http/http1/test_assemble.py b/test/mitmproxy/net/http/http1/test_assemble.py index 7d1a617f62..5d17e1bfb1 100644 --- a/test/mitmproxy/net/http/http1/test_assemble.py +++ b/test/mitmproxy/net/http/http1/test_assemble.py @@ -2,10 +2,15 @@ from mitmproxy.http import Headers from mitmproxy.net.http.http1.assemble import ( - assemble_request, assemble_request_head, assemble_response, - assemble_response_head, _assemble_request_line, _assemble_request_headers, + assemble_request, + assemble_request_head, + assemble_response, + assemble_response_head, + _assemble_request_line, + _assemble_request_headers, _assemble_response_headers, - assemble_body) + assemble_body, +) from mitmproxy.test.tutils import treq, tresp @@ -70,13 +75,25 @@ def test_assemble_body(): c = list(assemble_body(Headers(), [b"body"], Headers())) assert c == [b"body"] - c = list(assemble_body(Headers(transfer_encoding="chunked"), [b"123456789a", b""], Headers())) + c = list( + assemble_body( + Headers(transfer_encoding="chunked"), [b"123456789a", b""], Headers() + ) + ) assert c == [b"a\r\n123456789a\r\n", b"0\r\n\r\n"] - c = list(assemble_body(Headers(transfer_encoding="chunked"), [b"123456789a"], Headers())) + c = list( + assemble_body(Headers(transfer_encoding="chunked"), [b"123456789a"], Headers()) + ) assert c == [b"a\r\n123456789a\r\n", b"0\r\n\r\n"] - c = list(assemble_body(Headers(transfer_encoding="chunked"), [b"123456789a"], Headers(trailer="trailer"))) + c = list( + assemble_body( + Headers(transfer_encoding="chunked"), + [b"123456789a"], + Headers(trailer="trailer"), + ) + ) assert c == [b"a\r\n123456789a\r\n", b"0\r\ntrailer: trailer\r\n\r\n"] with pytest.raises(ValueError): @@ -90,7 +107,10 @@ def test_assemble_request_line(): assert _assemble_request_line(authority_request) == b"CONNECT address:22 HTTP/1.1" absolute_request = treq(scheme=b"http", authority=b"address:22").data - assert _assemble_request_line(absolute_request) == b"GET http://address:22/path HTTP/1.1" + assert ( + _assemble_request_line(absolute_request) + == b"GET http://address:22/path HTTP/1.1" + ) def test_assemble_request_headers(): diff --git a/test/mitmproxy/net/http/http1/test_read.py b/test/mitmproxy/net/http/http1/test_read.py index 576093896e..3f48a672e3 100644 --- a/test/mitmproxy/net/http/http1/test_read.py +++ b/test/mitmproxy/net/http/http1/test_read.py @@ -3,8 +3,14 @@ from mitmproxy.http import Headers from mitmproxy.net.http.http1.read import ( read_request_head, - read_response_head, connection_close, expected_http_body_size, - _read_request_line, _read_response_line, _read_headers, get_header_tokens + read_response_head, + connection_close, + expected_http_body_size, + _read_request_line, + _read_response_line, + _read_headers, + get_header_tokens, + validate_headers, ) from mitmproxy.test.tutils import treq, tresp @@ -59,50 +65,73 @@ def test_read_response_head(): assert r.content is None +def test_validate_headers(): + # both content-length and chunked (possible request smuggling) + with pytest.raises( + ValueError, + match="Received both a Transfer-Encoding and a Content-Length header", + ): + validate_headers( + Headers(transfer_encoding="chunked", content_length="42"), + ) + + with pytest.raises(ValueError, match="Received an invalid header name"): + validate_headers( + Headers([(b"content-length ", b"42")]), + ) + + def test_expected_http_body_size(): # Expect: 100-continue - assert expected_http_body_size( - treq(headers=Headers(expect="100-continue", content_length="42")), - ) == 42 + assert ( + expected_http_body_size( + treq(headers=Headers(expect="100-continue", content_length="42")), + ) + == 42 + ) # http://tools.ietf.org/html/rfc7230#section-3.3 - assert expected_http_body_size( - treq(method=b"HEAD"), - tresp(headers=Headers(content_length="42")) - ) == 0 - assert expected_http_body_size( - treq(method=b"CONNECT", headers=Headers()), - None, - ) == 0 - assert expected_http_body_size( - treq(method=b"CONNECT"), - tresp() - ) == 0 + assert ( + expected_http_body_size( + treq(method=b"HEAD"), tresp(headers=Headers(content_length="42")) + ) + == 0 + ) + assert ( + expected_http_body_size( + treq(method=b"CONNECT", headers=Headers()), + None, + ) + == 0 + ) + assert expected_http_body_size(treq(method=b"CONNECT"), tresp()) == 0 for code in (100, 204, 304): - assert expected_http_body_size( - treq(), - tresp(status_code=code) - ) == 0 + assert expected_http_body_size(treq(), tresp(status_code=code)) == 0 # chunked - assert expected_http_body_size( - treq(headers=Headers(transfer_encoding="chunked")), - ) is None - assert expected_http_body_size( - treq(headers=Headers(transfer_encoding="gzip,\tchunked")), - ) is None - # both content-length and chunked (possible request smuggling) - with pytest.raises(ValueError, match="Received both a Transfer-Encoding and a Content-Length header"): + assert ( + expected_http_body_size( + treq(headers=Headers(transfer_encoding="chunked")), + ) + is None + ) + assert ( expected_http_body_size( - treq(headers=Headers(transfer_encoding="chunked", content_length="42")), + treq(headers=Headers(transfer_encoding="gzip,\tchunked")), ) + is None + ) with pytest.raises(ValueError, match="Invalid transfer encoding"): expected_http_body_size( - treq(headers=Headers(transfer_encoding="chun\u212Aed")), # "chunKed".lower() == "chunked" + treq( + headers=Headers(transfer_encoding="chun\u212Aed") + ), # "chunKed".lower() == "chunked" ) with pytest.raises(ValueError, match="Unknown transfer encoding"): expected_http_body_size( - treq(headers=Headers(transfer_encoding="chun ked")), # "chunKed".lower() == "chunked" + treq( + headers=Headers(transfer_encoding="chun ked") + ), # "chunKed".lower() == "chunked" ) with pytest.raises(ValueError, match="Unknown transfer encoding"): expected_http_body_size( @@ -113,64 +142,87 @@ def test_expected_http_body_size(): expected_http_body_size( treq(headers=Headers(transfer_encoding="gzip")), ) - assert expected_http_body_size( - treq(), - tresp(headers=Headers(transfer_encoding="gzip")), - ) == -1 + assert ( + expected_http_body_size( + treq(), + tresp(headers=Headers(transfer_encoding="gzip")), + ) + == -1 + ) # explicit length for val in (b"foo", b"-7"): with pytest.raises(ValueError): - expected_http_body_size( - treq(headers=Headers(content_length=val)) - ) - assert expected_http_body_size( - treq(headers=Headers(content_length="42")) - ) == 42 + expected_http_body_size(treq(headers=Headers(content_length=val))) + assert expected_http_body_size(treq(headers=Headers(content_length="42"))) == 42 # multiple content-length headers with same value - assert expected_http_body_size( - treq(headers=Headers([(b'content-length', b'42'), (b'content-length', b'42')])) - ) == 42 + assert ( + expected_http_body_size( + treq( + headers=Headers( + [(b"content-length", b"42"), (b"content-length", b"42")] + ) + ) + ) + == 42 + ) # multiple content-length headers with conflicting value with pytest.raises(ValueError, match="Conflicting Content-Length headers"): expected_http_body_size( - treq(headers=Headers([(b'content-length', b'42'), (b'content-length', b'45')])) + treq( + headers=Headers( + [(b"content-length", b"42"), (b"content-length", b"45")] + ) + ) ) # non-int content-length with pytest.raises(ValueError, match="Invalid Content-Length header"): - expected_http_body_size( - treq(headers=Headers([(b'content-length', b'NaN')])) - ) + expected_http_body_size(treq(headers=Headers([(b"content-length", b"NaN")]))) # negative content-length with pytest.raises(ValueError, match="Negative Content-Length header"): - expected_http_body_size( - treq(headers=Headers([(b'content-length', b'-1')])) - ) + expected_http_body_size(treq(headers=Headers([(b"content-length", b"-1")]))) # no length - assert expected_http_body_size( - treq(headers=Headers()) - ) == 0 - assert expected_http_body_size( - treq(headers=Headers()), tresp(headers=Headers()) - ) == -1 + assert expected_http_body_size(treq(headers=Headers())) == 0 + assert ( + expected_http_body_size(treq(headers=Headers()), tresp(headers=Headers())) == -1 + ) def test_read_request_line(): def t(b): return _read_request_line(b) - assert (t(b"GET / HTTP/1.1") == - ("", 0, b"GET", b"", b"", b"/", b"HTTP/1.1")) - assert (t(b"OPTIONS * HTTP/1.1") == - ("", 0, b"OPTIONS", b"", b"", b"*", b"HTTP/1.1")) - assert (t(b"CONNECT foo:42 HTTP/1.1") == - ("foo", 42, b"CONNECT", b"", b"foo:42", b"", b"HTTP/1.1")) - assert (t(b"GET http://foo:42/bar HTTP/1.1") == - ("foo", 42, b"GET", b"http", b"foo:42", b"/bar", b"HTTP/1.1")) - assert (t(b"GET http://foo:42 HTTP/1.1") == - ("foo", 42, b"GET", b"http", b"foo:42", b"/", b"HTTP/1.1")) + assert t(b"GET / HTTP/1.1") == ("", 0, b"GET", b"", b"", b"/", b"HTTP/1.1") + assert t(b"OPTIONS * HTTP/1.1") == ("", 0, b"OPTIONS", b"", b"", b"*", b"HTTP/1.1") + assert t(b"CONNECT foo:42 HTTP/1.1") == ( + "foo", + 42, + b"CONNECT", + b"", + b"foo:42", + b"", + b"HTTP/1.1", + ) + assert t(b"GET http://foo:42/bar HTTP/1.1") == ( + "foo", + 42, + b"GET", + b"http", + b"foo:42", + b"/bar", + b"HTTP/1.1", + ) + assert t(b"GET http://foo:42 HTTP/1.1") == ( + "foo", + 42, + b"GET", + b"http", + b"foo:42", + b"/", + b"HTTP/1.1", + ) with pytest.raises(ValueError): t(b"GET / WTF/1.1") @@ -192,7 +244,11 @@ def t(b): assert t(b"HTTP/1.1 200") == (b"HTTP/1.1", 200, b"") # https://github.com/mitmproxy/mitmproxy/issues/784 - assert t(b"HTTP/1.1 200 Non-Autoris\xc3\xa9") == (b"HTTP/1.1", 200, b"Non-Autoris\xc3\xa9") + assert t(b"HTTP/1.1 200 Non-Autoris\xc3\xa9") == ( + b"HTTP/1.1", + 200, + b"Non-Autoris\xc3\xa9", + ) with pytest.raises(ValueError): assert t(b"HTTP/1.1") @@ -211,27 +267,17 @@ def _read(data): return _read_headers(data.splitlines(keepends=True)) def test_read_simple(self): - data = ( - b"Header: one\r\n" - b"Header2: two\r\n" - ) + data = b"Header: one\r\n" b"Header2: two\r\n" headers = self._read(data) assert headers.fields == ((b"Header", b"one"), (b"Header2", b"two")) def test_read_multi(self): - data = ( - b"Header: one\r\n" - b"Header: two\r\n" - ) + data = b"Header: one\r\n" b"Header: two\r\n" headers = self._read(data) assert headers.fields == ((b"Header", b"one"), (b"Header", b"two")) def test_read_continued(self): - data = ( - b"Header: one\r\n" - b"\ttwo\r\n" - b"Header2: three\r\n" - ) + data = b"Header: one\r\n" b"\ttwo\r\n" b"Header2: three\r\n" headers = self._read(data) assert headers.fields == ((b"Header", b"one\r\n two"), (b"Header2", b"three")) diff --git a/test/mitmproxy/net/http/test_cookies.py b/test/mitmproxy/net/http/test_cookies.py index 06cfe1d3f4..4b7f3dd652 100644 --- a/test/mitmproxy/net/http/test_cookies.py +++ b/test/mitmproxy/net/http/test_cookies.py @@ -6,50 +6,22 @@ cookie_pairs = [ + ["=uno", [["", "uno"]]], + ["", []], + ["one=uno", [["one", "uno"]]], + ["one", [["one", ""]]], + ["one=uno; two=due", [["one", "uno"], ["two", "due"]]], + ['one="uno"; two="\\due"', [["one", "uno"], ["two", "due"]]], + ['one="un\\"o"', [["one", 'un"o']]], + ['one="uno,due"', [["one", "uno,due"]]], + ["one=uno; two; three=tre", [["one", "uno"], ["two", ""], ["three", "tre"]]], [ - "=uno", - [["", "uno"]] - ], - [ - "", - [] - ], - [ - "one=uno", - [["one", "uno"]] - ], - [ - "one", - [["one", ""]] - ], - [ - "one=uno; two=due", - [["one", "uno"], ["two", "due"]] - ], - [ - 'one="uno"; two="\\due"', - [["one", "uno"], ["two", "due"]] - ], - [ - 'one="un\\"o"', - [["one", 'un"o']] - ], - [ - 'one="uno,due"', - [["one", 'uno,due']] - ], - [ - "one=uno; two; three=tre", - [["one", "uno"], ["two", ""], ["three", "tre"]] - ], - [ - "_lvs2=zHai1+Hq+Tc2vmc2r4GAbdOI5Jopg3EwsdUT9g=; " - "_rcc2=53VdltWl+Ov6ordflA==;", + "_lvs2=zHai1+Hq+Tc2vmc2r4GAbdOI5Jopg3EwsdUT9g=; " "_rcc2=53VdltWl+Ov6ordflA==;", [ ["_lvs2", "zHai1+Hq+Tc2vmc2r4GAbdOI5Jopg3EwsdUT9g="], - ["_rcc2", "53VdltWl+Ov6ordflA=="] - ] - ] + ["_rcc2", "53VdltWl+Ov6ordflA=="], + ], + ], ] @@ -72,8 +44,8 @@ def test_read_quoted_string(): [('"foo" x', 0), ("foo", 5)], [('"f\\oo" x', 0), ("foo", 6)], [(r'"f\\o" x', 0), (r"f\o", 6)], - [(r'"f\\" x', 0), (r"f" + '\\', 5)], - [('"fo\\\"" x', 0), ("fo\"", 6)], + [(r'"f\\" x', 0), (r"f" + "\\", 5)], + [('"fo\\"" x', 0), ('fo"', 6)], [('"foo" x', 7), ("", 8)], ] for q, a in tokens: @@ -82,38 +54,17 @@ def test_read_quoted_string(): def test_read_cookie_pairs(): vals = [ - [ - "=uno", - [["", "uno"]] - ], - [ - "one", - [["one", ""]] - ], - [ - "one=two", - [["one", "two"]] - ], - [ - "one=", - [["one", ""]] - ], - [ - 'one="two"', - [["one", "two"]] - ], - [ - 'one="two"; three=four', - [["one", "two"], ["three", "four"]] - ], + ["=uno", [["", "uno"]]], + ["one", [["one", ""]]], + ["one=two", [["one", "two"]]], + ["one=", [["one", ""]]], + ['one="two"', [["one", "two"]]], + ['one="two"; three=four', [["one", "two"], ["three", "four"]]], [ 'one="two"; three=four; five', - [["one", "two"], ["three", "four"], ["five", ""]] - ], - [ - 'one="\\"two"; three=four', - [["one", '"two'], ["three", "four"]] + [["one", "two"], ["three", "four"], ["five", ""]], ], + ['one="\\"two"; three=four', [["one", '"two'], ["three", "four"]]], ] for s, lst in vals: ret, off = cookies._read_cookie_pairs(s) @@ -142,74 +93,39 @@ def test_cookie_roundtrips(): def test_parse_set_cookie_pairs(): pairs = [ - [ - "=", - [[ - ["", ""] - ]] - ], - [ - "=;foo=bar", - [[ - ["", ""], - ["foo", "bar"] - ]] - ], - [ - "=;=;foo=bar", - [[ - ["", ""], - ["", ""], - ["foo", "bar"] - ]] - ], - [ - "=uno", - [[ - ["", "uno"] - ]] - ], - [ - "one=uno", - [[ - ["one", "uno"] - ]] - ], - [ - "one=un\x20", - [[ - ["one", "un\x20"] - ]] - ], - [ - "one=uno; foo", - [[ - ["one", "uno"], - ["foo", ""] - ]] - ], + ["=", [[["", ""]]]], + ["=;foo=bar", [[["", ""], ["foo", "bar"]]]], + ["=;=;foo=bar", [[["", ""], ["", ""], ["foo", "bar"]]]], + ["=uno", [[["", "uno"]]]], + ["one=uno", [[["one", "uno"]]]], + ["one=un\x20", [[["one", "un\x20"]]]], + ["one=uno; foo", [[["one", "uno"], ["foo", ""]]]], [ "mun=1.390.f60; " "expires=sun, 11-oct-2015 12:38:31 gmt; path=/; " "domain=b.aol.com", - [[ - ["mun", "1.390.f60"], - ["expires", "sun, 11-oct-2015 12:38:31 gmt"], - ["path", "/"], - ["domain", "b.aol.com"] - ]] + [ + [ + ["mun", "1.390.f60"], + ["expires", "sun, 11-oct-2015 12:38:31 gmt"], + ["path", "/"], + ["domain", "b.aol.com"], + ] + ], ], [ - r'rpb=190%3d1%2616726%3d1%2634832%3d1%2634874%3d1; ' - 'domain=.rubiconproject.com; ' - 'expires=mon, 11-may-2015 21:54:57 gmt; ' - 'path=/', - [[ - ['rpb', r'190%3d1%2616726%3d1%2634832%3d1%2634874%3d1'], - ['domain', '.rubiconproject.com'], - ['expires', 'mon, 11-may-2015 21:54:57 gmt'], - ['path', '/'] - ]] + r"rpb=190%3d1%2616726%3d1%2634832%3d1%2634874%3d1; " + "domain=.rubiconproject.com; " + "expires=mon, 11-may-2015 21:54:57 gmt; " + "path=/", + [ + [ + ["rpb", r"190%3d1%2616726%3d1%2634832%3d1%2634874%3d1"], + ["domain", ".rubiconproject.com"], + ["expires", "mon, 11-may-2015 21:54:57 gmt"], + ["path", "/"], + ] + ], ], ] for s, expected in pairs: @@ -228,35 +144,14 @@ def set_cookie_equal(obs, exp): assert obs[2].items(multi=True) == exp[2] vals = [ - [ - "", [] - ], - [ - ";", [] - ], - [ - "=uno", - [ - ("", "uno", ()) - ] - ], - [ - "one=uno", - [ - ("one", "uno", ()) - ] - ], - [ - "one=uno; foo=bar", - [ - ("one", "uno", (("foo", "bar"),)) - ] - ], + ["", []], + [";", []], + ["=uno", [("", "uno", ())]], + ["one=uno", [("one", "uno", ())]], + ["one=uno; foo=bar", [("one", "uno", (("foo", "bar"),))]], [ "one=uno; foo=bar; foo=baz", - [ - ("one", "uno", (("foo", "bar"), ("foo", "baz"))) - ] + [("one", "uno", (("foo", "bar"), ("foo", "baz")))], ], # Comma Separated Variant of Set-Cookie Headers [ @@ -264,27 +159,27 @@ def set_cookie_equal(obs, exp): [ ("foo", "bar", ()), ("doo", "dar", ()), - ] + ], ], [ "foo=bar; path=/, doo=dar; roo=rar; zoo=zar", [ ("foo", "bar", (("path", "/"),)), ("doo", "dar", (("roo", "rar"), ("zoo", "zar"))), - ] + ], ], [ "foo=bar; expires=Mon, 24 Aug 2037", [ ("foo", "bar", (("expires", "Mon, 24 Aug 2037"),)), - ] + ], ], [ "foo=bar; expires=Mon, 24 Aug 2037 00:00:00 GMT, doo=dar", [ ("foo", "bar", (("expires", "Mon, 24 Aug 2037 00:00:00 GMT"),)), ("doo", "dar", ()), - ] + ], ], ] for s, expected in vals: @@ -332,7 +227,7 @@ def test_refresh_cookie(): assert cookies.refresh_set_cookie_header(c, 60) == "" -@mock.patch('time.time') +@mock.patch("time.time") def test_get_expiration_ts(*args): # Freeze time now_ts = 17 @@ -359,11 +254,15 @@ def test_is_expired(): assert cookies.is_expired(CA([("Max-Age", "0")])) # or both - assert cookies.is_expired(CA([("Expires", "Thu, 01-Jan-1970 00:00:00 GMT"), ("Max-Age", "0")])) + assert cookies.is_expired( + CA([("Expires", "Thu, 01-Jan-1970 00:00:00 GMT"), ("Max-Age", "0")]) + ) assert not cookies.is_expired(CA([("Expires", "Mon, 24-Aug-2037 00:00:00 GMT")])) assert not cookies.is_expired(CA([("Max-Age", "1")])) - assert not cookies.is_expired(CA([("Expires", "Wed, 15-Jul-2037 00:00:00 GMT"), ("Max-Age", "1")])) + assert not cookies.is_expired( + CA([("Expires", "Wed, 15-Jul-2037 00:00:00 GMT"), ("Max-Age", "1")]) + ) assert not cookies.is_expired(CA([("Max-Age", "nan")])) assert not cookies.is_expired(CA([("Expires", "false")])) @@ -374,38 +273,28 @@ def test_group_cookies(): groups = [ [ "one=uno; foo=bar; foo=baz", - [ - ('one', 'uno', CA([])), - ('foo', 'bar', CA([])), - ('foo', 'baz', CA([])) - ] + [("one", "uno", CA([])), ("foo", "bar", CA([])), ("foo", "baz", CA([]))], ], [ "one=uno; Path=/; foo=bar; Max-Age=0; foo=baz; expires=24-08-1993", [ - ('one', 'uno', CA([('Path', '/')])), - ('foo', 'bar', CA([('Max-Age', '0')])), - ('foo', 'baz', CA([('expires', '24-08-1993')])) - ] - ], - [ - "one=uno;", - [ - ('one', 'uno', CA([])) - ] + ("one", "uno", CA([("Path", "/")])), + ("foo", "bar", CA([("Max-Age", "0")])), + ("foo", "baz", CA([("expires", "24-08-1993")])), + ], ], + ["one=uno;", [("one", "uno", CA([]))]], [ "one=uno; Path=/; Max-Age=0; Expires=24-08-1993", [ - ('one', 'uno', CA([('Path', '/'), ('Max-Age', '0'), ('Expires', '24-08-1993')])) - ] + ( + "one", + "uno", + CA([("Path", "/"), ("Max-Age", "0"), ("Expires", "24-08-1993")]), + ) + ], ], - [ - "path=val; Path=/", - [ - ('path', 'val', CA([('Path', '/')])) - ] - ] + ["path=val; Path=/", [("path", "val", CA([("Path", "/")]))]], ] for c, expected in groups: diff --git a/test/mitmproxy/net/http/test_headers.py b/test/mitmproxy/net/http/test_headers.py index 3214639a13..b7dff51d98 100644 --- a/test/mitmproxy/net/http/test_headers.py +++ b/test/mitmproxy/net/http/test_headers.py @@ -9,12 +9,18 @@ def test_parse_content_type(): assert p("text") is None v = p("text/html; charset=UTF-8") - assert v == ('text', 'html', {'charset': 'UTF-8'}) + assert v == ("text", "html", {"charset": "UTF-8"}) def test_assemble_content_type(): p = assemble_content_type assert p("text", "html", {}) == "text/html" assert p("text", "html", {"charset": "utf8"}) == "text/html; charset=utf8" - assert p("text", "html", - collections.OrderedDict([("charset", "utf8"), ("foo", "bar")])) == "text/html; charset=utf8; foo=bar" + assert ( + p( + "text", + "html", + collections.OrderedDict([("charset", "utf8"), ("foo", "bar")]), + ) + == "text/html; charset=utf8; foo=bar" + ) diff --git a/test/mitmproxy/net/http/test_multipart.py b/test/mitmproxy/net/http/test_multipart.py index c314d9340d..1045d70c60 100644 --- a/test/mitmproxy/net/http/test_multipart.py +++ b/test/mitmproxy/net/http/test_multipart.py @@ -5,48 +5,46 @@ def test_decode(): - boundary = 'somefancyboundary' + boundary = "somefancyboundary" content = ( "--{0}\n" - "Content-Disposition: form-data; name=\"field1\"\n\n" + 'Content-Disposition: form-data; name="field1"\n\n' "value1\n" "--{0}\n" - "Content-Disposition: form-data; name=\"field2\"\n\n" + 'Content-Disposition: form-data; name="field2"\n\n' "value2\n" "--{0}--".format(boundary).encode() ) - form = multipart.decode(f'multipart/form-data; boundary={boundary}', content) + form = multipart.decode(f"multipart/form-data; boundary={boundary}", content) assert len(form) == 2 assert form[0] == (b"field1", b"value1") assert form[1] == (b"field2", b"value2") - boundary = 'boundary茅莽' - result = multipart.decode(f'multipart/form-data; boundary={boundary}', content) + boundary = "boundary茅莽" + result = multipart.decode(f"multipart/form-data; boundary={boundary}", content) assert result == [] assert multipart.decode("", content) == [] def test_encode(): - data = [(b"file", b"shell.jpg"), - (b"file_size", b"1000")] - headers = Headers( - content_type='multipart/form-data; boundary=127824672498' - ) + data = [(b"file", b"shell.jpg"), (b"file_size", b"1000")] + headers = Headers(content_type="multipart/form-data; boundary=127824672498") content = multipart.encode(headers, data) assert b'Content-Disposition: form-data; name="file"' in content - assert b'Content-Type: text/plain; charset=utf-8\r\n\r\nshell.jpg\r\n\r\n--127824672498\r\n' in content - assert b'1000\r\n\r\n--127824672498--\r\n' + assert ( + b"Content-Type: text/plain; charset=utf-8\r\n\r\nshell.jpg\r\n\r\n--127824672498\r\n" + in content + ) + assert b"1000\r\n\r\n--127824672498--\r\n" assert len(content) == 252 with pytest.raises(ValueError, match=r"boundary found in encoded string"): multipart.encode(headers, [(b"key", b"--127824672498")]) - boundary = 'boundary茅莽' - headers = Headers( - content_type='multipart/form-data; boundary=' + boundary - ) + boundary = "boundary茅莽" + headers = Headers(content_type="multipart/form-data; boundary=" + boundary) result = multipart.encode(headers, data) - assert result == b'' + assert result == b"" diff --git a/test/mitmproxy/net/http/test_url.py b/test/mitmproxy/net/http/test_url.py index b444f81a6b..ca6e332b82 100644 --- a/test/mitmproxy/net/http/test_url.py +++ b/test/mitmproxy/net/http/test_url.py @@ -48,17 +48,22 @@ def test_parse(): url.parse("http://foo\0") # Invalid IPv6 URL - see http://www.ietf.org/rfc/rfc2732.txt with pytest.raises(ValueError): - url.parse('http://lo[calhost') + url.parse("http://lo[calhost") def test_ascii_check(): - test_url = "https://xyz.tax-edu.net?flag=selectCourse&lc_id=42825&lc_name=茅莽莽猫氓猫氓".encode() + test_url = ( + "https://xyz.tax-edu.net?flag=selectCourse&lc_id=42825&lc_name=茅莽莽猫氓猫氓".encode() + ) scheme, host, port, full_path = url.parse(test_url) - assert scheme == b'https' - assert host == b'xyz.tax-edu.net' + assert scheme == b"https" + assert host == b"xyz.tax-edu.net" assert port == 443 - assert full_path == b'/?flag%3DselectCourse%26lc_id%3D42825%26lc_name%3D%E8%8C%85%E8%8E%BD%E8%8E' \ - b'%BD%E7%8C%AB%E6%B0%93%E7%8C%AB%E6%B0%93' + assert ( + full_path + == b"/?flag%3DselectCourse%26lc_id%3D42825%26lc_name%3D%E8%8C%85%E8%8E%BD%E8%8E" + b"%BD%E7%8C%AB%E6%B0%93%E7%8C%AB%E6%B0%93" + ) def test_parse_port_range(): @@ -79,25 +84,27 @@ def test_unparse(): # In 3.6 it is escaped as %7E # In 3.7 it stays as ASCII character '~' # https://bugs.python.org/issue16285 -surrogates = (bytes(range(0, 126)) + bytes(range(127, 256))).decode("utf8", "surrogateescape") +surrogates = (bytes(range(0, 126)) + bytes(range(127, 256))).decode( + "utf8", "surrogateescape" +) surrogates_quoted = ( - '%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F' - '%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F' - '%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-./' - '0123456789%3A%3B%3C%3D%3E%3F%40' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - '%5B%5C%5D%5E_%60' - 'abcdefghijklmnopqrstuvwxyz' - '%7B%7C%7D%7F' # 7E or ~ is excluded! - '%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F' - '%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F' - '%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF' - '%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF' - '%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF' - '%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF' - '%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF' - '%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF' + "%00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F" + "%10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F" + "%20%21%22%23%24%25%26%27%28%29%2A%2B%2C-./" + "0123456789%3A%3B%3C%3D%3E%3F%40" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "%5B%5C%5D%5E_%60" + "abcdefghijklmnopqrstuvwxyz" + "%7B%7C%7D%7F" # 7E or ~ is excluded! + "%80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F" + "%90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F" + "%A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF" + "%B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF" + "%C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF" + "%D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF" + "%E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF" + "%F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF" ) @@ -112,18 +119,30 @@ def test_empty_key_trailing_equal_sign(): reference_without_equal = "key1=val1&key2&key3=val3" reference_with_equal = "key1=val1&key2=&key3=val3" - post_data_empty_key_middle = [('one', 'two'), ('emptykey', ''), ('three', 'four')] - post_data_empty_key_end = [('one', 'two'), ('three', 'four'), ('emptykey', '')] - - assert url.encode(post_data_empty_key_middle, similar_to=reference_with_equal) == "one=two&emptykey=&three=four" - assert url.encode(post_data_empty_key_end, similar_to=reference_with_equal) == "one=two&three=four&emptykey=" - assert url.encode(post_data_empty_key_middle, similar_to=reference_without_equal) == "one=two&emptykey&three=four" - assert url.encode(post_data_empty_key_end, similar_to=reference_without_equal) == "one=two&three=four&emptykey" + post_data_empty_key_middle = [("one", "two"), ("emptykey", ""), ("three", "four")] + post_data_empty_key_end = [("one", "two"), ("three", "four"), ("emptykey", "")] + + assert ( + url.encode(post_data_empty_key_middle, similar_to=reference_with_equal) + == "one=two&emptykey=&three=four" + ) + assert ( + url.encode(post_data_empty_key_end, similar_to=reference_with_equal) + == "one=two&three=four&emptykey=" + ) + assert ( + url.encode(post_data_empty_key_middle, similar_to=reference_without_equal) + == "one=two&emptykey&three=four" + ) + assert ( + url.encode(post_data_empty_key_end, similar_to=reference_without_equal) + == "one=two&three=four&emptykey" + ) def test_encode(): - assert url.encode([('foo', 'bar')]) - assert url.encode([('foo', surrogates)]) + assert url.encode([("foo", "bar")]) + assert url.encode([("foo", surrogates)]) assert not url.encode([], similar_to="justatext") @@ -156,20 +175,21 @@ def test_default_port(): @pytest.mark.parametrize( - "authority,valid,out", [ + "authority,valid,out", + [ ["foo:42", True, ("foo", 42)], [b"foo:42", True, ("foo", 42)], ["127.0.0.1:443", True, ("127.0.0.1", 443)], ["[2001:db8:42::]:443", True, ("2001:db8:42::", 443)], [b"xn--aaa-pla.example:80", True, ("äaaa.example", 80)], - [b"xn--r8jz45g.xn--zckzah:80", True, ('例え.テスト', 80)], + [b"xn--r8jz45g.xn--zckzah:80", True, ("例え.テスト", 80)], ["foo", True, ("foo", None)], ["foo..bar", False, ("foo..bar", None)], ["foo:bar", False, ("foo:bar", None)], [b"foo:bar", False, ("foo:bar", None)], ["foo:999999999", False, ("foo:999999999", None)], - [b"\xff", False, ('\udcff', None)] - ] + [b"\xff", False, ("\udcff", None)], + ], ) def test_parse_authority(authority: AnyStr, valid: bool, out): assert parse_authority(authority, False) == out diff --git a/test/mitmproxy/net/test_check.py b/test/mitmproxy/net/test_check.py index 635a9b1736..63b4695a78 100644 --- a/test/mitmproxy/net/test_check.py +++ b/test/mitmproxy/net/test_check.py @@ -12,63 +12,63 @@ def test_is_valid_host(): assert check.is_valid_host(b"::1") # IP Address Validations - assert check.is_valid_host(b'127.0.0.1') - assert check.is_valid_host(b'2001:0db8:85a3:0000:0000:8a2e:0370:7334') - assert check.is_valid_host(b'2001:db8:85a3:0:0:8a2e:370:7334') - assert check.is_valid_host(b'2001:db8:85a3::8a2e:370:7334') - assert not check.is_valid_host(b'2001:db8::85a3::7334') - assert check.is_valid_host(b'2001-db8-85a3-8d3-1319-8a2e-370-7348.ipv6-literal.net') + assert check.is_valid_host(b"127.0.0.1") + assert check.is_valid_host(b"2001:0db8:85a3:0000:0000:8a2e:0370:7334") + assert check.is_valid_host(b"2001:db8:85a3:0:0:8a2e:370:7334") + assert check.is_valid_host(b"2001:db8:85a3::8a2e:370:7334") + assert not check.is_valid_host(b"2001:db8::85a3::7334") + assert check.is_valid_host(b"2001-db8-85a3-8d3-1319-8a2e-370-7348.ipv6-literal.net") # TLD must be between 2 and 63 chars - assert check.is_valid_host(b'example.tl') - assert check.is_valid_host(b'example.tld') - assert check.is_valid_host(b'example.' + b"x" * 63) - assert not check.is_valid_host(b'example.' + b"x" * 64) + assert check.is_valid_host(b"example.tl") + assert check.is_valid_host(b"example.tld") + assert check.is_valid_host(b"example." + b"x" * 63) + assert not check.is_valid_host(b"example." + b"x" * 64) # misc characters test - assert not check.is_valid_host(b'ex@mple') - assert not check.is_valid_host(b'ex@mple.com') - assert not check.is_valid_host(b'example..com') - assert not check.is_valid_host(b'.example.com') - assert not check.is_valid_host(b'@.example.com') - assert not check.is_valid_host(b'!.example.com') + assert not check.is_valid_host(b"ex@mple") + assert not check.is_valid_host(b"ex@mple.com") + assert not check.is_valid_host(b"example..com") + assert not check.is_valid_host(b".example.com") + assert not check.is_valid_host(b"@.example.com") + assert not check.is_valid_host(b"!.example.com") # Every label must be between 1 and 63 chars - assert not check.is_valid_host(b'.tld') - assert check.is_valid_host(b'x' * 1 + b'.tld') - assert check.is_valid_host(b'x' * 30 + b'.tld') - assert not check.is_valid_host(b'x' * 64 + b'.tld') - assert check.is_valid_host(b'x' * 1 + b'.example.tld') - assert check.is_valid_host(b'x' * 30 + b'.example.tld') - assert not check.is_valid_host(b'x' * 64 + b'.example.tld') + assert not check.is_valid_host(b".tld") + assert check.is_valid_host(b"x" * 1 + b".tld") + assert check.is_valid_host(b"x" * 30 + b".tld") + assert not check.is_valid_host(b"x" * 64 + b".tld") + assert check.is_valid_host(b"x" * 1 + b".example.tld") + assert check.is_valid_host(b"x" * 30 + b".example.tld") + assert not check.is_valid_host(b"x" * 64 + b".example.tld") # Misc Underscore Test Cases - assert check.is_valid_host(b'_example') - assert check.is_valid_host(b'_example_') - assert check.is_valid_host(b'example_') - assert check.is_valid_host(b'_a.example.tld') - assert check.is_valid_host(b'a_.example.tld') - assert check.is_valid_host(b'_a_.example.tld') + assert check.is_valid_host(b"_example") + assert check.is_valid_host(b"_example_") + assert check.is_valid_host(b"example_") + assert check.is_valid_host(b"_a.example.tld") + assert check.is_valid_host(b"a_.example.tld") + assert check.is_valid_host(b"_a_.example.tld") # Misc Dash/Hyphen/Minus Test Cases - assert check.is_valid_host(b'-example') - assert check.is_valid_host(b'-example_') - assert check.is_valid_host(b'example-') - assert check.is_valid_host(b'-a.example.tld') - assert check.is_valid_host(b'a-.example.tld') - assert check.is_valid_host(b'-a-.example.tld') + assert check.is_valid_host(b"-example") + assert check.is_valid_host(b"-example_") + assert check.is_valid_host(b"example-") + assert check.is_valid_host(b"-a.example.tld") + assert check.is_valid_host(b"a-.example.tld") + assert check.is_valid_host(b"-a-.example.tld") # Misc Combo Test Cases - assert check.is_valid_host(b'api-.example.com') - assert check.is_valid_host(b'__a.example-site.com') - assert check.is_valid_host(b'_-a.example-site.com') - assert check.is_valid_host(b'_a_.example-site.com') - assert check.is_valid_host(b'-a-.example-site.com') - assert check.is_valid_host(b'api-.a.example.com') - assert check.is_valid_host(b'api-._a.example.com') - assert check.is_valid_host(b'api-.a_.example.com') - assert check.is_valid_host(b'api-.ab.example.com') + assert check.is_valid_host(b"api-.example.com") + assert check.is_valid_host(b"__a.example-site.com") + assert check.is_valid_host(b"_-a.example-site.com") + assert check.is_valid_host(b"_a_.example-site.com") + assert check.is_valid_host(b"-a-.example-site.com") + assert check.is_valid_host(b"api-.a.example.com") + assert check.is_valid_host(b"api-._a.example.com") + assert check.is_valid_host(b"api-.a_.example.com") + assert check.is_valid_host(b"api-.ab.example.com") # Test str - assert check.is_valid_host('example.tld') - assert not check.is_valid_host("foo..bar") # cannot be idna-encoded. \ No newline at end of file + assert check.is_valid_host("example.tld") + assert not check.is_valid_host("foo..bar") # cannot be idna-encoded. diff --git a/test/mitmproxy/net/test_encoding.py b/test/mitmproxy/net/test_encoding.py index 328306e1db..9d155961b8 100644 --- a/test/mitmproxy/net/test_encoding.py +++ b/test/mitmproxy/net/test_encoding.py @@ -4,10 +4,13 @@ from mitmproxy.net import encoding -@pytest.mark.parametrize("encoder", [ - 'identity', - 'none', -]) +@pytest.mark.parametrize( + "encoder", + [ + "identity", + "none", + ], +) def test_identity(encoder): assert b"string" == encoding.decode(b"string", encoder) assert b"string" == encoding.encode(b"string", encoder) @@ -15,29 +18,26 @@ def test_identity(encoder): encoding.encode(b"string", "nonexistent encoding") -@pytest.mark.parametrize("encoder", [ - 'gzip', - 'GZIP', - 'br', - 'deflate', - 'zstd', -]) +@pytest.mark.parametrize( + "encoder", + [ + "gzip", + "GZIP", + "br", + "deflate", + "zstd", + ], +) def test_encoders(encoder): """ - This test is for testing byte->byte encoding/decoding + This test is for testing byte->byte encoding/decoding """ assert encoding.decode(None, encoder) is None assert encoding.encode(None, encoder) is None assert b"" == encoding.decode(b"", encoder) - assert b"string" == encoding.decode( - encoding.encode( - b"string", - encoder - ), - encoder - ) + assert b"string" == encoding.decode(encoding.encode(b"string", encoder), encoder) with pytest.raises(TypeError): encoding.encode("string", encoder) @@ -48,24 +48,15 @@ def test_encoders(encoder): encoding.decode(b"foobar", encoder) -@pytest.mark.parametrize("encoder", [ - 'utf8', - 'latin-1' -]) +@pytest.mark.parametrize("encoder", ["utf8", "latin-1"]) def test_encoders_strings(encoder): """ - This test is for testing byte->str decoding - and str->byte encoding + This test is for testing byte->str decoding + and str->byte encoding """ assert "" == encoding.decode(b"", encoder) - assert "string" == encoding.decode( - encoding.encode( - "string", - encoder - ), - encoder - ) + assert "string" == encoding.decode(encoding.encode("string", encoder), encoder) with pytest.raises(TypeError): encoding.encode(b"string", encoder) diff --git a/test/mitmproxy/net/test_server_spec.py b/test/mitmproxy/net/test_server_spec.py index 4a5cc67865..ba527a20ee 100644 --- a/test/mitmproxy/net/test_server_spec.py +++ b/test/mitmproxy/net/test_server_spec.py @@ -3,16 +3,19 @@ from mitmproxy.net import server_spec -@pytest.mark.parametrize("spec,out", [ - ("example.com", ("https", ("example.com", 443))), - ("http://example.com", ("http", ("example.com", 80))), - ("smtp.example.com:25", ("http", ("smtp.example.com", 25))), - ("http://127.0.0.1", ("http", ("127.0.0.1", 80))), - ("http://[::1]", ("http", ("::1", 80))), - ("http://[::1]/", ("http", ("::1", 80))), - ("https://[::1]/", ("https", ("::1", 443))), - ("http://[::1]:8080", ("http", ("::1", 8080))), -]) +@pytest.mark.parametrize( + "spec,out", + [ + ("example.com", ("https", ("example.com", 443))), + ("http://example.com", ("http", ("example.com", 80))), + ("smtp.example.com:25", ("http", ("smtp.example.com", 25))), + ("http://127.0.0.1", ("http", ("127.0.0.1", 80))), + ("http://[::1]", ("http", ("::1", 80))), + ("http://[::1]/", ("http", ("::1", 80))), + ("https://[::1]/", ("https", ("::1", 443))), + ("http://[::1]:8080", ("http", ("::1", 8080))), + ], +) def test_parse(spec, out): assert server_spec.parse(spec) == out @@ -32,6 +35,9 @@ def test_parse_err(): def test_parse_with_mode(): - assert server_spec.parse_with_mode("m:example.com") == ("m", ("https", ("example.com", 443))) + assert server_spec.parse_with_mode("m:example.com") == ( + "m", + ("https", ("example.com", 443)), + ) with pytest.raises(ValueError): server_spec.parse_with_mode("moo") diff --git a/test/mitmproxy/net/test_tls.py b/test/mitmproxy/net/test_tls.py index 9fe6f0f826..67ade461e4 100644 --- a/test/mitmproxy/net/test_tls.py +++ b/test/mitmproxy/net/test_tls.py @@ -4,17 +4,6 @@ from mitmproxy import certs from mitmproxy.net import tls -CLIENT_HELLO_NO_EXTENSIONS = bytes.fromhex( - "03015658a756ab2c2bff55f636814deac086b7ca56b65058c7893ffc6074f5245f70205658a75475103a152637" - "78e1bb6d22e8bbd5b6b0a3a59760ad354e91ba20d353001a0035002f000a000500040009000300060008006000" - "61006200640100" -) -FULL_CLIENT_HELLO_NO_EXTENSIONS = ( - b"\x16\x03\x03\x00\x65" # record layer - b"\x01\x00\x00\x61" + # handshake header - CLIENT_HELLO_NO_EXTENSIONS -) - def test_make_master_secret_logger(): assert tls.make_master_secret_logger(None) is None @@ -23,11 +12,13 @@ def test_make_master_secret_logger(): def test_sslkeylogfile(tdata, monkeypatch): keylog = [] - monkeypatch.setattr(tls, "log_master_secret", lambda conn, secrets: keylog.append(secrets)) + monkeypatch.setattr( + tls, "log_master_secret", lambda conn, secrets: keylog.append(secrets) + ) store = certs.CertStore.from_files( Path(tdata.path("mitmproxy/net/data/verificationcerts/trusted-root.pem")), - Path(tdata.path("mitmproxy/net/data/dhparam.pem")) + Path(tdata.path("mitmproxy/net/data/dhparam.pem")), ) entry = store.get_cert("example.com", [], None) @@ -36,11 +27,9 @@ def test_sslkeylogfile(tdata, monkeypatch): max_version=tls.DEFAULT_MAX_VERSION, cipher_list=None, verify=tls.Verify.VERIFY_NONE, - hostname=None, ca_path=None, ca_pemfile=None, client_cert=None, - alpn_protos=(), ) sctx = tls.create_client_proxy_context( min_version=tls.DEFAULT_MIN_VERSION, @@ -84,43 +73,3 @@ def test_is_record_magic(): assert tls.is_tls_record_magic(b"\x16\x03\x01") assert tls.is_tls_record_magic(b"\x16\x03\x02") assert tls.is_tls_record_magic(b"\x16\x03\x03") - - -class TestClientHello: - def test_no_extensions(self): - c = tls.ClientHello(CLIENT_HELLO_NO_EXTENSIONS) - assert repr(c) - assert c.sni is None - assert c.cipher_suites == [53, 47, 10, 5, 4, 9, 3, 6, 8, 96, 97, 98, 100] - assert c.alpn_protocols == [] - assert c.extensions == [] - - def test_extensions(self): - data = bytes.fromhex( - "03033b70638d2523e1cba15f8364868295305e9c52aceabda4b5147210abc783e6e1000022c02bc02fc02cc030" - "cca9cca8cc14cc13c009c013c00ac014009c009d002f0035000a0100006cff0100010000000010000e00000b65" - "78616d706c652e636f6d0017000000230000000d00120010060106030501050304010403020102030005000501" - "00000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a00080006001d00" - "170018" - ) - c = tls.ClientHello(data) - assert repr(c) - assert c.sni == 'example.com' - assert c.cipher_suites == [ - 49195, 49199, 49196, 49200, 52393, 52392, 52244, 52243, 49161, - 49171, 49162, 49172, 156, 157, 47, 53, 10 - ] - assert c.alpn_protocols == [b'h2', b'http/1.1'] - assert c.extensions == [ - (65281, b'\x00'), - (0, b'\x00\x0e\x00\x00\x0bexample.com'), - (23, b''), - (35, b''), - (13, b'\x00\x10\x06\x01\x06\x03\x05\x01\x05\x03\x04\x01\x04\x03\x02\x01\x02\x03'), - (5, b'\x01\x00\x00\x00\x00'), - (18, b''), - (16, b'\x00\x0c\x02h2\x08http/1.1'), - (30032, b''), - (11, b'\x01\x00'), - (10, b'\x00\x06\x00\x1d\x00\x17\x00\x18') - ] diff --git a/test/mitmproxy/net/test_udp.py b/test/mitmproxy/net/test_udp.py new file mode 100644 index 0000000000..1db5a60997 --- /dev/null +++ b/test/mitmproxy/net/test_udp.py @@ -0,0 +1,19 @@ +import pytest + +from mitmproxy.net.udp import MAX_DATAGRAM_SIZE, DatagramReader + + +@pytest.mark.asyncio +async def test_reader(): + reader = DatagramReader() + addr = ("8.8.8.8", 53) + reader.feed_data(b"First message", addr) + with pytest.raises(AssertionError): + reader.feed_data(bytearray(MAX_DATAGRAM_SIZE + 1), addr) + reader.feed_data(b"Second message", addr) + reader.feed_eof() + assert await reader.read(65535) == b"First message" + with pytest.raises(AssertionError): + await reader.read(MAX_DATAGRAM_SIZE - 1) + assert await reader.read(65535) == b"Second message" + assert not await reader.read(65535) diff --git a/test/mitmproxy/platform/test_pf.py b/test/mitmproxy/platform/test_pf.py index 4a7dfe75f7..b9eafabd7e 100644 --- a/test/mitmproxy/platform/test_pf.py +++ b/test/mitmproxy/platform/test_pf.py @@ -4,7 +4,6 @@ class TestLookup: - def test_simple(self, tdata): if sys.platform == "freebsd10": p = tdata.path("mitmproxy/data/pf02") @@ -19,7 +18,10 @@ def test_simple(self, tdata): pf.lookup("192.168.1.112", 40000, d) with pytest.raises(Exception, match="Could not resolve original destination"): pf.lookup("192.168.1.111", 40001, d) - assert pf.lookup("2a01:e35:8bae:50f0:396f:e6c7:f4f1:f3db", 40002, d) == ("2a03:2880:f21f:c5:face:b00c::167", 443) + assert pf.lookup("2a01:e35:8bae:50f0:396f:e6c7:f4f1:f3db", 40002, d) == ( + "2a03:2880:f21f:c5:face:b00c::167", + 443, + ) with pytest.raises(Exception, match="Could not resolve original destination"): pf.lookup("2a01:e35:8bae:50f0:396f:e6c7:f4f1:f3db", 40003, d) with pytest.raises(Exception, match="Could not resolve original destination"): diff --git a/test/mitmproxy/proxy/conftest.py b/test/mitmproxy/proxy/conftest.py index 6037fb69fe..c271fb8f41 100644 --- a/test/mitmproxy/proxy/conftest.py +++ b/test/mitmproxy/proxy/conftest.py @@ -15,12 +15,7 @@ def tctx() -> context.Context: Proxyserver().load(opts) TermLog().load(opts) return context.Context( - connection.Client( - ("client", 1234), - ("127.0.0.1", 8080), - 1605699329 - ), - opts + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts ) diff --git a/test/mitmproxy/proxy/layers/http/hyper_h2_test_helpers.py b/test/mitmproxy/proxy/layers/http/hyper_h2_test_helpers.py index 7302e58ac2..d5f8e01821 100644 --- a/test/mitmproxy/proxy/layers/http/hyper_h2_test_helpers.py +++ b/test/mitmproxy/proxy/layers/http/hyper_h2_test_helpers.py @@ -10,9 +10,17 @@ """ from hpack.hpack import Encoder from hyperframe.frame import ( - HeadersFrame, DataFrame, SettingsFrame, WindowUpdateFrame, PingFrame, - GoAwayFrame, RstStreamFrame, PushPromiseFrame, PriorityFrame, - ContinuationFrame, AltSvcFrame + HeadersFrame, + DataFrame, + SettingsFrame, + WindowUpdateFrame, + PingFrame, + GoAwayFrame, + RstStreamFrame, + PushPromiseFrame, + PriorityFrame, + ContinuationFrame, + AltSvcFrame, ) SAMPLE_SETTINGS = { @@ -22,7 +30,7 @@ } -class FrameFactory(object): +class FrameFactory: """ A class containing lots of helper methods and state to build frames. This allows test cases to easily build correct HTTP/2 frames to feed to @@ -36,19 +44,15 @@ def refresh_encoder(self): self.encoder = Encoder() def preamble(self): - return b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n' + return b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" - def build_headers_frame(self, - headers, - flags=[], - stream_id=1, - **priority_kwargs): + def build_headers_frame(self, headers, flags=[], stream_id=1, **priority_kwargs): """ Builds a single valid headers frame out of the contained headers. """ f = HeadersFrame(stream_id) f.data = self.encoder.encode(headers) - f.flags.add('END_HEADERS') + f.flags.add("END_HEADERS") for flag in flags: f.flags.add(flag) @@ -77,7 +81,7 @@ def build_data_frame(self, data, flags=None, stream_id=1, padding_len=0): f.flags = flags if padding_len: - flags.add('PADDED') + flags.add("PADDED") f.pad_length = padding_len return f @@ -88,7 +92,7 @@ def build_settings_frame(self, settings, ack=False): """ f = SettingsFrame(0) if ack: - f.flags.add('ACK') + f.flags.add("ACK") f.settings = settings return f @@ -112,10 +116,7 @@ def build_ping_frame(self, ping_data, flags=None): return f - def build_goaway_frame(self, - last_stream_id, - error_code=0, - additional_data=b''): + def build_goaway_frame(self, last_stream_id, error_code=0, additional_data=b""): """ Builds a single GOAWAY frame. """ @@ -133,11 +134,9 @@ def build_rst_stream_frame(self, stream_id, error_code=0): f.error_code = error_code return f - def build_push_promise_frame(self, - stream_id, - promised_stream_id, - headers, - flags=[]): + def build_push_promise_frame( + self, stream_id, promised_stream_id, headers, flags=[] + ): """ Builds a single PUSH_PROMISE frame. """ @@ -145,14 +144,10 @@ def build_push_promise_frame(self, f.promised_stream_id = promised_stream_id f.data = self.encoder.encode(headers) f.flags = set(flags) - f.flags.add('END_HEADERS') + f.flags.add("END_HEADERS") return f - def build_priority_frame(self, - stream_id, - weight, - depends_on=0, - exclusive=False): + def build_priority_frame(self, stream_id, weight, depends_on=0, exclusive=False): """ Builds a single priority frame. """ diff --git a/test/mitmproxy/proxy/layers/http/test_http.py b/test/mitmproxy/proxy/layers/http/test_http.py index 619ced6d6f..6c82a58143 100644 --- a/test/mitmproxy/proxy/layers/http/test_http.py +++ b/test/mitmproxy/proxy/layers/http/test_http.py @@ -1,7 +1,8 @@ +import gc + import pytest from mitmproxy.connection import ConnectionState, Server -from mitmproxy.flow import Error from mitmproxy.http import HTTPFlow, Response from mitmproxy.net.server_spec import ServerSpec from mitmproxy.proxy import layer @@ -12,7 +13,13 @@ from mitmproxy.proxy.layers.tcp import TcpMessageInjected, TcpStartHook from mitmproxy.proxy.layers.websocket import WebsocketStartHook from mitmproxy.tcp import TCPFlow, TCPMessage -from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply, reply_next_layer +from test.mitmproxy.proxy.tutils import ( + BytesMatching, + Placeholder, + Playbook, + reply, + reply_next_layer, +) def test_http_proxy(tctx): @@ -20,22 +27,29 @@ def test_http_proxy(tctx): server = Placeholder(Server) flow = Placeholder(HTTPFlow) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular)) - >> DataReceived(tctx.client, b"GET http://example.com/foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n") - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World") - << http.HttpResponseHeadersHook(flow) - >> reply() - >> DataReceived(server, b"!") - << http.HttpResponseHook(flow) - >> reply() - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!") + Playbook(http.HttpLayer(tctx, HTTPMode.regular)) + >> DataReceived( + tctx.client, + b"GET http://example.com/foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived( + server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World" + ) + << http.HttpResponseHeadersHook(flow) + >> reply() + >> DataReceived(server, b"!") + << http.HttpResponseHook(flow) + >> reply() + << SendData( + tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!" + ) ) assert server().address == ("example.com", 80) @@ -48,35 +62,45 @@ def test_https_proxy(strategy, tctx): playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular)) tctx.options.connection_strategy = strategy - (playbook - >> DataReceived(tctx.client, b"CONNECT example.proxy:80 HTTP/1.1\r\n\r\n") - << http.HttpConnectHook(Placeholder()) - >> reply()) + ( + playbook + >> DataReceived(tctx.client, b"CONNECT example.proxy:80 HTTP/1.1\r\n\r\n") + << http.HttpConnectHook(Placeholder()) + >> reply() + ) if strategy == "eager": - (playbook - << OpenConnection(server) - >> reply(None)) - (playbook - << SendData(tctx.client, b'HTTP/1.1 200 Connection established\r\n\r\n') - >> DataReceived(tctx.client, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n") - << layer.NextLayerHook(Placeholder()) - >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply()) + playbook << OpenConnection(server) + playbook >> reply(None) + ( + playbook + << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") + >> DataReceived( + tctx.client, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n" + ) + << layer.NextLayerHook(Placeholder()) + >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + ) if strategy == "lazy": - (playbook - << OpenConnection(server) - >> reply(None)) - (playbook - << SendData(server, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!") - << http.HttpResponseHeadersHook(flow) - >> reply() - << http.HttpResponseHook(flow) - >> reply() - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!")) + playbook << OpenConnection(server) + playbook >> reply(None) + ( + playbook + << SendData(server, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived( + server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!" + ) + << http.HttpResponseHeadersHook(flow) + >> reply() + << http.HttpResponseHook(flow) + >> reply() + << SendData( + tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!" + ) + ) assert playbook @@ -104,19 +128,26 @@ def redirect(flow: HTTPFlow): if strategy == "eager": p << OpenConnection(Placeholder()) p >> reply(None) - p << SendData(tctx.client, b'HTTP/1.1 200 Connection established\r\n\r\n') + p << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") p >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") p << layer.NextLayerHook(Placeholder()) p >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) else: - p >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") + p >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) p << http.HttpRequestHook(flow) p >> reply(side_effect=redirect) p << OpenConnection(server) p >> reply(None) p << SendData(server, b"GET / HTTP/1.1\r\nHost: redirected.site\r\n\r\n") - p >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!") - p << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!") + p >> DataReceived( + server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!" + ) + p << SendData( + tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!" + ) assert p if https_server: @@ -138,26 +169,32 @@ def side_effect(flow: HTTPFlow): return side_effect assert ( - playbook - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << http.HttpRequestHook(Placeholder()) - >> reply(side_effect=redirect("http://one.redirect/")) - << OpenConnection(server1) - >> reply(None) - << SendData(server1, b"GET / HTTP/1.1\r\nHost: one.redirect\r\n\r\n") - >> DataReceived(server1, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + playbook + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << http.HttpRequestHook(Placeholder()) + >> reply(side_effect=redirect("http://one.redirect/")) + << OpenConnection(server1) + >> reply(None) + << SendData(server1, b"GET / HTTP/1.1\r\nHost: one.redirect\r\n\r\n") + >> DataReceived(server1, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") ) assert ( - playbook - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << http.HttpRequestHook(Placeholder()) - >> reply(side_effect=redirect("http://two.redirect/")) - << OpenConnection(server2) - >> reply(None) - << SendData(server2, b"GET / HTTP/1.1\r\nHost: two.redirect\r\n\r\n") - >> DataReceived(server2, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + playbook + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << http.HttpRequestHook(Placeholder()) + >> reply(side_effect=redirect("http://two.redirect/")) + << OpenConnection(server2) + >> reply(None) + << SendData(server2, b"GET / HTTP/1.1\r\nHost: two.redirect\r\n\r\n") + >> DataReceived(server2, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") ) assert server1().address == ("one.redirect", 80) assert server2().address == ("two.redirect", 80) @@ -172,31 +209,30 @@ def test_pipelining(tctx, transfer_encoding): req = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" if transfer_encoding == "identity": - resp = (b"HTTP/1.1 200 OK\r\n" - b"Content-Length: 12\r\n" - b"\r\n" - b"Hello World!") + resp = b"HTTP/1.1 200 OK\r\n" b"Content-Length: 12\r\n" b"\r\n" b"Hello World!" else: - resp = (b"HTTP/1.1 200 OK\r\n" - b"Transfer-Encoding: chunked\r\n" - b"\r\n" - b"c\r\n" - b"Hello World!\r\n" - b"0\r\n" - b"\r\n") + resp = ( + b"HTTP/1.1 200 OK\r\n" + b"Transfer-Encoding: chunked\r\n" + b"\r\n" + b"c\r\n" + b"Hello World!\r\n" + b"0\r\n" + b"\r\n" + ) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.transparent), hooks=False) - # Roundtrip 1 - >> DataReceived(tctx.client, req) - << SendData(tctx.server, req) - >> DataReceived(tctx.server, resp) - << SendData(tctx.client, resp) - # Roundtrip 2 - >> DataReceived(tctx.client, req) - << SendData(tctx.server, req) - >> DataReceived(tctx.server, resp) - << SendData(tctx.client, resp) + Playbook(http.HttpLayer(tctx, HTTPMode.transparent), hooks=False) + # Roundtrip 1 + >> DataReceived(tctx.client, req) + << SendData(tctx.server, req) + >> DataReceived(tctx.server, resp) + << SendData(tctx.client, resp) + # Roundtrip 2 + >> DataReceived(tctx.client, req) + << SendData(tctx.server, req) + >> DataReceived(tctx.server, resp) + << SendData(tctx.client, resp) ) @@ -207,11 +243,16 @@ def reply_from_proxy(flow: HTTPFlow): flow.response = Response.make(418) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << http.HttpRequestHook(Placeholder()) - >> reply(side_effect=reply_from_proxy) - << SendData(tctx.client, b"HTTP/1.1 418 I'm a teapot\r\ncontent-length: 0\r\n\r\n") + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << http.HttpRequestHook(Placeholder()) + >> reply(side_effect=reply_from_proxy) + << SendData( + tctx.client, b"HTTP/1.1 418 I'm a teapot\r\ncontent-length: 0\r\n\r\n" + ) ) @@ -219,16 +260,19 @@ def test_response_until_eof(tctx): """Test scenario where the server response body is terminated by EOF.""" server = Placeholder(Server) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\n\r\nfoo") - >> ConnectionClosed(server) - << CloseConnection(server) - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n\r\nfoo") - << CloseConnection(tctx.client) + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived(server, b"HTTP/1.1 200 OK\r\n\r\nfoo") + >> ConnectionClosed(server) + << CloseConnection(server) + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n\r\nfoo") + << CloseConnection(tctx.client) ) @@ -241,28 +285,29 @@ def test_disconnect_while_intercept(tctx): flow = Placeholder(HTTPFlow) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n") - << http.HttpConnectHook(Placeholder(HTTPFlow)) - >> reply() - << OpenConnection(server1) - >> reply(None) - << SendData(tctx.client, b'HTTP/1.1 200 Connection established\r\n\r\n') - >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") - << layer.NextLayerHook(Placeholder()) - >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) - << http.HttpRequestHook(flow) - >> ConnectionClosed(server1) - << CloseConnection(server1) - >> reply(to=-3) - << OpenConnection(server2) - >> reply(None) - << SendData(server2, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") - >> DataReceived(server2, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n") + << http.HttpConnectHook(Placeholder(HTTPFlow)) + >> reply() + << OpenConnection(server1) + >> reply(None) + << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") + >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + << layer.NextLayerHook(Placeholder()) + >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) + << http.HttpRequestHook(flow) + >> ConnectionClosed(server1) + << CloseConnection(server1) + >> reply(to=-3) + << OpenConnection(server2) + >> reply(None) + << SendData(server2, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived(server2, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") ) assert server1() != server2() assert flow().server_conn == server2() + assert not flow().live @pytest.mark.parametrize("why", ["body_size=0", "body_size=3", "addon"]) @@ -282,7 +327,10 @@ def enable_streaming(flow: HTTPFlow): assert ( playbook - >> DataReceived(tctx.client, b"GET http://example.com/largefile HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived( + tctx.client, + b"GET http://example.com/largefile HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) << http.HttpRequestHeadersHook(flow) >> reply() << http.HttpRequestHook(flow) @@ -292,32 +340,39 @@ def enable_streaming(flow: HTTPFlow): << SendData(server, b"GET /largefile HTTP/1.1\r\nHost: example.com\r\n\r\n") >> DataReceived(server, b"HTTP/1.1 200 OK\r\n") ) + assert flow().live if transfer_encoding == "identity": - playbook >> DataReceived(server, b"Content-Length: 6\r\n\r\n" - b"abc") + playbook >> DataReceived(server, b"Content-Length: 6\r\n\r\n" b"abc") else: - playbook >> DataReceived(server, b"Transfer-Encoding: chunked\r\n\r\n" - b"3\r\nabc\r\n") + playbook >> DataReceived( + server, b"Transfer-Encoding: chunked\r\n\r\n" b"3\r\nabc\r\n" + ) playbook << http.HttpResponseHeadersHook(flow) playbook >> reply(side_effect=enable_streaming) if transfer_encoding == "identity": - playbook << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n" - b"Content-Length: 6\r\n\r\n" - b"abc") + playbook << SendData( + tctx.client, b"HTTP/1.1 200 OK\r\n" b"Content-Length: 6\r\n\r\n" b"abc" + ) playbook >> DataReceived(server, b"def") playbook << SendData(tctx.client, b"def") else: if why == "body_size=3": playbook >> DataReceived(server, b"3\r\ndef\r\n") - playbook << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - b"6\r\nabcdef\r\n") + playbook << SendData( + tctx.client, + b"HTTP/1.1 200 OK\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"6\r\nabcdef\r\n", + ) else: - playbook << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - b"3\r\nabc\r\n") + playbook << SendData( + tctx.client, + b"HTTP/1.1 200 OK\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"3\r\nabc\r\n", + ) playbook >> DataReceived(server, b"3\r\ndef\r\n") playbook << SendData(tctx.client, b"3\r\ndef\r\n") playbook >> DataReceived(server, b"0\r\n\r\n") @@ -329,6 +384,7 @@ def enable_streaming(flow: HTTPFlow): playbook << SendData(tctx.client, b"0\r\n\r\n") assert playbook + assert not flow().live def test_stream_modify(tctx): @@ -344,33 +400,45 @@ def enable_streaming(flow: HTTPFlow): assert ( Playbook(http.HttpLayer(tctx, HTTPMode.regular)) - >> DataReceived(tctx.client, b"POST http://example.com/ HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - b"3\r\nabc\r\n" - b"0\r\n\r\n") + >> DataReceived( + tctx.client, + b"POST http://example.com/ HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"3\r\nabc\r\n" + b"0\r\n\r\n", + ) << http.HttpRequestHeadersHook(flow) >> reply(side_effect=enable_streaming) << OpenConnection(server) >> reply(None) - << SendData(server, b"POST / HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - b"5\r\n[abc]\r\n" - b"2\r\n[]\r\n") + << SendData( + server, + b"POST / HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"5\r\n[abc]\r\n" + b"2\r\n[]\r\n", + ) << http.HttpRequestHook(flow) >> reply() << SendData(server, b"0\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - b"3\r\ndef\r\n" - b"0\r\n\r\n") + >> DataReceived( + server, + b"HTTP/1.1 200 OK\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"3\r\ndef\r\n" + b"0\r\n\r\n", + ) << http.HttpResponseHeadersHook(flow) >> reply(side_effect=enable_streaming) - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - b"5\r\n[def]\r\n" - b"2\r\n[]\r\n") + << SendData( + tctx.client, + b"HTTP/1.1 200 OK\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"5\r\n[def]\r\n" + b"2\r\n[]\r\n", + ) << http.HttpResponseHook(flow) >> reply() << SendData(tctx.client, b"0\r\n\r\n") @@ -379,7 +447,9 @@ def enable_streaming(flow: HTTPFlow): @pytest.mark.parametrize("why", ["body_size=0", "body_size=3", "addon"]) @pytest.mark.parametrize("transfer_encoding", ["identity", "chunked"]) -@pytest.mark.parametrize("response", ["normal response", "early response", "early close", "early kill"]) +@pytest.mark.parametrize( + "response", ["normal response", "early response", "early close", "early kill"] +) def test_request_streaming(tctx, why, transfer_encoding, response): """ Test HTTP request streaming @@ -397,39 +467,42 @@ def enable_streaming(flow: HTTPFlow): if why == "addon": flow.request.stream = True - playbook >> DataReceived(tctx.client, b"POST http://example.com/ HTTP/1.1\r\n" - b"Host: example.com\r\n") + playbook >> DataReceived( + tctx.client, b"POST http://example.com/ HTTP/1.1\r\n" b"Host: example.com\r\n" + ) if transfer_encoding == "identity": - playbook >> DataReceived(tctx.client, b"Content-Length: 9\r\n\r\n" - b"abc") + playbook >> DataReceived(tctx.client, b"Content-Length: 9\r\n\r\n" b"abc") else: - playbook >> DataReceived(tctx.client, b"Transfer-Encoding: chunked\r\n\r\n" - b"3\r\nabc\r\n") + playbook >> DataReceived( + tctx.client, b"Transfer-Encoding: chunked\r\n\r\n" b"3\r\nabc\r\n" + ) playbook << http.HttpRequestHeadersHook(flow) playbook >> reply(side_effect=enable_streaming) - needs_more_data_before_open = (why == "body_size=3" and transfer_encoding == "chunked") + needs_more_data_before_open = ( + why == "body_size=3" and transfer_encoding == "chunked" + ) if needs_more_data_before_open: playbook >> DataReceived(tctx.client, b"3\r\ndef\r\n") playbook << OpenConnection(server) playbook >> reply(None) - playbook << SendData(server, b"POST / HTTP/1.1\r\n" - b"Host: example.com\r\n") + playbook << SendData(server, b"POST / HTTP/1.1\r\n" b"Host: example.com\r\n") if transfer_encoding == "identity": - playbook << SendData(server, b"Content-Length: 9\r\n\r\n" - b"abc") + playbook << SendData(server, b"Content-Length: 9\r\n\r\n" b"abc") playbook >> DataReceived(tctx.client, b"def") playbook << SendData(server, b"def") else: if needs_more_data_before_open: - playbook << SendData(server, b"Transfer-Encoding: chunked\r\n\r\n" - b"6\r\nabcdef\r\n") + playbook << SendData( + server, b"Transfer-Encoding: chunked\r\n\r\n" b"6\r\nabcdef\r\n" + ) else: - playbook << SendData(server, b"Transfer-Encoding: chunked\r\n\r\n" - b"3\r\nabc\r\n") + playbook << SendData( + server, b"Transfer-Encoding: chunked\r\n\r\n" b"3\r\nabc\r\n" + ) playbook >> DataReceived(tctx.client, b"3\r\ndef\r\n") playbook << SendData(server, b"3\r\ndef\r\n") @@ -460,12 +533,18 @@ def enable_streaming(flow: HTTPFlow): # https://tools.ietf.org/html/rfc7231#section-6.5.11 assert ( playbook - >> DataReceived(server, b"HTTP/1.1 413 Request Entity Too Large\r\nContent-Length: 0\r\n\r\n") + >> DataReceived( + server, + b"HTTP/1.1 413 Request Entity Too Large\r\nContent-Length: 0\r\n\r\n", + ) << http.HttpResponseHeadersHook(flow) >> reply() << http.HttpResponseHook(flow) >> reply() - << SendData(tctx.client, b"HTTP/1.1 413 Request Entity Too Large\r\nContent-Length: 0\r\n\r\n") + << SendData( + tctx.client, + b"HTTP/1.1 413 Request Entity Too Large\r\nContent-Length: 0\r\n\r\n", + ) ) if transfer_encoding == "identity": playbook >> DataReceived(tctx.client, b"ghi") @@ -480,29 +559,33 @@ def enable_streaming(flow: HTTPFlow): assert playbook elif response == "early close": assert ( - playbook - >> DataReceived(server, b"HTTP/1.1 413 Request Entity Too Large\r\nContent-Length: 0\r\n\r\n") - << http.HttpResponseHeadersHook(flow) - >> reply() - << http.HttpResponseHook(flow) - >> reply() - << SendData(tctx.client, b"HTTP/1.1 413 Request Entity Too Large\r\nContent-Length: 0\r\n\r\n") - >> ConnectionClosed(server) - << CloseConnection(server) - << CloseConnection(tctx.client) + playbook + >> DataReceived( + server, + b"HTTP/1.1 413 Request Entity Too Large\r\nContent-Length: 0\r\n\r\n", + ) + << http.HttpResponseHeadersHook(flow) + >> reply() + << http.HttpResponseHook(flow) + >> reply() + << SendData( + tctx.client, + b"HTTP/1.1 413 Request Entity Too Large\r\nContent-Length: 0\r\n\r\n", + ) + >> ConnectionClosed(server) + << CloseConnection(server) + << CloseConnection(tctx.client) ) elif response == "early kill": - err = Placeholder(bytes) assert ( - playbook - >> ConnectionClosed(server) - << CloseConnection(server) - << http.HttpErrorHook(flow) - >> reply() - << SendData(tctx.client, err) - << CloseConnection(tctx.client) - ) - assert b"502 Bad Gateway" in err() + playbook + >> ConnectionClosed(server) + << CloseConnection(server) + << http.HttpErrorHook(flow) + >> reply() + << SendData(tctx.client, BytesMatching(b"502 Bad Gateway")) + << CloseConnection(tctx.client) + ) else: # pragma: no cover assert False @@ -512,7 +595,6 @@ def enable_streaming(flow: HTTPFlow): def test_body_size_limit(tctx, where, transfer_encoding): """Test HTTP request body_size_limit""" tctx.options.body_size_limit = "3" - err = Placeholder(bytes) flow = Placeholder(HTTPFlow) if transfer_encoding == "identity": @@ -523,42 +605,46 @@ def test_body_size_limit(tctx, where, transfer_encoding): if where == "request": assert ( Playbook(http.HttpLayer(tctx, HTTPMode.regular)) - >> DataReceived(tctx.client, b"POST http://example.com/ HTTP/1.1\r\n" - b"Host: example.com\r\n" + body) + >> DataReceived( + tctx.client, + b"POST http://example.com/ HTTP/1.1\r\n" + b"Host: example.com\r\n" + body, + ) << http.HttpRequestHeadersHook(flow) >> reply() << http.HttpErrorHook(flow) >> reply() - << SendData(tctx.client, err) + << SendData( + tctx.client, BytesMatching(b"413 Payload Too Large.+body_size_limit") + ) << CloseConnection(tctx.client) ) - assert b"413 Payload Too Large" in err() - assert b"body_size_limit" in err() + assert not flow().live else: server = Placeholder(Server) assert ( Playbook(http.HttpLayer(tctx, HTTPMode.regular)) - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\n" - b"Host: example.com\r\n\r\n") + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\n" b"Host: example.com\r\n\r\n", + ) << http.HttpRequestHeadersHook(flow) >> reply() << http.HttpRequestHook(flow) >> reply() << OpenConnection(server) >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\n" - b"Host: example.com\r\n\r\n") + << SendData(server, b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n\r\n") >> DataReceived(server, b"HTTP/1.1 200 OK\r\n" + body) << http.HttpResponseHeadersHook(flow) >> reply() << http.HttpErrorHook(flow) >> reply() - << SendData(tctx.client, err) + << SendData(tctx.client, BytesMatching(b"502 Bad Gateway.+body_size_limit")) << CloseConnection(tctx.client) << CloseConnection(server) ) - assert b"502 Bad Gateway" in err() - assert b"body_size_limit" in err() + assert not flow().live @pytest.mark.parametrize("connect", [True, False]) @@ -567,12 +653,15 @@ def test_server_unreachable(tctx, connect): tctx.options.connection_strategy = "eager" server = Placeholder(Server) flow = Placeholder(HTTPFlow) - err = Placeholder(bytes) playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) if connect: - playbook >> DataReceived(tctx.client, b"CONNECT example.com:443 HTTP/1.1\r\n\r\n") + playbook >> DataReceived( + tctx.client, b"CONNECT example.com:443 HTTP/1.1\r\n\r\n" + ) else: - playbook >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\n\r\n") + playbook >> DataReceived( + tctx.client, b"GET http://example.com/ HTTP/1.1\r\n\r\n" + ) playbook << OpenConnection(server) playbook >> reply("Connection failed") @@ -582,53 +671,60 @@ def test_server_unreachable(tctx, connect): # or by adding dedicated ok/error hooks. playbook << http.HttpErrorHook(flow) playbook >> reply() - playbook << SendData(tctx.client, err) + playbook << SendData( + tctx.client, BytesMatching(b"502 Bad Gateway.+Connection failed") + ) if not connect: playbook << CloseConnection(tctx.client) assert playbook if not connect: assert flow().error - assert b"502 Bad Gateway" in err() - assert b"Connection failed" in err() + assert not flow().live -@pytest.mark.parametrize("data", [ - None, - b"I don't speak HTTP.", - b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nweee" -]) +@pytest.mark.parametrize( + "data", + [ + None, + b"I don't speak HTTP.", + b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\nweee", + ], +) def test_server_aborts(tctx, data): """Test the scenario where the server doesn't serve a response""" server = Placeholder(Server) flow = Placeholder(HTTPFlow) - err = Placeholder(bytes) playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) assert ( - playbook - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + playbook + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") ) if data: playbook >> DataReceived(server, data) assert ( - playbook - >> ConnectionClosed(server) - << CloseConnection(server) - << http.HttpErrorHook(flow) - >> reply() - << SendData(tctx.client, err) - << CloseConnection(tctx.client) + playbook + >> ConnectionClosed(server) + << CloseConnection(server) + << http.HttpErrorHook(flow) + >> reply() + << SendData(tctx.client, BytesMatching(b"502 Bad Gateway")) + << CloseConnection(tctx.client) ) assert flow().error - assert b"502 Bad Gateway" in err() + assert not flow().live @pytest.mark.parametrize("redirect", ["", "change-destination", "change-proxy"]) +@pytest.mark.parametrize("domain", [b"example.com", b"xn--eckwd4c7c.xn--zckzah"]) @pytest.mark.parametrize("scheme", ["http", "https"]) -def test_upstream_proxy(tctx, redirect, scheme): +def test_upstream_proxy(tctx, redirect, domain, scheme): """Test that an upstream HTTP proxy is used.""" server = Placeholder(Server) server2 = Placeholder(Server) @@ -638,26 +734,31 @@ def test_upstream_proxy(tctx, redirect, scheme): if scheme == "http": assert ( - playbook - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") + playbook + >> DataReceived( + tctx.client, + b"GET http://%s/ HTTP/1.1\r\nHost: %s\r\n\r\n" % (domain, domain), + ) + << OpenConnection(server) + >> reply(None) + << SendData( + server, b"GET http://%s/ HTTP/1.1\r\nHost: %s\r\n\r\n" % (domain, domain), + ) ) else: assert ( - playbook - >> DataReceived(tctx.client, b"CONNECT example.com:443 HTTP/1.1\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") - >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") - << layer.NextLayerHook(Placeholder()) - >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) - << OpenConnection(server) - >> reply(None) - << SendData(server, b"CONNECT example.com:443 HTTP/1.1\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 Connection established\r\n\r\n") - << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + playbook + >> DataReceived(tctx.client, b"CONNECT %s:443 HTTP/1.1\r\n\r\n" % domain) + << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") + >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\nHost: %s\r\n\r\n" % domain) + << layer.NextLayerHook(Placeholder()) + >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) + << OpenConnection(server) + >> reply(None) + << SendData(server, b"CONNECT %s:443 HTTP/1.1\r\n\r\n" % domain) + >> DataReceived(server, b"HTTP/1.1 200 Connection established\r\n\r\n") + << SendData(server, b"GET / HTTP/1.1\r\nHost: %s\r\n\r\n" % domain) ) playbook >> DataReceived(server, b"HTTP/1.1 418 OK\r\nContent-Length: 0\r\n\r\n") @@ -667,14 +768,19 @@ def test_upstream_proxy(tctx, redirect, scheme): assert server().address == ("proxy", 8080) if scheme == "http": - playbook >> DataReceived(tctx.client, b"GET http://example.com/two HTTP/1.1\r\nHost: example.com\r\n\r\n") + playbook >> DataReceived( + tctx.client, + b"GET http://%s/two HTTP/1.1\r\nHost: %s\r\n\r\n" % (domain, domain), + ) else: - playbook >> DataReceived(tctx.client, b"GET /two HTTP/1.1\r\nHost: example.com\r\n\r\n") + playbook >> DataReceived( + tctx.client, b"GET /two HTTP/1.1\r\nHost: %s\r\n\r\n" % domain + ) - assert (playbook << http.HttpRequestHook(flow)) + assert playbook << http.HttpRequestHook(flow) if redirect == "change-destination": - flow().request.host = "other-server" - flow().request.host_header = "example.com" + flow().request.host = domain + b".test" + flow().request.host_header = domain elif redirect == "change-proxy": flow().server_conn.via = ServerSpec("http", address=("other-proxy", 1234)) playbook >> reply() @@ -689,17 +795,27 @@ def test_upstream_proxy(tctx, redirect, scheme): if scheme == "http": if redirect == "change-destination": - playbook << SendData(server2, b"GET http://other-server/two HTTP/1.1\r\nHost: example.com\r\n\r\n") + playbook << SendData( + server2, + b"GET http://%s.test/two HTTP/1.1\r\nHost: %s\r\n\r\n" % (domain, domain), + ) else: - playbook << SendData(server2, b"GET http://example.com/two HTTP/1.1\r\nHost: example.com\r\n\r\n") + playbook << SendData( + server2, + b"GET http://%s/two HTTP/1.1\r\nHost: %s\r\n\r\n" % (domain, domain), + ) else: if redirect == "change-destination": - playbook << SendData(server2, b"CONNECT other-server:443 HTTP/1.1\r\n\r\n") - playbook >> DataReceived(server2, b"HTTP/1.1 200 Connection established\r\n\r\n") + playbook << SendData(server2, b"CONNECT %s.test:443 HTTP/1.1\r\n\r\n" % domain) + playbook >> DataReceived( + server2, b"HTTP/1.1 200 Connection established\r\n\r\n" + ) elif redirect == "change-proxy": - playbook << SendData(server2, b"CONNECT example.com:443 HTTP/1.1\r\n\r\n") - playbook >> DataReceived(server2, b"HTTP/1.1 200 Connection established\r\n\r\n") - playbook << SendData(server2, b"GET /two HTTP/1.1\r\nHost: example.com\r\n\r\n") + playbook << SendData(server2, b"CONNECT %s:443 HTTP/1.1\r\n\r\n" % domain) + playbook >> DataReceived( + server2, b"HTTP/1.1 200 Connection established\r\n\r\n" + ) + playbook << SendData(server2, b"GET /two HTTP/1.1\r\nHost: %s\r\n\r\n" % domain) playbook >> DataReceived(server2, b"HTTP/1.1 418 OK\r\nContent-Length: 0\r\n\r\n") playbook << SendData(tctx.client, b"HTTP/1.1 418 OK\r\nContent-Length: 0\r\n\r\n") @@ -707,20 +823,20 @@ def test_upstream_proxy(tctx, redirect, scheme): assert playbook if redirect == "change-destination": - assert flow().server_conn.address[0] == "other-server" + assert flow().server_conn.address[0] == (domain + b".test").decode("idna") else: - assert flow().server_conn.address[0] == "example.com" + assert flow().server_conn.address[0] == domain.decode("idna") if redirect == "change-proxy": - assert server2().address == flow().server_conn.via.address == ("other-proxy", 1234) + assert ( + server2().address == flow().server_conn.via.address == ("other-proxy", 1234) + ) else: assert server2().address == flow().server_conn.via.address == ("proxy", 8080) - assert ( - playbook - >> ConnectionClosed(tctx.client) - << CloseConnection(tctx.client) - ) + playbook >> ConnectionClosed(tctx.client) + playbook << CloseConnection(tctx.client) + assert playbook @pytest.mark.parametrize("mode", ["regular", "upstream"]) @@ -740,15 +856,15 @@ def test_http_proxy_tcp(tctx, mode, close_first): playbook = Playbook(toplayer, hooks=False) assert ( - playbook - >> DataReceived(tctx.client, b"CONNECT example:443 HTTP/1.1\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") - >> DataReceived(tctx.client, b"this is not http") - << layer.NextLayerHook(Placeholder()) - >> reply_next_layer(lambda ctx: TCPLayer(ctx, ignore=False)) - << TcpStartHook(f) - >> reply() - << OpenConnection(server) + playbook + >> DataReceived(tctx.client, b"CONNECT example:443 HTTP/1.1\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") + >> DataReceived(tctx.client, b"this is not http") + << layer.NextLayerHook(Placeholder()) + >> reply_next_layer(lambda ctx: TCPLayer(ctx, ignore=False)) + << TcpStartHook(f) + >> reply() + << OpenConnection(server) ) playbook >> reply(None) @@ -757,10 +873,10 @@ def test_http_proxy_tcp(tctx, mode, close_first): playbook >> DataReceived(server, b"HTTP/1.1 200 Connection established\r\n\r\n") assert ( - playbook - << SendData(server, b"this is not http") - >> DataReceived(server, b"true that") - << SendData(tctx.client, b"true that") + playbook + << SendData(server, b"this is not http") + >> DataReceived(server, b"true that") + << SendData(tctx.client, b"true that") ) if mode == "regular": @@ -770,7 +886,9 @@ def test_http_proxy_tcp(tctx, mode, close_first): assert ( playbook - >> TcpMessageInjected(f, TCPMessage(False, b"fake news from your friendly man-in-the-middle")) + >> TcpMessageInjected( + f, TCPMessage(False, b"fake news from your friendly man-in-the-middle") + ) << SendData(tctx.client, b"fake news from your friendly man-in-the-middle") ) @@ -779,11 +897,11 @@ def test_http_proxy_tcp(tctx, mode, close_first): else: a, b = server, tctx.client assert ( - playbook - >> ConnectionClosed(a) - << CloseConnection(b) - >> ConnectionClosed(b) - << CloseConnection(a) + playbook + >> ConnectionClosed(a) + << CloseConnection(b) + >> ConnectionClosed(b) + << CloseConnection(a) ) @@ -802,12 +920,14 @@ def test_proxy_chain(tctx, strategy): playbook >> DataReceived(tctx.client, b"CONNECT second-proxy:8080 HTTP/1.1\r\n\r\n") playbook << layer.NextLayerHook(Placeholder()) playbook >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) - playbook << SendData(tctx.client, - b"HTTP/1.1 502 Bad Gateway\r\n" - b"content-length: 198\r\n" - b"\r\n" - b"mitmproxy received an HTTP CONNECT request even though it is not running in regular/upstream mode. " - b"This usually indicates a misconfiguration, please see the mitmproxy mode documentation for details.") + playbook << SendData( + tctx.client, + b"HTTP/1.1 502 Bad Gateway\r\n" + b"content-length: 198\r\n" + b"\r\n" + b"mitmproxy received an HTTP CONNECT request even though it is not running in regular/upstream mode. " + b"This usually indicates a misconfiguration, please see the mitmproxy mode documentation for details.", + ) assert playbook @@ -816,64 +936,87 @@ def test_no_headers(tctx): """Test that we can correctly reassemble requests/responses with no headers.""" server = Placeholder(Server) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\n\r\n") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 204 No Content\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 204 No Content\r\n\r\n") + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\n\r\n") + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\n\r\n") + >> DataReceived(server, b"HTTP/1.1 204 No Content\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 204 No Content\r\n\r\n") ) assert server().address == ("example.com", 80) +def test_http_proxy_without_empty_chunk_in_head_request(tctx): + """Test handling of an empty, "chunked" head response.""" + server = Placeholder(Server) + assert ( + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived(tctx.client, b"HEAD http://example.com/ HTTP/1.1\r\n\r\n") + << OpenConnection(server) + >> reply(None) + << SendData(server, b"HEAD / HTTP/1.1\r\n\r\n") + >> DataReceived(server, b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n") + ) + + def test_http_proxy_relative_request(tctx): """Test handling of a relative-form "GET /" in regular proxy mode.""" server = Placeholder(Server) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 204 No Content\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 204 No Content\r\n\r\n") + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived(server, b"HTTP/1.1 204 No Content\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 204 No Content\r\n\r\n") ) assert server().address == ("example.com", 80) def test_http_proxy_relative_request_no_host_header(tctx): """Test handling of a relative-form "GET /" in regular proxy mode, but without a host header.""" - err = Placeholder(bytes) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n") - << SendData(tctx.client, err) - << CloseConnection(tctx.client) + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n") + << SendData( + tctx.client, + BytesMatching( + b"400 Bad Request.+" + b"HTTP request has no host header, destination unknown." + ), + ) + << CloseConnection(tctx.client) ) - assert b"400 Bad Request" in err() - assert b"HTTP request has no host header, destination unknown." in err() def test_http_expect(tctx): """Test handling of a 'Expect: 100-continue' header.""" server = Placeholder(Server) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"PUT http://example.com/large-file HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Content-Length: 15\r\n" - b"Expect: 100-continue\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 100 Continue\r\n\r\n") - >> DataReceived(tctx.client, b"lots of content") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"PUT /large-file HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Content-Length: 15\r\n\r\n" - b"lots of content") - >> DataReceived(server, b"HTTP/1.1 201 Created\r\nContent-Length: 0\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 201 Created\r\nContent-Length: 0\r\n\r\n") + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"PUT http://example.com/large-file HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length: 15\r\n" + b"Expect: 100-continue\r\n\r\n", + ) + << SendData(tctx.client, b"HTTP/1.1 100 Continue\r\n\r\n") + >> DataReceived(tctx.client, b"lots of content") + << OpenConnection(server) + >> reply(None) + << SendData( + server, + b"PUT /large-file HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length: 15\r\n\r\n" + b"lots of content", + ) + >> DataReceived(server, b"HTTP/1.1 201 Created\r\nContent-Length: 0\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 201 Created\r\nContent-Length: 0\r\n\r\n") ) assert server().address == ("example.com", 80) @@ -889,43 +1032,46 @@ def enable_streaming(flow: HTTPFlow): flow.request.stream = True assert ( - playbook - >> DataReceived(tctx.client, b"POST http://example.com/ HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Content-Length: 6\r\n" - b"\r\n" - b"abc") - << http.HttpRequestHeadersHook(flow) + playbook + >> DataReceived( + tctx.client, + b"POST http://example.com/ HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length: 6\r\n" + b"\r\n" + b"abc", + ) + << http.HttpRequestHeadersHook(flow) ) if stream: assert ( - playbook - >> reply(side_effect=enable_streaming) - << OpenConnection(server) - >> reply(None) - << SendData(server, b"POST / HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Content-Length: 6\r\n" - b"\r\n" - b"abc") + playbook + >> reply(side_effect=enable_streaming) + << OpenConnection(server) + >> reply(None) + << SendData( + server, + b"POST / HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length: 6\r\n" + b"\r\n" + b"abc", + ) ) else: assert playbook >> reply() - ( - playbook - >> ConnectionClosed(tctx.client) - << CloseConnection(tctx.client) - ) + playbook >> ConnectionClosed(tctx.client) + playbook << CloseConnection(tctx.client) if stream: playbook << CloseConnection(server) - assert ( - playbook - << http.HttpErrorHook(flow) - >> reply() - << None - ) + + playbook << http.HttpErrorHook(flow) + playbook >> reply() + playbook << None + assert playbook assert "peer closed connection" in flow().error.msg + assert not flow().live @pytest.mark.parametrize("stream", [True, False]) @@ -939,63 +1085,70 @@ def enable_streaming(flow: HTTPFlow): flow.response.stream = True assert ( - playbook - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\n" - b"Host: example.com\r\n\r\n") - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\n" - b"Host: example.com\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\n" - b"Content-Length: 6\r\n" - b"\r\n" - b"abc") - << http.HttpResponseHeadersHook(flow) + playbook + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\n" b"Host: example.com\r\n\r\n", + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n\r\n") + >> DataReceived( + server, b"HTTP/1.1 200 OK\r\n" b"Content-Length: 6\r\n" b"\r\n" b"abc" + ) + << http.HttpResponseHeadersHook(flow) ) if stream: assert ( - playbook - >> reply(side_effect=enable_streaming) - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n" - b"Content-Length: 6\r\n" - b"\r\n" - b"abc") + playbook + >> reply(side_effect=enable_streaming) + << SendData( + tctx.client, + b"HTTP/1.1 200 OK\r\n" b"Content-Length: 6\r\n" b"\r\n" b"abc", + ) ) else: assert playbook >> reply() assert ( - playbook - >> ConnectionClosed(server) - << CloseConnection(server) - << http.HttpErrorHook(flow) + playbook + >> ConnectionClosed(server) + << CloseConnection(server) + << http.HttpErrorHook(flow) ) if stream: - assert ( - playbook - >> reply() - << CloseConnection(tctx.client) - ) + playbook >> reply() + playbook << CloseConnection(tctx.client) + assert playbook else: - error_html = Placeholder(bytes) assert ( - playbook - >> reply() - << SendData(tctx.client, error_html) - << CloseConnection(tctx.client) + playbook + >> reply() + << SendData( + tctx.client, BytesMatching(b"502 Bad Gateway.+peer closed connection") + ) + << CloseConnection(tctx.client) ) - assert b"502 Bad Gateway" in error_html() - assert b"peer closed connection" in error_html() assert "peer closed connection" in flow().error.msg - - -@pytest.mark.parametrize("when", ["http_connect", "requestheaders", "request", "script-response-responseheaders", - "responseheaders", - "response", "error"]) + assert not flow().live + + +@pytest.mark.parametrize( + "when", + [ + "http_connect", + "requestheaders", + "request", + "script-response-responseheaders", + "responseheaders", + "response", + "error", + ], +) def test_kill_flow(tctx, when): """Test that we properly kill flows if instructed to do so""" tctx.options.connection_strategy = "lazy" @@ -1004,8 +1157,7 @@ def test_kill_flow(tctx, when): flow = Placeholder(HTTPFlow) def kill(flow: HTTPFlow): - # Can't use flow.kill() here because that currently still depends on a reply object. - flow.error = Error(Error.KILLED_MESSAGE) + flow.kill() def assert_kill(err_hook: bool = True): playbook >> reply(side_effect=kill) @@ -1014,54 +1166,71 @@ def assert_kill(err_hook: bool = True): playbook >> reply() playbook << CloseConnection(tctx.client) assert playbook + if flow(): + assert not flow().live playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular)) - assert (playbook - >> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n") - << http.HttpConnectHook(connect_flow)) + assert ( + playbook + >> DataReceived(tctx.client, b"CONNECT example.com:80 HTTP/1.1\r\n\r\n") + << http.HttpConnectHook(connect_flow) + ) if when == "http_connect": return assert_kill(False) - assert (playbook - >> reply() - << SendData(tctx.client, b'HTTP/1.1 200 Connection established\r\n\r\n') - >> DataReceived(tctx.client, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n") - << layer.NextLayerHook(Placeholder()) - >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) - << http.HttpRequestHeadersHook(flow)) + assert ( + playbook + >> reply() + << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") + >> DataReceived( + tctx.client, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n" + ) + << layer.NextLayerHook(Placeholder()) + >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) + << http.HttpRequestHeadersHook(flow) + ) if when == "requestheaders": return assert_kill() - assert (playbook - >> reply() - << http.HttpRequestHook(flow)) + playbook >> reply() + playbook << http.HttpRequestHook(flow) if when == "request": return assert_kill() if when == "script-response-responseheaders": - assert (playbook - >> reply(side_effect=lambda f: setattr(f, "response", Response.make())) - << http.HttpResponseHeadersHook(flow)) + assert ( + playbook + >> reply(side_effect=lambda f: setattr(f, "response", Response.make())) + << http.HttpResponseHeadersHook(flow) + ) return assert_kill() - assert (playbook - >> reply() - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World") - << http.HttpResponseHeadersHook(flow)) + assert ( + playbook + >> reply() + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET /foo?hello=1 HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived( + server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World" + ) + << http.HttpResponseHeadersHook(flow) + ) if when == "responseheaders": return assert_kill() if when == "response": - assert (playbook - >> reply() - >> DataReceived(server, b"!") - << http.HttpResponseHook(flow)) + assert ( + playbook + >> reply() + >> DataReceived(server, b"!") + << http.HttpResponseHook(flow) + ) return assert_kill(False) elif when == "error": - assert (playbook - >> reply() - >> ConnectionClosed(server) - << CloseConnection(server) - << http.HttpErrorHook(flow)) + assert ( + playbook + >> reply() + >> ConnectionClosed(server) + << CloseConnection(server) + << http.HttpErrorHook(flow) + ) return assert_kill(False) else: raise AssertionError @@ -1070,16 +1239,18 @@ def assert_kill(err_hook: bool = True): def test_close_during_connect_hook(tctx): flow = Placeholder(HTTPFlow) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular)) - >> DataReceived(tctx.client, - b'CONNECT hi.ls:443 HTTP/1.1\r\n' - b'Proxy-Connection: keep-alive\r\n' - b'Connection: keep-alive\r\n' - b'Host: hi.ls:443\r\n\r\n') - << http.HttpConnectHook(flow) - >> ConnectionClosed(tctx.client) - << CloseConnection(tctx.client) - >> reply(to=-3) + Playbook(http.HttpLayer(tctx, HTTPMode.regular)) + >> DataReceived( + tctx.client, + b"CONNECT hi.ls:443 HTTP/1.1\r\n" + b"Proxy-Connection: keep-alive\r\n" + b"Connection: keep-alive\r\n" + b"Host: hi.ls:443\r\n\r\n", + ) + << http.HttpConnectHook(flow) + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.client) + >> reply(to=-3) ) @@ -1091,23 +1262,27 @@ def test_connection_close_header(tctx, client_close, server_close): return server = Placeholder(Server) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"GET http://example/ HTTP/1.1\r\n" - b"Host: example\r\n" + client_close + - b"\r\n") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\n" - b"Host: example\r\n" + client_close + - b"\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\n" - b"Content-Length: 0\r\n" + server_close + - b"\r\n") - << CloseConnection(server) - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n" - b"Content-Length: 0\r\n" + server_close + - b"\r\n") - << CloseConnection(tctx.client) + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"GET http://example/ HTTP/1.1\r\n" + b"Host: example\r\n" + client_close + b"\r\n", + ) + << OpenConnection(server) + >> reply(None) + << SendData( + server, b"GET / HTTP/1.1\r\n" b"Host: example\r\n" + client_close + b"\r\n" + ) + >> DataReceived( + server, + b"HTTP/1.1 200 OK\r\n" b"Content-Length: 0\r\n" + server_close + b"\r\n", + ) + << CloseConnection(server) + << SendData( + tctx.client, + b"HTTP/1.1 200 OK\r\n" b"Content-Length: 0\r\n" + server_close + b"\r\n", + ) + << CloseConnection(tctx.client) ) @@ -1124,34 +1299,45 @@ def test_upgrade(tctx, proto): http_flow = Placeholder(HTTPFlow) playbook = Playbook(http.HttpLayer(tctx, HTTPMode.transparent)) ( - playbook - >> DataReceived(tctx.client, - b"GET / HTTP/1.1\r\n" - b"Connection: upgrade\r\n" - b"Upgrade: websocket\r\n" - b"Sec-WebSocket-Version: 13\r\n" - b"\r\n") - << http.HttpRequestHeadersHook(http_flow) - >> reply() - << http.HttpRequestHook(http_flow) - >> reply() - << SendData(tctx.server, b"GET / HTTP/1.1\r\n" - b"Connection: upgrade\r\n" - b"Upgrade: websocket\r\n" - b"Sec-WebSocket-Version: 13\r\n" - b"\r\n") - >> DataReceived(tctx.server, b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n" - b"\r\n") - << http.HttpResponseHeadersHook(http_flow) - >> reply() - << http.HttpResponseHook(http_flow) - >> reply() - << SendData(tctx.client, b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n" - b"\r\n") + playbook + >> DataReceived( + tctx.client, + b"GET / HTTP/1.1\r\n" + b"Connection: upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n", + ) + << http.HttpRequestHeadersHook(http_flow) + >> reply() + << http.HttpRequestHook(http_flow) + >> reply() + << SendData( + tctx.server, + b"GET / HTTP/1.1\r\n" + b"Connection: upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n", + ) + >> DataReceived( + tctx.server, + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"\r\n", + ) + << http.HttpResponseHeadersHook(http_flow) + >> reply() + << http.HttpResponseHook(http_flow) + >> reply() + << SendData( + tctx.client, + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"\r\n", + ) ) if proto == "websocket": assert playbook << WebsocketStartHook(http_flow) @@ -1160,7 +1346,10 @@ def test_upgrade(tctx, proto): else: assert ( playbook - << Log("Sent HTTP 101 response, but no protocol is enabled to upgrade to.", "warn") + << Log( + "Sent HTTP 101 response, but no protocol is enabled to upgrade to.", + "warn", + ) << CloseConnection(tctx.client) ) @@ -1170,21 +1359,27 @@ def test_dont_reuse_closed(tctx): server = Placeholder(Server) server2 = Placeholder(Server) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") - >> ConnectionClosed(server) - << CloseConnection(server) - >> DataReceived(tctx.client, b"GET http://example.com/two HTTP/1.1\r\nHost: example.com\r\n\r\n") - << OpenConnection(server2) - >> reply(None) - << SendData(server2, b"GET /two HTTP/1.1\r\nHost: example.com\r\n\r\n") - >> DataReceived(server2, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + >> ConnectionClosed(server) + << CloseConnection(server) + >> DataReceived( + tctx.client, + b"GET http://example.com/two HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << OpenConnection(server2) + >> reply(None) + << SendData(server2, b"GET /two HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived(server2, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") ) @@ -1192,15 +1387,12 @@ def test_reuse_error(tctx): """Test that an errored connection is reused.""" tctx.server.address = ("example.com", 443) tctx.server.error = "tls verify failed" - error_html = Placeholder(bytes) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.transparent), hooks=False) - >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n") - << SendData(tctx.client, error_html) - << CloseConnection(tctx.client) + Playbook(http.HttpLayer(tctx, HTTPMode.transparent), hooks=False) + >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n") + << SendData(tctx.client, BytesMatching(b"502 Bad Gateway.+tls verify failed")) + << CloseConnection(tctx.client) ) - assert b"502 Bad Gateway" in error_html() - assert b"tls verify failed" in error_html() def test_transparent_sni(tctx): @@ -1213,13 +1405,13 @@ def test_transparent_sni(tctx): server = Placeholder(Server) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.transparent)) - >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n") - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) + Playbook(http.HttpLayer(tctx, HTTPMode.transparent)) + >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n") + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) ) assert server().address == ("192.0.2.42", 443) assert server().sni == "example.com" @@ -1229,58 +1421,105 @@ def test_original_server_disconnects(tctx): """Test that we correctly handle the case where the initial server conn is just closed.""" tctx.server.state = ConnectionState.OPEN assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.transparent)) - >> ConnectionClosed(tctx.server) - << CloseConnection(tctx.server) + Playbook(http.HttpLayer(tctx, HTTPMode.transparent)) + >> ConnectionClosed(tctx.server) + << CloseConnection(tctx.server) ) def test_request_smuggling(tctx): """Test that we reject request smuggling""" - err = Placeholder(bytes) assert ( Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Content-Length: 42\r\n" - b"Transfer-Encoding: chunked\r\n\r\n") - << SendData(tctx.client, err) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length: 42\r\n" + b"Transfer-Encoding: chunked\r\n\r\n", + ) + << SendData( + tctx.client, + BytesMatching( + b"Received both a Transfer-Encoding and a Content-Length header" + ), + ) << CloseConnection(tctx.client) ) - assert b"Received both a Transfer-Encoding and a Content-Length header" in err() + + +def test_request_smuggling_whitespace(tctx): + """Test that we reject header names with whitespace""" + assert ( + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length : 42\r\n\r\n", + ) + << SendData(tctx.client, BytesMatching(b"Received an invalid header name")) + << CloseConnection(tctx.client) + ) + + +def test_request_smuggling_validation_disabled(tctx): + """Test that we don't reject request smuggling when validation is disabled.""" + tctx.options.validate_inbound_headers = False + assert ( + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length: 4\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"4\r\n" + b"abcd\r\n" + b"0\r\n" + b"\r\n", + ) + << OpenConnection(Placeholder(Server)) + ) def test_request_smuggling_te_te(tctx): """Test that we reject transfer-encoding headers that are weird in some way""" - err = Placeholder(bytes) assert ( Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) - >> DataReceived(tctx.client, ("GET http://example.com/ HTTP/1.1\r\n" - "Host: example.com\r\n" - "Transfer-Encoding: chunKed\r\n\r\n").encode()) # note the non-standard "K" - << SendData(tctx.client, err) + >> DataReceived( + tctx.client, + ( + "GET http://example.com/ HTTP/1.1\r\n" + "Host: example.com\r\n" + "Transfer-Encoding: chunKed\r\n\r\n" + ).encode(), + ) # note the non-standard "K" + << SendData(tctx.client, BytesMatching(b"Invalid transfer encoding")) << CloseConnection(tctx.client) ) - assert b"Invalid transfer encoding" in err() def test_invalid_content_length(tctx): """Test that we still trigger flow hooks for requests with semantic errors""" - err = Placeholder(bytes) flow = Placeholder(HTTPFlow) assert ( Playbook(http.HttpLayer(tctx, HTTPMode.regular)) - >> DataReceived(tctx.client, ("GET http://example.com/ HTTP/1.1\r\n" - "Host: example.com\r\n" - "Content-Length: NaN\r\n\r\n").encode()) - << SendData(tctx.client, err) + >> DataReceived( + tctx.client, + ( + b"GET http://example.com/ HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length: NaN\r\n\r\n" + ), + ) + << SendData(tctx.client, BytesMatching(b"Invalid Content-Length header")) << CloseConnection(tctx.client) << http.HttpRequestHeadersHook(flow) >> reply() << http.HttpErrorHook(flow) >> reply() ) - assert b"Invalid Content-Length header" in err() def test_chunked_and_content_length_set_by_addon(tctx): @@ -1299,30 +1538,39 @@ def make_chunked(flow: HTTPFlow): flow.request.headers["Transfer-Encoding"] = "chunked" assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.regular)) - >> DataReceived(tctx.client, b"POST http://example.com/ HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Content-Length: 0\r\n\r\n") - << http.HttpRequestHeadersHook(flow) - >> reply(side_effect=make_chunked) - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None) - << SendData(server, b"POST / HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"Content-Length: 0\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - b"0\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") - << http.HttpResponseHeadersHook(flow) - >> reply() - << http.HttpResponseHook(flow) - >> reply(side_effect=make_chunked) - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n" - b"Content-Length: 0\r\n" - b"Transfer-Encoding: chunked\r\n\r\n" - b"0\r\n\r\n") + Playbook(http.HttpLayer(tctx, HTTPMode.regular)) + >> DataReceived( + tctx.client, + b"POST http://example.com/ HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length: 0\r\n\r\n", + ) + << http.HttpRequestHeadersHook(flow) + >> reply(side_effect=make_chunked) + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None) + << SendData( + server, + b"POST / HTTP/1.1\r\n" + b"Host: example.com\r\n" + b"Content-Length: 0\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"0\r\n\r\n", + ) + >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") + << http.HttpResponseHeadersHook(flow) + >> reply() + << http.HttpResponseHook(flow) + >> reply(side_effect=make_chunked) + << SendData( + tctx.client, + b"HTTP/1.1 200 OK\r\n" + b"Content-Length: 0\r\n" + b"Transfer-Encoding: chunked\r\n\r\n" + b"0\r\n\r\n", + ) ) @@ -1339,8 +1587,78 @@ def test_connect_more_newlines(tctx): >> reply() << OpenConnection(server) >> reply(None) - << SendData(tctx.client, b'HTTP/1.1 200 Connection established\r\n\r\n') + << SendData(tctx.client, b"HTTP/1.1 200 Connection established\r\n\r\n") >> DataReceived(tctx.client, b"\x16\x03\x03\x00\xb3\x01\x00\x00\xaf\x03\x03") << layer.NextLayerHook(nl) ) assert nl().data_client() == b"\x16\x03\x03\x00\xb3\x01\x00\x00\xaf\x03\x03" + + +def flows_tracked() -> int: + return sum(isinstance(x, HTTPFlow) for x in gc.get_objects()) + + +def test_memory_usage_completed_flows(tctx): + """Make sure that flows are not kept in memory after they are completed.""" + gc.collect() + flow_count = flows_tracked() + + server = Placeholder(Server) + assert ( + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived(server, b"HTTP/1.1 204 No Content\r\n\r\n") + << SendData(tctx.client, b"HTTP/1.1 204 No Content\r\n\r\n") + ) + + gc.collect() + assert flows_tracked() == flow_count + + +def test_memory_usage_errored_flows(tctx): + """Make sure that flows are not kept in memory after they errored.""" + gc.collect() + flow_count = flows_tracked() + + assert ( + Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << OpenConnection(Placeholder(Server)) + >> reply("connection failed") + << SendData(tctx.client, BytesMatching(b"502 Bad Gateway.+connection failed")) + << CloseConnection(tctx.client) + ) + + gc.collect() + assert flows_tracked() == flow_count + + +def test_drop_stream_with_paused_events(tctx): + """Make sure that we don't crash if we still have paused events for streams that error.""" + tctx.options.stream_large_bodies = "1" + server = Placeholder(Server) + flow = Placeholder(HTTPFlow) + assert ( + Playbook(http.HttpLayer(tctx, HTTPMode.regular)) + >> DataReceived( + tctx.client, + b"POST http://example.com/ HTTP/1.1\r\nHost: example.com\r\nContent-Length: 4\r\n\r\ndata", + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << OpenConnection(server) + >> reply('Connection killed: error') + << http.HttpErrorHook(flow) + >> reply() + << SendData(tctx.client, BytesMatching(b"502 Bad Gateway.+Connection killed")) + << CloseConnection(tctx.client) + ) diff --git a/test/mitmproxy/proxy/layers/http/test_http1.py b/test/mitmproxy/proxy/layers/http/test_http1.py index 26c6ea0f53..05e84e1fc1 100644 --- a/test/mitmproxy/proxy/layers/http/test_http1.py +++ b/test/mitmproxy/proxy/layers/http/test_http1.py @@ -3,8 +3,17 @@ from mitmproxy import http from mitmproxy.proxy.commands import SendData from mitmproxy.proxy.events import DataReceived -from mitmproxy.proxy.layers.http import Http1Server, ReceiveHttp, RequestHeaders, RequestEndOfMessage, \ - ResponseHeaders, ResponseEndOfMessage, RequestData, Http1Client, ResponseData +from mitmproxy.proxy.layers.http import ( + Http1Server, + ReceiveHttp, + RequestHeaders, + RequestEndOfMessage, + ResponseHeaders, + ResponseEndOfMessage, + RequestData, + Http1Client, + ResponseData, +) from test.mitmproxy.proxy.tutils import Placeholder, Playbook @@ -14,95 +23,95 @@ def test_simple(self, tctx, pipeline): hdrs1 = Placeholder(RequestHeaders) hdrs2 = Placeholder(RequestHeaders) req2 = ( - b"GET http://example.com/two HTTP/1.1\r\n" - b"Host: example.com\r\n" - b"\r\n" + b"GET http://example.com/two HTTP/1.1\r\n" b"Host: example.com\r\n" b"\r\n" ) playbook = Playbook(Http1Server(tctx)) ( - playbook - >> DataReceived(tctx.client, - b"POST http://example.com/one HTTP/1.1\r\n" - b"Content-Length: 3\r\n" - b"\r\n" - b"abc" - + (req2 if pipeline else b"")) - << ReceiveHttp(hdrs1) - << ReceiveHttp(RequestData(1, b"abc")) - << ReceiveHttp(RequestEndOfMessage(1)) - >> ResponseHeaders(1, http.Response.make(200)) - << SendData(tctx.client, b'HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n') - >> ResponseEndOfMessage(1) + playbook + >> DataReceived( + tctx.client, + b"POST http://example.com/one HTTP/1.1\r\n" + b"Content-Length: 3\r\n" + b"\r\n" + b"abc" + (req2 if pipeline else b""), + ) + << ReceiveHttp(hdrs1) + << ReceiveHttp(RequestData(1, b"abc")) + << ReceiveHttp(RequestEndOfMessage(1)) + >> ResponseHeaders(1, http.Response.make(200)) + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") + >> ResponseEndOfMessage(1) ) if not pipeline: playbook >> DataReceived(tctx.client, req2) - assert ( - playbook - << ReceiveHttp(hdrs2) - << ReceiveHttp(RequestEndOfMessage(3)) - ) + playbook << ReceiveHttp(hdrs2) + playbook << ReceiveHttp(RequestEndOfMessage(3)) + assert playbook @pytest.mark.parametrize("pipeline", ["pipeline", None]) def test_connect(self, tctx, pipeline): playbook = Playbook(Http1Server(tctx)) ( - playbook - >> DataReceived(tctx.client, - b"CONNECT example.com:443 HTTP/1.1\r\n" - b"content-length: 0\r\n" - b"\r\n" - + (b"some plain tcp" if pipeline else b"")) - << ReceiveHttp(Placeholder(RequestHeaders)) - # << ReceiveHttp(RequestEndOfMessage(1)) - >> ResponseHeaders(1, http.Response.make(200)) - << SendData(tctx.client, b'HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n') - >> ResponseEndOfMessage(1) + playbook + >> DataReceived( + tctx.client, + b"CONNECT example.com:443 HTTP/1.1\r\n" + b"content-length: 0\r\n" + b"\r\n" + (b"some plain tcp" if pipeline else b""), + ) + << ReceiveHttp(Placeholder(RequestHeaders)) + # << ReceiveHttp(RequestEndOfMessage(1)) + >> ResponseHeaders(1, http.Response.make(200)) + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") + >> ResponseEndOfMessage(1) ) if not pipeline: playbook >> DataReceived(tctx.client, b"some plain tcp") - assert (playbook - << ReceiveHttp(RequestData(1, b"some plain tcp")) - ) + assert playbook << ReceiveHttp(RequestData(1, b"some plain tcp")) @pytest.mark.parametrize("pipeline", ["pipeline", None]) def test_upgrade(self, tctx, pipeline): playbook = Playbook(Http1Server(tctx)) ( - playbook - >> DataReceived(tctx.client, - b"POST http://example.com/one HTTP/1.1\r\n" - b"Connection: Upgrade\r\n" - b"Upgrade: websocket\r\n" - b"\r\n" - + (b"some websockets" if pipeline else b"")) - << ReceiveHttp(Placeholder(RequestHeaders)) - << ReceiveHttp(RequestEndOfMessage(1)) - >> ResponseHeaders(1, http.Response.make(101)) - << SendData(tctx.client, b'HTTP/1.1 101 Switching Protocols\r\ncontent-length: 0\r\n\r\n') - >> ResponseEndOfMessage(1) + playbook + >> DataReceived( + tctx.client, + b"POST http://example.com/one HTTP/1.1\r\n" + b"Connection: Upgrade\r\n" + b"Upgrade: websocket\r\n" + b"\r\n" + (b"some websockets" if pipeline else b""), + ) + << ReceiveHttp(Placeholder(RequestHeaders)) + << ReceiveHttp(RequestEndOfMessage(1)) + >> ResponseHeaders(1, http.Response.make(101)) + << SendData( + tctx.client, + b"HTTP/1.1 101 Switching Protocols\r\ncontent-length: 0\r\n\r\n", + ) + >> ResponseEndOfMessage(1) ) if not pipeline: playbook >> DataReceived(tctx.client, b"some websockets") - assert (playbook - << ReceiveHttp(RequestData(1, b"some websockets")) - ) + assert playbook << ReceiveHttp(RequestData(1, b"some websockets")) def test_upgrade_denied(self, tctx): assert ( - Playbook(Http1Server(tctx)) - >> DataReceived(tctx.client, - b"GET http://example.com/ HTTP/1.1\r\n" - b"Connection: Upgrade\r\n" - b"Upgrade: websocket\r\n" - b"\r\n") - << ReceiveHttp(Placeholder(RequestHeaders)) - << ReceiveHttp(RequestEndOfMessage(1)) - >> ResponseHeaders(1, http.Response.make(200)) - << SendData(tctx.client, b'HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n') - >> ResponseEndOfMessage(1) - >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n") - << ReceiveHttp(Placeholder(RequestHeaders)) - << ReceiveHttp(RequestEndOfMessage(3)) + Playbook(Http1Server(tctx)) + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\n" + b"Connection: Upgrade\r\n" + b"Upgrade: websocket\r\n" + b"\r\n", + ) + << ReceiveHttp(Placeholder(RequestHeaders)) + << ReceiveHttp(RequestEndOfMessage(1)) + >> ResponseHeaders(1, http.Response.make(200)) + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") + >> ResponseEndOfMessage(1) + >> DataReceived(tctx.client, b"GET / HTTP/1.1\r\n\r\n") + << ReceiveHttp(Placeholder(RequestHeaders)) + << ReceiveHttp(RequestEndOfMessage(3)) ) @@ -114,25 +123,27 @@ def test_simple(self, tctx, pipeline): playbook = Playbook(Http1Client(tctx)) ( - playbook - >> RequestHeaders(1, req, True) - << SendData(tctx.server, b"GET / HTTP/1.1\r\ncontent-length: 0\r\n\r\n") - >> RequestEndOfMessage(1) + playbook + >> RequestHeaders(1, req, True) + << SendData(tctx.server, b"GET / HTTP/1.1\r\ncontent-length: 0\r\n\r\n") + >> RequestEndOfMessage(1) ) if pipeline: - with pytest.raises(AssertionError, match="assert self.stream_id == event.stream_id"): - assert (playbook - >> RequestHeaders(3, req, True) - ) + with pytest.raises( + AssertionError, match="assert self.stream_id == event.stream_id" + ): + assert playbook >> RequestHeaders(3, req, True) return assert ( - playbook - >> DataReceived(tctx.server, b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n") - << ReceiveHttp(resp) - << ReceiveHttp(ResponseEndOfMessage(1)) - # no we can send the next request - >> RequestHeaders(3, req, True) - << SendData(tctx.server, b"GET / HTTP/1.1\r\ncontent-length: 0\r\n\r\n") + playbook + >> DataReceived( + tctx.server, b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\n" + ) + << ReceiveHttp(resp) + << ReceiveHttp(ResponseEndOfMessage(1)) + # no we can send the next request + >> RequestHeaders(3, req, True) + << SendData(tctx.server, b"GET / HTTP/1.1\r\ncontent-length: 0\r\n\r\n") ) assert resp().response.status_code == 200 @@ -143,59 +154,82 @@ def test_connect(self, tctx): playbook = Playbook(Http1Client(tctx)) assert ( - playbook - >> RequestHeaders(1, req, True) - << SendData(tctx.server, b"CONNECT example.com:443 HTTP/1.1\r\ncontent-length: 0\r\n\r\n") - >> RequestEndOfMessage(1) - >> DataReceived(tctx.server, b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\nsome plain tcp") - << ReceiveHttp(resp) - # << ReceiveHttp(ResponseEndOfMessage(1)) - << ReceiveHttp(ResponseData(1, b"some plain tcp")) - # no we can send plain data - >> RequestData(1, b"some more tcp") - << SendData(tctx.server, b"some more tcp") + playbook + >> RequestHeaders(1, req, True) + << SendData( + tctx.server, + b"CONNECT example.com:443 HTTP/1.1\r\ncontent-length: 0\r\n\r\n", + ) + >> RequestEndOfMessage(1) + >> DataReceived( + tctx.server, + b"HTTP/1.1 200 OK\r\ncontent-length: 0\r\n\r\nsome plain tcp", + ) + << ReceiveHttp(resp) + # << ReceiveHttp(ResponseEndOfMessage(1)) + << ReceiveHttp(ResponseData(1, b"some plain tcp")) + # no we can send plain data + >> RequestData(1, b"some more tcp") + << SendData(tctx.server, b"some more tcp") ) def test_upgrade(self, tctx): - req = http.Request.make("GET", "http://example.com/ws", headers={ - "Connection": "Upgrade", - "Upgrade": "websocket", - }) + req = http.Request.make( + "GET", + "http://example.com/ws", + headers={ + "Connection": "Upgrade", + "Upgrade": "websocket", + }, + ) resp = Placeholder(ResponseHeaders) playbook = Playbook(Http1Client(tctx)) assert ( - playbook - >> RequestHeaders(1, req, True) - << SendData(tctx.server, - b"GET /ws HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\ncontent-length: 0\r\n\r\n") - >> RequestEndOfMessage(1) - >> DataReceived(tctx.server, b"HTTP/1.1 101 Switching Protocols\r\ncontent-length: 0\r\n\r\nhello") - << ReceiveHttp(resp) - << ReceiveHttp(ResponseEndOfMessage(1)) - << ReceiveHttp(ResponseData(1, b"hello")) - # no we can send plain data - >> RequestData(1, b"some more websockets") - << SendData(tctx.server, b"some more websockets") + playbook + >> RequestHeaders(1, req, True) + << SendData( + tctx.server, + b"GET /ws HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\ncontent-length: 0\r\n\r\n", + ) + >> RequestEndOfMessage(1) + >> DataReceived( + tctx.server, + b"HTTP/1.1 101 Switching Protocols\r\ncontent-length: 0\r\n\r\nhello", + ) + << ReceiveHttp(resp) + << ReceiveHttp(ResponseEndOfMessage(1)) + << ReceiveHttp(ResponseData(1, b"hello")) + # no we can send plain data + >> RequestData(1, b"some more websockets") + << SendData(tctx.server, b"some more websockets") ) def test_upgrade_denied(self, tctx): - req = http.Request.make("GET", "http://example.com/ws", headers={ - "Connection": "Upgrade", - "Upgrade": "websocket", - }) + req = http.Request.make( + "GET", + "http://example.com/ws", + headers={ + "Connection": "Upgrade", + "Upgrade": "websocket", + }, + ) resp = Placeholder(ResponseHeaders) playbook = Playbook(Http1Client(tctx)) assert ( - playbook - >> RequestHeaders(1, req, True) - << SendData(tctx.server, - b"GET /ws HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\ncontent-length: 0\r\n\r\n") - >> RequestEndOfMessage(1) - >> DataReceived(tctx.server, b"HTTP/1.1 200 Ok\r\ncontent-length: 0\r\n\r\n") - << ReceiveHttp(resp) - << ReceiveHttp(ResponseEndOfMessage(1)) - >> RequestHeaders(3, req, True) - << SendData(tctx.server, Placeholder(bytes)) + playbook + >> RequestHeaders(1, req, True) + << SendData( + tctx.server, + b"GET /ws HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\ncontent-length: 0\r\n\r\n", + ) + >> RequestEndOfMessage(1) + >> DataReceived( + tctx.server, b"HTTP/1.1 200 Ok\r\ncontent-length: 0\r\n\r\n" + ) + << ReceiveHttp(resp) + << ReceiveHttp(ResponseEndOfMessage(1)) + >> RequestHeaders(3, req, True) + << SendData(tctx.server, Placeholder(bytes)) ) diff --git a/test/mitmproxy/proxy/layers/http/test_http2.py b/test/mitmproxy/proxy/layers/http/test_http2.py index 5d76324cc7..6e8297af49 100644 --- a/test/mitmproxy/proxy/layers/http/test_http2.py +++ b/test/mitmproxy/proxy/layers/http/test_http2.py @@ -1,16 +1,21 @@ -from typing import List, Tuple - import h2.settings import hpack import hyperframe.frame import pytest +import time from h2.errors import ErrorCodes from mitmproxy.connection import ConnectionState, Server from mitmproxy.flow import Error from mitmproxy.http import HTTPFlow, Headers, Request from mitmproxy.net.http import status_codes -from mitmproxy.proxy.commands import CloseConnection, OpenConnection, SendData +from mitmproxy.proxy.commands import ( + CloseConnection, + Log, + OpenConnection, + SendData, + RequestWakeup, +) from mitmproxy.proxy.context import Context from mitmproxy.proxy.events import ConnectionClosed, DataReceived from mitmproxy.proxy.layers import http @@ -20,25 +25,17 @@ from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply example_request_headers = ( - (b':method', b'GET'), - (b':scheme', b'http'), - (b':path', b'/'), - (b':authority', b'example.com'), + (b":method", b"GET"), + (b":scheme", b"http"), + (b":path", b"/"), + (b":authority", b"example.com"), ) -example_response_headers = ( - (b':status', b'200'), -) +example_response_headers = ((b":status", b"200"),) -example_request_trailers = ( - (b'req-trailer-a', b'a'), - (b'req-trailer-b', b'b') -) +example_request_trailers = ((b"req-trailer-a", b"a"), (b"req-trailer-b", b"b")) -example_response_trailers = ( - (b'resp-trailer-a', b'a'), - (b'resp-trailer-b', b'b') -) +example_response_trailers = ((b"resp-trailer-a", b"a"), (b"resp-trailer-b", b"b")) @pytest.fixture @@ -51,29 +48,32 @@ def open_h2_server_conn(): return s -def decode_frames(data: bytes) -> List[hyperframe.frame.Frame]: +def decode_frames(data: bytes) -> list[hyperframe.frame.Frame]: # swallow preamble if data.startswith(b"PRI * HTTP/2.0"): data = data[24:] frames = [] while data: f, length = hyperframe.frame.Frame.parse_frame_header(data[:9]) - f.parse_body(memoryview(data[9:9 + length])) + f.parse_body(memoryview(data[9 : 9 + length])) frames.append(f) - data = data[9 + length:] + data = data[9 + length :] return frames -def start_h2_client(tctx: Context) -> Tuple[Playbook, FrameFactory]: +def start_h2_client(tctx: Context, keepalive: int = 0) -> tuple[Playbook, FrameFactory]: tctx.client.alpn = b"h2" + tctx.options.http2_ping_keepalive = keepalive frame_factory = FrameFactory() playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular)) assert ( - playbook - << SendData(tctx.client, Placeholder()) # initial settings frame - >> DataReceived(tctx.client, frame_factory.preamble()) - >> DataReceived(tctx.client, frame_factory.build_settings_frame({}, ack=True).serialize()) + playbook + << SendData(tctx.client, Placeholder()) # initial settings frame + >> DataReceived(tctx.client, frame_factory.preamble()) + >> DataReceived( + tctx.client, frame_factory.build_settings_frame({}, ack=True).serialize() + ) ) return playbook, frame_factory @@ -88,16 +88,20 @@ def test_simple(tctx): server = Placeholder(Server) initial = Placeholder(bytes) assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"]).serialize()) - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None, side_effect=make_h2) - << SendData(server, initial) + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, initial) ) frames = decode_frames(initial()) assert [type(x) for x in frames] == [ @@ -106,18 +110,25 @@ def test_simple(tctx): ] sff = FrameFactory() assert ( - playbook - # a conforming h2 server would send settings first, we disregard this for now. - >> DataReceived(server, sff.build_headers_frame(example_response_headers).serialize()) - << http.HttpResponseHeadersHook(flow) - >> reply() - >> DataReceived(server, sff.build_data_frame(b"Hello, World!", flags=["END_STREAM"]).serialize()) - << http.HttpResponseHook(flow) - >> reply() - << SendData(tctx.client, - cff.build_headers_frame(example_response_headers).serialize() + - cff.build_data_frame(b"Hello, World!").serialize() + - cff.build_data_frame(b"", flags=["END_STREAM"]).serialize()) + playbook + # a conforming h2 server would send settings first, we disregard this for now. + >> DataReceived( + server, sff.build_headers_frame(example_response_headers).serialize() + ) + << http.HttpResponseHeadersHook(flow) + >> reply() + >> DataReceived( + server, + sff.build_data_frame(b"Hello, World!", flags=["END_STREAM"]).serialize(), + ) + << http.HttpResponseHook(flow) + >> reply() + << SendData( + tctx.client, + cff.build_headers_frame(example_response_headers).serialize() + + cff.build_data_frame(b"Hello, World!").serialize() + + cff.build_data_frame(b"", flags=["END_STREAM"]).serialize(), + ) ) assert flow().request.url == "http://example.com/" assert flow().response.text == "Hello, World!" @@ -135,28 +146,40 @@ def enable_streaming(flow: HTTPFlow): flow = Placeholder(HTTPFlow) ( playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"]).serialize()) + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) << http.HttpRequestHeadersHook(flow) >> reply() << http.HttpRequestHook(flow) >> reply() << SendData(tctx.server, Placeholder(bytes)) # a conforming h2 server would send settings first, we disregard this for now. - >> DataReceived(tctx.server, sff.build_headers_frame(example_response_headers).serialize() + - sff.build_data_frame(b"Hello, World!").serialize()) + >> DataReceived( + tctx.server, + sff.build_headers_frame(example_response_headers).serialize() + + sff.build_data_frame(b"Hello, World!").serialize(), + ) << http.HttpResponseHeadersHook(flow) >> reply(side_effect=enable_streaming) ) if stream: playbook << SendData( tctx.client, - cff.build_headers_frame(example_response_headers).serialize() + - cff.build_data_frame(b"Hello, World!").serialize() + cff.build_headers_frame(example_response_headers).serialize() + + cff.build_data_frame(b"Hello, World!").serialize(), ) assert ( playbook - >> DataReceived(tctx.server, sff.build_headers_frame(example_response_trailers, flags=["END_STREAM"]).serialize()) + >> DataReceived( + tctx.server, + sff.build_headers_frame( + example_response_trailers, flags=["END_STREAM"] + ).serialize(), + ) << http.HttpResponseHook(flow) ) assert flow().response.trailers @@ -165,17 +188,26 @@ def enable_streaming(flow: HTTPFlow): assert ( playbook >> reply() - << SendData(tctx.client, - cff.build_headers_frame(example_response_trailers[1:], flags=["END_STREAM"]).serialize()) + << SendData( + tctx.client, + cff.build_headers_frame( + example_response_trailers[1:], flags=["END_STREAM"] + ).serialize(), + ) ) else: assert ( playbook >> reply() - << SendData(tctx.client, - cff.build_headers_frame(example_response_headers).serialize() + - cff.build_data_frame(b"Hello, World!").serialize() + - cff.build_headers_frame(example_response_trailers[1:], flags=["END_STREAM"]).serialize())) + << SendData( + tctx.client, + cff.build_headers_frame(example_response_headers).serialize() + + cff.build_data_frame(b"Hello, World!").serialize() + + cff.build_headers_frame( + example_response_trailers[1:], flags=["END_STREAM"] + ).serialize(), + ) + ) @pytest.mark.parametrize("stream", ["stream", ""]) @@ -191,10 +223,11 @@ def enable_streaming(flow: HTTPFlow): server_data2 = Placeholder(bytes) ( playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers).serialize() + - cff.build_data_frame(b"Hello, World!").serialize() - ) + >> DataReceived( + tctx.client, + cff.build_headers_frame(example_request_headers).serialize() + + cff.build_data_frame(b"Hello, World!").serialize(), + ) << http.HttpRequestHeadersHook(flow) >> reply(side_effect=enable_streaming) ) @@ -202,8 +235,12 @@ def enable_streaming(flow: HTTPFlow): playbook << SendData(tctx.server, server_data1) assert ( playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_trailers, flags=["END_STREAM"]).serialize()) + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_trailers, flags=["END_STREAM"] + ).serialize(), + ) << http.HttpRequestHook(flow) >> reply() << SendData(tctx.server, server_data2) @@ -223,18 +260,22 @@ def test_upstream_error(tctx): server = Placeholder(Server) err = Placeholder(bytes) assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"]).serialize()) - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply("oops server <> error") - << http.HttpErrorHook(flow) - >> reply() - << SendData(tctx.client, err) + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply("oops server <> error") + << http.HttpErrorHook(flow) + >> reply() + << SendData(tctx.client, err) ) frames = decode_frames(err()) assert [type(x) for x in frames] == [ @@ -247,6 +288,163 @@ def test_upstream_error(tctx): assert b"server <> error" in d.data +@pytest.mark.parametrize("trailers", ["trailers", ""]) +def test_long_response(tctx: Context, trailers): + playbook, cff = start_h2_client(tctx) + flow = Placeholder(HTTPFlow) + server = Placeholder(Server) + initial = Placeholder(bytes) + assert ( + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, initial) + ) + frames = decode_frames(initial()) + assert [type(x) for x in frames] == [ + hyperframe.frame.SettingsFrame, + hyperframe.frame.HeadersFrame, + ] + sff = FrameFactory() + assert ( + playbook + # a conforming h2 server would send settings first, we disregard this for now. + >> DataReceived( + server, sff.build_headers_frame(example_response_headers).serialize() + ) + << http.HttpResponseHeadersHook(flow) + >> reply() + >> DataReceived( + server, + sff.build_data_frame(b"a" * 10000, flags=[]).serialize() + ) + >> DataReceived( + server, + sff.build_data_frame(b"a" * 10000, flags=[]).serialize(), + ) + >> DataReceived( + server, + sff.build_data_frame(b"a" * 10000, flags=[]).serialize(), + ) + >> DataReceived( + server, + sff.build_data_frame(b"a" * 10000, flags=[]).serialize(), + ) + << SendData( + server, + sff.build_window_update_frame(0, 40000).serialize() + + sff.build_window_update_frame(1, 40000).serialize(), + ) + >> DataReceived( + server, + sff.build_data_frame(b"a" * 10000, flags=[]).serialize(), + ) + >> DataReceived( + server, + sff.build_data_frame(b"a" * 10000, flags=[]).serialize(), + ) + >> DataReceived( + server, + sff.build_data_frame(b"a" * 10000, flags=[]).serialize(), + ) + ) + if trailers: + ( + playbook + >> DataReceived( + server, + sff.build_headers_frame( + example_response_trailers, flags=["END_STREAM"] + ).serialize(), + ) + ) + else: + ( + playbook + >> DataReceived( + server, + sff.build_data_frame( + b'', flags=["END_STREAM"] + ).serialize(), + ) + ) + ( + playbook + << http.HttpResponseHook(flow) + >> reply() + << SendData( + tctx.client, + cff.build_headers_frame(example_response_headers).serialize() + + cff.build_data_frame(b"a" * 16384).serialize(), + ) + << SendData( + tctx.client, + cff.build_data_frame(b"a" * 16384).serialize(), + ) + << SendData( + tctx.client, + cff.build_data_frame(b"a" * 16384).serialize(), + ) + << SendData( + tctx.client, + cff.build_data_frame(b"a" * 16383).serialize(), + ) + >> DataReceived( + tctx.client, + cff.build_window_update_frame(0, 65535).serialize() + + cff.build_window_update_frame(1, 65535).serialize(), + ) + ) + if trailers: + assert ( + playbook + << SendData( + tctx.client, + cff.build_data_frame(b"a" * 1).serialize(), + ) + << SendData( + tctx.client, + cff.build_data_frame(b"a" * 4464).serialize() + ) + << SendData( + tctx.client, + cff.build_headers_frame( + example_response_trailers, flags=["END_STREAM"] + ).serialize(), + ) + ) + else: + assert ( + playbook + << SendData( + tctx.client, + cff.build_data_frame(b"a" * 1).serialize(), + ) + << SendData( + tctx.client, + cff.build_data_frame(b"a" * 4464).serialize() + ) + << SendData( + tctx.client, + cff.build_data_frame( + b"", flags=["END_STREAM"] + ).serialize(), + ) + ) + assert flow().request.url == "http://example.com/" + assert flow().response.text == "a" * 70000 + + @pytest.mark.parametrize("stream", ["stream", ""]) @pytest.mark.parametrize("when", ["request", "response"]) @pytest.mark.parametrize("how", ["RST", "disconnect", "RST+disconnect"]) @@ -269,25 +467,29 @@ def enable_response_streaming(flow: HTTPFlow): flow.response.stream = True assert ( - playbook - >> DataReceived(tctx.client, cff.build_headers_frame(example_request_headers).serialize()) - << http.HttpRequestHeadersHook(flow) + playbook + >> DataReceived( + tctx.client, cff.build_headers_frame(example_request_headers).serialize() + ) + << http.HttpRequestHeadersHook(flow) ) if stream and when == "request": assert ( - playbook - >> reply(side_effect=enable_request_streaming) - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\n" - b"Host: example.com\r\n\r\n") + playbook + >> reply(side_effect=enable_request_streaming) + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n\r\n") ) else: assert playbook >> reply() if when == "request": if "RST" in how: - playbook >> DataReceived(tctx.client, cff.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize()) + playbook >> DataReceived( + tctx.client, + cff.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize(), + ) else: playbook >> ConnectionClosed(tctx.client) playbook << CloseConnection(tctx.client) @@ -302,49 +504,51 @@ def enable_response_streaming(flow: HTTPFlow): playbook << CloseConnection(tctx.client) assert playbook - assert "stream reset" in flow().error.msg or "peer closed connection" in flow().error.msg + assert ( + "stream reset" in flow().error.msg + or "peer closed connection" in flow().error.msg + ) return assert ( - playbook - >> DataReceived(tctx.client, cff.build_data_frame(b"", flags=["END_STREAM"]).serialize()) - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\n" - b"Host: example.com\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\n123") - << http.HttpResponseHeadersHook(flow) + playbook + >> DataReceived( + tctx.client, cff.build_data_frame(b"", flags=["END_STREAM"]).serialize() + ) + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n\r\n") + >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 6\r\n\r\n123") + << http.HttpResponseHeadersHook(flow) ) if stream: assert ( - playbook - >> reply(side_effect=enable_response_streaming) - << SendData(tctx.client, resp) + playbook + >> reply(side_effect=enable_response_streaming) + << SendData(tctx.client, resp) ) else: assert playbook >> reply() if "RST" in how: - playbook >> DataReceived(tctx.client, cff.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize()) + playbook >> DataReceived( + tctx.client, cff.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize() + ) else: playbook >> ConnectionClosed(tctx.client) playbook << CloseConnection(tctx.client) - assert ( - playbook - << CloseConnection(server) - << http.HttpErrorHook(flow) - >> reply() - ) + playbook << CloseConnection(server) + playbook << http.HttpErrorHook(flow) + playbook >> reply() + assert playbook if how == "RST+disconnect": - assert ( - playbook - >> ConnectionClosed(tctx.client) - << CloseConnection(tctx.client) - ) + playbook >> ConnectionClosed(tctx.client) + playbook << CloseConnection(tctx.client) + assert playbook if "RST" in how: assert "stream reset" in flow().error.msg @@ -352,60 +556,86 @@ def enable_response_streaming(flow: HTTPFlow): assert "peer closed connection" in flow().error.msg -@pytest.mark.xfail(reason="inbound validation turned on to protect against request smuggling") -def test_no_normalization(tctx): +@pytest.mark.parametrize("normalize", [True, False]) +def test_no_normalization(tctx, normalize): """Test that we don't normalize headers when we just pass them through.""" + tctx.options.normalize_outbound_headers = normalize + tctx.options.validate_inbound_headers = False server = Placeholder(Server) flow = Placeholder(HTTPFlow) playbook, cff = start_h2_client(tctx) - request_headers = example_request_headers + ( - (b"Should-Not-Be-Capitalized! ", b" :) "), - ) - response_headers = example_response_headers + ( - (b"Same", b"Here"), - ) + request_headers = list(example_request_headers) + [ + (b"Should-Not-Be-Capitalized! ", b" :) ") + ] + request_headers_lower = [(k.lower(), v) for (k, v) in request_headers] + response_headers = list(example_response_headers) + [(b"Same", b"Here")] + response_headers_lower = [(k.lower(), v) for (k, v) in response_headers] initial = Placeholder(bytes) assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(request_headers, flags=["END_STREAM"]).serialize()) - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None, side_effect=make_h2) - << SendData(server, initial) + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame(request_headers, flags=["END_STREAM"]).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, initial) ) frames = decode_frames(initial()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, hyperframe.frame.HeadersFrame, ] - assert hpack.hpack.Decoder().decode(frames[1].data, True) == list(request_headers) + assert ( + hpack.hpack.Decoder().decode(frames[1].data, True) == request_headers_lower + if normalize + else request_headers + ) sff = FrameFactory() - assert ( - playbook - >> DataReceived(server, sff.build_headers_frame(response_headers, flags=["END_STREAM"]).serialize()) - << http.HttpResponseHeadersHook(flow) - >> reply() - << http.HttpResponseHook(flow) - >> reply() - << SendData(tctx.client, cff.build_headers_frame(response_headers, flags=["END_STREAM"]).serialize()) + ( + playbook + >> DataReceived( + server, + sff.build_headers_frame(response_headers, flags=["END_STREAM"]).serialize(), + ) + << http.HttpResponseHeadersHook(flow) + >> reply() + << http.HttpResponseHook(flow) + >> reply() + ) + if normalize: + playbook << Log( + "Lowercased 'Same' header as uppercase is not allowed with HTTP/2." + ) + hdrs = response_headers_lower if normalize else response_headers + assert playbook << SendData( + tctx.client, cff.build_headers_frame(hdrs, flags=["END_STREAM"]).serialize() ) + assert flow().request.headers.fields == ((b"Should-Not-Be-Capitalized! ", b" :) "),) assert flow().response.headers.fields == ((b"Same", b"Here"),) -@pytest.mark.parametrize("input,pseudo,headers", [ - ([(b"foo", b"bar")], {}, {"foo": "bar"}), - ([(b":status", b"418")], {b":status": b"418"}, {}), - ([(b":status", b"418"), (b"foo", b"bar")], {b":status": b"418"}, {"foo": "bar"}), -]) +@pytest.mark.parametrize( + "input,pseudo,headers", + [ + ([(b"foo", b"bar")], {}, {"foo": "bar"}), + ([(b":status", b"418")], {b":status": b"418"}, {}), + ( + [(b":status", b"418"), (b"foo", b"bar")], + {b":status": b"418"}, + {"foo": "bar"}, + ), + ], +) def test_split_pseudo_headers(input, pseudo, headers): actual_pseudo, actual_headers = split_pseudo_headers(input) assert pseudo == actual_pseudo @@ -428,21 +658,30 @@ def test_rst_then_close(tctx): server = Placeholder(Server) assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"]).serialize()) - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> DataReceived(tctx.client, cff.build_data_frame(b"unexpected data frame").serialize()) - << SendData(tctx.client, cff.build_rst_stream_frame(1, ErrorCodes.STREAM_CLOSED).serialize()) - >> ConnectionClosed(tctx.client) - << CloseConnection(tctx.client) - >> reply("connection cancelled", to=-5) - << http.HttpErrorHook(flow) - >> reply() + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> DataReceived( + tctx.client, cff.build_data_frame(b"unexpected data frame").serialize() + ) + << SendData( + tctx.client, + cff.build_rst_stream_frame(1, ErrorCodes.STREAM_CLOSED).serialize(), + ) + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.client) + >> reply("connection cancelled", to=-5) + << http.HttpErrorHook(flow) + >> reply() ) assert flow().error.msg == "connection cancelled" @@ -460,22 +699,28 @@ def test_cancel_then_server_disconnect(tctx): server = Placeholder(Server) assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"]).serialize()) - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None) - << SendData(server, b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n') - >> DataReceived(tctx.client, cff.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize()) - << CloseConnection(server) - << http.HttpErrorHook(flow) - >> reply() - >> ConnectionClosed(server) - << None + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived( + tctx.client, cff.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize() + ) + << CloseConnection(server) + << http.HttpErrorHook(flow) + >> reply() + >> ConnectionClosed(server) + << None ) @@ -494,23 +739,29 @@ def test_cancel_during_response_hook(tctx): server = Placeholder(Server) assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"]).serialize()) - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None) - << SendData(server, b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n') - >> DataReceived(server, b"HTTP/1.1 204 No Content\r\n\r\n") - << http.HttpResponseHeadersHook(flow) - << CloseConnection(server) - >> reply(to=-2) - << http.HttpResponseHook(flow) - >> DataReceived(tctx.client, cff.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize()) - >> reply(to=-2) + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived(server, b"HTTP/1.1 204 No Content\r\n\r\n") + << http.HttpResponseHeadersHook(flow) + << CloseConnection(server) + >> reply(to=-2) + << http.HttpResponseHook(flow) + >> DataReceived( + tctx.client, cff.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize() + ) + >> reply(to=-2) ) @@ -529,25 +780,31 @@ def test_stream_concurrency(tctx): data_req1 = Placeholder(bytes) data_req2 = Placeholder(bytes) - assert (playbook - >> DataReceived( - tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"], stream_id=1).serialize() + - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"], stream_id=3).serialize()) - << reqheadershook1 - << reqheadershook2 - >> reply(to=reqheadershook1) - << reqhook1 - >> reply(to=reqheadershook2) - << reqhook2 - # req 2 overtakes 1 and we already have a reply: - >> reply(to=reqhook2) - << OpenConnection(server) - >> reply(None, side_effect=make_h2) - << SendData(server, data_req2) - >> reply(to=reqhook1) - << SendData(server, data_req1) - ) + assert ( + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"], stream_id=1 + ).serialize() + + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"], stream_id=3 + ).serialize(), + ) + << reqheadershook1 + << reqheadershook2 + >> reply(to=reqheadershook1) + << reqhook1 + >> reply(to=reqheadershook2) + << reqhook2 + # req 2 overtakes 1 and we already have a reply: + >> reply(to=reqhook2) + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, data_req2) + >> reply(to=reqhook1) + << SendData(server, data_req1) + ) frames = decode_frames(data_req2()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, @@ -569,36 +826,50 @@ def test_max_concurrency(tctx): sff = FrameFactory() assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"], - stream_id=1).serialize()) - << OpenConnection(server) - >> reply(None, side_effect=make_h2) - << SendData(server, req1_bytes) - >> DataReceived(server, - sff.build_settings_frame( - {h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 1}).serialize()) - << SendData(server, settings_ack_bytes) - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers, - flags=["END_STREAM"], - stream_id=3).serialize()) - # Can't send it upstream yet, all streams in use! - >> DataReceived(server, sff.build_headers_frame(example_response_headers, - flags=["END_STREAM"], - stream_id=1).serialize()) - # But now we can! - << SendData(server, req2_bytes) - << SendData(tctx.client, Placeholder(bytes)) - >> DataReceived(server, sff.build_headers_frame(example_response_headers, - flags=["END_STREAM"], - stream_id=3).serialize()) - << SendData(tctx.client, Placeholder(bytes)) + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"], stream_id=1 + ).serialize(), + ) + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, req1_bytes) + >> DataReceived( + server, + sff.build_settings_frame( + {h2.settings.SettingCodes.MAX_CONCURRENT_STREAMS: 1} + ).serialize(), + ) + << SendData(server, settings_ack_bytes) + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"], stream_id=3 + ).serialize(), + ) + # Can't send it upstream yet, all streams in use! + >> DataReceived( + server, + sff.build_headers_frame( + example_response_headers, flags=["END_STREAM"], stream_id=1 + ).serialize(), + ) + # But now we can! + << SendData(server, req2_bytes) + << SendData(tctx.client, Placeholder(bytes)) + >> DataReceived( + server, + sff.build_headers_frame( + example_response_headers, flags=["END_STREAM"], stream_id=3 + ).serialize(), + ) + << SendData(tctx.client, Placeholder(bytes)) ) settings, req1 = decode_frames(req1_bytes()) - settings_ack, = decode_frames(settings_ack_bytes()) - req2, = decode_frames(req2_bytes()) + (settings_ack,) = decode_frames(settings_ack_bytes()) + (req2,) = decode_frames(req2_bytes()) assert type(settings) == hyperframe.frame.SettingsFrame assert type(req1) == hyperframe.frame.HeadersFrame @@ -616,15 +887,24 @@ def test_stream_concurrent_get_connection(tctx): server = Placeholder(Server) data = Placeholder(bytes) - assert (playbook - >> DataReceived(tctx.client, cff.build_headers_frame(example_request_headers, flags=["END_STREAM"], - stream_id=1).serialize()) - << (o := OpenConnection(server)) - >> DataReceived(tctx.client, cff.build_headers_frame(example_request_headers, flags=["END_STREAM"], - stream_id=3).serialize()) - >> reply(None, to=o, side_effect=make_h2) - << SendData(server, data) - ) + assert ( + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"], stream_id=1 + ).serialize(), + ) + << (o := OpenConnection(server)) + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"], stream_id=3 + ).serialize(), + ) + >> reply(None, to=o, side_effect=make_h2) + << SendData(server, data) + ) frames = decode_frames(data()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, @@ -648,24 +928,35 @@ def kill(flow: HTTPFlow): server = Placeholder(Server) data_req1 = Placeholder(bytes) - assert (playbook - >> DataReceived( - tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"], stream_id=1).serialize() + - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"], stream_id=3).serialize()) - << req_headers_hook_1 - << http.HttpRequestHeadersHook(flow2) - >> reply(side_effect=kill) - << http.HttpErrorHook(flow2) - >> reply() - << SendData(tctx.client, cff.build_rst_stream_frame(3, error_code=ErrorCodes.INTERNAL_ERROR).serialize()) - >> reply(to=req_headers_hook_1) - << http.HttpRequestHook(flow1) - >> reply() - << OpenConnection(server) - >> reply(None, side_effect=make_h2) - << SendData(server, data_req1) - ) + assert ( + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"], stream_id=1 + ).serialize() + + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"], stream_id=3 + ).serialize(), + ) + << req_headers_hook_1 + << http.HttpRequestHeadersHook(flow2) + >> reply(side_effect=kill) + << http.HttpErrorHook(flow2) + >> reply() + << SendData( + tctx.client, + cff.build_rst_stream_frame( + 3, error_code=ErrorCodes.INTERNAL_ERROR + ).serialize(), + ) + >> reply(to=req_headers_hook_1) + << http.HttpRequestHook(flow1) + >> reply() + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, data_req1) + ) frames = decode_frames(data_req1()) assert [type(x) for x in frames] == [ hyperframe.frame.SettingsFrame, @@ -675,26 +966,75 @@ def kill(flow: HTTPFlow): class TestClient: def test_no_data_on_closed_stream(self, tctx): + tctx.options.http2_ping_keepalive = 0 frame_factory = FrameFactory() req = Request.make("GET", "http://example.com/") - resp = { - ":status": 200 - } + resp = {":status": 200} assert ( - Playbook(Http2Client(tctx)) - << SendData(tctx.server, Placeholder(bytes)) # preamble + initial settings frame - >> DataReceived(tctx.server, frame_factory.build_settings_frame({}, ack=True).serialize()) - >> http.RequestHeaders(1, req, end_stream=True) - << SendData(tctx.server, b"\x00\x00\x06\x01\x05\x00\x00\x00\x01\x82\x86\x84\\\x81\x07") - >> http.RequestEndOfMessage(1) - >> DataReceived(tctx.server, frame_factory.build_headers_frame(resp).serialize()) - << http.ReceiveHttp(Placeholder(http.ResponseHeaders)) - >> http.RequestProtocolError(1, "cancelled", code=status_codes.CLIENT_CLOSED_REQUEST) - << SendData(tctx.server, frame_factory.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize()) - >> DataReceived(tctx.server, frame_factory.build_data_frame(b"foo").serialize()) - << SendData(tctx.server, frame_factory.build_rst_stream_frame(1, ErrorCodes.STREAM_CLOSED).serialize()) + Playbook(Http2Client(tctx)) + << SendData( + tctx.server, Placeholder(bytes) + ) # preamble + initial settings frame + >> DataReceived( + tctx.server, + frame_factory.build_settings_frame({}, ack=True).serialize(), + ) + >> http.RequestHeaders(1, req, end_stream=True) + << SendData( + tctx.server, + b"\x00\x00\x06\x01\x05\x00\x00\x00\x01\x82\x86\x84\\\x81\x07", + ) + >> http.RequestEndOfMessage(1) + >> DataReceived( + tctx.server, frame_factory.build_headers_frame(resp).serialize() + ) + << http.ReceiveHttp(Placeholder(http.ResponseHeaders)) + >> http.RequestProtocolError( + 1, "cancelled", code=status_codes.CLIENT_CLOSED_REQUEST + ) + << SendData( + tctx.server, + frame_factory.build_rst_stream_frame(1, ErrorCodes.CANCEL).serialize(), + ) + >> DataReceived( + tctx.server, frame_factory.build_data_frame(b"foo").serialize() + ) + << SendData( + tctx.server, + frame_factory.build_rst_stream_frame( + 1, ErrorCodes.STREAM_CLOSED + ).serialize(), + ) ) # important: no ResponseData event here! + @pytest.mark.parametrize( + "code,log_msg", + [ + (b"103", "103 Early Hints"), + (b"1not_a_number", " "), + ], + ) + def test_informational_response(self, tctx, code, log_msg): + tctx.options.http2_ping_keepalive = 0 + frame_factory = FrameFactory() + req = Request.make("GET", "http://example.com/") + resp = {":status": code} + assert ( + Playbook(Http2Client(tctx), logs=True) + << SendData( + tctx.server, Placeholder(bytes) + ) # preamble + initial settings frame + >> http.RequestHeaders(1, req, end_stream=True) + << SendData( + tctx.server, + b"\x00\x00\x06\x01\x05\x00\x00\x00\x01\x82\x86\x84\\\x81\x07", + ) + >> DataReceived( + tctx.server, frame_factory.build_headers_frame(resp).serialize() + ) + << Log(f"Swallowing HTTP/2 informational response: {log_msg}", "info") + ) + def test_early_server_data(tctx): playbook, cff = start_h2_client(tctx) @@ -708,18 +1048,22 @@ def test_early_server_data(tctx): server1 = Placeholder(bytes) server2 = Placeholder(bytes) assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(example_request_headers, flags=["END_STREAM"]).serialize()) - << http.HttpRequestHeadersHook(flow) - >> reply() - << (h := http.HttpRequestHook(flow)) - # Surprise! We get data from the server before the request hook finishes. - >> DataReceived(tctx.server, sff.build_settings_frame({}).serialize()) - << SendData(tctx.server, server1) - # Request hook finishes... - >> reply(to=h) - << SendData(tctx.server, server2) + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << (h := http.HttpRequestHook(flow)) + # Surprise! We get data from the server before the request hook finishes. + >> DataReceived(tctx.server, sff.build_settings_frame({}).serialize()) + << SendData(tctx.server, server1) + # Request hook finishes... + >> reply(to=h) + << SendData(tctx.server, server2) ) assert [type(x) for x in decode_frames(server1())] == [ hyperframe.frame.SettingsFrame, @@ -736,21 +1080,24 @@ def test_request_smuggling_cl(tctx): err = Placeholder(bytes) headers = ( - (b':method', b'POST'), - (b':scheme', b'http'), - (b':path', b'/'), - (b':authority', b'example.com'), - (b'content-length', b'3') + (b":method", b"POST"), + (b":scheme", b"http"), + (b":path", b"/"), + (b":authority", b"example.com"), + (b"content-length", b"3"), ) assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(headers).serialize()) - >> DataReceived(tctx.client, - cff.build_data_frame(b"abcPOST / HTTP/1.1 ...", flags=["END_STREAM"]).serialize()) - << SendData(tctx.client, err) - << CloseConnection(tctx.client) + playbook + >> DataReceived(tctx.client, cff.build_headers_frame(headers).serialize()) + >> DataReceived( + tctx.client, + cff.build_data_frame( + b"abcPOST / HTTP/1.1 ...", flags=["END_STREAM"] + ).serialize(), + ) + << SendData(tctx.client, err) + << CloseConnection(tctx.client) ) assert b"InvalidBodyLengthError" in err() @@ -761,18 +1108,91 @@ def test_request_smuggling_te(tctx): err = Placeholder(bytes) headers = ( - (b':method', b'POST'), - (b':scheme', b'http'), - (b':path', b'/'), - (b':authority', b'example.com'), - (b'transfer-encoding', b'chunked') + (b":method", b"POST"), + (b":scheme", b"http"), + (b":path", b"/"), + (b":authority", b"example.com"), + (b"transfer-encoding", b"chunked"), ) assert ( - playbook - >> DataReceived(tctx.client, - cff.build_headers_frame(headers, flags=["END_STREAM"]).serialize()) - << SendData(tctx.client, err) - << CloseConnection(tctx.client) + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame(headers, flags=["END_STREAM"]).serialize(), + ) + << SendData(tctx.client, err) + << CloseConnection(tctx.client) ) assert b"Connection-specific header field present" in err() + + +def test_request_keepalive(tctx, monkeypatch): + playbook, cff = start_h2_client(tctx, 58) + flow = Placeholder(HTTPFlow) + server = Placeholder(Server) + initial = Placeholder(bytes) + + def advance_time(_): + t = time.time() + monkeypatch.setattr(time, "time", lambda: t + 60) + + assert ( + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << RequestWakeup(58) + << SendData(server, initial) + >> reply(to=-2, side_effect=advance_time) + << SendData( + server, b"\x00\x00\x08\x06\x00\x00\x00\x00\x0000000000" + ) # ping frame + << RequestWakeup(58) + ) + + +def test_keepalive_disconnect(tctx, monkeypatch): + playbook, cff = start_h2_client(tctx, 58) + playbook.hooks = False + sff = FrameFactory() + server = Placeholder(Server) + wakeup_command = RequestWakeup(58) + + http_response = ( + sff.build_headers_frame(example_response_headers).serialize() + + sff.build_data_frame(b"", flags=["END_STREAM"]).serialize() + ) + + def advance_time(_): + t = time.time() + monkeypatch.setattr(time, "time", lambda: t + 60) + + assert ( + playbook + >> DataReceived( + tctx.client, + cff.build_headers_frame( + example_request_headers, flags=["END_STREAM"] + ).serialize(), + ) + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << wakeup_command + << SendData(server, Placeholder(bytes)) + >> DataReceived(server, http_response) + << SendData(tctx.client, Placeholder(bytes)) + >> ConnectionClosed(server) + << CloseConnection(server) + >> reply(to=wakeup_command, side_effect=advance_time) + << None + ) diff --git a/test/mitmproxy/proxy/layers/http/test_http_fuzz.py b/test/mitmproxy/proxy/layers/http/test_http_fuzz.py index 9ca6894075..6ad352efcf 100644 --- a/test/mitmproxy/proxy/layers/http/test_http_fuzz.py +++ b/test/mitmproxy/proxy/layers/http/test_http_fuzz.py @@ -1,10 +1,20 @@ -from typing import Tuple, Dict, Any +from typing import Any import pytest from h2.settings import SettingCodes from hypothesis import example, given -from hypothesis.strategies import binary, booleans, composite, dictionaries, integers, lists, sampled_from, sets, text, \ - data +from hypothesis.strategies import ( + binary, + booleans, + composite, + dictionaries, + integers, + lists, + sampled_from, + sets, + text, + data, +) from mitmproxy import options, connection from mitmproxy.addons.proxyserver import Proxyserver @@ -16,9 +26,19 @@ from mitmproxy.proxy.events import DataReceived, Start, ConnectionClosed from mitmproxy.proxy.layers import http from test.mitmproxy.proxy.layers.http.hyper_h2_test_helpers import FrameFactory -from test.mitmproxy.proxy.layers.http.test_http2 import make_h2, example_response_headers, example_request_headers, \ - start_h2_client -from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply, _TracebackInPlaybook, _eq +from test.mitmproxy.proxy.layers.http.test_http2 import ( + make_h2, + example_response_headers, + example_request_headers, + start_h2_client, +) +from test.mitmproxy.proxy.tutils import ( + Placeholder, + Playbook, + reply, + _TracebackInPlaybook, + _eq, +) from mitmproxy.proxy.layers.http import _http2 opts = options.Options() @@ -35,31 +55,35 @@ def disable_h2_error_catching(): _http2.CATCH_HYPER_H2_ERRORS = errs -request_lines = sampled_from([ - b"GET / HTTP/1.1", - b"GET http://example.com/ HTTP/1.1", - b"CONNECT example.com:443 HTTP/1.1", - b"HEAD /foo HTTP/0.9", -]) -response_lines = sampled_from([ - b"HTTP/1.1 200 OK", - b"HTTP/1.1 100 Continue", - b"HTTP/0.9 204 No Content", - b"HEAD /foo HTTP/0.9", -]) -headers = lists(sampled_from([ - b"Host: example.com", - b"Content-Length: 5", - b"Expect: 100-continue", - b"Transfer-Encoding: chunked", - b"Connection: close", - b"", -])) -bodies = sampled_from([ - b"", - b"12345", - b"5\r\n12345\r\n0\r\n\r\n" -]) +request_lines = sampled_from( + [ + b"GET / HTTP/1.1", + b"GET http://example.com/ HTTP/1.1", + b"CONNECT example.com:443 HTTP/1.1", + b"HEAD /foo HTTP/0.9", + ] +) +response_lines = sampled_from( + [ + b"HTTP/1.1 200 OK", + b"HTTP/1.1 100 Continue", + b"HTTP/0.9 204 No Content", + b"HEAD /foo HTTP/0.9", + ] +) +headers = lists( + sampled_from( + [ + b"Host: example.com", + b"Content-Length: 5", + b"Expect: 100-continue", + b"Transfer-Encoding: chunked", + b"Connection: close", + b"", + ] + ) +) +bodies = sampled_from([b"", b"12345", b"5\r\n12345\r\n0\r\n\r\n"]) @composite @@ -80,10 +104,7 @@ def chunks(draw, elements): data = draw(elements) chunks = [] - a, b = sorted([ - draw(integers(0, len(data))), - draw(integers(0, len(data))) - ]) + a, b = sorted([draw(integers(0, len(data))), draw(integers(0, len(data)))]) if a > 0: chunks.append(data[:a]) if a != b: @@ -112,7 +133,9 @@ def h2_responses(draw): @given(chunks(mutations(h1_requests()))) def test_fuzz_h1_request(data): - tctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts) + tctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts + ) layer = http.HttpLayer(tctx, HTTPMode.regular) for _ in layer.handle_event(Start()): @@ -123,27 +146,36 @@ def test_fuzz_h1_request(data): @given(chunks(mutations(h2_responses()))) -@example([b'0 OK\r\n\r\n', b'\r\n', b'5\r\n12345\r\n0\r\n\r\n']) +@example([b"0 OK\r\n\r\n", b"\r\n", b"5\r\n12345\r\n0\r\n\r\n"]) def test_fuzz_h1_response(data): - tctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts) + tctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts + ) server = Placeholder(connection.Server) playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) assert ( - playbook - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") + playbook + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") ) for chunk in data: for _ in playbook.layer.handle_event(events.DataReceived(server(), chunk)): pass -h2_flags = sets(sampled_from([ - "END_STREAM", - "END_HEADERS", -])) +h2_flags = sets( + sampled_from( + [ + "END_STREAM", + "END_HEADERS", + ] + ) +) h2_stream_ids = integers(0, 3) h2_stream_ids_nonzero = integers(1, 3) @@ -151,12 +183,12 @@ def test_fuzz_h1_response(data): @composite def h2_headers(draw): required_headers = [ - [":path", '/'], - [":scheme", draw(sampled_from(['http', 'https']))], - [":method", draw(sampled_from(['GET', 'POST', 'CONNECT']))], + [":path", "/"], + [":scheme", draw(sampled_from(["http", "https"]))], + [":method", draw(sampled_from(["GET", "POST", "CONNECT"]))], ] optional_headers = [ - [":authority", draw(sampled_from(['example.com:443', 'example.com']))], + [":authority", draw(sampled_from(["example.com:443", "example.com"]))], ["cookie", "foobaz"], ["host", "example.com"], ["content-length", "42"], @@ -177,25 +209,32 @@ def h2_frames(draw): headers1 = ff.build_headers_frame(headers=draw(h2_headers())) headers1.flags.clear() headers1.flags |= draw(h2_flags) - headers2 = ff.build_headers_frame(headers=draw(h2_headers()), - depends_on=draw(h2_stream_ids), - stream_weight=draw(integers(0, 255)), - exclusive=draw(booleans())) + headers2 = ff.build_headers_frame( + headers=draw(h2_headers()), + depends_on=draw(h2_stream_ids), + stream_weight=draw(integers(0, 255)), + exclusive=draw(booleans()), + ) headers2.flags.clear() headers2.flags |= draw(h2_flags) settings = ff.build_settings_frame( - settings=draw(dictionaries( - keys=sampled_from(SettingCodes), - values=integers(0, 2 ** 32 - 1), - max_size=5, - )), ack=draw(booleans()) + settings=draw( + dictionaries( + keys=sampled_from(SettingCodes), + values=integers(0, 2 ** 32 - 1), + max_size=5, + ) + ), + ack=draw(booleans()), + ) + continuation = ff.build_continuation_frame( + header_block=ff.encoder.encode(draw(h2_headers())), flags=draw(h2_flags) ) - continuation = ff.build_continuation_frame(header_block=ff.encoder.encode(draw(h2_headers())), flags=draw(h2_flags)) goaway = ff.build_goaway_frame(draw(h2_stream_ids)) push_promise = ff.build_push_promise_frame( stream_id=draw(h2_stream_ids_nonzero), promised_stream_id=draw(h2_stream_ids), - headers=draw(h2_headers()) + headers=draw(h2_headers()), ) rst = ff.build_rst_stream_frame(draw(h2_stream_ids_nonzero)) prio = ff.build_priority_frame( @@ -204,28 +243,51 @@ def h2_frames(draw): depends_on=draw(h2_stream_ids), exclusive=draw(booleans()), ) - data1 = ff.build_data_frame( - draw(binary()), draw(h2_flags) - ) + data1 = ff.build_data_frame(draw(binary()), draw(h2_flags)) data2 = ff.build_data_frame( draw(binary()), draw(h2_flags), stream_id=draw(h2_stream_ids_nonzero) ) - window_update = ff.build_window_update_frame(draw(h2_stream_ids), draw(integers(0, 2 ** 32 - 1))) + window_update = ff.build_window_update_frame( + draw(h2_stream_ids), draw(integers(0, 2 ** 32 - 1)) + ) - frames = draw(lists(sampled_from([ - headers1, headers2, settings, continuation, goaway, push_promise, rst, prio, data1, data2, window_update - ]), min_size=1, max_size=11)) + frames = draw( + lists( + sampled_from( + [ + headers1, + headers2, + settings, + continuation, + goaway, + push_promise, + rst, + prio, + data1, + data2, + window_update, + ] + ), + min_size=1, + max_size=11, + ) + ) return b"".join(x.serialize() for x in frames) def h2_layer(opts): - tctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts) + tctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts + ) + tctx.options.http2_ping_keepalive = 0 tctx.client.alpn = b"h2" layer = http.HttpLayer(tctx, HTTPMode.regular) for _ in layer.handle_event(Start()): pass - for _ in layer.handle_event(DataReceived(tctx.client, b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n')): + for _ in layer.handle_event( + DataReceived(tctx.client, b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") + ): pass return tctx, layer @@ -237,6 +299,7 @@ def _h2_request(chunks): pass +# fmt: off @given(chunks(h2_frames())) @example([b'\x00\x00\x00\x01\x05\x00\x00\x00\x01\x00\x00\x00\x01\x05\x00\x00\x00\x01']) @example([b'\x00\x00\x00\x01\x07\x00\x00\x00\x01A\x88/\x91\xd3]\x05\\\x87\xa7\x84\x86\x82`\x80f\x80\\\x80']) @@ -251,6 +314,7 @@ def _h2_request(chunks): @example([b'\x00\x00\x03\x01\x04\x00\x00\x00\x01\x84\x86\x82\x00\x00\x08\x07\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00']) def test_fuzz_h2_request_chunks(chunks): _h2_request(chunks) +# fmt: on @given(chunks(mutations(h2_frames()))) @@ -259,21 +323,27 @@ def test_fuzz_h2_request_mutations(chunks): def _h2_response(chunks): - tctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts) + tctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts + ) playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular), hooks=False) server = Placeholder(connection.Server) assert ( - playbook - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << OpenConnection(server) - >> reply(None, side_effect=make_h2) - << SendData(server, Placeholder()) + playbook + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, Placeholder()) ) for chunk in chunks: for _ in playbook.layer.handle_event(events.DataReceived(server(), chunk)): pass +# fmt: off @given(chunks(h2_frames())) @example([b'\x00\x00\x03\x01\x04\x00\x00\x00\x01\x84\x86\x82', b'\x00\x00\x07\x05\x04\x00\x00\x00\x01\x00\x00\x00\x00\x84\x86\x82']) @@ -284,6 +354,7 @@ def _h2_response(chunks): @example([b'\x00\x00\x06\x01\x04\x00\x00\x00\x01@\x80\x81c\x86\x82']) def test_fuzz_h2_response_chunks(chunks): _h2_response(chunks) +# fmt: on @given(chunks(mutations(h2_frames()))) @@ -291,13 +362,37 @@ def test_fuzz_h2_response_mutations(chunks): _h2_response(chunks) -@pytest.mark.parametrize("example", [( - True, False, - ["data_req", "reply_hook_req_headers", "reply_openconn", "data_resp", "data_reqbody", - "data_respbody", "err_server_rst", "reply_hook_resp_headers"]), - (True, False, ["data_req", "reply_hook_req_headers", "reply_openconn", "err_server_rst", - "data_reqbody", "reply_hook_error"]), -]) +@pytest.mark.parametrize( + "example", + [ + ( + True, + False, + [ + "data_req", + "reply_hook_req_headers", + "reply_openconn", + "data_resp", + "data_reqbody", + "data_respbody", + "err_server_rst", + "reply_hook_resp_headers", + ], + ), + ( + True, + False, + [ + "data_req", + "reply_hook_req_headers", + "reply_openconn", + "err_server_rst", + "data_reqbody", + "reply_hook_error", + ], + ), + ], +) def test_cancel_examples(example): """ We can't specify examples in test_fuzz_cancel (because we use data, see @@ -312,7 +407,9 @@ def draw(lst): for name, evt in lst: if name == this_draw: return name, evt - raise AssertionError(f"{this_draw} not in list: {[name for name, _ in lst]}") + raise AssertionError( + f"{this_draw} not in list: {[name for name, _ in lst]}" + ) else: return lst[0] @@ -321,14 +418,18 @@ def draw(lst): @given(stream_request=booleans(), stream_response=booleans(), data=data()) def test_fuzz_cancel(stream_request, stream_response, data): - _test_cancel(stream_request, stream_response, lambda lst: data.draw(sampled_from(lst))) + _test_cancel( + stream_request, stream_response, lambda lst: data.draw(sampled_from(lst)) + ) def _test_cancel(stream_req, stream_resp, draw): """ Test that we don't raise an exception if someone disconnects. """ - tctx = context.Context(connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts) + tctx = context.Context( + connection.Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), opts + ) playbook, cff = start_h2_client(tctx) flow = Placeholder(HTTPFlow) server = Placeholder(Server) @@ -347,26 +448,46 @@ def maybe_stream(flow: HTTPFlow): openconn = OpenConnection(server) send_upstream = SendData(server, Placeholder(bytes)) - data_req = DataReceived(tctx.client, cff.build_headers_frame(example_request_headers).serialize()) - data_reqbody = DataReceived(tctx.client, cff.build_data_frame(b"foo", flags=["END_STREAM"]).serialize()) - data_resp = DataReceived(server, cff.build_headers_frame(example_response_headers).serialize()) - data_respbody = DataReceived(server, cff.build_data_frame(b"bar", flags=["END_STREAM"]).serialize()) + data_req = DataReceived( + tctx.client, cff.build_headers_frame(example_request_headers).serialize() + ) + data_reqbody = DataReceived( + tctx.client, cff.build_data_frame(b"foo", flags=["END_STREAM"]).serialize() + ) + data_resp = DataReceived( + server, cff.build_headers_frame(example_response_headers).serialize() + ) + data_respbody = DataReceived( + server, cff.build_data_frame(b"bar", flags=["END_STREAM"]).serialize() + ) client_disc = ConnectionClosed(tctx.client) client_rst = DataReceived(tctx.client, cff.build_rst_stream_frame(1).serialize()) server_disc = ConnectionClosed(server) server_rst = DataReceived(server, cff.build_rst_stream_frame(1).serialize()) - evts: Dict[str, Tuple[Any, Any, Any]] = {} + evts: dict[str, tuple[Any, Any, Any]] = {} # precondition, but-not-after-this evts["data_req"] = data_req, None, client_disc evts["data_reqbody"] = data_reqbody, data_req, client_disc - evts["reply_hook_req_headers"] = reply(to=hook_req_headers, side_effect=maybe_stream), hook_req_headers, None + evts["reply_hook_req_headers"] = ( + reply(to=hook_req_headers, side_effect=maybe_stream), + hook_req_headers, + None, + ) evts["reply_hook_req"] = reply(to=hook_req), hook_req, None - evts["reply_openconn"] = reply(None, to=openconn, side_effect=make_h2), openconn, None + evts["reply_openconn"] = ( + reply(None, to=openconn, side_effect=make_h2), + openconn, + None, + ) evts["data_resp"] = data_resp, send_upstream, server_disc evts["data_respbody"] = data_respbody, data_resp, server_disc - evts["reply_hook_resp_headers"] = reply(to=hook_resp_headers, side_effect=maybe_stream), hook_resp_headers, None + evts["reply_hook_resp_headers"] = ( + reply(to=hook_resp_headers, side_effect=maybe_stream), + hook_resp_headers, + None, + ) evts["reply_hook_resp"] = reply(to=hook_resp), hook_resp, None evts["reply_hook_error"] = reply(to=hook_error), hook_error, None @@ -386,11 +507,11 @@ def eq_maybe(a, b): while evts: candidates = [] for name, (evt, precon, negprecon) in evts.items(): - precondition_ok = ( - precon is None or any(eq_maybe(x, precon) for x in playbook.actual) + precondition_ok = precon is None or any( + eq_maybe(x, precon) for x in playbook.actual ) - neg_precondition_ok = ( - negprecon is None or not any(eq_maybe(x, negprecon) for x in playbook.actual) + neg_precondition_ok = negprecon is None or not any( + eq_maybe(x, negprecon) for x in playbook.actual ) if precondition_ok and neg_precondition_ok: # crude hack to increase fuzzing efficiency: make it more likely that we progress. @@ -404,11 +525,8 @@ def eq_maybe(a, b): try: assert playbook >> evt except AssertionError: - if any( - isinstance(x, _TracebackInPlaybook) - for x in playbook.actual - ): + if any(isinstance(x, _TracebackInPlaybook) for x in playbook.actual): raise else: # add commands that the server issued. - playbook.expected.extend(playbook.actual[len(playbook.expected):]) + playbook.expected.extend(playbook.actual[len(playbook.expected) :]) diff --git a/test/mitmproxy/proxy/layers/http/test_http_version_interop.py b/test/mitmproxy/proxy/layers/http/test_http_version_interop.py index 9235168541..d0ae84248a 100644 --- a/test/mitmproxy/proxy/layers/http/test_http_version_interop.py +++ b/test/mitmproxy/proxy/layers/http/test_http_version_interop.py @@ -1,5 +1,3 @@ -from typing import Tuple - import h2.config import h2.connection import h2.events @@ -12,9 +10,21 @@ from mitmproxy.proxy.events import DataReceived from mitmproxy.proxy.layers import http from test.mitmproxy.proxy.layers.http.hyper_h2_test_helpers import FrameFactory -from test.mitmproxy.proxy.layers.http.test_http2 import example_request_headers, example_response_headers, make_h2 +from test.mitmproxy.proxy.layers.http.test_http2 import ( + example_response_headers, + make_h2, +) from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply +example_request_headers = ( + (b":method", b"GET"), + (b":scheme", b"http"), + (b":path", b"/"), + (b":authority", b"example.com"), + (b"cookie", "a=1"), + (b"cookie", "b=2"), +) + h2f = FrameFactory() @@ -22,27 +32,29 @@ def event_types(events): return [type(x) for x in events] -def h2_client(tctx: Context) -> Tuple[h2.connection.H2Connection, Playbook]: +def h2_client(tctx: Context) -> tuple[h2.connection.H2Connection, Playbook]: tctx.client.alpn = b"h2" + tctx.options.http2_ping_keepalive = 0 playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular)) conn = h2.connection.H2Connection() conn.initiate_connection() server_preamble = Placeholder(bytes) - assert ( - playbook - << SendData(tctx.client, server_preamble) - ) - assert event_types(conn.receive_data(server_preamble())) == [h2.events.RemoteSettingsChanged] + assert playbook << SendData(tctx.client, server_preamble) + assert event_types(conn.receive_data(server_preamble())) == [ + h2.events.RemoteSettingsChanged + ] settings_ack = Placeholder(bytes) assert ( - playbook - >> DataReceived(tctx.client, conn.data_to_send()) - << SendData(tctx.client, settings_ack) + playbook + >> DataReceived(tctx.client, conn.data_to_send()) + << SendData(tctx.client, settings_ack) ) - assert event_types(conn.receive_data(settings_ack())) == [h2.events.SettingsAcknowledged] + assert event_types(conn.receive_data(settings_ack())) == [ + h2.events.SettingsAcknowledged + ] return conn, playbook @@ -57,31 +69,34 @@ def test_h2_to_h1(tctx): conn.send_headers(1, example_request_headers, end_stream=True) response = Placeholder(bytes) assert ( - playbook - >> DataReceived(tctx.client, conn.data_to_send()) - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None) - << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") - >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\n") - << http.HttpResponseHeadersHook(flow) - >> reply() - >> DataReceived(server, b"Hello World!") - << http.HttpResponseHook(flow) - << CloseConnection(server) - >> reply(to=-2) - << SendData(tctx.client, response) + playbook + >> DataReceived(tctx.client, conn.data_to_send()) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None) + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\ncookie: a=1; b=2\r\n\r\n") + >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\n") + << http.HttpResponseHeadersHook(flow) + >> reply() + >> DataReceived(server, b"Hello World!") + << http.HttpResponseHook(flow) + << CloseConnection(server) + >> reply(to=-2) + << SendData(tctx.client, response) ) events = conn.receive_data(response()) assert event_types(events) == [ - h2.events.ResponseReceived, h2.events.DataReceived, h2.events.DataReceived, h2.events.StreamEnded + h2.events.ResponseReceived, + h2.events.DataReceived, + h2.events.DataReceived, + h2.events.StreamEnded, ] resp: h2.events.ResponseReceived = events[0] body: h2.events.DataReceived = events[1] - assert resp.headers == [(b':status', b'200'), (b'content-length', b'12')] + assert resp.headers == [(b":status", b"200"), (b"content-length", b"12")] assert body.data == b"Hello World!" @@ -89,6 +104,7 @@ def test_h1_to_h2(tctx): """Test HTTP/1 -> HTTP/2 request translation""" server = Placeholder(Server) flow = Placeholder(HTTPFlow) + tctx.options.http2_ping_keepalive = 0 playbook = Playbook(http.HttpLayer(tctx, HTTPMode.regular)) @@ -98,33 +114,38 @@ def test_h1_to_h2(tctx): request = Placeholder(bytes) assert ( - playbook - >> DataReceived(tctx.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << OpenConnection(server) - >> reply(None, side_effect=make_h2) - << SendData(server, request) + playbook + >> DataReceived( + tctx.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << OpenConnection(server) + >> reply(None, side_effect=make_h2) + << SendData(server, request) ) events = conn.receive_data(request()) assert event_types(events) == [ - h2.events.RemoteSettingsChanged, h2.events.RequestReceived, h2.events.StreamEnded + h2.events.RemoteSettingsChanged, + h2.events.RequestReceived, + h2.events.StreamEnded, ] conn.send_headers(1, example_response_headers) conn.send_data(1, b"Hello World!", end_stream=True) settings_ack = Placeholder(bytes) assert ( - playbook - >> DataReceived(server, conn.data_to_send()) - << http.HttpResponseHeadersHook(flow) - << SendData(server, settings_ack) - >> reply(to=-2) - << http.HttpResponseHook(flow) - >> reply() - << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n\r\nHello World!") - << CloseConnection(tctx.client) + playbook + >> DataReceived(server, conn.data_to_send()) + << http.HttpResponseHeadersHook(flow) + << SendData(server, settings_ack) + >> reply(to=-2) + << http.HttpResponseHook(flow) + >> reply() + << SendData(tctx.client, b"HTTP/1.1 200 OK\r\n\r\nHello World!") + << CloseConnection(tctx.client) ) - assert settings_ack() == b'\x00\x00\x00\x04\x01\x00\x00\x00\x00' + assert settings_ack() == b"\x00\x00\x00\x04\x01\x00\x00\x00\x00" diff --git a/test/mitmproxy/proxy/layers/test_dns.py b/test/mitmproxy/proxy/layers/test_dns.py new file mode 100644 index 0000000000..6f4e2bb077 --- /dev/null +++ b/test/mitmproxy/proxy/layers/test_dns.py @@ -0,0 +1,203 @@ +import time + +from mitmproxy.proxy.commands import CloseConnection, Log, OpenConnection, SendData +from mitmproxy.proxy.events import ConnectionClosed, DataReceived +from mitmproxy.proxy.layers import dns +from mitmproxy.dns import DNSFlow +from mitmproxy.test.tutils import tdnsreq, tdnsresp +from ..tutils import Placeholder, Playbook, reply + + +def test_invalid_and_dummy_end(tctx): + assert ( + Playbook(dns.DNSLayer(tctx)) + >> DataReceived(tctx.client, b"Not a DNS packet") + << Log( + "Client(client:1234, state=open) sent an invalid message: question #0: unpack encountered a label of length 99" + ) + >> ConnectionClosed(tctx.client) + >> DataReceived(tctx.client, b"You still there?") + >> DataReceived(tctx.client, tdnsreq().packed) + >> DataReceived(tctx.client, b"Hello?") + << None + ) + + +def test_regular(tctx): + f = Placeholder(DNSFlow) + + req = tdnsreq() + resp = tdnsresp() + + def resolve(flow: DNSFlow): + nonlocal req, resp + assert flow.request + req.timestamp = flow.request.timestamp + assert flow.request == req + resp.timestamp = time.time() + flow.response = resp + + assert ( + Playbook(dns.DNSLayer(tctx)) + >> DataReceived(tctx.client, req.packed) + << dns.DnsRequestHook(f) + >> reply(side_effect=resolve) + << dns.DnsResponseHook(f) + >> reply() + << SendData(tctx.client, resp.packed) + >> ConnectionClosed(tctx.client) + << None + ) + assert f().request == req + assert f().response == resp + assert not f().live + + +def test_regular_mode_no_hook(tctx): + f = Placeholder(DNSFlow) + layer = dns.DNSLayer(tctx) + layer.context.server.address = None + + req = tdnsreq() + + def no_resolve(flow: DNSFlow): + nonlocal req + assert flow.request + req.timestamp = flow.request.timestamp + assert flow.request == req + + assert ( + Playbook(layer) + >> DataReceived(tctx.client, req.packed) + << dns.DnsRequestHook(f) + >> reply(side_effect=no_resolve) + << dns.DnsErrorHook(f) + >> reply() + >> ConnectionClosed(tctx.client) + << None + ) + assert f().request == req + assert not f().response + assert not f().live + + +def test_reverse_premature_close(tctx): + f = Placeholder(DNSFlow) + layer = dns.DNSLayer(tctx) + layer.context.server.address = ("8.8.8.8", 53) + + req = tdnsreq() + + assert ( + Playbook(layer) + >> DataReceived(tctx.client, req.packed) + << dns.DnsRequestHook(f) + >> reply() + << OpenConnection(tctx.server) + >> reply(None) + << SendData(tctx.server, req.packed) + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.server) + << None + ) + assert f().request + assert not f().response + assert not f().live + req.timestamp = f().request.timestamp + assert f().request == req + + +def test_reverse(tctx): + f = Placeholder(DNSFlow) + layer = dns.DNSLayer(tctx) + layer.context.server.address = ("8.8.8.8", 53) + + req = tdnsreq() + resp = tdnsresp() + + assert ( + Playbook(layer) + >> DataReceived(tctx.client, req.packed) + << dns.DnsRequestHook(f) + >> reply() + << OpenConnection(tctx.server) + >> reply(None) + << SendData(tctx.server, req.packed) + >> DataReceived(tctx.server, resp.packed) + << dns.DnsResponseHook(f) + >> reply() + << SendData(tctx.client, resp.packed) + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.server) + << None + ) + assert f().request + assert f().response + assert not f().live + req.timestamp = f().request.timestamp + resp.timestamp = f().response.timestamp + assert f().request == req and f().response == resp + + +def test_reverse_fail_connection(tctx): + f = Placeholder(DNSFlow) + layer = dns.DNSLayer(tctx) + layer.context.server.address = ("8.8.8.8", 53) + + req = tdnsreq() + + assert ( + Playbook(layer) + >> DataReceived(tctx.client, req.packed) + << dns.DnsRequestHook(f) + >> reply() + << OpenConnection(tctx.server) + >> reply("UDP no likey today.") + << dns.DnsErrorHook(f) + >> reply() + << None + ) + assert f().request + assert not f().response + assert f().error.msg == "UDP no likey today." + req.timestamp = f().request.timestamp + assert f().request == req + + +def test_reverse_with_query_resend(tctx): + f = Placeholder(DNSFlow) + layer = dns.DNSLayer(tctx) + layer.context.server.address = ("8.8.8.8", 53) + + req = tdnsreq() + req2 = tdnsreq() + req2.reserved = 4 + resp = tdnsresp() + + assert ( + Playbook(layer) + >> DataReceived(tctx.client, req.packed) + << dns.DnsRequestHook(f) + >> reply() + << OpenConnection(tctx.server) + >> reply(None) + << SendData(tctx.server, req.packed) + >> DataReceived(tctx.client, req2.packed) + << dns.DnsRequestHook(f) + >> reply() + << SendData(tctx.server, req2.packed) + >> DataReceived(tctx.server, resp.packed) + << dns.DnsResponseHook(f) + >> reply() + << SendData(tctx.client, resp.packed) + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.server) + << None + ) + assert f().request + assert f().response + assert not f().live + req2.timestamp = f().request.timestamp + resp.timestamp = f().response.timestamp + assert f().request == req2 + assert f().response == resp diff --git a/test/mitmproxy/proxy/layers/test_modes.py b/test/mitmproxy/proxy/layers/test_modes.py index b04a1cf62a..6ebe4943bd 100644 --- a/test/mitmproxy/proxy/layers/test_modes.py +++ b/test/mitmproxy/proxy/layers/test_modes.py @@ -5,16 +5,29 @@ from mitmproxy import platform from mitmproxy.addons.proxyauth import ProxyAuth from mitmproxy.connection import Client, Server -from mitmproxy.proxy.commands import CloseConnection, GetSocket, Log, OpenConnection, SendData +from mitmproxy.proxy.commands import ( + CloseConnection, + GetSocket, + Log, + OpenConnection, + SendData, +) from mitmproxy.proxy.context import Context from mitmproxy.proxy.events import ConnectionClosed, DataReceived from mitmproxy.proxy.layer import NextLayer, NextLayerHook from mitmproxy.proxy.layers import http, modes, tcp, tls from mitmproxy.proxy.layers.http import HTTPMode from mitmproxy.proxy.layers.tcp import TcpMessageHook, TcpStartHook -from mitmproxy.proxy.layers.tls import ClientTLSLayer, TlsStartClientHook, TlsStartServerHook +from mitmproxy.proxy.layers.tls import ( + ClientTLSLayer, + TlsStartClientHook, + TlsStartServerHook, +) from mitmproxy.tcp import TCPFlow -from test.mitmproxy.proxy.layers.test_tls import reply_tls_start_client, reply_tls_start_server +from test.mitmproxy.proxy.layers.test_tls import ( + reply_tls_start_client, + reply_tls_start_server, +) from test.mitmproxy.proxy.tutils import Placeholder, Playbook, reply, reply_next_layer @@ -28,21 +41,13 @@ def test_upstream_https(tctx): curl -x localhost:8080 -k http://example.com """ tctx1 = Context( - Client( - ("client", 1234), - ("127.0.0.1", 8080), - 1605699329 - ), - copy.deepcopy(tctx.options) + Client(("client", 1234), ("127.0.0.1", 8080), 1605699329), + copy.deepcopy(tctx.options), ) tctx1.options.mode = "upstream:https://example.mitmproxy.org:8081" tctx2 = Context( - Client( - ("client", 4321), - ("127.0.0.1", 8080), - 1605699329 - ), - copy.deepcopy(tctx.options) + Client(("client", 4321), ("127.0.0.1", 8080), 1605699329), + copy.deepcopy(tctx.options), ) assert tctx2.options.mode == "regular" del tctx @@ -60,7 +65,10 @@ def test_upstream_https(tctx): assert ( proxy1 - >> DataReceived(tctx1.client, b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n") + >> DataReceived( + tctx1.client, + b"GET http://example.com/ HTTP/1.1\r\nHost: example.com\r\n\r\n", + ) << NextLayerHook(Placeholder(NextLayer)) >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.upstream)) << OpenConnection(upstream) @@ -70,6 +78,7 @@ def test_upstream_https(tctx): << SendData(upstream, clienthello) ) assert upstream().address == ("example.mitmproxy.org", 8081) + assert upstream().sni == "example.mitmproxy.org" assert ( proxy2 >> DataReceived(tctx2.client, clienthello()) @@ -80,6 +89,7 @@ def test_upstream_https(tctx): << SendData(tctx2.client, serverhello) ) assert ( + # forward serverhello to proxy1 proxy1 >> DataReceived(upstream, serverhello()) << SendData(upstream, request) @@ -92,7 +102,7 @@ def test_upstream_https(tctx): >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.regular)) << OpenConnection(server) >> reply(None) - << SendData(server, b'GET / HTTP/1.1\r\nHost: example.com\r\n\r\n') + << SendData(server, b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n") >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") << SendData(tctx2.client, response) ) @@ -109,9 +119,9 @@ def test_upstream_https(tctx): def test_reverse_proxy(tctx, keep_host_header): """Test mitmproxy in reverse proxy mode. - - make sure that we connect to the right host - - make sure that we respect keep_host_header - - make sure that we include non-standard ports in the host header (#4280) + - make sure that we connect to the right host + - make sure that we respect keep_host_header + - make sure that we include non-standard ports in the host header (#4280) """ server = Placeholder(Server) tctx.options.mode = "reverse:http://localhost:8000" @@ -119,14 +129,20 @@ def test_reverse_proxy(tctx, keep_host_header): tctx.options.keep_host_header = keep_host_header assert ( Playbook(modes.ReverseProxy(tctx), hooks=False) - >> DataReceived(tctx.client, b"GET /foo HTTP/1.1\r\n" - b"Host: example.com\r\n\r\n") + >> DataReceived( + tctx.client, b"GET /foo HTTP/1.1\r\n" b"Host: example.com\r\n\r\n" + ) << NextLayerHook(Placeholder(NextLayer)) >> reply_next_layer(lambda ctx: http.HttpLayer(ctx, HTTPMode.transparent)) << OpenConnection(server) >> reply(None) - << SendData(server, b"GET /foo HTTP/1.1\r\n" - b"Host: " + (b"example.com" if keep_host_header else b"localhost:8000") + b"\r\n\r\n") + << SendData( + server, + b"GET /foo HTTP/1.1\r\n" + b"Host: " + + (b"example.com" if keep_host_header else b"localhost:8000") + + b"\r\n\r\n", + ) >> DataReceived(server, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") << SendData(tctx.client, b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") ) @@ -135,7 +151,9 @@ def test_reverse_proxy(tctx, keep_host_header): @pytest.mark.parametrize("patch", [True, False]) @pytest.mark.parametrize("connection_strategy", ["eager", "lazy"]) -def test_reverse_proxy_tcp_over_tls(tctx: Context, monkeypatch, patch, connection_strategy): +def test_reverse_proxy_tcp_over_tls( + tctx: Context, monkeypatch, patch, connection_strategy +): """ Test client --TCP-- mitmproxy --TCP over TLS-- server @@ -158,10 +176,7 @@ def test_reverse_proxy_tcp_over_tls(tctx: Context, monkeypatch, patch, connectio >> reply(None, to=OpenConnection(tctx.server)) ) else: - ( - playbook - >> DataReceived(tctx.client, b"\x01\x02\x03") - ) + (playbook >> DataReceived(tctx.client, b"\x01\x02\x03")) if patch: ( playbook @@ -172,15 +187,13 @@ def test_reverse_proxy_tcp_over_tls(tctx: Context, monkeypatch, patch, connectio ) if connection_strategy == "lazy": ( + # only now we open a connection playbook << OpenConnection(tctx.server) >> reply(None) ) assert ( - playbook - << TcpMessageHook(flow) - >> reply() - << SendData(tctx.server, data) + playbook << TcpMessageHook(flow) >> reply() << SendData(tctx.server, data) ) assert data() == b"\x01\x02\x03" else: @@ -212,11 +225,8 @@ def test_transparent_tcp(tctx: Context, monkeypatch, connection_strategy): sock = object() playbook = Playbook(modes.TransparentProxy(tctx)) - ( - playbook - << GetSocket(tctx.client) - >> reply(sock) - ) + playbook << GetSocket(tctx.client) + playbook >> reply(sock) if connection_strategy == "lazy": assert playbook else: @@ -250,7 +260,9 @@ def raise_err(sock): Playbook(modes.TransparentProxy(tctx), logs=True) << GetSocket(tctx.client) >> reply(object()) - << Log("Transparent mode failure: RuntimeError('platform-specific error')", "info") + << Log( + "Transparent mode failure: RuntimeError('platform-specific error')", "info" + ) ) @@ -293,11 +305,17 @@ def test_transparent_eager_connect_failure(tctx: Context, monkeypatch): SERVER_HELLO = b"\x05\x00" -@pytest.mark.parametrize("address,packed", [ - ("127.0.0.1", b"\x01\x7f\x00\x00\x01"), - ("::1", b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01"), - ("example.com", b"\x03\x0bexample.com"), -]) +@pytest.mark.parametrize( + "address,packed", + [ + ("127.0.0.1", b"\x01\x7f\x00\x00\x01"), + ( + "::1", + b"\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01", + ), + ("example.com", b"\x03\x0bexample.com"), + ], +) def test_socks5_success(address: str, packed: bytes, tctx: Context): tctx.options.connection_strategy = "eager" playbook = Playbook(modes.Socks5Proxy(tctx)) @@ -307,7 +325,9 @@ def test_socks5_success(address: str, packed: bytes, tctx: Context): playbook >> DataReceived(tctx.client, CLIENT_HELLO) << SendData(tctx.client, SERVER_HELLO) - >> DataReceived(tctx.client, b"\x05\x01\x00" + packed + b"\x12\x34applicationdata") + >> DataReceived( + tctx.client, b"\x05\x01\x00" + packed + b"\x12\x34applicationdata" + ) << OpenConnection(server) >> reply(None) << SendData(tctx.client, b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00") @@ -336,27 +356,35 @@ def test_socks5_trickle(tctx: Context): playbook << SendData(tctx.client, b"\x01\x00") for x in b"\x05\x01\x00\x01\x7f\x00\x00\x01\x12\x34": playbook >> DataReceived(tctx.client, bytes([x])) - assert playbook << SendData(tctx.client, b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00") - - -@pytest.mark.parametrize("data,err,msg", [ - (b"GET / HTTP/1.1", - None, - "Probably not a SOCKS request but a regular HTTP request. Invalid SOCKS version. Expected 0x05, got 0x47"), - (b"abcd", - None, - "Invalid SOCKS version. Expected 0x05, got 0x61"), - (CLIENT_HELLO + b"\x05\x02\x00\x01\x7f\x00\x00\x01\x12\x34", - SERVER_HELLO + b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00", - r"Unsupported SOCKS5 request: b'\x05\x02\x00\x01\x7f\x00\x00\x01\x124'"), - (CLIENT_HELLO + b"\x05\x01\x00\xFF\x00\x00", - SERVER_HELLO + b"\x05\x08\x00\x01\x00\x00\x00\x00\x00\x00", - r"Unknown address type: 255"), -]) + assert playbook << SendData( + tctx.client, b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00" + ) + + +@pytest.mark.parametrize( + "data,err,msg", + [ + ( + b"GET / HTTP/1.1", + None, + "Probably not a SOCKS request but a regular HTTP request. Invalid SOCKS version. Expected 0x05, got 0x47", + ), + (b"abcd", None, "Invalid SOCKS version. Expected 0x05, got 0x61"), + ( + CLIENT_HELLO + b"\x05\x02\x00\x01\x7f\x00\x00\x01\x12\x34", + SERVER_HELLO + b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00", + r"Unsupported SOCKS5 request: b'\x05\x02\x00\x01\x7f\x00\x00\x01\x124'", + ), + ( + CLIENT_HELLO + b"\x05\x01\x00\xFF\x00\x00", + SERVER_HELLO + b"\x05\x08\x00\x01\x00\x00\x00\x00\x00\x00", + r"Unknown address type: 255", + ), + ], +) def test_socks5_err(data: bytes, err: bytes, msg: str, tctx: Context): - playbook = ( - Playbook(modes.Socks5Proxy(tctx), logs=True) - >> DataReceived(tctx.client, data) + playbook = Playbook(modes.Socks5Proxy(tctx), logs=True) >> DataReceived( + tctx.client, data ) if err: playbook << SendData(tctx.client, err) @@ -365,22 +393,36 @@ def test_socks5_err(data: bytes, err: bytes, msg: str, tctx: Context): assert playbook -@pytest.mark.parametrize("client_greeting,server_choice,client_auth,server_resp,address,packed", [ - (b"\x05\x01\x02", - b"\x05\x02", - b"\x01\x04user\x08password", - b"\x01\x00", - "127.0.0.1", - b"\x01\x7f\x00\x00\x01"), - (b"\x05\x02\x01\x02", - b"\x05\x02", - b"\x01\x04user\x08password", - b"\x01\x00", - "127.0.0.1", - b"\x01\x7f\x00\x00\x01"), -]) -def test_socks5_auth_success(client_greeting: bytes, server_choice: bytes, client_auth: bytes, server_resp: bytes, - address: bytes, packed: bytes, tctx: Context): +@pytest.mark.parametrize( + "client_greeting,server_choice,client_auth,server_resp,address,packed", + [ + ( + b"\x05\x01\x02", + b"\x05\x02", + b"\x01\x04user\x08password", + b"\x01\x00", + "127.0.0.1", + b"\x01\x7f\x00\x00\x01", + ), + ( + b"\x05\x02\x01\x02", + b"\x05\x02", + b"\x01\x04user\x08password", + b"\x01\x00", + "127.0.0.1", + b"\x01\x7f\x00\x00\x01", + ), + ], +) +def test_socks5_auth_success( + client_greeting: bytes, + server_choice: bytes, + client_auth: bytes, + server_resp: bytes, + address: bytes, + packed: bytes, + tctx: Context, +): ProxyAuth().load(tctx.options) tctx.options.proxyauth = "user:password" server = Placeholder(Server) @@ -393,7 +435,9 @@ def test_socks5_auth_success(client_greeting: bytes, server_choice: bytes, clien << modes.Socks5AuthHook(Placeholder(modes.Socks5AuthData)) >> reply(side_effect=_valid_socks_auth) << SendData(tctx.client, server_resp) - >> DataReceived(tctx.client, b"\x05\x01\x00" + packed + b"\x12\x34applicationdata") + >> DataReceived( + tctx.client, b"\x05\x01\x00" + packed + b"\x12\x34applicationdata" + ) << OpenConnection(server) >> reply(None) << SendData(tctx.client, b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00") @@ -404,25 +448,37 @@ def test_socks5_auth_success(client_greeting: bytes, server_choice: bytes, clien assert nextlayer().data_client() == b"applicationdata" -@pytest.mark.parametrize("client_greeting,server_choice,client_auth,err,msg", [ - (b"\x05\x01\x00", - None, - None, - b"\x05\xFF\x00\x01\x00\x00\x00\x00\x00\x00", - "Client does not support SOCKS5 with user/password authentication."), - (b"\x05\x02\x00\x02", - b"\x05\x02", - b"\x01\x04" + b"user" + b"\x07" + b"errcode", - b"\x01\x01", - "authentication failed"), -]) -def test_socks5_auth_fail(client_greeting: bytes, server_choice: bytes, client_auth: bytes, err: bytes, msg: str, - tctx: Context): +@pytest.mark.parametrize( + "client_greeting,server_choice,client_auth,err,msg", + [ + ( + b"\x05\x01\x00", + None, + None, + b"\x05\xFF\x00\x01\x00\x00\x00\x00\x00\x00", + "Client does not support SOCKS5 with user/password authentication.", + ), + ( + b"\x05\x02\x00\x02", + b"\x05\x02", + b"\x01\x04" + b"user" + b"\x07" + b"errcode", + b"\x01\x01", + "authentication failed", + ), + ], +) +def test_socks5_auth_fail( + client_greeting: bytes, + server_choice: bytes, + client_auth: bytes, + err: bytes, + msg: str, + tctx: Context, +): ProxyAuth().load(tctx.options) tctx.options.proxyauth = "user:password" - playbook = ( - Playbook(modes.Socks5Proxy(tctx), logs=True) - >> DataReceived(tctx.client, client_greeting) + playbook = Playbook(modes.Socks5Proxy(tctx), logs=True) >> DataReceived( + tctx.client, client_greeting ) if server_choice is None: playbook << SendData(tctx.client, err) diff --git a/test/mitmproxy/proxy/layers/test_tcp.py b/test/mitmproxy/proxy/layers/test_tcp.py index 299aa99939..d4f4947b09 100644 --- a/test/mitmproxy/proxy/layers/test_tcp.py +++ b/test/mitmproxy/proxy/layers/test_tcp.py @@ -13,29 +13,23 @@ def test_open_connection(tctx): If there is no server connection yet, establish one, because the server may send data first. """ - assert ( - Playbook(tcp.TCPLayer(tctx, True)) - << OpenConnection(tctx.server) - ) + assert Playbook(tcp.TCPLayer(tctx, True)) << OpenConnection(tctx.server) tctx.server.timestamp_start = 1624544785 - assert ( - Playbook(tcp.TCPLayer(tctx, True)) - << None - ) + assert Playbook(tcp.TCPLayer(tctx, True)) << None def test_open_connection_err(tctx): f = Placeholder(TCPFlow) assert ( - Playbook(tcp.TCPLayer(tctx)) - << tcp.TcpStartHook(f) - >> reply() - << OpenConnection(tctx.server) - >> reply("Connect call failed") - << tcp.TcpErrorHook(f) - >> reply() - << CloseConnection(tctx.client) + Playbook(tcp.TCPLayer(tctx)) + << tcp.TcpStartHook(f) + >> reply() + << OpenConnection(tctx.server) + >> reply("Connect call failed") + << tcp.TcpErrorHook(f) + >> reply() + << CloseConnection(tctx.client) ) @@ -44,27 +38,27 @@ def test_simple(tctx): f = Placeholder(TCPFlow) assert ( - Playbook(tcp.TCPLayer(tctx)) - << tcp.TcpStartHook(f) - >> reply() - << OpenConnection(tctx.server) - >> reply(None) - >> DataReceived(tctx.client, b"hello!") - << tcp.TcpMessageHook(f) - >> reply() - << SendData(tctx.server, b"hello!") - >> DataReceived(tctx.server, b"hi") - << tcp.TcpMessageHook(f) - >> reply() - << SendData(tctx.client, b"hi") - >> ConnectionClosed(tctx.server) - << CloseConnection(tctx.client, half_close=True) - >> ConnectionClosed(tctx.client) - << CloseConnection(tctx.server) - << tcp.TcpEndHook(f) - >> reply() - >> ConnectionClosed(tctx.client) - << None + Playbook(tcp.TCPLayer(tctx)) + << tcp.TcpStartHook(f) + >> reply() + << OpenConnection(tctx.server) + >> reply(None) + >> DataReceived(tctx.client, b"hello!") + << tcp.TcpMessageHook(f) + >> reply() + << SendData(tctx.server, b"hello!") + >> DataReceived(tctx.server, b"hi") + << tcp.TcpMessageHook(f) + >> reply() + << SendData(tctx.client, b"hi") + >> ConnectionClosed(tctx.server) + << CloseConnection(tctx.client, half_close=True) + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.server) + << tcp.TcpEndHook(f) + >> reply() + >> ConnectionClosed(tctx.client) + << None ) assert len(f().messages) == 2 @@ -75,11 +69,11 @@ def test_receive_data_before_server_connected(tctx): will still be forwarded. """ assert ( - Playbook(tcp.TCPLayer(tctx), hooks=False) - << OpenConnection(tctx.server) - >> DataReceived(tctx.client, b"hello!") - >> reply(None, to=-2) - << SendData(tctx.server, b"hello!") + Playbook(tcp.TCPLayer(tctx), hooks=False) + << OpenConnection(tctx.server) + >> DataReceived(tctx.client, b"hello!") + >> reply(None, to=-2) + << SendData(tctx.server, b"hello!") ) @@ -88,17 +82,17 @@ def test_receive_data_after_half_close(tctx): data received after the other connection has been half-closed should still be forwarded. """ assert ( - Playbook(tcp.TCPLayer(tctx), hooks=False) - << OpenConnection(tctx.server) - >> reply(None) - >> DataReceived(tctx.client, b"eof-delimited-request") - << SendData(tctx.server, b"eof-delimited-request") - >> ConnectionClosed(tctx.client) - << CloseConnection(tctx.server, half_close=True) - >> DataReceived(tctx.server, b"i'm late") - << SendData(tctx.client, b"i'm late") - >> ConnectionClosed(tctx.server) - << CloseConnection(tctx.client) + Playbook(tcp.TCPLayer(tctx), hooks=False) + << OpenConnection(tctx.server) + >> reply(None) + >> DataReceived(tctx.client, b"eof-delimited-request") + << SendData(tctx.server, b"eof-delimited-request") + >> ConnectionClosed(tctx.client) + << CloseConnection(tctx.server, half_close=True) + >> DataReceived(tctx.server, b"i'm late") + << SendData(tctx.client, b"i'm late") + >> ConnectionClosed(tctx.server) + << CloseConnection(tctx.client) ) @@ -110,11 +104,11 @@ def test_ignore(tctx, ignore): def no_flow_hooks(): assert ( - Playbook(tcp.TCPLayer(tctx, ignore=ignore), hooks=True) - << OpenConnection(tctx.server) - >> reply(None) - >> DataReceived(tctx.client, b"hello!") - << SendData(tctx.server, b"hello!") + Playbook(tcp.TCPLayer(tctx, ignore=ignore), hooks=True) + << OpenConnection(tctx.server) + >> reply(None) + >> DataReceived(tctx.client, b"hello!") + << SendData(tctx.server, b"hello!") ) if ignore: @@ -129,20 +123,22 @@ def test_inject(tctx): f = Placeholder(TCPFlow) assert ( - Playbook(tcp.TCPLayer(tctx)) - << tcp.TcpStartHook(f) - >> TcpMessageInjected(f, TCPMessage(True, b"hello!")) - >> reply(to=-2) - << OpenConnection(tctx.server) - >> reply(None) - << tcp.TcpMessageHook(f) - >> reply() - << SendData(tctx.server, b"hello!") - # and the other way... - >> TcpMessageInjected(f, TCPMessage(False, b"I have already done the greeting for you.")) - << tcp.TcpMessageHook(f) - >> reply() - << SendData(tctx.client, b"I have already done the greeting for you.") - << None + Playbook(tcp.TCPLayer(tctx)) + << tcp.TcpStartHook(f) + >> TcpMessageInjected(f, TCPMessage(True, b"hello!")) + >> reply(to=-2) + << OpenConnection(tctx.server) + >> reply(None) + << tcp.TcpMessageHook(f) + >> reply() + << SendData(tctx.server, b"hello!") + # and the other way... + >> TcpMessageInjected( + f, TCPMessage(False, b"I have already done the greeting for you.") + ) + << tcp.TcpMessageHook(f) + >> reply() + << SendData(tctx.client, b"I have already done the greeting for you.") + << None ) assert len(f().messages) == 2 diff --git a/test/mitmproxy/proxy/layers/test_tls.py b/test/mitmproxy/proxy/layers/test_tls.py index 0f6b3da41f..0a28010f7b 100644 --- a/test/mitmproxy/proxy/layers/test_tls.py +++ b/test/mitmproxy/proxy/layers/test_tls.py @@ -1,5 +1,6 @@ import ssl -import typing +import time +from typing import Optional import pytest @@ -8,8 +9,10 @@ from mitmproxy.connection import ConnectionState, Server from mitmproxy.proxy import commands, context, events, layer from mitmproxy.proxy.layers import tls +from mitmproxy.tls import ClientHelloData, TlsData from mitmproxy.utils import data from test.mitmproxy.proxy import tutils +from test.mitmproxy.proxy.tutils import BytesMatching, StrMatching tlsdata = data.Data(__name__) @@ -26,13 +29,8 @@ def test_is_tls_handshake_record(): def test_record_contents(): - data = bytes.fromhex( - "1603010002beef" - "1603010001ff" - ) - assert list(tls.handshake_record_contents(data)) == [ - b"\xbe\xef", b"\xff" - ] + data = bytes.fromhex("1603010002beef" "1603010001ff") + assert list(tls.handshake_record_contents(data)) == [b"\xbe\xef", b"\xff"] for i in range(6): assert list(tls.handshake_record_contents(data[:i])) == [] @@ -67,8 +65,10 @@ def test_get_client_hello(): assert tls.get_client_hello(single_record) == client_hello_no_extensions split_over_two_records = ( - bytes.fromhex("1603010020") + client_hello_no_extensions[:32] + - bytes.fromhex("1603010045") + client_hello_no_extensions[32:] + bytes.fromhex("1603010020") + + client_hello_no_extensions[:32] + + bytes.fromhex("1603010045") + + client_hello_no_extensions[32:] ) assert tls.get_client_hello(split_over_two_records) == client_hello_no_extensions @@ -80,7 +80,9 @@ def test_parse_client_hello(): assert tls.parse_client_hello(client_hello_with_extensions).sni == "example.com" assert tls.parse_client_hello(client_hello_with_extensions[:50]) is None with pytest.raises(ValueError): - tls.parse_client_hello(client_hello_with_extensions[:183] + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00') + tls.parse_client_hello( + client_hello_with_extensions[:183] + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00" + ) class SSLTest: @@ -89,13 +91,15 @@ class SSLTest: def __init__( self, server_side: bool = False, - alpn: typing.Optional[typing.List[str]] = None, - sni: typing.Optional[bytes] = b"example.mitmproxy.org", - max_ver: typing.Optional[ssl.TLSVersion] = None, + alpn: Optional[list[str]] = None, + sni: Optional[bytes] = b"example.mitmproxy.org", + max_ver: Optional[ssl.TLSVersion] = None, ): self.inc = ssl.MemoryBIO() self.out = ssl.MemoryBIO() - self.ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER if server_side else ssl.PROTOCOL_TLS_CLIENT) + self.ctx = ssl.SSLContext( + ssl.PROTOCOL_TLS_SERVER if server_side else ssl.PROTOCOL_TLS_CLIENT + ) self.ctx.verify_mode = ssl.CERT_OPTIONAL self.ctx.load_verify_locations( @@ -105,9 +109,17 @@ def __init__( if alpn: self.ctx.set_alpn_protocols(alpn) if server_side: + if sni == b"192.0.2.42": + filename = "trusted-leaf-ip" + else: + filename = "trusted-leaf" self.ctx.load_cert_chain( - certfile=tlsdata.path("../../net/data/verificationcerts/trusted-leaf.crt"), - keyfile=tlsdata.path("../../net/data/verificationcerts/trusted-leaf.key"), + certfile=tlsdata.path( + f"../../net/data/verificationcerts/{filename}.crt" + ), + keyfile=tlsdata.path( + f"../../net/data/verificationcerts/{filename}.key" + ), ) if max_ver: self.ctx.maximum_version = max_ver @@ -129,50 +141,68 @@ def do_handshake(self) -> None: return self.obj.do_handshake() -def _test_echo(playbook: tutils.Playbook, tssl: SSLTest, conn: connection.Connection) -> None: +def _test_echo( + playbook: tutils.Playbook, tssl: SSLTest, conn: connection.Connection +) -> None: tssl.obj.write(b"Hello World") data = tutils.Placeholder(bytes) assert ( - playbook - >> events.DataReceived(conn, tssl.bio_read()) - << commands.SendData(conn, data) + playbook + >> events.DataReceived(conn, tssl.bio_read()) + << commands.SendData(conn, data) ) tssl.bio_write(data()) assert tssl.obj.read() == b"hello world" class TlsEchoLayer(tutils.EchoLayer): - err: typing.Optional[str] = None + err: Optional[str] = None def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: if isinstance(event, events.DataReceived) and event.data == b"open-connection": err = yield commands.OpenConnection(self.context.server) if err: - yield commands.SendData(event.connection, f"open-connection failed: {err}".encode()) + yield commands.SendData( + event.connection, f"open-connection failed: {err}".encode() + ) else: yield from super()._handle_event(event) -def interact(playbook: tutils.Playbook, conn: connection.Connection, tssl: SSLTest): +def finish_handshake( + playbook: tutils.Playbook, conn: connection.Connection, tssl: SSLTest +): data = tutils.Placeholder(bytes) + tls_hook_data = tutils.Placeholder(TlsData) + if isinstance(conn, connection.Client): + established_hook = tls.TlsEstablishedClientHook(tls_hook_data) + else: + established_hook = tls.TlsEstablishedServerHook(tls_hook_data) assert ( - playbook - >> events.DataReceived(conn, tssl.bio_read()) - << commands.SendData(conn, data) + playbook + >> events.DataReceived(conn, tssl.bio_read()) + << established_hook + >> tutils.reply() + << commands.SendData(conn, data) ) + assert tls_hook_data().conn.error is None tssl.bio_write(data()) -def reply_tls_start_client(alpn: typing.Optional[bytes] = None, *args, **kwargs) -> tutils.reply: +def reply_tls_start_client( + alpn: Optional[bytes] = None, *args, **kwargs +) -> tutils.reply: """ Helper function to simplify the syntax for tls_start_client hooks. """ - def make_client_conn(tls_start: tls.TlsStartData) -> None: + def make_client_conn(tls_start: TlsData) -> None: # ssl_context = SSL.Context(Method.TLS_METHOD) # ssl_context.set_min_proto_version(SSL.TLS1_3_VERSION) ssl_context = SSL.Context(SSL.SSLv23_METHOD) - ssl_context.set_options(SSL.OP_NO_SSLv3 | SSL.OP_NO_TLSv1 | SSL.OP_NO_TLSv1_1 | SSL.OP_NO_TLSv1_2) + ssl_context.set_options( + SSL.OP_NO_SSLv3 | SSL.OP_NO_TLSv1 | SSL.OP_NO_TLSv1_1 | SSL.OP_NO_TLSv1_2 + ) ssl_context.use_privatekey_file( tlsdata.path("../../net/data/verificationcerts/trusted-leaf.key") ) @@ -188,16 +218,20 @@ def make_client_conn(tls_start: tls.TlsStartData) -> None: return tutils.reply(*args, side_effect=make_client_conn, **kwargs) -def reply_tls_start_server(alpn: typing.Optional[bytes] = None, *args, **kwargs) -> tutils.reply: +def reply_tls_start_server( + alpn: Optional[bytes] = None, *args, **kwargs +) -> tutils.reply: """ Helper function to simplify the syntax for tls_start_server hooks. """ - def make_server_conn(tls_start: tls.TlsStartData) -> None: + def make_server_conn(tls_start: TlsData) -> None: # ssl_context = SSL.Context(Method.TLS_METHOD) # ssl_context.set_min_proto_version(SSL.TLS1_3_VERSION) ssl_context = SSL.Context(SSL.SSLv23_METHOD) - ssl_context.set_options(SSL.OP_NO_SSLv3 | SSL.OP_NO_TLSv1 | SSL.OP_NO_TLSv1_1 | SSL.OP_NO_TLSv1_2) + ssl_context.set_options( + SSL.OP_NO_SSLv3 | SSL.OP_NO_TLSv1 | SSL.OP_NO_TLSv1_1 | SSL.OP_NO_TLSv1_2 + ) ssl_context.load_verify_locations( cafile=tlsdata.path("../../net/data/verificationcerts/trusted-root.crt") ) @@ -219,10 +253,12 @@ def make_server_conn(tls_start: tls.TlsStartData) -> None: # https://www.chromestatus.com/feature/4981025180483584 SSL._lib.X509_VERIFY_PARAM_set_hostflags( param, - SSL._lib.X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS | SSL._lib.X509_CHECK_FLAG_NEVER_CHECK_SUBJECT + SSL._lib.X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS + | SSL._lib.X509_CHECK_FLAG_NEVER_CHECK_SUBJECT, ) SSL._openssl_assert( - SSL._lib.X509_VERIFY_PARAM_set1_host(param, tls_start.conn.sni.encode(), 0) == 1 + SSL._lib.X509_VERIFY_PARAM_set1_host(param, tls_start.conn.sni.encode(), 0) + == 1 ) return tutils.reply(*args, side_effect=make_server_conn, **kwargs) @@ -238,9 +274,9 @@ def test_not_connected(self, tctx: context.Context): layer.child_layer = TlsEchoLayer(tctx) assert ( - tutils.Playbook(layer) - >> events.DataReceived(tctx.client, b"Hello World") - << commands.SendData(tctx.client, b"hello world") + tutils.Playbook(layer) + >> events.DataReceived(tctx.client, b"Hello World") + << commands.SendData(tctx.client, b"hello world") ) def test_simple(self, tctx): @@ -251,49 +287,47 @@ def test_simple(self, tctx): tssl = SSLTest(server_side=True) - # send ClientHello + # send ClientHello, receive ClientHello data = tutils.Placeholder(bytes) assert ( - playbook - << tls.TlsStartServerHook(tutils.Placeholder()) - >> reply_tls_start_server() - << commands.SendData(tctx.server, data) + playbook + << tls.TlsStartServerHook(tutils.Placeholder()) + >> reply_tls_start_server() + << commands.SendData(tctx.server, data) ) - - # receive ServerHello, finish client handshake tssl.bio_write(data()) with pytest.raises(ssl.SSLWantReadError): tssl.do_handshake() - interact(playbook, tctx.server, tssl) - # finish server handshake + # finish handshake (mitmproxy) + finish_handshake(playbook, tctx.server, tssl) + + # finish handshake (locally) tssl.do_handshake() - assert ( - playbook - >> events.DataReceived(tctx.server, tssl.bio_read()) - << None - ) + playbook >> events.DataReceived(tctx.server, tssl.bio_read()) + playbook << None + assert playbook assert tctx.server.tls_established # Echo assert ( - playbook - >> events.DataReceived(tctx.client, b"foo") - << layer.NextLayerHook(tutils.Placeholder()) - >> tutils.reply_next_layer(TlsEchoLayer) - << commands.SendData(tctx.client, b"foo") + playbook + >> events.DataReceived(tctx.client, b"foo") + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(TlsEchoLayer) + << commands.SendData(tctx.client, b"foo") ) _test_echo(playbook, tssl, tctx.server) with pytest.raises(ssl.SSLWantReadError): tssl.obj.unwrap() assert ( - playbook - >> events.DataReceived(tctx.server, tssl.bio_read()) - << commands.CloseConnection(tctx.server) - >> events.ConnectionClosed(tctx.server) - << None + playbook + >> events.DataReceived(tctx.server, tssl.bio_read()) + << commands.CloseConnection(tctx.server) + >> events.ConnectionClosed(tctx.server) + << None ) def test_untrusted_cert(self, tctx): @@ -307,15 +341,15 @@ def test_untrusted_cert(self, tctx): # send ClientHello data = tutils.Placeholder(bytes) assert ( - playbook - >> events.DataReceived(tctx.client, b"open-connection") - << layer.NextLayerHook(tutils.Placeholder()) - >> tutils.reply_next_layer(TlsEchoLayer) - << commands.OpenConnection(tctx.server) - >> tutils.reply(None) - << tls.TlsStartServerHook(tutils.Placeholder()) - >> reply_tls_start_server() - << commands.SendData(tctx.server, data) + playbook + >> events.DataReceived(tctx.client, b"open-connection") + << layer.NextLayerHook(tutils.Placeholder()) + >> tutils.reply_next_layer(TlsEchoLayer) + << commands.OpenConnection(tctx.server) + >> tutils.reply(None) + << tls.TlsStartServerHook(tutils.Placeholder()) + >> reply_tls_start_server() + << commands.SendData(tctx.server, data) ) # receive ServerHello, finish client handshake @@ -323,13 +357,26 @@ def test_untrusted_cert(self, tctx): with pytest.raises(ssl.SSLWantReadError): tssl.do_handshake() + tls_hook_data = tutils.Placeholder(TlsData) assert ( - playbook - >> events.DataReceived(tctx.server, tssl.bio_read()) - << commands.Log("Server TLS handshake failed. Certificate verify failed: Hostname mismatch", "warn") - << commands.CloseConnection(tctx.server) - << commands.SendData(tctx.client, - b"open-connection failed: Certificate verify failed: Hostname mismatch") + playbook + >> events.DataReceived(tctx.server, tssl.bio_read()) + << commands.Log( + # different casing in OpenSSL < 3.0 + StrMatching("Server TLS handshake failed. Certificate verify failed: [Hh]ostname mismatch"), + "warn", + ) + << tls.TlsFailedServerHook(tls_hook_data) + >> tutils.reply() + << commands.CloseConnection(tctx.server) + << commands.SendData( + tctx.client, + # different casing in OpenSSL < 3.0 + BytesMatching(b"open-connection failed: Certificate verify failed: [Hh]ostname mismatch"), + ) + ) + assert ( + tls_hook_data().conn.error.lower() == "Certificate verify failed: Hostname mismatch".lower() ) assert not tctx.server.tls_established @@ -340,15 +387,22 @@ def test_remote_speaks_no_tls(self, tctx): # send ClientHello, receive random garbage back data = tutils.Placeholder(bytes) + tls_hook_data = tutils.Placeholder(TlsData) assert ( - playbook - << tls.TlsStartServerHook(tutils.Placeholder()) - >> reply_tls_start_server() - << commands.SendData(tctx.server, data) - >> events.DataReceived(tctx.server, b"HTTP/1.1 404 Not Found\r\n") - << commands.Log("Server TLS handshake failed. The remote server does not speak TLS.", "warn") - << commands.CloseConnection(tctx.server) + playbook + << tls.TlsStartServerHook(tutils.Placeholder()) + >> reply_tls_start_server() + << commands.SendData(tctx.server, data) + >> events.DataReceived(tctx.server, b"HTTP/1.1 404 Not Found\r\n") + << commands.Log( + "Server TLS handshake failed. The remote server does not speak TLS.", + "warn", + ) + << tls.TlsFailedServerHook(tls_hook_data) + >> tutils.reply() + << commands.CloseConnection(tctx.server) ) + assert tls_hook_data().conn.error == "The remote server does not speak TLS." def test_unsupported_protocol(self, tctx: context.Context): """Test the scenario where the server only supports an outdated TLS version by default.""" @@ -375,19 +429,25 @@ def test_unsupported_protocol(self, tctx: context.Context): tssl.do_handshake() # send back error + tls_hook_data = tutils.Placeholder(TlsData) assert ( playbook >> events.DataReceived(tctx.server, tssl.bio_read()) - << commands.Log("Server TLS handshake failed. The remote server and mitmproxy cannot agree on a TLS version" - " to use. You may need to adjust mitmproxy's tls_version_server_min option.", "warn") + << commands.Log( + "Server TLS handshake failed. The remote server and mitmproxy cannot agree on a TLS version" + " to use. You may need to adjust mitmproxy's tls_version_server_min option.", + "warn", + ) + << tls.TlsFailedServerHook(tls_hook_data) + >> tutils.reply() << commands.CloseConnection(tctx.server) ) + assert tls_hook_data().conn.error def make_client_tls_layer( - tctx: context.Context, - **kwargs -) -> typing.Tuple[tutils.Playbook, tls.ClientTLSLayer, SSLTest]: + tctx: context.Context, **kwargs +) -> tuple[tutils.Playbook, tls.ClientTLSLayer, SSLTest]: # This is a bit contrived as the client layer expects a server layer as parent. # We also set child layers manually to avoid NextLayer noise. server_layer = tls.ServerTLSLayer(tctx) @@ -397,7 +457,10 @@ def make_client_tls_layer( playbook = tutils.Playbook(server_layer) # Add some server config, this is needed anyways. - tctx.server.__dict__["address"] = ("example.mitmproxy.org", 443) # .address fails because connection is open + tctx.server.__dict__["address"] = ( + "example.mitmproxy.org", + 443, + ) # .address fails because connection is open tctx.server.sni = "example.mitmproxy.org" tssl_client = SSLTest(**kwargs) @@ -418,18 +481,18 @@ def test_client_only(self, tctx: context.Context): # Send ClientHello, receive ServerHello data = tutils.Placeholder(bytes) assert ( - playbook - >> events.DataReceived(tctx.client, tssl_client.bio_read()) - << tls.TlsClienthelloHook(tutils.Placeholder()) - >> tutils.reply() - << tls.TlsStartClientHook(tutils.Placeholder()) - >> reply_tls_start_client() - << commands.SendData(tctx.client, data) + playbook + >> events.DataReceived(tctx.client, tssl_client.bio_read()) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply() + << tls.TlsStartClientHook(tutils.Placeholder()) + >> reply_tls_start_client() + << commands.SendData(tctx.client, data) ) tssl_client.bio_write(data()) tssl_client.do_handshake() # Finish Handshake - interact(playbook, tctx.client, tssl_client) + finish_handshake(playbook, tctx.client, tssl_client) assert tssl_client.obj.getpeercert(True) assert tctx.client.tls_established @@ -438,18 +501,18 @@ def test_client_only(self, tctx: context.Context): _test_echo(playbook, tssl_client, tctx.client) other_server = Server(None) assert ( - playbook - >> events.DataReceived(other_server, b"Plaintext") - << commands.SendData(other_server, b"plaintext") + playbook + >> events.DataReceived(other_server, b"Plaintext") + << commands.SendData(other_server, b"plaintext") ) - @pytest.mark.parametrize("eager", ["eager", ""]) - def test_server_required(self, tctx, eager): + @pytest.mark.parametrize("server_state", ["open", "closed"]) + def test_server_required(self, tctx, server_state): """ Test the scenario where a server connection is required (for example, because of an unknown ALPN) to establish TLS with the client. """ - if eager: + if server_state == "open": tctx.server.state = ConnectionState.OPEN tssl_server = SSLTest(server_side=True, alpn=["quux"]) playbook, client_layer, tssl_client = make_client_tls_layer(tctx, alpn=["quux"]) @@ -457,21 +520,18 @@ def test_server_required(self, tctx, eager): # We should now get instructed to open a server connection. data = tutils.Placeholder(bytes) - def require_server_conn(client_hello: tls.ClientHelloData) -> None: + def require_server_conn(client_hello: ClientHelloData) -> None: client_hello.establish_server_tls_first = True ( - playbook - >> events.DataReceived(tctx.client, tssl_client.bio_read()) - << tls.TlsClienthelloHook(tutils.Placeholder()) - >> tutils.reply(side_effect=require_server_conn) + playbook + >> events.DataReceived(tctx.client, tssl_client.bio_read()) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply(side_effect=require_server_conn) ) - if not eager: - ( - playbook - << commands.OpenConnection(tctx.server) - >> tutils.reply(None) - ) + if server_state == "closed": + playbook << commands.OpenConnection(tctx.server) + playbook >> tutils.reply(None) assert ( playbook << tls.TlsStartServerHook(tutils.Placeholder()) @@ -486,10 +546,12 @@ def require_server_conn(client_hello: tls.ClientHelloData) -> None: data = tutils.Placeholder(bytes) assert ( - playbook - >> events.DataReceived(tctx.server, tssl_server.bio_read()) - << commands.SendData(tctx.server, data) - << tls.TlsStartClientHook(tutils.Placeholder()) + playbook + >> events.DataReceived(tctx.server, tssl_server.bio_read()) + << tls.TlsEstablishedServerHook(tutils.Placeholder()) + >> tutils.reply() + << commands.SendData(tctx.server, data) + << tls.TlsStartClientHook(tutils.Placeholder()) ) tssl_server.bio_write(data()) assert tctx.server.tls_established @@ -497,13 +559,13 @@ def require_server_conn(client_hello: tls.ClientHelloData) -> None: data = tutils.Placeholder(bytes) assert ( - playbook - >> reply_tls_start_client(alpn=b"quux") - << commands.SendData(tctx.client, data) + playbook + >> reply_tls_start_client(alpn=b"quux") + << commands.SendData(tctx.client, data) ) tssl_client.bio_write(data()) tssl_client.do_handshake() - interact(playbook, tctx.client, tssl_client) + finish_handshake(playbook, tctx.client, tssl_client) # Both handshakes completed! assert tctx.client.tls_established @@ -514,18 +576,58 @@ def require_server_conn(client_hello: tls.ClientHelloData) -> None: _test_echo(playbook, tssl_server, tctx.server) _test_echo(playbook, tssl_client, tctx.client) + @pytest.mark.parametrize("server_state", ["open", "closed"]) + def test_passthrough_from_clienthello(self, tctx, server_state): + """ + Test the scenario where the connection is moved to passthrough mode in the tls_clienthello hook. + """ + if server_state == "open": + tctx.server.timestamp_start = time.time() + tctx.server.state = ConnectionState.OPEN + + playbook, client_layer, tssl_client = make_client_tls_layer(tctx, alpn=["quux"]) + + def make_passthrough(client_hello: ClientHelloData) -> None: + client_hello.ignore_connection = True + + client_hello = tssl_client.bio_read() + ( + playbook + >> events.DataReceived(tctx.client, client_hello) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply(side_effect=make_passthrough) + ) + if server_state == "closed": + playbook << commands.OpenConnection(tctx.server) + playbook >> tutils.reply(None) + assert ( + playbook + << commands.SendData(tctx.server, client_hello) # passed through unmodified + >> events.DataReceived( + tctx.server, b"ServerHello" + ) # and the same for the serverhello. + << commands.SendData(tctx.client, b"ServerHello") + ) + def test_cannot_parse_clienthello(self, tctx: context.Context): """Test the scenario where we cannot parse the ClientHello""" playbook, client_layer, tssl_client = make_client_tls_layer(tctx) + tls_hook_data = tutils.Placeholder(TlsData) invalid = b"\x16\x03\x01\x00\x00" assert ( - playbook - >> events.DataReceived(tctx.client, invalid) - << commands.Log(f"Client TLS handshake failed. Cannot parse ClientHello: {invalid.hex()}", level="warn") - << commands.CloseConnection(tctx.client) + playbook + >> events.DataReceived(tctx.client, invalid) + << commands.Log( + f"Client TLS handshake failed. Cannot parse ClientHello: {invalid.hex()}", + level="warn", + ) + << tls.TlsFailedClientHook(tls_hook_data) + >> tutils.reply() + << commands.CloseConnection(tctx.client) ) + assert tls_hook_data().conn.error assert not tctx.client.tls_established # Make sure that an active server connection does not cause child layers to spawn. @@ -533,46 +635,63 @@ def test_cannot_parse_clienthello(self, tctx: context.Context): assert ( playbook >> events.DataReceived(Server(None), b"data on other stream") - << commands.Log(">> DataReceived(server, b'data on other stream')", 'debug') - << commands.Log("Swallowing DataReceived(server, b'data on other stream') as handshake failed.", "debug") + << commands.Log(">> DataReceived(server, b'data on other stream')", "debug") + << commands.Log( + "Swallowing DataReceived(server, b'data on other stream') as handshake failed.", + "debug", + ) ) def test_mitmproxy_ca_is_untrusted(self, tctx: context.Context): """Test the scenario where the client doesn't trust the mitmproxy CA.""" - playbook, client_layer, tssl_client = make_client_tls_layer(tctx, sni=b"wrong.host.mitmproxy.org") + playbook, client_layer, tssl_client = make_client_tls_layer( + tctx, sni=b"wrong.host.mitmproxy.org" + ) playbook.logs = True data = tutils.Placeholder(bytes) assert ( - playbook - >> events.DataReceived(tctx.client, tssl_client.bio_read()) - << tls.TlsClienthelloHook(tutils.Placeholder()) - >> tutils.reply() - << tls.TlsStartClientHook(tutils.Placeholder()) - >> reply_tls_start_client() - << commands.SendData(tctx.client, data) + playbook + >> events.DataReceived(tctx.client, tssl_client.bio_read()) + << tls.TlsClienthelloHook(tutils.Placeholder()) + >> tutils.reply() + << tls.TlsStartClientHook(tutils.Placeholder()) + >> reply_tls_start_client() + << commands.SendData(tctx.client, data) ) tssl_client.bio_write(data()) with pytest.raises(ssl.SSLCertVerificationError): tssl_client.do_handshake() # Finish Handshake + tls_hook_data = tutils.Placeholder(TlsData) assert ( - playbook - >> events.DataReceived(tctx.client, tssl_client.bio_read()) - << commands.Log("Client TLS handshake failed. The client does not trust the proxy's certificate " - "for wrong.host.mitmproxy.org (sslv3 alert bad certificate)", "warn") - << commands.CloseConnection(tctx.client) - >> events.ConnectionClosed(tctx.client) + playbook + >> events.DataReceived(tctx.client, tssl_client.bio_read()) + << commands.Log( + "Client TLS handshake failed. The client does not trust the proxy's certificate " + "for wrong.host.mitmproxy.org (sslv3 alert bad certificate)", + "warn", + ) + << tls.TlsFailedClientHook(tls_hook_data) + >> tutils.reply() + << commands.CloseConnection(tctx.client) + >> events.ConnectionClosed(tctx.client) ) assert not tctx.client.tls_established + assert tls_hook_data().conn.error - @pytest.mark.parametrize("close_at", ["tls_clienthello", "tls_start_client", "handshake"]) + @pytest.mark.parametrize( + "close_at", ["tls_clienthello", "tls_start_client", "handshake"] + ) def test_immediate_disconnect(self, tctx: context.Context, close_at): """Test the scenario where the client is disconnecting during the handshake. This may happen because they are not interested in the connection anymore, or because they do not like the proxy certificate.""" - playbook, client_layer, tssl_client = make_client_tls_layer(tctx, sni=b"wrong.host.mitmproxy.org") + playbook, client_layer, tssl_client = make_client_tls_layer( + tctx, sni=b"wrong.host.mitmproxy.org" + ) playbook.logs = True + tls_hook_data = tutils.Placeholder(TlsData) playbook >> events.DataReceived(tctx.client, tssl_client.bio_read()) playbook << tls.TlsClienthelloHook(tutils.Placeholder()) @@ -584,8 +703,11 @@ def test_immediate_disconnect(self, tctx: context.Context, close_at): >> tutils.reply(to=-2) << tls.TlsStartClientHook(tutils.Placeholder()) >> reply_tls_start_client() + << tls.TlsFailedClientHook(tls_hook_data) + >> tutils.reply() << commands.CloseConnection(tctx.client) ) + assert tls_hook_data().conn.error return playbook >> tutils.reply() @@ -596,8 +718,11 @@ def test_immediate_disconnect(self, tctx: context.Context, close_at): playbook >> events.ConnectionClosed(tctx.client) >> reply_tls_start_client(to=-2) + << tls.TlsFailedClientHook(tls_hook_data) + >> tutils.reply() << commands.CloseConnection(tctx.client) ) + assert tls_hook_data().conn.error return assert ( @@ -605,17 +730,26 @@ def test_immediate_disconnect(self, tctx: context.Context, close_at): >> reply_tls_start_client() << commands.SendData(tctx.client, tutils.Placeholder()) >> events.ConnectionClosed(tctx.client) - << commands.Log("Client TLS handshake failed. The client disconnected during the handshake. " - "If this happens consistently for wrong.host.mitmproxy.org, this may indicate that the " - "client does not trust the proxy's certificate.", "info") + << commands.Log( + "Client TLS handshake failed. The client disconnected during the handshake. " + "If this happens consistently for wrong.host.mitmproxy.org, this may indicate that the " + "client does not trust the proxy's certificate.", + "info", + ) + << tls.TlsFailedClientHook(tls_hook_data) + >> tutils.reply() << commands.CloseConnection(tctx.client) ) + assert tls_hook_data().conn.error def test_unsupported_protocol(self, tctx: context.Context): """Test the scenario where the client only supports an outdated TLS version by default.""" - playbook, client_layer, tssl_client = make_client_tls_layer(tctx, max_ver=ssl.TLSVersion.TLSv1_2) + playbook, client_layer, tssl_client = make_client_tls_layer( + tctx, max_ver=ssl.TLSVersion.TLSv1_2 + ) playbook.logs = True + tls_hook_data = tutils.Placeholder(TlsData) assert ( playbook >> events.DataReceived(tctx.client, tssl_client.bio_read()) @@ -623,7 +757,13 @@ def test_unsupported_protocol(self, tctx: context.Context): >> tutils.reply() << tls.TlsStartClientHook(tutils.Placeholder()) >> reply_tls_start_client() - << commands.Log("Client TLS handshake failed. Client and mitmproxy cannot agree on a TLS version to " - "use. You may need to adjust mitmproxy's tls_version_client_min option.", "warn") + << commands.Log( + "Client TLS handshake failed. Client and mitmproxy cannot agree on a TLS version to " + "use. You may need to adjust mitmproxy's tls_version_client_min option.", + "warn", + ) + << tls.TlsFailedClientHook(tls_hook_data) + >> tutils.reply() << commands.CloseConnection(tctx.client) ) + assert tls_hook_data().conn.error diff --git a/test/mitmproxy/proxy/layers/test_tls_fuzz.py b/test/mitmproxy/proxy/layers/test_tls_fuzz.py index f95b40bb0f..402c08a5fc 100644 --- a/test/mitmproxy/proxy/layers/test_tls_fuzz.py +++ b/test/mitmproxy/proxy/layers/test_tls_fuzz.py @@ -1,7 +1,7 @@ from hypothesis import given, example from hypothesis.strategies import binary, integers -from mitmproxy.net.tls import ClientHello +from mitmproxy.tls import ClientHello from mitmproxy.proxy.layers.tls import parse_client_hello client_hello_with_extensions = bytes.fromhex( @@ -16,8 +16,8 @@ @given(i=integers(0, len(client_hello_with_extensions)), data=binary()) -@example(i=183, data=b'\x00\x00\x00\x00\x00\x00\x00\x00\x00') -def test_fuzz_h2_request_chunks(i, data): +@example(i=183, data=b"\x00\x00\x00\x00\x00\x00\x00\x00\x00") +def test_fuzz_parse_client_hello(i, data): try: ch = parse_client_hello(client_hello_with_extensions[:i] + data) except ValueError: diff --git a/test/mitmproxy/proxy/layers/test_websocket.py b/test/mitmproxy/proxy/layers/test_websocket.py index 8b51517ec7..a1d96133f4 100644 --- a/test/mitmproxy/proxy/layers/test_websocket.py +++ b/test/mitmproxy/proxy/layers/test_websocket.py @@ -27,7 +27,7 @@ def __eq__(self, other): other[1] &= 0b0111_1111 # remove mask bit assert other[1] < 126 # (we don't support extended payload length here) mask = other[2:6] - payload = bytes([x ^ mask[i % 4] for i, x in enumerate(other[6:])]) + payload = bytes(x ^ mask[i % 4] for i, x in enumerate(other[6:])) return self.unmasked == other[:2] + payload @@ -41,7 +41,7 @@ def masked_bytes(unmasked: bytes) -> bytes: assert header[1] < 126 # assert that this is neither masked nor extended payload header[1] |= 0b1000_0000 mask = secrets.token_bytes(4) - masked = bytes([x ^ mask[i % 4] for i, x in enumerate(unmasked[2:])]) + masked = bytes(x ^ mask[i % 4] for i, x in enumerate(unmasked[2:])) return bytes(header + mask + masked) @@ -57,48 +57,59 @@ def test_upgrade(tctx): tctx.server.state = ConnectionState.OPEN flow = Placeholder(HTTPFlow) assert ( - Playbook(http.HttpLayer(tctx, HTTPMode.transparent)) - >> DataReceived(tctx.client, - b"GET / HTTP/1.1\r\n" - b"Connection: upgrade\r\n" - b"Upgrade: websocket\r\n" - b"Sec-WebSocket-Version: 13\r\n" - b"\r\n") - << http.HttpRequestHeadersHook(flow) - >> reply() - << http.HttpRequestHook(flow) - >> reply() - << SendData(tctx.server, b"GET / HTTP/1.1\r\n" - b"Connection: upgrade\r\n" - b"Upgrade: websocket\r\n" - b"Sec-WebSocket-Version: 13\r\n" - b"\r\n") - >> DataReceived(tctx.server, b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n" - b"\r\n") - << http.HttpResponseHeadersHook(flow) - >> reply() - << http.HttpResponseHook(flow) - >> reply() - << SendData(tctx.client, b"HTTP/1.1 101 Switching Protocols\r\n" - b"Upgrade: websocket\r\n" - b"Connection: Upgrade\r\n" - b"\r\n") - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.client, masked_bytes(b"\x81\x0bhello world")) - << websocket.WebsocketMessageHook(flow) - >> reply() - << SendData(tctx.server, masked(b"\x81\x0bhello world")) - >> DataReceived(tctx.server, b"\x82\nhello back") - << websocket.WebsocketMessageHook(flow) - >> reply() - << SendData(tctx.client, b"\x82\nhello back") - >> DataReceived(tctx.client, masked_bytes(b"\x81\x0bhello again")) - << websocket.WebsocketMessageHook(flow) - >> reply() - << SendData(tctx.server, masked(b"\x81\x0bhello again")) + Playbook(http.HttpLayer(tctx, HTTPMode.transparent)) + >> DataReceived( + tctx.client, + b"GET / HTTP/1.1\r\n" + b"Connection: upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n", + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << SendData( + tctx.server, + b"GET / HTTP/1.1\r\n" + b"Connection: upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n", + ) + >> DataReceived( + tctx.server, + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"\r\n", + ) + << http.HttpResponseHeadersHook(flow) + >> reply() + << http.HttpResponseHook(flow) + >> reply() + << SendData( + tctx.client, + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"\r\n", + ) + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.client, masked_bytes(b"\x81\x0bhello world")) + << websocket.WebsocketMessageHook(flow) + >> reply() + << SendData(tctx.server, masked(b"\x81\x0bhello world")) + >> DataReceived(tctx.server, b"\x82\nhello back") + << websocket.WebsocketMessageHook(flow) + >> reply() + << SendData(tctx.client, b"\x82\nhello back") + >> DataReceived(tctx.client, masked_bytes(b"\x81\x0bhello again")) + << websocket.WebsocketMessageHook(flow) + >> reply() + << SendData(tctx.server, masked(b"\x81\x0bhello again")) ) assert len(flow().websocket.messages) == 3 assert flow().websocket.messages[0].content == b"hello world" @@ -107,25 +118,96 @@ def test_upgrade(tctx): assert flow().websocket.messages[1].content == b"hello back" assert flow().websocket.messages[1].from_client is False assert flow().websocket.messages[1].type == Opcode.BINARY + assert flow().live + + +def test_upgrade_streamed(tctx): + """If the HTTP response is streamed, we may get early data from the client.""" + tctx.server.address = ("example.com", 80) + tctx.server.state = ConnectionState.OPEN + flow = Placeholder(HTTPFlow) + + def enable_streaming(flow: HTTPFlow): + flow.response.stream = True + + assert ( + Playbook(http.HttpLayer(tctx, HTTPMode.transparent)) + >> DataReceived( + tctx.client, + b"GET / HTTP/1.1\r\n" + b"Connection: upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n", + ) + << http.HttpRequestHeadersHook(flow) + >> reply() + << http.HttpRequestHook(flow) + >> reply() + << SendData( + tctx.server, + b"GET / HTTP/1.1\r\n" + b"Connection: upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Sec-WebSocket-Version: 13\r\n" + b"\r\n", + ) + >> DataReceived( + tctx.server, + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"\r\n", + ) + << http.HttpResponseHeadersHook(flow) + >> reply(side_effect=enable_streaming) + << SendData( + tctx.client, + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: Upgrade\r\n" + b"\r\n", + ) + << http.HttpResponseHook(flow) + >> DataReceived(tctx.client, masked_bytes(b"\x81\x0bhello world")) # early !! + >> reply(to=-2) + << websocket.WebsocketStartHook(flow) + >> reply() + << websocket.WebsocketMessageHook(flow) + >> reply() + << SendData(tctx.server, masked(b"\x81\x0bhello world")) + >> DataReceived(tctx.server, b"\x82\nhello back") + << websocket.WebsocketMessageHook(flow) + >> reply() + << SendData(tctx.client, b"\x82\nhello back") + >> DataReceived(tctx.client, masked_bytes(b"\x81\x0bhello again")) + << websocket.WebsocketMessageHook(flow) + >> reply() + << SendData(tctx.server, masked(b"\x81\x0bhello again")) + ) @pytest.fixture() def ws_testdata(tctx): tctx.server.address = ("example.com", 80) tctx.server.state = ConnectionState.OPEN - flow = HTTPFlow( - tctx.client, - tctx.server + flow = HTTPFlow(tctx.client, tctx.server) + flow.request = Request.make( + "GET", + "http://example.com/", + headers={ + "Connection": "upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + }, + ) + flow.response = Response.make( + 101, + headers={ + "Connection": "upgrade", + "Upgrade": "websocket", + }, ) - flow.request = Request.make("GET", "http://example.com/", headers={ - "Connection": "upgrade", - "Upgrade": "websocket", - "Sec-WebSocket-Version": "13", - }) - flow.response = Response.make(101, headers={ - "Connection": "upgrade", - "Upgrade": "websocket", - }) flow.websocket = WebSocketData() return tctx, Playbook(websocket.WebsocketLayer(tctx, flow)), flow @@ -133,66 +215,62 @@ def ws_testdata(tctx): def test_modify_message(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.server, b"\x81\x03foo") - << websocket.WebsocketMessageHook(flow) + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.server, b"\x81\x03foo") + << websocket.WebsocketMessageHook(flow) ) - flow.websocket.messages[-1].content = flow.websocket.messages[-1].content.replace(b"foo", b"foobar") - assert ( - playbook - >> reply() - << SendData(tctx.client, b"\x81\x06foobar") + flow.websocket.messages[-1].content = flow.websocket.messages[-1].content.replace( + b"foo", b"foobar" ) + playbook >> reply() + playbook << SendData(tctx.client, b"\x81\x06foobar") + assert playbook def test_empty_message(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.server, b"\x81\x00") - << websocket.WebsocketMessageHook(flow) + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.server, b"\x81\x00") + << websocket.WebsocketMessageHook(flow) ) assert flow.websocket.messages[-1].content == b"" - assert ( - playbook - >> reply() - << SendData(tctx.client, b"\x81\x00") - ) + playbook >> reply() + playbook << SendData(tctx.client, b"\x81\x00") + assert playbook def test_drop_message(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.server, b"\x81\x03foo") - << websocket.WebsocketMessageHook(flow) + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.server, b"\x81\x03foo") + << websocket.WebsocketMessageHook(flow) ) flow.websocket.messages[-1].drop() - assert ( - playbook - >> reply() - << None - ) + playbook >> reply() + playbook << None + assert playbook def test_fragmented(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.server, b"\x01\x03foo") - >> DataReceived(tctx.server, b"\x80\x03bar") - << websocket.WebsocketMessageHook(flow) - >> reply() - << SendData(tctx.client, b"\x01\x03foo") - << SendData(tctx.client, b"\x80\x03bar") + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.server, b"\x01\x03foo") + >> DataReceived(tctx.server, b"\x80\x03bar") + << websocket.WebsocketMessageHook(flow) + >> reply() + << SendData(tctx.client, b"\x01\x03foo") + << SendData(tctx.client, b"\x80\x03bar") ) assert flow.websocket.messages[-1].content == b"foobar" @@ -200,19 +278,19 @@ def test_fragmented(ws_testdata): def test_unfragmented(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.server, b"\x81\x06foo") + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.server, b"\x81\x06foo") ) # This already triggers wsproto to emit a wsproto.events.Message, see # https://github.com/mitmproxy/mitmproxy/issues/4701 - assert( - playbook - >> DataReceived(tctx.server, b"bar") - << websocket.WebsocketMessageHook(flow) - >> reply() - << SendData(tctx.client, b"\x81\x06foobar") + assert ( + playbook + >> DataReceived(tctx.server, b"bar") + << websocket.WebsocketMessageHook(flow) + >> reply() + << SendData(tctx.client, b"\x81\x06foobar") ) assert flow.websocket.messages[-1].content == b"foobar" @@ -220,34 +298,39 @@ def test_unfragmented(ws_testdata): def test_protocol_error(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.server, b"\x01\x03foo") - >> DataReceived(tctx.server, b"\x02\x03bar") - << SendData(tctx.server, masked(b"\x88/\x03\xeaexpected CONTINUATION, got ")) - << CloseConnection(tctx.server) - << SendData(tctx.client, b"\x88/\x03\xeaexpected CONTINUATION, got ") - << CloseConnection(tctx.client) - << websocket.WebsocketEndHook(flow) - >> reply() - + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.server, b"\x01\x03foo") + >> DataReceived(tctx.server, b"\x02\x03bar") + << SendData( + tctx.server, + masked(b"\x88/\x03\xeaexpected CONTINUATION, got "), + ) + << CloseConnection(tctx.server) + << SendData( + tctx.client, b"\x88/\x03\xeaexpected CONTINUATION, got " + ) + << CloseConnection(tctx.client) + << websocket.WebsocketEndHook(flow) + >> reply() ) assert not flow.websocket.messages + assert not flow.live def test_ping(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.client, masked_bytes(b"\x89\x11ping-with-payload")) - << Log("Received WebSocket ping from client (payload: b'ping-with-payload')") - << SendData(tctx.server, masked(b"\x89\x11ping-with-payload")) - >> DataReceived(tctx.server, b"\x8a\x11pong-with-payload") - << Log("Received WebSocket pong from server (payload: b'pong-with-payload')") - << SendData(tctx.client, b"\x8a\x11pong-with-payload") + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.client, masked_bytes(b"\x89\x11ping-with-payload")) + << Log("Received WebSocket ping from client (payload: b'ping-with-payload')") + << SendData(tctx.server, masked(b"\x89\x11ping-with-payload")) + >> DataReceived(tctx.server, b"\x8a\x11pong-with-payload") + << Log("Received WebSocket pong from server (payload: b'pong-with-payload')") + << SendData(tctx.client, b"\x8a\x11pong-with-payload") ) assert not flow.websocket.messages @@ -257,73 +340,80 @@ def test_close_normal(ws_testdata): masked_close = Placeholder(bytes) close = Placeholder(bytes) assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.client, masked_bytes(b"\x88\x00")) - << SendData(tctx.server, masked_close) - << CloseConnection(tctx.server) - << SendData(tctx.client, close) - << CloseConnection(tctx.client) - << websocket.WebsocketEndHook(flow) - >> reply() + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.client, masked_bytes(b"\x88\x00")) + << SendData(tctx.server, masked_close) + << CloseConnection(tctx.server) + << SendData(tctx.client, close) + << CloseConnection(tctx.client) + << websocket.WebsocketEndHook(flow) + >> reply() ) # wsproto currently handles this inconsistently, see # https://github.com/python-hyper/wsproto/pull/153/files - assert masked_close() == masked(b"\x88\x02\x03\xe8") or masked_close() == masked(b"\x88\x00") + assert masked_close() == masked(b"\x88\x02\x03\xe8") or masked_close() == masked( + b"\x88\x00" + ) assert close() == b"\x88\x02\x03\xe8" or close() == b"\x88\x00" assert flow.websocket.close_code == 1005 + assert not flow.live def test_close_disconnect(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> ConnectionClosed(tctx.server) - << CloseConnection(tctx.server) - << SendData(tctx.client, b"\x88\x02\x03\xe8") - << CloseConnection(tctx.client) - << websocket.WebsocketEndHook(flow) - >> reply() - >> ConnectionClosed(tctx.client) + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> ConnectionClosed(tctx.server) + << CloseConnection(tctx.server) + << SendData(tctx.client, b"\x88\x02\x03\xe8") + << CloseConnection(tctx.client) + << websocket.WebsocketEndHook(flow) + >> reply() + >> ConnectionClosed(tctx.client) ) # The \x03\xe8 above is code 1000 (normal closure). # But 1006 (ABNORMAL_CLOSURE) is expected, because the connection was already closed. assert flow.websocket.close_code == 1006 + assert not flow.live def test_close_code(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> DataReceived(tctx.server, b"\x88\x02\x0f\xa0") - << SendData(tctx.server, masked(b"\x88\x02\x0f\xa0")) - << CloseConnection(tctx.server) - << SendData(tctx.client, b"\x88\x02\x0f\xa0") - << CloseConnection(tctx.client) - << websocket.WebsocketEndHook(flow) - >> reply() + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> DataReceived(tctx.server, b"\x88\x02\x0f\xa0") + << SendData(tctx.server, masked(b"\x88\x02\x0f\xa0")) + << CloseConnection(tctx.server) + << SendData(tctx.client, b"\x88\x02\x0f\xa0") + << CloseConnection(tctx.client) + << websocket.WebsocketEndHook(flow) + >> reply() ) assert flow.websocket.close_code == 4000 + assert not flow.live def test_deflate(ws_testdata): tctx, playbook, flow = ws_testdata - flow.response.headers["Sec-WebSocket-Extensions"] = "permessage-deflate; server_max_window_bits=10" + flow.response.headers[ + "Sec-WebSocket-Extensions" + ] = "permessage-deflate; server_max_window_bits=10" assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - # https://tools.ietf.org/html/rfc7692#section-7.2.3.1 - >> DataReceived(tctx.server, bytes.fromhex("c1 07 f2 48 cd c9 c9 07 00")) - << websocket.WebsocketMessageHook(flow) - >> reply() - << SendData(tctx.client, bytes.fromhex("c1 07 f2 48 cd c9 c9 07 00")) + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + # https://tools.ietf.org/html/rfc7692#section-7.2.3.1 + >> DataReceived(tctx.server, bytes.fromhex("c1 07 f2 48 cd c9 c9 07 00")) + << websocket.WebsocketMessageHook(flow) + >> reply() + << SendData(tctx.client, bytes.fromhex("c1 07 f2 48 cd c9 c9 07 00")) ) assert flow.websocket.messages[0].content == b"Hello" @@ -332,10 +422,10 @@ def test_unknown_ext(ws_testdata): tctx, playbook, flow = ws_testdata flow.response.headers["Sec-WebSocket-Extensions"] = "funky-bits; param=42" assert ( - playbook - << Log("Ignoring unknown WebSocket extension 'funky-bits'.") - << websocket.WebsocketStartHook(flow) - >> reply() + playbook + << Log("Ignoring unknown WebSocket extension 'funky-bits'.") + << websocket.WebsocketStartHook(flow) + >> reply() ) @@ -370,16 +460,15 @@ def test_rechunk(self): def test_inject_message(ws_testdata): tctx, playbook, flow = ws_testdata assert ( - playbook - << websocket.WebsocketStartHook(flow) - >> reply() - >> WebSocketMessageInjected(flow, WebSocketMessage(Opcode.TEXT, False, b"hello")) - << websocket.WebsocketMessageHook(flow) + playbook + << websocket.WebsocketStartHook(flow) + >> reply() + >> WebSocketMessageInjected( + flow, WebSocketMessage(Opcode.TEXT, False, b"hello") + ) + << websocket.WebsocketMessageHook(flow) ) assert flow.websocket.messages[-1].content == b"hello" assert flow.websocket.messages[-1].from_client is False - assert ( - playbook - >> reply() - << SendData(tctx.client, b"\x81\x05hello") - ) + assert flow.websocket.messages[-1].injected is True + assert playbook >> reply() << SendData(tctx.client, b"\x81\x05hello") diff --git a/test/mitmproxy/proxy/test_commands.py b/test/mitmproxy/proxy/test_commands.py index ead32ca3a8..2ca7c23f0a 100644 --- a/test/mitmproxy/proxy/test_commands.py +++ b/test/mitmproxy/proxy/test_commands.py @@ -13,6 +13,7 @@ def tconn() -> connection.Server: def test_dataclasses(tconn): + assert repr(commands.RequestWakeup(58)) assert repr(commands.SendData(tconn, b"foo")) assert repr(commands.OpenConnection(tconn)) assert repr(commands.CloseConnection(tconn)) diff --git a/test/mitmproxy/proxy/test_context.py b/test/mitmproxy/proxy/test_context.py index 031502b329..62e9c9b7a7 100644 --- a/test/mitmproxy/proxy/test_context.py +++ b/test/mitmproxy/proxy/test_context.py @@ -4,10 +4,7 @@ def test_context(): with taddons.context() as tctx: - c = context.Context( - tflow.tclient_conn(), - tctx.options - ) + c = context.Context(tflow.tclient_conn(), tctx.options) assert repr(c) c.layers.append(1) assert repr(c) diff --git a/test/mitmproxy/proxy/test_events.py b/test/mitmproxy/proxy/test_events.py index df35b216f2..2061f27c8c 100644 --- a/test/mitmproxy/proxy/test_events.py +++ b/test/mitmproxy/proxy/test_events.py @@ -26,6 +26,7 @@ class FooCommand(commands.Command): pass with pytest.warns(RuntimeWarning, match="properly annotated"): + class FooCompleted(events.CommandCompleted): pass @@ -33,5 +34,6 @@ class FooCompleted1(events.CommandCompleted): command: FooCommand with pytest.warns(RuntimeWarning, match="conflicting subclasses"): + class FooCompleted2(events.CommandCompleted): command: FooCommand diff --git a/test/mitmproxy/proxy/test_layer.py b/test/mitmproxy/proxy/test_layer.py index e2a3850f63..bd9ce7de3b 100644 --- a/test/mitmproxy/proxy/test_layer.py +++ b/test/mitmproxy/proxy/test_layer.py @@ -8,7 +8,9 @@ class TestLayer: def test_continue(self, tctx: Context): class TLayer(layer.Layer): - def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + def _handle_event( + self, event: events.Event + ) -> layer.CommandGenerator[None]: yield commands.OpenConnection(self.context.server) yield commands.OpenConnection(self.context.server) @@ -26,7 +28,9 @@ def test_debug_messages(self, tctx: Context): class TLayer(layer.Layer): debug = " " - def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: + def _handle_event( + self, event: events.Event + ) -> layer.CommandGenerator[None]: yield from self.state(event) def state_foo(self, event: events.Event) -> layer.CommandGenerator[None]: @@ -42,21 +46,25 @@ def state_bar(self, event: events.Event) -> layer.CommandGenerator[None]: tlayer = TLayer(tctx) assert ( - tutils.Playbook(tlayer, hooks=True, logs=True) - << commands.Log(" >> Start({})", "debug") - << commands.Log(" << OpenConnection({'connection': Server({'id': '…rverid', 'address': None, " - "'state': })})", - "debug") - << commands.OpenConnection(tctx.server) - >> events.DataReceived(tctx.client, b"foo") - << commands.Log(" >! DataReceived(client, b'foo')", "debug") - >> tutils.reply(None, to=-3) - << commands.Log(" >> Reply(OpenConnection({'connection': Server(" - "{'id': '…rverid', 'address': None, 'state': , " - "'timestamp_start': 1624544785})}), None)", "debug") - << commands.Log(" !> DataReceived(client, b'foo')", "debug") - - << commands.Log("baz", "info") + tutils.Playbook(tlayer, hooks=True, logs=True) + << commands.Log(" >> Start({})", "debug") + << commands.Log( + " << OpenConnection({'connection': Server({'id': '…rverid', 'address': None, " + "'state': , 'transport_protocol': 'tcp'})})", + "debug", + ) + << commands.OpenConnection(tctx.server) + >> events.DataReceived(tctx.client, b"foo") + << commands.Log(" >! DataReceived(client, b'foo')", "debug") + >> tutils.reply(None, to=-3) + << commands.Log( + " >> Reply(OpenConnection({'connection': Server(" + "{'id': '…rverid', 'address': None, 'state': , " + "'transport_protocol': 'tcp', 'timestamp_start': 1624544785})}), None)", + "debug", + ) + << commands.Log(" !> DataReceived(client, b'foo')", "debug") + << commands.Log("baz", "info") ) assert repr(tlayer) == "TLayer(state: bar)" @@ -75,24 +83,24 @@ def test_simple(self, tctx: Context): playbook = tutils.Playbook(nl, hooks=True) assert ( - playbook - << layer.NextLayerHook(nl) - >> tutils.reply() - >> events.DataReceived(tctx.client, b"foo") - << layer.NextLayerHook(nl) - >> tutils.reply() - >> events.DataReceived(tctx.client, b"bar") - << layer.NextLayerHook(nl) + playbook + << layer.NextLayerHook(nl) + >> tutils.reply() + >> events.DataReceived(tctx.client, b"foo") + << layer.NextLayerHook(nl) + >> tutils.reply() + >> events.DataReceived(tctx.client, b"bar") + << layer.NextLayerHook(nl) ) assert nl.data_client() == b"foobar" assert nl.data_server() == b"" nl.layer = tutils.EchoLayer(tctx) assert ( - playbook - >> tutils.reply() - << commands.SendData(tctx.client, b"foo") - << commands.SendData(tctx.client, b"bar") + playbook + >> tutils.reply() + << commands.SendData(tctx.client, b"foo") + << commands.SendData(tctx.client, b"bar") ) def test_late_hook_reply(self, tctx: Context): @@ -104,19 +112,19 @@ def test_late_hook_reply(self, tctx: Context): playbook = tutils.Playbook(nl) assert ( - playbook - >> events.DataReceived(tctx.client, b"foo") - << layer.NextLayerHook(nl) - >> events.DataReceived(tctx.client, b"bar") + playbook + >> events.DataReceived(tctx.client, b"foo") + << layer.NextLayerHook(nl) + >> events.DataReceived(tctx.client, b"bar") ) assert nl.data_client() == b"foo" # "bar" is paused. nl.layer = tutils.EchoLayer(tctx) assert ( - playbook - >> tutils.reply(to=-2) - << commands.SendData(tctx.client, b"foo") - << commands.SendData(tctx.client, b"bar") + playbook + >> tutils.reply(to=-2) + << commands.SendData(tctx.client, b"foo") + << commands.SendData(tctx.client, b"bar") ) @pytest.mark.parametrize("layer_found", [True, False]) @@ -125,23 +133,21 @@ def test_receive_close(self, tctx: Context, layer_found: bool): nl = layer.NextLayer(tctx) playbook = tutils.Playbook(nl) assert ( - playbook - >> events.DataReceived(tctx.client, b"foo") - << layer.NextLayerHook(nl) - >> events.ConnectionClosed(tctx.client) + playbook + >> events.DataReceived(tctx.client, b"foo") + << layer.NextLayerHook(nl) + >> events.ConnectionClosed(tctx.client) ) if layer_found: nl.layer = tutils.RecordLayer(tctx) - assert ( - playbook - >> tutils.reply(to=-2) - ) + assert playbook >> tutils.reply(to=-2) assert isinstance(nl.layer.event_log[-1], events.ConnectionClosed) else: assert ( - playbook - >> tutils.reply(to=-2) - << commands.CloseConnection(tctx.client) + playbook + >> tutils.reply(to=-2) + << commands.CloseConnection(tctx.client) + << None ) def test_func_references(self, tctx: Context): @@ -149,18 +155,17 @@ def test_func_references(self, tctx: Context): playbook = tutils.Playbook(nl) assert ( - playbook - >> events.DataReceived(tctx.client, b"foo") - << layer.NextLayerHook(nl) + playbook + >> events.DataReceived(tctx.client, b"foo") + << layer.NextLayerHook(nl) ) nl.layer = tutils.EchoLayer(tctx) handle = nl.handle_event - assert ( - playbook - >> tutils.reply() - << commands.SendData(tctx.client, b"foo") - ) - sd, = handle(events.DataReceived(tctx.client, b"bar")) + + playbook >> tutils.reply() + playbook << commands.SendData(tctx.client, b"foo") + assert playbook + (sd,) = handle(events.DataReceived(tctx.client, b"bar")) assert isinstance(sd, commands.SendData) def test_repr(self, tctx: Context): diff --git a/test/mitmproxy/proxy/test_tunnel.py b/test/mitmproxy/proxy/test_tunnel.py index b002c073da..24103637c6 100644 --- a/test/mitmproxy/proxy/test_tunnel.py +++ b/test/mitmproxy/proxy/test_tunnel.py @@ -1,4 +1,4 @@ -from typing import Tuple, Optional +from typing import Optional import pytest @@ -36,7 +36,9 @@ class TTunnelLayer(tunnel.TunnelLayer): def start_handshake(self) -> layer.CommandGenerator[None]: yield SendData(self.tunnel_connection, b"handshake-hello") - def receive_handshake_data(self, data: bytes) -> layer.CommandGenerator[Tuple[bool, Optional[str]]]: + def receive_handshake_data( + self, data: bytes + ) -> layer.CommandGenerator[tuple[bool, Optional[str]]]: yield SendData(self.tunnel_connection, data) if data == b"handshake-success": return True, None @@ -63,11 +65,11 @@ def test_tunnel_handshake_start(tctx: Context, success): playbook = Playbook(tl, logs=True) ( - playbook - << SendData(server, b"handshake-hello") - >> DataReceived(tctx.client, b"client-hello") - >> DataReceived(server, b"handshake-" + success.encode()) - << SendData(server, b"handshake-" + success.encode()) + playbook + << SendData(server, b"handshake-hello") + >> DataReceived(tctx.client, b"client-hello") + >> DataReceived(server, b"handshake-" + success.encode()) + << SendData(server, b"handshake-" + success.encode()) ) if success == "success": playbook << Log("Got start. Server state: OPEN") @@ -92,40 +94,40 @@ def test_tunnel_handshake_command(tctx: Context, success): playbook = Playbook(tl, logs=True) ( - playbook - << Log("Got start. Server state: CLOSED") - >> DataReceived(tctx.client, b"client-hello") - << SendData(tctx.client, b"client-hello-reply") - >> DataReceived(tctx.client, b"open") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"handshake-hello") - >> DataReceived(server, b"handshake-" + success.encode()) - << SendData(server, b"handshake-" + success.encode()) + playbook + << Log("Got start. Server state: CLOSED") + >> DataReceived(tctx.client, b"client-hello") + << SendData(tctx.client, b"client-hello-reply") + >> DataReceived(tctx.client, b"open") + << OpenConnection(server) + >> reply(None) + << SendData(server, b"handshake-hello") + >> DataReceived(server, b"handshake-" + success.encode()) + << SendData(server, b"handshake-" + success.encode()) ) if success == "success": assert ( - playbook - << Log(f"Opened: err=None. Server state: OPEN") - >> DataReceived(server, b"tunneled-server-hello") - << SendData(server, b"tunneled-server-hello-reply") - >> ConnectionClosed(tctx.client) - << Log("Got client close.") - << CloseConnection(tctx.client) + playbook + << Log(f"Opened: err=None. Server state: OPEN") + >> DataReceived(server, b"tunneled-server-hello") + << SendData(server, b"tunneled-server-hello-reply") + >> ConnectionClosed(tctx.client) + << Log("Got client close.") + << CloseConnection(tctx.client) ) assert tl.tunnel_state is tunnel.TunnelState.OPEN assert ( - playbook - >> ConnectionClosed(server) - << Log("Got server close.") - << CloseConnection(server) + playbook + >> ConnectionClosed(server) + << Log("Got server close.") + << CloseConnection(server) ) assert tl.tunnel_state is tunnel.TunnelState.CLOSED else: assert ( - playbook - << CloseConnection(server) - << Log("Opened: err='handshake error'. Server state: CLOSED") + playbook + << CloseConnection(server) + << Log("Opened: err='handshake error'. Server state: CLOSED") ) assert tl.tunnel_state is tunnel.TunnelState.CLOSED @@ -141,28 +143,28 @@ def test_tunnel_default_impls(tctx: Context): tl.child_layer = TChildLayer(tctx) playbook = Playbook(tl, logs=True) assert ( - playbook - << Log("Got start. Server state: OPEN") - >> DataReceived(server, b"server-hello") - << SendData(server, b"server-hello-reply") + playbook + << Log("Got start. Server state: OPEN") + >> DataReceived(server, b"server-hello") + << SendData(server, b"server-hello-reply") ) assert tl.tunnel_state is tunnel.TunnelState.OPEN assert ( - playbook - >> ConnectionClosed(server) - << Log("Got server close.") - << CloseConnection(server) + playbook + >> ConnectionClosed(server) + << Log("Got server close.") + << CloseConnection(server) ) assert tl.tunnel_state is tunnel.TunnelState.CLOSED assert ( - playbook - >> DataReceived(tctx.client, b"open") - << OpenConnection(server) - >> reply(None) - << Log("Opened: err=None. Server state: OPEN") - >> DataReceived(server, b"half-close") - << CloseConnection(server, half_close=True) + playbook + >> DataReceived(tctx.client, b"open") + << OpenConnection(server) + >> reply(None) + << Log("Opened: err=None. Server state: OPEN") + >> DataReceived(server, b"half-close") + << CloseConnection(server, half_close=True) ) @@ -174,16 +176,16 @@ def test_tunnel_openconnection_error(tctx: Context): playbook = Playbook(tl, logs=True) assert ( - playbook - << Log("Got start. Server state: CLOSED") - >> DataReceived(tctx.client, b"open") - << OpenConnection(server) + playbook + << Log("Got start. Server state: CLOSED") + >> DataReceived(tctx.client, b"open") + << OpenConnection(server) ) assert tl.tunnel_state is tunnel.TunnelState.ESTABLISHING assert ( - playbook - >> reply("IPoAC packet dropped.") - << Log("Opened: err='IPoAC packet dropped.'. Server state: CLOSED") + playbook + >> reply("IPoAC packet dropped.") + << Log("Opened: err='IPoAC packet dropped.'. Server state: CLOSED") ) assert tl.tunnel_state is tunnel.TunnelState.CLOSED @@ -198,26 +200,25 @@ def test_disconnect_during_handshake_start(tctx: Context, disconnect): playbook = Playbook(tl, logs=True) - assert ( - playbook - << SendData(server, b"handshake-hello") - ) + assert playbook << SendData(server, b"handshake-hello") if disconnect == "client": assert ( - playbook - >> ConnectionClosed(tctx.client) - >> ConnectionClosed(server) # proxyserver will cancel all other connections as well. - << CloseConnection(server) - << Log("Got start. Server state: CLOSED") - << Log("Got client close.") - << CloseConnection(tctx.client) + playbook + >> ConnectionClosed(tctx.client) + >> ConnectionClosed( + server + ) # proxyserver will cancel all other connections as well. + << CloseConnection(server) + << Log("Got start. Server state: CLOSED") + << Log("Got client close.") + << CloseConnection(tctx.client) ) else: assert ( - playbook - >> ConnectionClosed(server) - << CloseConnection(server) - << Log("Got start. Server state: CLOSED") + playbook + >> ConnectionClosed(server) + << CloseConnection(server) + << Log("Got start. Server state: CLOSED") ) @@ -230,31 +231,33 @@ def test_disconnect_during_handshake_command(tctx: Context, disconnect): playbook = Playbook(tl, logs=True) assert ( - playbook - << Log("Got start. Server state: CLOSED") - >> DataReceived(tctx.client, b"client-hello") - << SendData(tctx.client, b"client-hello-reply") - >> DataReceived(tctx.client, b"open") - << OpenConnection(server) - >> reply(None) - << SendData(server, b"handshake-hello") + playbook + << Log("Got start. Server state: CLOSED") + >> DataReceived(tctx.client, b"client-hello") + << SendData(tctx.client, b"client-hello-reply") + >> DataReceived(tctx.client, b"open") + << OpenConnection(server) + >> reply(None) + << SendData(server, b"handshake-hello") ) if disconnect == "client": assert ( - playbook - >> ConnectionClosed(tctx.client) - >> ConnectionClosed(server) # proxyserver will cancel all other connections as well. - << CloseConnection(server) - << Log("Opened: err='connection closed'. Server state: CLOSED") - << Log("Got client close.") - << CloseConnection(tctx.client) + playbook + >> ConnectionClosed(tctx.client) + >> ConnectionClosed( + server + ) # proxyserver will cancel all other connections as well. + << CloseConnection(server) + << Log("Opened: err='connection closed'. Server state: CLOSED") + << Log("Got client close.") + << CloseConnection(tctx.client) ) else: assert ( - playbook - >> ConnectionClosed(server) - << CloseConnection(server) - << Log("Opened: err='connection closed'. Server state: CLOSED") + playbook + >> ConnectionClosed(server) + << CloseConnection(server) + << Log("Opened: err='connection closed'. Server state: CLOSED") ) diff --git a/test/mitmproxy/proxy/test_tutils.py b/test/mitmproxy/proxy/test_tutils.py index cb40df344a..04880990e4 100644 --- a/test/mitmproxy/proxy/test_tutils.py +++ b/test/mitmproxy/proxy/test_tutils.py @@ -1,5 +1,6 @@ -import typing +from collections.abc import Iterable from dataclasses import dataclass +from typing import Any import pytest @@ -8,14 +9,14 @@ class TEvent(events.Event): - commands: typing.Iterable[typing.Any] + commands: Iterable[Any] def __init__(self, cmds=(None,)): self.commands = cmds class TCommand(commands.Command): - x: typing.Any + x: Any def __init__(self, x=None): self.x = x @@ -43,36 +44,30 @@ def tplaybook(tctx): def test_simple(tplaybook): - assert ( - tplaybook - >> TEvent() - << TCommand() - >> TEvent([]) - << None - ) + tplaybook >> TEvent() + tplaybook << TCommand() + tplaybook >> TEvent([]) + tplaybook << None + assert tplaybook def test_mismatch(tplaybook): with pytest.raises(AssertionError, match="Playbook mismatch"): - assert ( - tplaybook - >> TEvent([]) - << TCommand() - ) + tplaybook >> TEvent([]) + tplaybook << TCommand() + assert tplaybook def test_partial_assert(tplaybook): """Developers can assert parts of a playbook and the continue later on.""" - assert ( - tplaybook - >> TEvent() - << TCommand() - ) - assert ( - tplaybook - >> TEvent() - << TCommand() - ) + tplaybook >> TEvent() + tplaybook << TCommand() + assert tplaybook + + tplaybook >> TEvent() + tplaybook << TCommand() + assert tplaybook + assert len(tplaybook.actual) == len(tplaybook.expected) == 4 @@ -83,23 +78,21 @@ def test_placeholder(tplaybook, typed): f = tutils.Placeholder(int) else: f = tutils.Placeholder() - assert ( - tplaybook - >> TEvent([42]) - << TCommand(f) - ) + tplaybook >> TEvent([42]) + tplaybook << TCommand(f) + assert tplaybook assert f() == 42 def test_placeholder_type_mismatch(tplaybook): """Developers can specify placeholders for yet unknown attributes.""" f = tutils.Placeholder(str) - with pytest.raises(TypeError, match="Placeholder type error for TCommand.x: expected str, got int."): - assert ( - tplaybook - >> TEvent([42]) - << TCommand(f) - ) + with pytest.raises( + TypeError, match="Placeholder type error for TCommand.x: expected str, got int." + ): + tplaybook >> TEvent([42]) + tplaybook << TCommand(f) + assert tplaybook def test_unfinished(tplaybook): @@ -113,12 +106,10 @@ def test_unfinished(tplaybook): def test_command_reply(tplaybook): """CommandReplies can use relative offsets to point to the matching command.""" - assert ( - tplaybook - >> TEvent() - << TCommand() - >> tutils.reply() - ) + tplaybook >> TEvent() + tplaybook << TCommand() + tplaybook >> tutils.reply() + assert tplaybook assert tplaybook.actual[1] == tplaybook.actual[2].command @@ -162,12 +153,11 @@ def test_command_multiple_replies(tplaybook, swap): command1 = TCommand(a) command2 = TCommand(b) - (tplaybook - >> TEvent([1]) - << command1 - >> TEvent([2]) - << command2 - ) + tplaybook >> TEvent([1]) + tplaybook << command1 + tplaybook >> TEvent([2]) + tplaybook << command2 + if swap: tplaybook >> tutils.reply(to=command1) tplaybook >> tutils.reply(to=command2) diff --git a/test/mitmproxy/proxy/tutils.py b/test/mitmproxy/proxy/tutils.py index 75172a14b0..0f78aabf7a 100644 --- a/test/mitmproxy/proxy/tutils.py +++ b/test/mitmproxy/proxy/tutils.py @@ -4,7 +4,8 @@ import re import textwrap import traceback -import typing +from collections.abc import Callable, Iterable +from typing import Any, AnyStr, Generic, Optional, TypeVar, Union from mitmproxy.proxy import commands, context, layer from mitmproxy.proxy import events @@ -12,14 +13,11 @@ from mitmproxy.proxy.events import command_reply_subclasses from mitmproxy.proxy.layer import Layer -PlaybookEntry = typing.Union[commands.Command, events.Event] -PlaybookEntryList = typing.List[PlaybookEntry] +PlaybookEntry = Union[commands.Command, events.Event] +PlaybookEntryList = list[PlaybookEntry] -def _eq( - a: PlaybookEntry, - b: PlaybookEntry -) -> bool: +def _eq(a: PlaybookEntry, b: PlaybookEntry) -> bool: """Compare two commands/events, and possibly update placeholders.""" if type(a) != type(b): return False @@ -40,7 +38,9 @@ def _eq( try: x = x.setdefault(y) except TypeError as e: - raise TypeError(f"Placeholder type error for {type(a).__name__}.{k}: {e}") + raise TypeError( + f"Placeholder type error for {type(a).__name__}.{k}: {e}" + ) if x != y: return False @@ -48,29 +48,31 @@ def _eq( def eq( - a: typing.Union[PlaybookEntry, typing.Iterable[PlaybookEntry]], - b: typing.Union[PlaybookEntry, typing.Iterable[PlaybookEntry]] + a: Union[PlaybookEntry, Iterable[PlaybookEntry]], + b: Union[PlaybookEntry, Iterable[PlaybookEntry]], ): """ Compare an indiviual event/command or a list of events/commands. """ - if isinstance(a, collections.abc.Iterable) and isinstance(b, collections.abc.Iterable): - return all( - _eq(x, y) for x, y in itertools.zip_longest(a, b) - ) + if isinstance(a, collections.abc.Iterable) and isinstance( + b, collections.abc.Iterable + ): + return all(_eq(x, y) for x, y in itertools.zip_longest(a, b)) return _eq(a, b) def _fmt_entry(x: PlaybookEntry): arrow = ">>" if isinstance(x, events.Event) else "<<" x = str(x) - x = re.sub('Placeholder:None', '', x, flags=re.IGNORECASE) - x = re.sub('Placeholder:', '', x, flags=re.IGNORECASE) + x = re.sub("Placeholder:None", "", x, flags=re.IGNORECASE) + x = re.sub("Placeholder:", "", x, flags=re.IGNORECASE) x = textwrap.indent(x, " ")[5:] return f"{arrow} {x}" -def _merge_sends(lst: typing.List[commands.Command], ignore_hooks: bool, ignore_logs: bool) -> PlaybookEntryList: +def _merge_sends( + lst: list[commands.Command], ignore_hooks: bool, ignore_logs: bool +) -> PlaybookEntryList: current_send = None for x in lst: if isinstance(x, commands.SendData): @@ -80,10 +82,8 @@ def _merge_sends(lst: typing.List[commands.Command], ignore_hooks: bool, ignore_ else: current_send.data += x.data else: - ignore = ( - (ignore_hooks and isinstance(x, commands.StartHook)) - or - (ignore_logs and isinstance(x, commands.Log)) + ignore = (ignore_hooks and isinstance(x, commands.StartHook)) or ( + ignore_logs and isinstance(x, commands.Log) ) if not ignore: current_send = None @@ -118,6 +118,7 @@ class Playbook: x2 = list(t.handle_event(events.OpenConnectionReply(x1[-1]))) assert x2 == [] """ + layer: Layer """The base layer""" expected: PlaybookEntryList @@ -132,16 +133,14 @@ class Playbook: """If False, the playbook specification doesn't include hooks or hook replies. They are automatically replied to.""" def __init__( - self, - layer: Layer, - hooks: bool = True, - logs: bool = False, - expected: typing.Optional[PlaybookEntryList] = None, + self, + layer: Layer, + hooks: bool = True, + logs: bool = False, + expected: Optional[PlaybookEntryList] = None, ): if expected is None: - expected = [ - events.Start() - ] + expected = [events.Start()] self.layer = layer self.expected = expected @@ -164,9 +163,9 @@ def __lshift__(self, c): prev = self.expected[-1] two_subsequent_sends_to_the_same_remote = ( - isinstance(c, commands.SendData) - and isinstance(prev, commands.SendData) - and prev.connection is c.connection + isinstance(c, commands.SendData) + and isinstance(prev, commands.SendData) + and prev.connection is c.connection ) if two_subsequent_sends_to_the_same_remote: prev.data += c.data @@ -201,13 +200,21 @@ def __bool__(self): x.connection.timestamp_end = 1624544787 self.actual.append(x) + cmds: list[commands.Command] = [] try: - cmds: typing.List[commands.Command] = list(self.layer.handle_event(x)) + # consume them one by one so that we can extend the log with all commands until traceback. + for cmd in self.layer.handle_event(x): + cmds.append(cmd) except Exception: + self.actual.extend(cmds) self.actual.append(_TracebackInPlaybook(traceback.format_exc())) break - cmds = list(_merge_sends(cmds, ignore_hooks=not self.hooks, ignore_logs=not self.logs)) + cmds = list( + _merge_sends( + cmds, ignore_hooks=not self.hooks, ignore_logs=not self.logs + ) + ) self.actual.extend(cmds) pos = len(self.actual) - len(cmds) - 1 @@ -222,25 +229,24 @@ def __bool__(self): cmd.connection.state = ConnectionState.CLOSED elif isinstance(cmd, commands.Log): need_to_emulate_log = ( - not self.logs and - cmd.level in ("debug", "info") and - ( - pos >= len(self.expected) - or not isinstance(self.expected[pos], commands.Log) - ) + not self.logs + and cmd.level in ("debug", "info") + and ( + pos >= len(self.expected) + or not isinstance(self.expected[pos], commands.Log) + ) ) if need_to_emulate_log: self.expected.insert(pos, cmd) elif isinstance(cmd, commands.StartHook) and not self.hooks: - need_to_emulate_hook = ( - not self.hooks - and ( - pos >= len(self.expected) or - (not ( - isinstance(self.expected[pos], commands.StartHook) - and self.expected[pos].name == cmd.name - )) + need_to_emulate_hook = not self.hooks and ( + pos >= len(self.expected) + or ( + not ( + isinstance(self.expected[pos], commands.StartHook) + and self.expected[pos].name == cmd.name ) + ) ) if need_to_emulate_hook: self.expected.insert(pos, cmd) @@ -248,17 +254,23 @@ def __bool__(self): # the current event may still have yielded more events, so we need to insert # the reply *after* those additional events. hook_replies.append(events.HookCompleted(cmd)) - self.expected = self.expected[:pos + 1] + hook_replies + self.expected[pos + 1:] + self.expected = ( + self.expected[: pos + 1] + hook_replies + self.expected[pos + 1 :] + ) - eq(self.expected[i:], self.actual[i:]) # compare now already to set placeholders + eq( + self.expected[i:], self.actual[i:] + ) # compare now already to set placeholders i += 1 if not eq(self.expected, self.actual): self._errored = True - diffs = list(difflib.ndiff( - [_fmt_entry(x) for x in self.expected], - [_fmt_entry(x) for x in self.actual] - )) + diffs = list( + difflib.ndiff( + [_fmt_entry(x) for x in self.expected], + [_fmt_entry(x) for x in self.actual], + ) + ) if already_asserted: diffs.insert(already_asserted, "==== asserted until here ====") diff = "\n".join(diffs) @@ -271,20 +283,22 @@ def __del__(self): # complete), so we need to signal if someone forgets to assert and playbooks aren't # evaluated. is_final_destruct = not hasattr(self, "_errored") - if is_final_destruct or (not self._errored and len(self.actual) < len(self.expected)): + if is_final_destruct or ( + not self._errored and len(self.actual) < len(self.expected) + ): raise RuntimeError("Unfinished playbook!") class reply(events.Event): - args: typing.Tuple[typing.Any, ...] - to: typing.Union[commands.Command, int] - side_effect: typing.Callable[[typing.Any], typing.Any] + args: tuple[Any, ...] + to: Union[commands.Command, int] + side_effect: Callable[[Any], Any] def __init__( - self, - *args, - to: typing.Union[commands.Command, int] = -1, - side_effect: typing.Callable[[typing.Any], None] = lambda x: None + self, + *args, + to: Union[commands.Command, int] = -1, + side_effect: Callable[[Any], None] = lambda x: None, ): """Utility method to reply to the latest hook in playbooks.""" assert not args or not isinstance(args[0], commands.Command) @@ -294,7 +308,7 @@ def __init__( def playbook_eval(self, playbook: Playbook) -> events.CommandCompleted: if isinstance(self.to, int): - expected = playbook.expected[:playbook.expected.index(self)] + expected = playbook.expected[: playbook.expected.index(self)] assert abs(self.to) < len(expected) to = expected[self.to] if not isinstance(to, commands.Command): @@ -322,10 +336,10 @@ def playbook_eval(self, playbook: Playbook) -> events.CommandCompleted: return inst -T = typing.TypeVar("T") +T = TypeVar("T") -class _Placeholder(typing.Generic[T]): +class _Placeholder(Generic[T]): """ Placeholder value in playbooks, so that objects (flows in particular) can be referenced before they are known. Example: @@ -340,7 +354,7 @@ class _Placeholder(typing.Generic[T]): assert f().messages == 0 """ - def __init__(self, cls: typing.Type[T]): + def __init__(self, cls: type[T]): self._obj = None self._cls = cls @@ -350,8 +364,10 @@ def __call__(self) -> T: def setdefault(self, value: T) -> T: if self._obj is None: - if self._cls is not typing.Any and not isinstance(value, self._cls): - raise TypeError(f"expected {self._cls.__name__}, got {type(value).__name__}.") + if self._cls is not Any and not isinstance(value, self._cls): + raise TypeError( + f"expected {self._cls.__name__}, got {type(value).__name__}." + ) self._obj = value return self._obj @@ -363,10 +379,33 @@ def __str__(self): # noinspection PyPep8Naming -def Placeholder(cls: typing.Type[T] = typing.Any) -> typing.Union[T, _Placeholder[T]]: +def Placeholder(cls: type[T] = Any) -> Union[T, _Placeholder[T]]: return _Placeholder(cls) +class _AnyStrPlaceholder(_Placeholder[AnyStr]): + def __init__(self, match: AnyStr): + super().__init__(type(match)) + self._match = match + + def setdefault(self, value: AnyStr) -> AnyStr: + if self._obj is None: + super().setdefault(value) + if not re.search(self._match, self._obj, re.DOTALL): # type: ignore + raise ValueError(f"{self._obj!r} does not match {self._match!r}.") + return self._obj + + +# noinspection PyPep8Naming +def BytesMatching(match: bytes) -> Union[bytes, _AnyStrPlaceholder[bytes]]: + return _AnyStrPlaceholder(match) + + +# noinspection PyPep8Naming +def StrMatching(match: str) -> Union[str, _AnyStrPlaceholder[str]]: + return _AnyStrPlaceholder(match) + + class EchoLayer(Layer): """Echo layer that sends all data back to the client in lowercase.""" @@ -379,7 +418,8 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: class RecordLayer(Layer): """Layer that records all events but does nothing.""" - event_log: typing.List[events.Event] + + event_log: list[events.Event] def __init__(self, context: context.Context) -> None: super().__init__(context) @@ -391,13 +431,11 @@ def _handle_event(self, event: events.Event) -> layer.CommandGenerator[None]: def reply_next_layer( - child_layer: typing.Union[typing.Type[Layer], typing.Callable[[context.Context], Layer]], - *args, - **kwargs + child_layer: Union[type[Layer], Callable[[context.Context], Layer]], *args, **kwargs ) -> reply: """Helper function to simplify the syntax for next_layer events to this: - << NextLayerHook(nl) - >> reply_next_layer(tutils.EchoLayer) + << NextLayerHook(nl) + >> reply_next_layer(tutils.EchoLayer) """ def set_layer(next_layer: layer.NextLayer) -> None: diff --git a/test/mitmproxy/script/test_concurrent.py b/test/mitmproxy/script/test_concurrent.py index 9743b7344a..eb23f6ebc8 100644 --- a/test/mitmproxy/script/test_concurrent.py +++ b/test/mitmproxy/script/test_concurrent.py @@ -1,58 +1,36 @@ +import asyncio +import os import time import pytest -from mitmproxy import controller - from mitmproxy.test import tflow from mitmproxy.test import taddons -class Thing: - def __init__(self): - self.reply = controller.DummyReply() - self.live = True - - class TestConcurrent: - def test_concurrent(self, tdata): + @pytest.mark.parametrize( + "addon", ["concurrent_decorator.py", "concurrent_decorator_class.py"] + ) + async def test_concurrent(self, addon, tdata): with taddons.context() as tctx: - sc = tctx.script( - tdata.path( - "mitmproxy/data/addonscripts/concurrent_decorator.py" - ) - ) + sc = tctx.script(tdata.path(f"mitmproxy/data/addonscripts/{addon}")) f1, f2 = tflow.tflow(), tflow.tflow() - tctx.cycle(sc, f1) - tctx.cycle(sc, f2) start = time.time() - while time.time() - start < 5: - if f1.reply.state == f2.reply.state == "committed": - return - raise ValueError("Script never acked") + await asyncio.gather( + tctx.cycle(sc, f1), + tctx.cycle(sc, f2), + ) + end = time.time() + # This test may fail on overloaded CI systems, increase upper bound if necessary. + if os.environ.get("CI"): + assert 0.5 <= end - start + else: + assert 0.5 <= end - start < 1 - @pytest.mark.asyncio async def test_concurrent_err(self, tdata): with taddons.context() as tctx: tctx.script( - tdata.path( - "mitmproxy/data/addonscripts/concurrent_decorator_err.py" - ) + tdata.path("mitmproxy/data/addonscripts/concurrent_decorator_err.py") ) await tctx.master.await_log("decorator not supported") - - def test_concurrent_class(self, tdata): - with taddons.context() as tctx: - sc = tctx.script( - tdata.path( - "mitmproxy/data/addonscripts/concurrent_decorator_class.py" - ) - ) - f1, f2 = tflow.tflow(), tflow.tflow() - tctx.cycle(sc, f1) - tctx.cycle(sc, f2) - start = time.time() - while time.time() - start < 5: - if f1.reply.state == f2.reply.state == "committed": - return - raise ValueError("Script never acked") diff --git a/test/mitmproxy/test_addonmanager.py b/test/mitmproxy/test_addonmanager.py index 20f0b77a49..1686d4923a 100644 --- a/test/mitmproxy/test_addonmanager.py +++ b/test/mitmproxy/test_addonmanager.py @@ -36,11 +36,24 @@ def running(self): self.running_called = True +class AsyncTAddon(TAddon): + async def done(self): + pass + + async def running(self): + self.running_called = True + + class THalt: def running(self): raise exceptions.AddonHalt +class AsyncTHalt: + async def running(self): + raise exceptions.AddonHalt + + class AOption: def load(self, l): l.add_option("custom_option", bool, False, "help") @@ -57,7 +70,7 @@ def test_command(): assert tctx.master.commands.execute("test.command") == "here" -def test_halt(): +async def test_halt(): o = options.Options() m = master.Master(o) a = addonmanager.AddonManager(m) @@ -75,7 +88,24 @@ def test_halt(): assert end.running_called -@pytest.mark.asyncio +async def test_async_halt(): + o = options.Options() + m = master.Master(o) + a = addonmanager.AddonManager(m) + halt = AsyncTHalt() + end = AsyncTAddon("end") + a.add(halt) + a.add(end) + + assert not end.running_called + await a.trigger_event(hooks.RunningHook()) + assert not end.running_called + + a.remove(halt) + await a.trigger_event(hooks.RunningHook()) + assert end.running_called + + async def test_lifecycle(): o = options.Options() m = master.Master(o) @@ -97,7 +127,31 @@ def test_defaults(): assert addons.default_addons() -@pytest.mark.asyncio +async def test_mixed_async_sync(): + with taddons.context(loadcore=False) as tctx: + a = tctx.master.addons + + assert len(a) == 0 + a1 = TAddon("sync") + a2 = AsyncTAddon("async") + a.add(a1) + a.add(a2) + + # test that we can call both sync and async hooks asynchronously + assert not a1.running_called + assert not a2.running_called + await a.trigger_event(hooks.RunningHook()) + assert a1.running_called + assert a2.running_called + + # test that calling an async hook synchronously fails + a1.running_called = False + a2.running_called = False + a.trigger(hooks.RunningHook()) + assert a1.running_called + await tctx.master.await_log("called from sync context") + + async def test_loader(): with taddons.context() as tctx: with mock.patch("mitmproxy.ctx.log.warn") as warn: @@ -119,7 +173,6 @@ def cmd(a: str) -> str: l.add_command("test.command", cmd) -@pytest.mark.asyncio async def test_simple(): with taddons.context(loadcore=False) as tctx: a = tctx.master.addons @@ -160,7 +213,7 @@ async def test_simple(): assert ta in a -def test_load_option(): +async def test_load_option(): o = options.Options() m = master.Master(o) a = addonmanager.AddonManager(m) @@ -168,19 +221,13 @@ def test_load_option(): assert "custom_option" in m.options._options -def test_nesting(): +async def test_nesting(): o = options.Options() m = master.Master(o) a = addonmanager.AddonManager(m) a.add( - TAddon( - "one", - addons=[ - TAddon("two"), - TAddon("three", addons=[TAddon("four")]) - ] - ) + TAddon("one", addons=[TAddon("two"), TAddon("three", addons=[TAddon("four")])]) ) assert len(a.chain) == 1 assert a.get("one") @@ -199,7 +246,6 @@ def test_nesting(): assert not a.get("four") -@pytest.mark.asyncio async def test_old_api(): with taddons.context(loadcore=False) as tctx: tctx.master.addons.add(AOldAPI()) diff --git a/test/mitmproxy/test_certs.py b/test/mitmproxy/test_certs.py index 1084b0a411..7ccb52d5c2 100644 --- a/test/mitmproxy/test_certs.py +++ b/test/mitmproxy/test_certs.py @@ -42,11 +42,12 @@ @pytest.fixture() def tstore(tdata): - return certs.CertStore.from_store(tdata.path("mitmproxy/data/confdir"), "mitmproxy", 2048) + return certs.CertStore.from_store( + tdata.path("mitmproxy/data/confdir"), "mitmproxy", 2048 + ) class TestCertStore: - def test_create_explicit(self, tmpdir): ca = certs.CertStore.from_store(str(tmpdir), "test", 2048) assert ca.get_cert("foo", []) @@ -132,25 +133,20 @@ def test_umask_secret(self, tmpdir): class TestDummyCert: - def test_with_ca(self, tstore): r = certs.dummy_cert( tstore.default_privatekey, tstore.default_ca._cert, "foo.com", ["one.com", "two.com", "*.three.com", "127.0.0.1"], - "Foo Ltd." + "Foo Ltd.", ) assert r.cn == "foo.com" assert r.altnames == ["one.com", "two.com", "*.three.com", "127.0.0.1"] assert r.organization == "Foo Ltd." r = certs.dummy_cert( - tstore.default_privatekey, - tstore.default_ca._cert, - None, - [], - None + tstore.default_privatekey, tstore.default_ca._cert, None, [], None ) assert r.cn is None assert r.organization is None @@ -158,7 +154,6 @@ def test_with_ca(self, tstore): class TestCert: - def test_simple(self, tdata): with open(tdata.path("mitmproxy/net/data/text_cert"), "rb") as f: d = f.read() @@ -198,7 +193,10 @@ def test_simple(self, tdata): assert c2.issuer assert c2.to_pem() assert c2.has_expired() is not None - assert repr(c2) == "" + assert ( + repr(c2) + == "" + ) assert c1 != c2 @@ -211,11 +209,14 @@ def test_convert(self, tdata): assert c == certs.Cert.from_state(c.get_state()) assert c == certs.Cert.from_pyopenssl(c.to_pyopenssl()) - @pytest.mark.parametrize("filename,name,bits", [ - ("text_cert", "RSA", 1024), - ("dsa_cert.pem", "DSA", 1024), - ("ec_cert.pem", "EC (secp256r1)", 256), - ]) + @pytest.mark.parametrize( + "filename,name,bits", + [ + ("text_cert", "RSA", 1024), + ("dsa_cert.pem", "DSA", 1024), + ("ec_cert.pem", "EC (secp256r1)", 256), + ], + ) def test_keyinfo(self, tdata, filename, name, bits): with open(tdata.path(f"mitmproxy/net/data/{filename}"), "rb") as f: d = f.read() @@ -246,32 +247,56 @@ def test_state(self, tdata): assert c == c2 def test_from_store_with_passphrase(self, tdata, tstore): - tstore.add_cert_file("unencrypted-no-pass", Path(tdata.path("mitmproxy/data/testkey.pem")), None) - tstore.add_cert_file("unencrypted-pass", Path(tdata.path("mitmproxy/data/testkey.pem")), b"password") - tstore.add_cert_file("encrypted-pass", Path(tdata.path("mitmproxy/data/mitmproxy.pem")), b"password") + tstore.add_cert_file( + "unencrypted-no-pass", Path(tdata.path("mitmproxy/data/testkey.pem")), None + ) + tstore.add_cert_file( + "unencrypted-pass", + Path(tdata.path("mitmproxy/data/testkey.pem")), + b"password", + ) + tstore.add_cert_file( + "encrypted-pass", + Path(tdata.path("mitmproxy/data/mitmproxy.pem")), + b"password", + ) with pytest.raises(TypeError): - tstore.add_cert_file("encrypted-no-pass", Path(tdata.path("mitmproxy/data/mitmproxy.pem")), None) + tstore.add_cert_file( + "encrypted-no-pass", + Path(tdata.path("mitmproxy/data/mitmproxy.pem")), + None, + ) def test_special_character(self, tdata): with open(tdata.path("mitmproxy/net/data/text_cert_with_comma"), "rb") as f: d = f.read() c = certs.Cert.from_pem(d) - assert dict(c.issuer).get('O') == 'DigiCert, Inc.' - assert dict(c.subject).get('O') == 'GitHub, Inc.' + assert dict(c.issuer).get("O") == "DigiCert, Inc." + assert dict(c.subject).get("O") == "GitHub, Inc." def test_multi_valued_rdns(self, tdata): - subject = x509.Name([ - x509.RelativeDistinguishedName([ - x509.NameAttribute(NameOID.TITLE, u'Test'), - x509.NameAttribute(NameOID.COMMON_NAME, u'Multivalue'), - x509.NameAttribute(NameOID.SURNAME, u'RDNs'), - x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'TSLA'), - ]), - x509.RelativeDistinguishedName([ - x509.NameAttribute(NameOID.ORGANIZATION_NAME, u'PyCA') - ]), - ]) - expected = [('2.5.4.12', 'Test'), ('CN', 'Multivalue'), ('2.5.4.4', 'RDNs'), ('O', 'TSLA'), ('O', 'PyCA')] - assert(certs._name_to_keyval(subject)) == expected + subject = x509.Name( + [ + x509.RelativeDistinguishedName( + [ + x509.NameAttribute(NameOID.TITLE, "Test"), + x509.NameAttribute(NameOID.COMMON_NAME, "Multivalue"), + x509.NameAttribute(NameOID.SURNAME, "RDNs"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "TSLA"), + ] + ), + x509.RelativeDistinguishedName( + [x509.NameAttribute(NameOID.ORGANIZATION_NAME, "PyCA")] + ), + ] + ) + expected = [ + ("2.5.4.12", "Test"), + ("CN", "Multivalue"), + ("2.5.4.4", "RDNs"), + ("O", "TSLA"), + ("O", "PyCA"), + ] + assert (certs._name_to_keyval(subject)) == expected diff --git a/test/mitmproxy/test_command.py b/test/mitmproxy/test_command.py index a74503841d..614e4d7d4d 100644 --- a/test/mitmproxy/test_command.py +++ b/test/mitmproxy/test_command.py @@ -1,6 +1,6 @@ import inspect import io -import typing +from collections.abc import Sequence import pytest @@ -31,7 +31,9 @@ def cmd4(self, a: int, b: str, c: mitmproxy.types.Path) -> str: return "ok" @command.command("subcommand") - def subcommand(self, cmd: mitmproxy.types.Cmd, *args: mitmproxy.types.CmdArgs) -> str: + def subcommand( + self, cmd: mitmproxy.types.Cmd, *args: mitmproxy.types.CmdArgs + ) -> str: return "ok" @command.command("empty") @@ -39,14 +41,14 @@ def empty(self) -> None: pass @command.command("varargs") - def varargs(self, one: str, *var: str) -> typing.Sequence[str]: + def varargs(self, one: str, *var: str) -> Sequence[str]: return list(var) - def choices(self) -> typing.Sequence[str]: + def choices(self) -> Sequence[str]: return ["one", "two", "three"] @command.argument("arg", type=mitmproxy.types.Choice("choices")) - def choose(self, arg: str) -> typing.Sequence[str]: + def choose(self, arg: str) -> Sequence[str]: return ["one", "two", "three"] @command.command("path") @@ -115,18 +117,28 @@ def test_parse_partial(self): [ "foo bar", [ - command.ParseResult(value="foo", type=mitmproxy.types.Cmd, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="bar", type=mitmproxy.types.Unknown, valid=False) + command.ParseResult( + value="foo", type=mitmproxy.types.Cmd, valid=False + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="bar", type=mitmproxy.types.Unknown, valid=False + ), ], [], ], [ "cmd1 'bar", [ - command.ParseResult(value="cmd1", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="'bar", type=str, valid=True) + command.ParseResult( + value="cmd1", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult(value="'bar", type=str, valid=True), ], [], ], @@ -140,55 +152,89 @@ def test_parse_partial(self): [], [ command.CommandParameter("", mitmproxy.types.Cmd), - command.CommandParameter("", mitmproxy.types.CmdArgs) - ] + command.CommandParameter("", mitmproxy.types.CmdArgs), + ], ], [ "cmd3 1", [ - command.ParseResult(value="cmd3", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="cmd3", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), command.ParseResult(value="1", type=int, valid=True), ], - [] + [], ], [ "cmd3 ", [ - command.ParseResult(value="cmd3", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="cmd3", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), ], - [command.CommandParameter('foo', int)] + [command.CommandParameter("foo", int)], ], [ "subcommand ", [ - command.ParseResult(value="subcommand", type=mitmproxy.types.Cmd, valid=True, ), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="subcommand", + type=mitmproxy.types.Cmd, + valid=True, + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), ], [ - command.CommandParameter('cmd', mitmproxy.types.Cmd), - command.CommandParameter('args', mitmproxy.types.CmdArgs, kind=inspect.Parameter.VAR_POSITIONAL), + command.CommandParameter("cmd", mitmproxy.types.Cmd), + command.CommandParameter( + "args", + mitmproxy.types.CmdArgs, + kind=inspect.Parameter.VAR_POSITIONAL, + ), ], ], [ "varargs one", [ - command.ParseResult(value="varargs", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="varargs", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), command.ParseResult(value="one", type=str, valid=True), ], - [command.CommandParameter('var', str, kind=inspect.Parameter.VAR_POSITIONAL)] + [ + command.CommandParameter( + "var", str, kind=inspect.Parameter.VAR_POSITIONAL + ) + ], ], [ "varargs one two three", [ - command.ParseResult(value="varargs", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="varargs", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), command.ParseResult(value="one", type=str, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), command.ParseResult(value="two", type=str, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), command.ParseResult(value="three", type=str, valid=True), ], [], @@ -196,199 +242,302 @@ def test_parse_partial(self): [ "subcommand cmd3 ", [ - command.ParseResult(value="subcommand", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="cmd3", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="subcommand", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="cmd3", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), ], - [command.CommandParameter('foo', int)] + [command.CommandParameter("foo", int)], ], [ "cmd4", [ - command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True), + command.ParseResult( + value="cmd4", type=mitmproxy.types.Cmd, valid=True + ), ], [ - command.CommandParameter('a', int), - command.CommandParameter('b', str), - command.CommandParameter('c', mitmproxy.types.Path), - ] + command.CommandParameter("a", int), + command.CommandParameter("b", str), + command.CommandParameter("c", mitmproxy.types.Path), + ], ], [ "cmd4 ", [ - command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="cmd4", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), ], [ - command.CommandParameter('a', int), - command.CommandParameter('b', str), - command.CommandParameter('c', mitmproxy.types.Path), - ] + command.CommandParameter("a", int), + command.CommandParameter("b", str), + command.CommandParameter("c", mitmproxy.types.Path), + ], ], [ "cmd4 1", [ - command.ParseResult(value="cmd4", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="cmd4", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), command.ParseResult(value="1", type=int, valid=True), ], [ - command.CommandParameter('b', str), - command.CommandParameter('c', mitmproxy.types.Path), - ] + command.CommandParameter("b", str), + command.CommandParameter("c", mitmproxy.types.Path), + ], ], [ "flow", [ - command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True), + command.ParseResult( + value="flow", type=mitmproxy.types.Cmd, valid=True + ), ], [ - command.CommandParameter('f', flow.Flow), - command.CommandParameter('s', str), - ] + command.CommandParameter("f", flow.Flow), + command.CommandParameter("s", str), + ], ], [ "flow ", [ - command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="flow", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), ], [ - command.CommandParameter('f', flow.Flow), - command.CommandParameter('s', str), - ] + command.CommandParameter("f", flow.Flow), + command.CommandParameter("s", str), + ], ], [ "flow x", [ - command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="flow", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), command.ParseResult(value="x", type=flow.Flow, valid=False), ], [ - command.CommandParameter('s', str), - ] + command.CommandParameter("s", str), + ], ], [ "flow x ", [ - command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value="flow", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), command.ParseResult(value="x", type=flow.Flow, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), ], [ - command.CommandParameter('s', str), - ] + command.CommandParameter("s", str), + ], ], [ - "flow \"one two", - [ - command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="\"one two", type=flow.Flow, valid=False), + 'flow "one two', + [ + command.ParseResult( + value="flow", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult(value='"one two', type=flow.Flow, valid=False), ], [ - command.CommandParameter('s', str), - ] + command.CommandParameter("s", str), + ], ], [ - "flow \"three four\"", - [ - command.ParseResult(value="flow", type=mitmproxy.types.Cmd, valid=True), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value='"three four"', type=flow.Flow, valid=False), + 'flow "three four"', + [ + command.ParseResult( + value="flow", type=mitmproxy.types.Cmd, valid=True + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value='"three four"', type=flow.Flow, valid=False + ), ], [ - command.CommandParameter('s', str), - ] + command.CommandParameter("s", str), + ], ], [ "spaces ' '", [ - command.ParseResult(value="spaces", type=mitmproxy.types.Cmd, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="' '", type=mitmproxy.types.Unknown, valid=False) + command.ParseResult( + value="spaces", type=mitmproxy.types.Cmd, valid=False + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="' '", type=mitmproxy.types.Unknown, valid=False + ), ], [], ], [ 'spaces2 " "', [ - command.ParseResult(value="spaces2", type=mitmproxy.types.Cmd, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value='" "', type=mitmproxy.types.Unknown, valid=False) + command.ParseResult( + value="spaces2", type=mitmproxy.types.Cmd, valid=False + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value='" "', type=mitmproxy.types.Unknown, valid=False + ), ], [], ], [ '"abc"', [ - command.ParseResult(value='"abc"', type=mitmproxy.types.Cmd, valid=False), + command.ParseResult( + value='"abc"', type=mitmproxy.types.Cmd, valid=False + ), ], [], ], [ "'def'", [ - command.ParseResult(value="'def'", type=mitmproxy.types.Cmd, valid=False), + command.ParseResult( + value="'def'", type=mitmproxy.types.Cmd, valid=False + ), ], [], ], [ "cmd10 'a' \"b\" c", [ - command.ParseResult(value="cmd10", type=mitmproxy.types.Cmd, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="'a'", type=mitmproxy.types.Unknown, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value='"b"', type=mitmproxy.types.Unknown, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="c", type=mitmproxy.types.Unknown, valid=False), + command.ParseResult( + value="cmd10", type=mitmproxy.types.Cmd, valid=False + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="'a'", type=mitmproxy.types.Unknown, valid=False + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value='"b"', type=mitmproxy.types.Unknown, valid=False + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="c", type=mitmproxy.types.Unknown, valid=False + ), ], [], ], [ "cmd11 'a \"b\" c'", [ - command.ParseResult(value="cmd11", type=mitmproxy.types.Cmd, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="'a \"b\" c'", type=mitmproxy.types.Unknown, valid=False), + command.ParseResult( + value="cmd11", type=mitmproxy.types.Cmd, valid=False + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="'a \"b\" c'", type=mitmproxy.types.Unknown, valid=False + ), ], [], ], [ - 'cmd12 "a \'b\' c"', - [ - command.ParseResult(value="cmd12", type=mitmproxy.types.Cmd, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value='"a \'b\' c"', type=mitmproxy.types.Unknown, valid=False), + "cmd12 \"a 'b' c\"", + [ + command.ParseResult( + value="cmd12", type=mitmproxy.types.Cmd, valid=False + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="\"a 'b' c\"", type=mitmproxy.types.Unknown, valid=False + ), ], [], ], [ " spaces_at_the_begining_are_not_stripped", [ - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="spaces_at_the_begining_are_not_stripped", type=mitmproxy.types.Cmd, - valid=False), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="spaces_at_the_begining_are_not_stripped", + type=mitmproxy.types.Cmd, + valid=False, + ), ], [], ], [ " spaces_at_the_begining_are_not_stripped neither_at_the_end ", [ - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="spaces_at_the_begining_are_not_stripped", type=mitmproxy.types.Cmd, - valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), - command.ParseResult(value="neither_at_the_end", type=mitmproxy.types.Unknown, valid=False), - command.ParseResult(value=" ", type=mitmproxy.types.Space, valid=True), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="spaces_at_the_begining_are_not_stripped", + type=mitmproxy.types.Cmd, + valid=False, + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), + command.ParseResult( + value="neither_at_the_end", + type=mitmproxy.types.Unknown, + valid=False, + ), + command.ParseResult( + value=" ", type=mitmproxy.types.Space, valid=True + ), ], [], ], - ] with taddons.context() as tctx: @@ -403,11 +552,11 @@ def test_simple(): c = command.CommandManager(tctx.master) a = TAddon() c.add("one.two", a.cmd1) - assert (c.commands["one.two"].help == "cmd1 help") - assert (c.execute("one.two foo") == "ret foo") - assert (c.execute("one.two \"foo\"") == "ret foo") - assert (c.execute("one.two 'foo bar'") == "ret foo bar") - assert (c.call("one.two", "foo") == "ret foo") + assert c.commands["one.two"].help == "cmd1 help" + assert c.execute("one.two foo") == "ret foo" + assert c.execute('one.two "foo"') == "ret foo" + assert c.execute("one.two 'foo bar'") == "ret foo bar" + assert c.call("one.two", "foo") == "ret foo" with pytest.raises(exceptions.CommandError, match="Unknown"): c.execute("nonexistent") with pytest.raises(exceptions.CommandError, match="Invalid"): @@ -429,13 +578,13 @@ def test_simple(): def test_typename(): assert command.typename(str) == "str" - assert command.typename(typing.Sequence[flow.Flow]) == "flow[]" + assert command.typename(Sequence[flow.Flow]) == "flow[]" assert command.typename(mitmproxy.types.Data) == "data[][]" assert command.typename(mitmproxy.types.CutSpec) == "cut[]" assert command.typename(flow.Flow) == "flow" - assert command.typename(typing.Sequence[str]) == "str[]" + assert command.typename(Sequence[str]) == "str[]" assert command.typename(mitmproxy.types.Choice("foo")) == "choice" assert command.typename(mitmproxy.types.Path) == "path" @@ -449,7 +598,7 @@ def test_typename(): class DummyConsole: @command.command("view.flows.resolve") - def resolve(self, spec: str) -> typing.Sequence[flow.Flow]: + def resolve(self, spec: str) -> Sequence[flow.Flow]: n = int(spec) return [tflow.tflow(resp=True)] * n @@ -503,11 +652,10 @@ def empty(self) -> None: pass -@pytest.mark.asyncio async def test_collect_commands(): """ - This tests for errors thrown by getattr() or __getattr__ implementations - that return an object for .command_name. + This tests for errors thrown by getattr() or __getattr__ implementations + that return an object for .command_name. """ with taddons.context() as tctx: c = command.CommandManager(tctx.master) @@ -538,5 +686,5 @@ def test_decorator(): def test_verify_arg_signature(): with pytest.raises(exceptions.CommandError): command.verify_arg_signature(lambda: None, [1, 2], {}) - print('hello there') + print("hello there") command.verify_arg_signature(lambda a, b: None, [1, 2], {}) diff --git a/test/mitmproxy/test_command_lexer.py b/test/mitmproxy/test_command_lexer.py index f94255cdb9..dfe9b27198 100644 --- a/test/mitmproxy/test_command_lexer.py +++ b/test/mitmproxy/test_command_lexer.py @@ -7,35 +7,40 @@ @pytest.mark.parametrize( - "test_input,valid", [ + "test_input,valid", + [ ("'foo'", True), ('"foo"', True), ("'foo' bar'", False), ("'foo' 'bar'", False), ("'foo'x", False), - ('''"foo ''', True), - ('''"foo 'bar' ''', True), + (""""foo """, True), + (""""foo 'bar' """, True), ('"foo\\', True), - ] + ], ) def test_partial_quoted_string(test_input, valid): if valid: - assert command_lexer.PartialQuotedString.parseString(test_input, parseAll=True)[0] == test_input + assert ( + command_lexer.PartialQuotedString.parseString(test_input, parseAll=True)[0] + == test_input + ) else: with pytest.raises(pyparsing.ParseException): command_lexer.PartialQuotedString.parseString(test_input, parseAll=True) @pytest.mark.parametrize( - "test_input,expected", [ + "test_input,expected", + [ ("'foo'", ["'foo'"]), ('"foo"', ['"foo"']), - ("'foo' 'bar'", ["'foo'", ' ', "'bar'"]), - ("'foo'x", ["'foo'", 'x']), - ('''"foo''', ['"foo']), - ('''"foo 'bar' ''', ['''"foo 'bar' ''']), + ("'foo' 'bar'", ["'foo'", " ", "'bar'"]), + ("'foo'x", ["'foo'", "x"]), + (""""foo""", ['"foo']), + (""""foo 'bar' """, [""""foo 'bar' """]), ('"foo\\', ['"foo\\']), - ] + ], ) def test_expr(test_input, expected): assert list(command_lexer.expr.parseString(test_input, parseAll=True)) == expected @@ -49,9 +54,9 @@ def test_expr(test_input, expected): @example(r'"foo\'"') @example("'foo\\'") @example("'foo\\\\'") -@example("\"foo\\'\"") -@example("\"foo\\\\'\"") -@example('\'foo\\"\'') +@example('"foo\\\'"') +@example('"foo\\\\\'"') +@example("'foo\\\"'") @example(r"\\\foo") def test_quote_unquote_cycle(s): assert command_lexer.unquote(command_lexer.quote(s)).replace(r"\x22", '"') == s diff --git a/test/mitmproxy/test_connection.py b/test/mitmproxy/test_connection.py index 5f3f23dc02..27761e2ac9 100644 --- a/test/mitmproxy/test_connection.py +++ b/test/mitmproxy/test_connection.py @@ -6,11 +6,7 @@ class TestConnection: def test_basic(self): - c = Client( - ("127.0.0.1", 52314), - ("127.0.0.1", 8080), - 1607780791 - ) + c = Client(("127.0.0.1", 52314), ("127.0.0.1", 8080), 1607780791) assert not c.tls_established c.timestamp_tls_setup = 1607780792 assert c.tls_established @@ -32,11 +28,7 @@ def test_eq(self): class TestClient: def test_basic(self): - c = Client( - ("127.0.0.1", 52314), - ("127.0.0.1", 8080), - 1607780791 - ) + c = Client(("127.0.0.1", 52314), ("127.0.0.1", 8080), 1607780791) assert repr(c) assert str(c) c.timestamp_tls_setup = 1607780791 diff --git a/test/mitmproxy/test_controller.py b/test/mitmproxy/test_controller.py deleted file mode 100644 index c0f677da2a..0000000000 --- a/test/mitmproxy/test_controller.py +++ /dev/null @@ -1,80 +0,0 @@ -import asyncio - -import pytest - -import mitmproxy.ctx -from mitmproxy import controller -from mitmproxy.exceptions import ControlException -from mitmproxy.test import taddons - - -@pytest.mark.asyncio -async def test_master(): - class tAddon: - def add_log(self, _): - mitmproxy.ctx.master.should_exit.set() - - with taddons.context(tAddon()) as tctx: - assert not tctx.master.should_exit.is_set() - - async def test(): - mitmproxy.ctx.log("test") - - asyncio.ensure_future(test()) - await tctx.master.await_log("test") - assert tctx.master.should_exit.is_set() - - -class TestReply: - @pytest.mark.asyncio - async def test_simple(self): - reply = controller.Reply(42) - assert reply.state == "start" - - reply.take() - assert reply.state == "taken" - - assert not reply.done.is_set() - reply.commit() - assert reply.state == "committed" - assert await asyncio.wait_for(reply.done.wait(), 1) - - def test_double_commit(self): - reply = controller.Reply(47) - reply.take() - reply.commit() - with pytest.raises(ControlException): - reply.commit() - - def test_del(self): - reply = controller.Reply(47) - with pytest.raises(ControlException): - reply.__del__() - reply.take() - reply.commit() - - -class TestDummyReply: - def test_simple(self): - reply = controller.DummyReply() - for _ in range(2): - reply.take() - reply.commit() - reply.mark_reset() - reply.reset() - assert reply.state == "start" - - def test_reset(self): - reply = controller.DummyReply() - reply.take() - with pytest.raises(ControlException): - reply.mark_reset() - reply.commit() - reply.mark_reset() - assert reply.state == "committed" - reply.reset() - assert reply.state == "start" - - def test_del(self): - reply = controller.DummyReply() - reply.__del__() diff --git a/test/mitmproxy/test_dns.py b/test/mitmproxy/test_dns.py new file mode 100644 index 0000000000..6a5075c91f --- /dev/null +++ b/test/mitmproxy/test_dns.py @@ -0,0 +1,250 @@ +import ipaddress +import struct +import pytest + +from mitmproxy import dns +from mitmproxy import flowfilter +from mitmproxy.test import tflow +from mitmproxy.test import tutils + + +class TestResourceRecord: + def test_str(self): + assert ( + str(dns.ResourceRecord.A("test", ipaddress.IPv4Address("1.2.3.4"))) + == "1.2.3.4" + ) + assert ( + str(dns.ResourceRecord.AAAA("test", ipaddress.IPv6Address("::1"))) == "::1" + ) + assert ( + str(dns.ResourceRecord.CNAME("test", "some.other.host")) + == "some.other.host" + ) + assert ( + str(dns.ResourceRecord.PTR("test", "some.other.host")) == "some.other.host" + ) + assert str(dns.ResourceRecord.TXT("test", "unicode text 😀")) == "unicode text 😀" + assert ( + str( + dns.ResourceRecord( + "test", + dns.types.A, + dns.classes.IN, + dns.ResourceRecord.DEFAULT_TTL, + b"", + ) + ) + == "0x (invalid A data)" + ) + assert ( + str( + dns.ResourceRecord( + "test", + dns.types.SOA, + dns.classes.IN, + dns.ResourceRecord.DEFAULT_TTL, + b"\x00\x01\x02\x03", + ) + ) + == "0x00010203" + ) + + def test_setter(self): + rr = dns.ResourceRecord( + "test", dns.types.ANY, dns.classes.IN, dns.ResourceRecord.DEFAULT_TTL, b"" + ) + rr.ipv4_address = ipaddress.IPv4Address("8.8.4.4") + assert rr.ipv4_address == ipaddress.IPv4Address("8.8.4.4") + rr.ipv6_address = ipaddress.IPv6Address("2001:4860:4860::8844") + assert rr.ipv6_address == ipaddress.IPv6Address("2001:4860:4860::8844") + rr.domain_name = "www.example.org" + assert rr.domain_name == "www.example.org" + rr.text = "sample text" + assert rr.text == "sample text" + + +class TestMessage: + def test_json(self): + resp = tutils.tdnsresp() + json = resp.to_json() + assert json["id"] == resp.id + assert len(json["questions"]) == len(resp.questions) + assert json["questions"][0]["name"] == resp.questions[0].name + assert len(json["answers"]) == len(resp.answers) + assert json["answers"][0]["data"] == str(resp.answers[0]) + + def test_responses(self): + req = tutils.tdnsreq() + resp = tutils.tdnsresp() + resp2 = req.succeed( + [ + dns.ResourceRecord.A( + "dns.google", ipaddress.IPv4Address("8.8.8.8"), ttl=32 + ), + dns.ResourceRecord.A( + "dns.google", ipaddress.IPv4Address("8.8.4.4"), ttl=32 + ), + ] + ) + resp2.timestamp = resp.timestamp + assert resp == resp2 + assert resp2.size == 8 + with pytest.raises(ValueError): + req.fail(dns.response_codes.NOERROR) + assert ( + req.fail(dns.response_codes.FORMERR).response_code + == dns.response_codes.FORMERR + ) + + def test_range(self): + def test(what: str, min: int, max: int): + req = tutils.tdnsreq() + setattr(req, what, min) + assert getattr(dns.Message.unpack(req.packed), what) == min + setattr(req, what, min - 1) + with pytest.raises(ValueError): + req.packed + setattr(req, what, max) + assert getattr(dns.Message.unpack(req.packed), what) == max + setattr(req, what, max + 1) + with pytest.raises(ValueError): + req.packed + + test("id", 0, 2 ** 16 - 1) + test("reserved", 0, 7) + test("op_code", 0, 0b1111) + test("response_code", 0, 0b1111) + + def test_packing(self): + def assert_eq(m: dns.Message, b: bytes) -> None: + m_b = dns.Message.unpack(b) + m_b.timestamp = m.timestamp + assert m_b == m + assert m_b.packed == m.packed + + assert_eq( + tutils.tdnsreq(), + b"\x00\x2a\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03dns\x06google\x00\x00\x01\x00\x01", + ) + with pytest.raises(struct.error): + dns.Message.unpack( + b"\x00\x2a\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03dns\x06google\x00\x00\x01\x00\x01\x00" + ) + assert_eq( + tutils.tdnsresp(), + ( + b"\x00\x2a\x81\x80\x00\x01\x00\x02\x00\x00\x00\x00\x03dns\x06google\x00\x00\x01\x00\x01" + b"\xc0\x0c\x00\x01\x00\x01\x00\x00\x00 \x00\x04\x08\x08\x08\x08\xc0\x0c\x00\x01\x00\x01" + b"\x00\x00\x00 \x00\x04\x08\x08\x04\x04" + ), + ) + with pytest.raises(struct.error): # question error + dns.Message.unpack( + b"\x00\x2a\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x03dns\x06goo" + ) + with pytest.raises(struct.error): # rr length error + dns.Message.unpack( + b"\x00\x2a\x81\x80\x00\x01\x00\x02\x00\x00\x00\x00\x03dns\x06google\x00\x00\x01\x00\x01" + + b"\xc0\x0c\x00\x01\x00\x01\x00\x00\x00 \x00\x04\x08\x08\x08\x08\xc0\x0c\x00\x01\x00\x01\x00\x00\x00 \x00\x04\x08\x08\x04" + ) + txt = dns.Message.unpack( + b"V\x1a\x81\x80\x00\x01\x00\x01\x00\x01\x00\x01\x05alive\x06github\x03com\x00\x00" + + b"\x10\x00\x01\xc0\x0c\x00\x05\x00\x01\x00\x00\x0b\xc6\x00\x07\x04live\xc0\x12\xc0\x12\x00\x06\x00\x01" + + b"\x00\x00\x03\x84\x00H\x07ns-1707\tawsdns-21\x02co\x02uk\x00\x11awsdns-hostmaster\x06amazon\xc0\x19\x00" + + b"\x00\x00\x01\x00\x00\x1c \x00\x00\x03\x84\x00\x12u\x00\x00\x01Q\x80\x00\x00)\x02\x00\x00\x00\x00\x00\x00\x00" + ) + assert txt.answers[0].domain_name == "live.github.com" + invalid_rr_domain_name = dns.Message.unpack( + b"V\x1a\x81\x80\x00\x01\x00\x01\x00\x01\x00\x01\x05alive\x06github\x03com\x00\x00" + + b"\x10\x00\x01\xc0\x0c\x00\x05\x00\x01\x00\x00\x0b\xc6\x00\x07\x99live\xc0\x12\xc0\x12\x00\x06\x00\x01" + + b"\x00\x00\x03\x84\x00H\x07ns-1707\tawsdns-21\x02co\x02uk\x00\x11awsdns-hostmaster\x06amazon\xc0\x19\x00" + + b"\x00\x00\x01\x00\x00\x1c \x00\x00\x03\x84\x00\x12u\x00\x00\x01Q\x80\x00\x00)\x02\x00\x00\x00\x00\x00\x00\x00" + ) + assert invalid_rr_domain_name.answers[0].data == b"\x99live\xc0\x12" + + req = tutils.tdnsreq() + for flag in ( + "authoritative_answer", + "truncation", + "recursion_desired", + "recursion_available", + ): + setattr(req, flag, True) + assert getattr(dns.Message.unpack(req.packed), flag) is True + setattr(req, flag, False) + assert getattr(dns.Message.unpack(req.packed), flag) is False + + def test_copy(self): + msg = tutils.tdnsresp() + assert dns.Message.from_state(msg.get_state()) == msg + copy = msg.copy() + assert copy is not msg + assert copy != msg + copy.id = msg.id + assert copy == msg + assert copy.questions is not msg.questions + assert copy.questions == msg.questions + assert copy.answers is not msg.answers + assert copy.answers == msg.answers + assert copy.authorities is not msg.authorities + assert copy.authorities == msg.authorities + assert copy.additionals is not msg.additionals + assert copy.additionals == msg.additionals + + +class TestDNSFlow: + def test_copy(self): + f = tflow.tdnsflow(resp=True) + assert repr(f) + f.get_state() + f2 = f.copy() + a = f.get_state() + b = f2.get_state() + del a["id"] + del b["id"] + assert a == b + assert not f == f2 + assert f is not f2 + + assert f.request.get_state() == f2.request.get_state() + assert f.request is not f2.request + assert f.request == f2.request + assert f.response is not f2.response + assert f.response.get_state() == f2.response.get_state() + assert f.response == f2.response + + f = tflow.tdnsflow(err=True) + f2 = f.copy() + assert f is not f2 + assert f.request is not f2.request + assert f.request == f2.request + assert f.error.get_state() == f2.error.get_state() + assert f.error is not f2.error + + def test_match(self): + f = tflow.tdnsflow(resp=True) + assert not flowfilter.match("~b nonexistent", f) + assert flowfilter.match(None, f) + assert flowfilter.match("~b dns.google", f) + assert flowfilter.match("~b 8.8.8.8", f) + + assert flowfilter.match("~bq dns.google", f) + assert not flowfilter.match("~bq 8.8.8.8", f) + + assert flowfilter.match("~bs dns.google", f) + assert flowfilter.match("~bs 8.8.4.4", f) + + assert flowfilter.match("~dns", f) + assert not flowfilter.match("~dns", tflow.ttcpflow()) + assert not flowfilter.match("~dns", tflow.tflow()) + + f = tflow.tdnsflow(err=True) + assert flowfilter.match("~e", f) + + with pytest.raises(ValueError): + flowfilter.match("~", f) + + def test_repr(self): + f = tflow.tdnsflow() + assert "DNSFlow" in repr(f) diff --git a/test/mitmproxy/test_eventsequence.py b/test/mitmproxy/test_eventsequence.py index 03ad5da577..57330f80f5 100644 --- a/test/mitmproxy/test_eventsequence.py +++ b/test/mitmproxy/test_eventsequence.py @@ -5,12 +5,15 @@ from mitmproxy.test import tflow -@pytest.mark.parametrize("resp, err", [ - (False, False), - (True, False), - (False, True), - (True, True), -]) +@pytest.mark.parametrize( + "resp, err", + [ + (False, False), + (True, False), + (False, True), + (True, True), + ], +) def test_http_flow(resp, err): f = tflow.tflow(resp=resp, err=err) i = eventsequence.iterate(f) @@ -59,6 +62,25 @@ def test_tcp_flow(err): assert isinstance(next(i), layers.tcp.TcpEndHook) +@pytest.mark.parametrize( + "resp, err", + [ + (False, False), + (True, False), + (False, True), + (True, True), + ], +) +def test_dns(resp, err): + f = tflow.tdnsflow(resp=resp, err=err) + i = eventsequence.iterate(f) + assert isinstance(next(i), layers.dns.DnsRequestHook) + if resp: + assert isinstance(next(i), layers.dns.DnsResponseHook) + if err: + assert isinstance(next(i), layers.dns.DnsErrorHook) + + def test_invalid(): with pytest.raises(TypeError): next(eventsequence.iterate(42)) diff --git a/test/mitmproxy/test_flow.py b/test/mitmproxy/test_flow.py index cb1e351c6c..55beb7d42d 100644 --- a/test/mitmproxy/test_flow.py +++ b/test/mitmproxy/test_flow.py @@ -30,7 +30,6 @@ def websocket_start(self, f): class TestSerialize: - def test_roundtrip(self): sio = io.BytesIO() f = tflow.tflow() @@ -69,20 +68,21 @@ def test_filter(self): assert len(list(r.stream())) def test_error(self): - sio = io.BytesIO() - sio.write(b"bogus") - sio.seek(0) - r = mitmproxy.io.FlowReader(sio) - with pytest.raises(FlowReadException, match='Invalid data format'): + buf = io.BytesIO() + buf.write(b"bogus") + buf.seek(0) + r = mitmproxy.io.FlowReader(buf) + with pytest.raises(FlowReadException, match="Invalid data format"): list(r.stream()) - sio = io.BytesIO() + buf = io.BytesIO() f = tflow.tdummyflow() - w = mitmproxy.io.FlowWriter(sio) + w = mitmproxy.io.FlowWriter(buf) w.add(f) - sio.seek(0) - r = mitmproxy.io.FlowReader(sio) - with pytest.raises(FlowReadException, match='Unknown flow type'): + + buf = io.BytesIO(buf.getvalue().replace(b"dummy", b"nknwn")) + r = mitmproxy.io.FlowReader(buf) + with pytest.raises(FlowReadException, match="Unknown flow type"): list(r.stream()) f = FlowReadException("foo") @@ -113,26 +113,22 @@ def test_copy(self): class TestFlowMaster: - @pytest.mark.asyncio async def test_load_http_flow_reverse(self): - opts = options.Options( - mode="reverse:https://use-this-domain" - ) + opts = options.Options(mode="reverse:https://use-this-domain") s = State() with taddons.context(s, options=opts) as ctx: f = tflow.tflow(resp=True) await ctx.master.load_flow(f) assert s.flows[0].request.host == "use-this-domain" - @pytest.mark.asyncio async def test_all(self): - opts = options.Options( - mode="reverse:https://use-this-domain" - ) + opts = options.Options(mode="reverse:https://use-this-domain") s = State() with taddons.context(s, options=opts) as ctx: f = tflow.tflow(req=None) - await ctx.master.addons.handle_lifecycle(server_hooks.ClientConnectedHook(f.client_conn)) + await ctx.master.addons.handle_lifecycle( + server_hooks.ClientConnectedHook(f.client_conn) + ) f.request = mitmproxy.test.tutils.treq() await ctx.master.addons.handle_lifecycle(layers.http.HttpRequestHook(f)) assert len(s.flows) == 1 @@ -141,14 +137,15 @@ async def test_all(self): await ctx.master.addons.handle_lifecycle(layers.http.HttpResponseHook(f)) assert len(s.flows) == 1 - await ctx.master.addons.handle_lifecycle(server_hooks.ClientDisconnectedHook(f.client_conn)) + await ctx.master.addons.handle_lifecycle( + server_hooks.ClientDisconnectedHook(f.client_conn) + ) f.error = flow.Error("msg") await ctx.master.addons.handle_lifecycle(layers.http.HttpErrorHook(f)) class TestError: - def test_getset_state(self): e = flow.Error("Error") state = e.get_state() diff --git a/test/mitmproxy/test_flowfilter.py b/test/mitmproxy/test_flowfilter.py index 718332520a..a28540f4db 100644 --- a/test/mitmproxy/test_flowfilter.py +++ b/test/mitmproxy/test_flowfilter.py @@ -6,7 +6,6 @@ class TestParsing: - def _dump(self, x): c = io.StringIO() x.dump(fp=c) @@ -86,7 +85,6 @@ def test_wideops(self): class TestMatchingHTTPFlow: - def req(self): return tflow.tflow() @@ -154,7 +152,7 @@ def test_fmarker_char(self): t = tflow.tflow() t.marked = ":default:" assert not self.q("~marker X", t) - t.marked = 'X' + t.marked = "X" assert self.q("~marker X", t) def test_head(self): @@ -189,30 +187,30 @@ def match_body(self, q, s): assert not self.q("~bq message", q) assert not self.q("~bq message", s) - s.response.text = 'яч' # Cyrillic + s.response.text = "яч" # Cyrillic assert self.q("~bs яч", s) - s.response.text = '测试' # Chinese - assert self.q('~bs 测试', s) - s.response.text = 'ॐ' # Hindi - assert self.q('~bs ॐ', s) - s.response.text = 'لله' # Arabic - assert self.q('~bs لله', s) - s.response.text = 'θεός' # Greek - assert self.q('~bs θεός', s) - s.response.text = 'לוהים' # Hebrew - assert self.q('~bs לוהים', s) - s.response.text = '神' # Japanese - assert self.q('~bs 神', s) - s.response.text = '하나님' # Korean - assert self.q('~bs 하나님', s) - s.response.text = 'Äÿ' # Latin - assert self.q('~bs Äÿ', s) + s.response.text = "测试" # Chinese + assert self.q("~bs 测试", s) + s.response.text = "ॐ" # Hindi + assert self.q("~bs ॐ", s) + s.response.text = "لله" # Arabic + assert self.q("~bs لله", s) + s.response.text = "θεός" # Greek + assert self.q("~bs θεός", s) + s.response.text = "לוהים" # Hebrew + assert self.q("~bs לוהים", s) + s.response.text = "神" # Japanese + assert self.q("~bs 神", s) + s.response.text = "하나님" # Korean + assert self.q("~bs 하나님", s) + s.response.text = "Äÿ" # Latin + assert self.q("~bs Äÿ", s) assert not self.q("~bs nomatch", s) assert not self.q("~bs content", q) assert not self.q("~bs content", s) assert not self.q("~bs message", q) - s.response.text = 'message' + s.response.text = "message" assert self.q("~bs message", s) def test_body(self): @@ -268,9 +266,9 @@ def test_src(self): assert self.q("~src 127.0.0.1:22", q) q.client_conn.peername = None - assert not self.q('~src address:22', q) + assert not self.q("~src address:22", q) q.client_conn = None - assert not self.q('~src address:22', q) + assert not self.q("~src address:22", q) def test_dst(self): q = self.req() @@ -282,9 +280,9 @@ def test_dst(self): assert self.q("~dst address:22", q) q.server_conn.address = None - assert not self.q('~dst address:22', q) + assert not self.q("~dst address:22", q) q.server_conn = None - assert not self.q('~dst address:22', q) + assert not self.q("~dst address:22", q) def test_and(self): s = self.resp() @@ -331,12 +329,65 @@ def test_metadata(self): assert self.q("~meta string", f) assert self.q("~meta key", f) assert self.q("~meta value", f) - assert self.q("~meta \"b: string\"", f) + assert self.q('~meta "b: string"', f) assert self.q("~meta \"'key': 'value'\"", f) -class TestMatchingTCPFlow: +class TestMatchingDNSFlow: + def req(self): + return tflow.tdnsflow() + + def resp(self): + return tflow.tdnsflow(resp=True) + + def err(self): + return tflow.tdnsflow(err=True) + + def q(self, q, o): + return flowfilter.parse(q)(o) + + def test_dns(self): + s = self.req() + assert self.q("~dns", s) + assert not self.q("~http", s) + assert not self.q("~tcp", s) + + def test_freq_fresp(self): + q = self.req() + s = self.resp() + + assert self.q("~q", q) + assert not self.q("~q", s) + assert not self.q("~s", q) + assert self.q("~s", s) + + def test_ferr(self): + e = self.err() + assert self.q("~e", e) + + def test_body(self): + q = self.req() + s = self.resp() + assert not self.q("~b nonexistent", q) + assert self.q("~b dns.google", q) + assert self.q("~b 8.8.8.8", s) + + assert not self.q("~bq 8.8.8.8", s) + assert self.q("~bq dns.google", q) + assert self.q("~bq dns.google", s) + + assert not self.q("~bs dns.google", q) + assert self.q("~bs dns.google", s) + assert self.q("~bs 8.8.8.8", s) + + def test_url(self): + f = self.req() + assert not self.q("~u whatever", f) + assert self.q("~u dns.google", f) + + +class TestMatchingTCPFlow: def flow(self): return tflow.ttcpflow() @@ -461,7 +512,6 @@ def test_url(self): class TestMatchingWebSocketFlow: - def flow(self) -> http.HTTPFlow: return tflow.twebsocketflow() @@ -557,7 +607,6 @@ def test_not(self): class TestMatchingDummyFlow: - def flow(self): return tflow.tdummyflow() @@ -620,7 +669,7 @@ def test_filters(self): assert self.q("~comment .", f) -@patch('traceback.extract_tb') +@patch("traceback.extract_tb") def test_pyparsing_bug(extract_tb): """https://github.com/mitmproxy/mitmproxy/issues/1087""" # The text is a string with leading and trailing whitespace stripped; if the source is not available it is None. @@ -630,7 +679,7 @@ def test_pyparsing_bug(extract_tb): def test_match(): with pytest.raises(ValueError): - flowfilter.match('[foobar', None) + flowfilter.match("[foobar", None) assert flowfilter.match(None, None) - assert not flowfilter.match('foobar', None) + assert not flowfilter.match("foobar", None) diff --git a/test/mitmproxy/test_hooks.py b/test/mitmproxy/test_hooks.py index a7288322f5..cbba45e2b1 100644 --- a/test/mitmproxy/test_hooks.py +++ b/test/mitmproxy/test_hooks.py @@ -25,6 +25,7 @@ class FooHook(hooks.Hook): assert FooHook in hooks.all_hooks.values() with pytest.warns(RuntimeWarning, match="Two conflicting event classes"): + @dataclass class FooHook2(hooks.Hook): name = "foo" @@ -33,4 +34,4 @@ class FooHook2(hooks.Hook): class AnotherABC(hooks.Hook): name = "" - assert AnotherABC not in hooks.all_hooks.values() \ No newline at end of file + assert AnotherABC not in hooks.all_hooks.values() diff --git a/test/mitmproxy/test_http.py b/test/mitmproxy/test_http.py index 2a489aeb94..c44bde049a 100644 --- a/test/mitmproxy/test_http.py +++ b/test/mitmproxy/test_http.py @@ -1,3 +1,4 @@ +import asyncio import email import time import json @@ -7,7 +8,6 @@ from mitmproxy import flow from mitmproxy import flowfilter -from mitmproxy.exceptions import ControlException from mitmproxy.http import Headers, Request, Response, HTTPFlow from mitmproxy.net.http.cookies import CookieAttrs from mitmproxy.test.tflow import tflow @@ -15,7 +15,6 @@ class TestRequest: - def test_simple(self): f = tflow() r = f.request @@ -205,7 +204,7 @@ def test_get_host_header(self): h1 = treq( headers=((b"host", b"header.example.com"),), - authority=b"authority.example.com" + authority=b"authority.example.com", ) assert h1.host_header == "header.example.com" @@ -316,7 +315,7 @@ def test_set_query(self): request.query["foo"] = "bar" assert request.query["foo"] == "bar" assert request.path == "/path?foo=bar" - request.query = [('foo', 'bar')] + request.query = [("foo", "bar")] assert request.query["foo"] == "bar" assert request.path == "/path?foo=bar" @@ -329,23 +328,27 @@ def test_get_cookies_single(self): request = treq() request.headers = Headers(cookie="cookiename=cookievalue") assert len(request.cookies) == 1 - assert request.cookies['cookiename'] == 'cookievalue' + assert request.cookies["cookiename"] == "cookievalue" def test_get_cookies_double(self): request = treq() - request.headers = Headers(cookie="cookiename=cookievalue;othercookiename=othercookievalue") + request.headers = Headers( + cookie="cookiename=cookievalue;othercookiename=othercookievalue" + ) result = request.cookies assert len(result) == 2 - assert result['cookiename'] == 'cookievalue' - assert result['othercookiename'] == 'othercookievalue' + assert result["cookiename"] == "cookievalue" + assert result["othercookiename"] == "othercookievalue" def test_get_cookies_withequalsign(self): request = treq() - request.headers = Headers(cookie="cookiename=coo=kievalue;othercookiename=othercookievalue") + request.headers = Headers( + cookie="cookiename=coo=kievalue;othercookiename=othercookievalue" + ) result = request.cookies assert len(result) == 2 - assert result['cookiename'] == 'coo=kievalue' - assert result['othercookiename'] == 'othercookievalue' + assert result["cookiename"] == "coo=kievalue" + assert result["othercookiename"] == "othercookievalue" def test_set_cookies(self): request = treq() @@ -413,7 +416,7 @@ def test_get_urlencoded_form(self): def test_set_urlencoded_form(self): request = treq(content=b"\xec\xed") - request.urlencoded_form = [('foo', 'bar'), ('rab', 'oof')] + request.urlencoded_form = [("foo", "bar"), ("rab", "oof")] assert request.headers["Content-Type"] == "application/x-www-form-urlencoded" assert request.content @@ -424,19 +427,21 @@ def test_get_multipart_form(self): request.headers["Content-Type"] = "multipart/form-data" assert list(request.multipart_form.items()) == [] - with mock.patch('mitmproxy.net.http.multipart.decode') as m: + with mock.patch("mitmproxy.net.http.multipart.decode") as m: m.side_effect = ValueError assert list(request.multipart_form.items()) == [] def test_set_multipart_form(self): request = treq() request.multipart_form = [(b"file", b"shell.jpg"), (b"file_size", b"1000")] - assert request.headers["Content-Type"].startswith('multipart/form-data') - assert list(request.multipart_form.items()) == [(b"file", b"shell.jpg"), (b"file_size", b"1000")] + assert request.headers["Content-Type"].startswith("multipart/form-data") + assert list(request.multipart_form.items()) == [ + (b"file", b"shell.jpg"), + (b"file_size", b"1000"), + ] class TestResponse: - def test_simple(self): f = tflow(resp=True) resp = f.response @@ -517,7 +522,7 @@ def test_reason(self): resp.reason = b"DEF" assert resp.data.reason == b"DEF" - resp.data.reason = b'cr\xe9e' + resp.data.reason = b"cr\xe9e" assert resp.reason == "crée" @@ -561,7 +566,9 @@ def test_get_cookies_with_parameters(self): def test_get_cookies_no_value(self): resp = tresp() - resp.headers = Headers(set_cookie="cookiename=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/") + resp.headers = Headers( + set_cookie="cookiename=; Expires=Thu, 01-Jan-1970 00:00:01 GMT; path=/" + ) result = resp.cookies assert len(result) == 1 assert "cookiename" in result @@ -570,10 +577,12 @@ def test_get_cookies_no_value(self): def test_get_cookies_twocookies(self): resp = tresp() - resp.headers = Headers([ - [b"Set-Cookie", b"cookiename=cookievalue"], - [b"Set-Cookie", b"othercookie=othervalue"] - ]) + resp.headers = Headers( + [ + [b"Set-Cookie", b"cookiename=cookievalue"], + [b"Set-Cookie", b"othercookie=othervalue"], + ] + ) result = resp.cookies assert len(result) == 2 assert "cookiename" in result @@ -586,7 +595,10 @@ def test_set_cookies(self): resp.cookies["foo"] = ("bar", {}) assert len(resp.cookies) == 1 assert resp.cookies["foo"] == ("bar", CookieAttrs()) - resp.cookies = [["one", ("uno", CookieAttrs())], ["two", ("due", CookieAttrs())]] + resp.cookies = [ + ["one", ("uno", CookieAttrs())], + ["two", ("due", CookieAttrs())], + ] assert list(resp.cookies.keys()) == ["one", "two"] def test_refresh(self): @@ -609,13 +621,17 @@ def test_refresh(self): # Cookie refreshing is tested in test_cookies, we just make sure that it's triggered here. assert cookie != r.headers["set-cookie"] - with mock.patch('mitmproxy.net.http.cookies.refresh_set_cookie_header') as m: + with mock.patch("mitmproxy.net.http.cookies.refresh_set_cookie_header") as m: m.side_effect = ValueError r.refresh(n) + # Test negative unixtime, which raises on at least Windows. + r.headers["date"] = pre = "Mon, 01 Jan 1601 00:00:00 GMT" + r.refresh(946681202) + assert r.headers["date"] == pre -class TestHTTPFlow: +class TestHTTPFlow: def test_copy(self): f = tflow(resp=True) assert repr(f) @@ -677,14 +693,12 @@ def test_backup_idempotence(self): def test_getset_state(self): f = tflow(resp=True) state = f.get_state() - assert f.get_state() == HTTPFlow.from_state( - state).get_state() + assert f.get_state() == HTTPFlow.from_state(state).get_state() f.response = None f.error = flow.Error("error") state = f.get_state() - assert f.get_state() == HTTPFlow.from_state( - state).get_state() + assert f.get_state() == HTTPFlow.from_state(state).get_state() f2 = f.copy() f2.id = f.id # copy creates a different uuid @@ -699,10 +713,11 @@ def test_getset_state(self): def test_kill(self): f = tflow() - with pytest.raises(ControlException): - f.intercept() - f.resume() - f.kill() + f.intercept() + f.resume() + assert f.killable + f.kill() + assert not f.killable f = tflow() f.intercept() @@ -714,16 +729,45 @@ def test_kill(self): def test_intercept(self): f = tflow() f.intercept() - assert f.reply.state == "taken" + assert f.intercepted f.intercept() - assert f.reply.state == "taken" + assert f.intercepted def test_resume(self): f = tflow() + f.resume() + assert not f.intercepted + f.intercept() + assert f.intercepted + f.resume() + assert not f.intercepted + + async def test_wait_for_resume(self): + f = tflow() + await f.wait_for_resume() + + f = tflow() + f.intercept() + f.resume() + await f.wait_for_resume() + + f = tflow() + f.intercept() + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(f.wait_for_resume(), 0.2) + f.resume() + await f.wait_for_resume() + + f = tflow() + f.intercept() + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(f.wait_for_resume(), 0.2) + f.resume() f.intercept() - assert f.reply.state == "taken" + with pytest.raises(asyncio.TimeoutError): + await asyncio.wait_for(f.wait_for_resume(), 0.2) f.resume() - assert f.reply.state == "committed" + await f.wait_for_resume() def test_resume_duplicated(self): f = tflow() @@ -741,12 +785,7 @@ def test_timestamp_start(self): class TestHeaders: def _2host(self): - return Headers( - ( - (b"Host", b"example.com"), - (b"host", b"example.org") - ) - ) + return Headers(((b"Host", b"example.com"), (b"host", b"example.org"))) def test_init(self): headers = Headers() @@ -760,16 +799,12 @@ def test_init(self): assert len(headers) == 1 assert headers["Host"] == "example.com" - headers = Headers( - [(b"Host", b"invalid")], - Host="example.com" - ) + headers = Headers([(b"Host", b"invalid")], Host="example.com") assert len(headers) == 1 assert headers["Host"] == "example.com" headers = Headers( - [(b"Host", b"invalid"), (b"Accept", b"text/plain")], - Host="example.com" + [(b"Host", b"invalid"), (b"Accept", b"text/plain")], Host="example.com" ) assert len(headers) == 2 assert headers["Host"] == "example.com" @@ -791,44 +826,37 @@ def test_bytes(self): headers = Headers(Host="example.com") assert bytes(headers) == b"Host: example.com\r\n" - headers = Headers([ - (b"Host", b"example.com"), - (b"Accept", b"text/plain") - ]) + headers = Headers([(b"Host", b"example.com"), (b"Accept", b"text/plain")]) assert bytes(headers) == b"Host: example.com\r\nAccept: text/plain\r\n" headers = Headers() assert bytes(headers) == b"" def test_iter(self): - headers = Headers([ - (b"Set-Cookie", b"foo"), - (b"Set-Cookie", b"bar") - ]) + headers = Headers([(b"Set-Cookie", b"foo"), (b"Set-Cookie", b"bar")]) assert list(headers) == ["Set-Cookie"] def test_insert(self): headers = Headers(Accept="text/plain") headers.insert(0, b"Host", "example.com") - assert headers.fields == ( - (b'Host', b'example.com'), - (b'Accept', b'text/plain') - ) + assert headers.fields == ((b"Host", b"example.com"), (b"Accept", b"text/plain")) def test_items(self): - headers = Headers([ - (b"Set-Cookie", b"foo"), - (b"Set-Cookie", b"bar"), - (b'Accept', b'text/plain'), - ]) + headers = Headers( + [ + (b"Set-Cookie", b"foo"), + (b"Set-Cookie", b"bar"), + (b"Accept", b"text/plain"), + ] + ) assert list(headers.items()) == [ - ('Set-Cookie', 'foo, bar'), - ('Accept', 'text/plain') + ("Set-Cookie", "foo, bar"), + ("Accept", "text/plain"), ] assert list(headers.items(multi=True)) == [ - ('Set-Cookie', 'foo'), - ('Set-Cookie', 'bar'), - ('Accept', 'text/plain') + ("Set-Cookie", "foo"), + ("Set-Cookie", "bar"), + ("Accept", "text/plain"), ] @@ -844,7 +872,9 @@ def _test_decoded_attr(message, attr): setattr(message, attr, "foo") assert getattr(message.data, attr) == b"foo" # Set raw bytes, get decoded - setattr(message.data, attr, b"BAR") # use uppercase so that we can also cover request.method + setattr( + message.data, attr, b"BAR" + ) # use uppercase so that we can also cover request.method assert getattr(message, attr) == "BAR" # Set bytes, get raw bytes setattr(message, attr, b"baz") @@ -883,7 +913,6 @@ def test_serializable(self): class TestMessage: - def test_init(self): resp = tresp() assert resp.data @@ -1039,7 +1068,7 @@ def test_cannot_encode(self): class TestMessageText: def test_simple(self): - r = tresp(content=b'\xfc') + r = tresp(content=b"\xfc") assert r.raw_content == b"\xfc" assert r.content == b"\xfc" assert r.text == "ü" @@ -1061,28 +1090,37 @@ def test_guess_json(self): assert r.text == '"ü"' def test_guess_meta_charset(self): - r = tresp(content=b'\xe6\x98\x8e\xe4\xbc\xaf') + r = tresp( + content=b'\xe6\x98\x8e\xe4\xbc\xaf' + ) + r.headers["content-type"] = "text/html" # "鏄庝集" is decoded form of \xe6\x98\x8e\xe4\xbc\xaf in gb18030 assert "鏄庝集" in r.text def test_guess_css_charset(self): # @charset but not text/css - r = tresp(content=b'@charset "gb2312";' - b'#foo::before {content: "\xe6\x98\x8e\xe4\xbc\xaf"}') + r = tresp( + content=b'@charset "gb2312";' + b'#foo::before {content: "\xe6\x98\x8e\xe4\xbc\xaf"}' + ) # "鏄庝集" is decoded form of \xe6\x98\x8e\xe4\xbc\xaf in gb18030 assert "鏄庝集" not in r.text # @charset not at the beginning - r = tresp(content=b'foo@charset "gb2312";' - b'#foo::before {content: "\xe6\x98\x8e\xe4\xbc\xaf"}') + r = tresp( + content=b'foo@charset "gb2312";' + b'#foo::before {content: "\xe6\x98\x8e\xe4\xbc\xaf"}' + ) r.headers["content-type"] = "text/css" # "鏄庝集" is decoded form of \xe6\x98\x8e\xe4\xbc\xaf in gb18030 assert "鏄庝集" not in r.text # @charset and text/css - r = tresp(content=b'@charset "gb2312";' - b'#foo::before {content: "\xe6\x98\x8e\xe4\xbc\xaf"}') + r = tresp( + content=b'@charset "gb2312";' + b'#foo::before {content: "\xe6\x98\x8e\xe4\xbc\xaf"}' + ) r.headers["content-type"] = "text/css" # "鏄庝集" is decoded form of \xe6\x98\x8e\xe4\xbc\xaf in gb18030 assert "鏄庝集" in r.text @@ -1125,7 +1163,7 @@ def test_cannot_decode(self): with pytest.raises(ValueError): assert r.text - assert r.get_text(strict=False) == '\udcff' + assert r.get_text(strict=False) == "\udcff" def test_cannot_encode(self): r = tresp() @@ -1136,20 +1174,20 @@ def test_cannot_encode(self): r.headers["content-type"] = "text/html; charset=latin1; foo=bar" r.text = "☃" assert r.headers["content-type"] == "text/html; charset=utf-8; foo=bar" - assert r.raw_content == b'\xe2\x98\x83' + assert r.raw_content == b"\xe2\x98\x83" r.headers["content-type"] = "gibberish" r.text = "☃" assert r.headers["content-type"] == "text/plain; charset=utf-8" - assert r.raw_content == b'\xe2\x98\x83' + assert r.raw_content == b"\xe2\x98\x83" del r.headers["content-type"] r.text = "☃" assert r.headers["content-type"] == "text/plain; charset=utf-8" - assert r.raw_content == b'\xe2\x98\x83' + assert r.raw_content == b"\xe2\x98\x83" r.headers["content-type"] = "text/html; charset=latin1" - r.text = '\udcff' + r.text = "\udcff" assert r.headers["content-type"] == "text/html; charset=utf-8" assert r.raw_content == b"\xFF" @@ -1158,17 +1196,17 @@ def test_get_json(self): with pytest.raises(TypeError): req.json() - req = treq(content=b'') + req = treq(content=b"") with pytest.raises(json.decoder.JSONDecodeError): req.json() - req = treq(content=b'{}') + req = treq(content=b"{}") assert req.json() == {} req = treq(content=b'{"a": 1}') assert req.json() == {"a": 1} - req = treq(content=b'{') + req = treq(content=b"{") with pytest.raises(json.decoder.JSONDecodeError): req.json() diff --git a/test/mitmproxy/test_master.py b/test/mitmproxy/test_master.py index 777ab4dd18..f74fe68e2c 100644 --- a/test/mitmproxy/test_master.py +++ b/test/mitmproxy/test_master.py @@ -1 +1,16 @@ -# TODO: write tests +import asyncio + +from mitmproxy.test.taddons import RecordingMaster + + +async def err(): + raise RuntimeError + + +async def test_exception_handler(): + m = RecordingMaster(None) + running = asyncio.create_task(m.run()) + asyncio.create_task(err()) + await m.await_log("Traceback", level="error") + m.shutdown() + await running diff --git a/test/mitmproxy/test_optmanager.py b/test/mitmproxy/test_optmanager.py index d0f05678f0..769959dc47 100644 --- a/test/mitmproxy/test_optmanager.py +++ b/test/mitmproxy/test_optmanager.py @@ -1,7 +1,9 @@ import copy import io +from collections.abc import Sequence +from typing import Optional + import pytest -import typing import argparse from mitmproxy import options @@ -12,8 +14,8 @@ class TO(optmanager.OptManager): def __init__(self): super().__init__() - self.add_option("one", typing.Optional[int], None, "help") - self.add_option("two", typing.Optional[int], 2, "help") + self.add_option("one", Optional[int], None, "help") + self.add_option("two", Optional[int], 2, "help") self.add_option("bool", bool, False, "help") self.add_option("required_int", int, 2, "help") @@ -35,8 +37,8 @@ def __init__(self): class TM(optmanager.OptManager): def __init__(self): super().__init__() - self.add_option("two", typing.Sequence[str], ["foo"], "help") - self.add_option("one", typing.Optional[str], None, "help") + self.add_option("two", Sequence[str], ["foo"], "help") + self.add_option("one", Optional[str], None, "help") def test_defaults(): @@ -71,7 +73,7 @@ def test_defaults(): def test_required_int(): o = TO() with pytest.raises(exceptions.OptionsError): - o.parse_setval(o._options["required_int"], None, None) + o._parse_setval(o._options["required_int"], []) def test_deepcopy(): @@ -89,12 +91,12 @@ def test_options(): assert o.one == 1 with pytest.raises(TypeError): - TO(nonexistent = "value") + TO(nonexistent="value") with pytest.raises(Exception, match="Unknown options"): o.nonexistent = "value" with pytest.raises(Exception, match="Unknown options"): - o.update(nonexistent = "value") - assert o.update_known(nonexistent = "value") == {"nonexistent": "value"} + o.update(nonexistent="value") + assert o.update_known(nonexistent="value") == {"nonexistent": "value"} rec = [] @@ -240,7 +242,9 @@ def test_items(): def test_serialize(): - def serialize(opts: optmanager.OptManager, text: str, defaults: bool = False) -> str: + def serialize( + opts: optmanager.OptManager, text: str, defaults: bool = False + ) -> str: buf = io.StringIO() optmanager.serialize(opts, buf, text, defaults) return buf.getvalue() @@ -305,24 +309,24 @@ def test_saving(tmpdir): optmanager.load_paths(o, dst) assert o.three == "foo" - with open(dst, 'a') as f: + with open(dst, "a") as f: f.write("foobar: '123'") optmanager.load_paths(o, dst) assert o.deferred == {"foobar": "123"} - with open(dst, 'a') as f: + with open(dst, "a") as f: f.write("'''") with pytest.raises(exceptions.OptionsError): optmanager.load_paths(o, dst) - with open(dst, 'wb') as f: + with open(dst, "wb") as f: f.write(b"\x01\x02\x03") with pytest.raises(exceptions.OptionsError): optmanager.load_paths(o, dst) with pytest.raises(exceptions.OptionsError): optmanager.save(o, dst) - with open(dst, 'wb') as f: + with open(dst, "wb") as f: f.write(b"\xff\xff\xff") with pytest.raises(exceptions.OptionsError): optmanager.load_paths(o, dst) @@ -364,7 +368,7 @@ def test_dump_defaults(): def test_dump_dicts(): o = options.Options() assert optmanager.dump_dicts(o) - assert optmanager.dump_dicts(o, ['http2', 'listen_port']) + assert optmanager.dump_dicts(o, ["http2", "listen_port"]) class TTypes(optmanager.OptManager): @@ -372,12 +376,12 @@ def __init__(self): super().__init__() self.add_option("str", str, "str", "help") self.add_option("choices", str, "foo", "help", ["foo", "bar", "baz"]) - self.add_option("optstr", typing.Optional[str], "optstr", "help") + self.add_option("optstr", Optional[str], "optstr", "help") self.add_option("bool", bool, False, "help") self.add_option("bool_on", bool, True, "help") self.add_option("int", int, 0, "help") - self.add_option("optint", typing.Optional[int], 0, "help") - self.add_option("seqstr", typing.Sequence[str], [], "help") + self.add_option("optint", Optional[int], 0, "help") + self.add_option("seqstr", Sequence[str], [], "help") self.add_option("unknown", float, 0.0, "help") @@ -402,13 +406,15 @@ def test_set(): opts.set("str=foo") assert opts.str == "foo" - with pytest.raises(TypeError): + with pytest.raises(exceptions.OptionsError): opts.set("str") opts.set("optstr=foo") assert opts.optstr == "foo" opts.set("optstr") assert opts.optstr is None + with pytest.raises(exceptions.OptionsError, match="Received multiple values"): + opts.set("optstr=foo", "optstr=bar") opts.set("bool=false") assert opts.bool is False @@ -451,11 +457,11 @@ def test_set(): assert "deferredoption" not in opts.deferred assert opts.deferredoption == "wobble" - opts.set(*('deferredsequenceoption=a', 'deferredsequenceoption=b'), defer=True) + opts.set(*("deferredsequenceoption=a", "deferredsequenceoption=b"), defer=True) assert "deferredsequenceoption" in opts.deferred opts.process_deferred() assert "deferredsequenceoption" in opts.deferred - opts.add_option("deferredsequenceoption", typing.Sequence[str], [], "help") + opts.add_option("deferredsequenceoption", Sequence[str], [], "help") opts.process_deferred() assert "deferredsequenceoption" not in opts.deferred assert opts.deferredsequenceoption == ["a", "b"] diff --git a/test/mitmproxy/test_proxy.py b/test/mitmproxy/test_proxy.py index 29b415b4e6..718c367aa3 100644 --- a/test/mitmproxy/test_proxy.py +++ b/test/mitmproxy/test_proxy.py @@ -18,7 +18,6 @@ def error(self, message): class TestProcessProxyOptions: - def p(self, *args): parser = MockParser() opts = options.Options() @@ -37,6 +36,4 @@ def test_simple(self): def test_certs(self, tdata): with pytest.raises(Exception, match="ambiguous option"): - self.assert_noerr( - "--cert", - tdata.path("mitmproxy/data/testkey.pem")) + self.assert_noerr("--cert", tdata.path("mitmproxy/data/testkey.pem")) diff --git a/test/mitmproxy/test_stateobject.py b/test/mitmproxy/test_stateobject.py index a2df57fc77..8c7147de51 100644 --- a/test/mitmproxy/test_stateobject.py +++ b/test/mitmproxy/test_stateobject.py @@ -1,4 +1,4 @@ -import typing +from typing import Any import pytest @@ -17,42 +17,30 @@ def from_state(cls, state): class Child(TObject): - _stateobject_attributes = dict( - x=int - ) + _stateobject_attributes = dict(x=int) def __eq__(self, other): return isinstance(other, Child) and self.x == other.x class TTuple(TObject): - _stateobject_attributes = dict( - x=typing.Tuple[int, Child] - ) + _stateobject_attributes = dict(x=tuple[int, Child]) class TList(TObject): - _stateobject_attributes = dict( - x=typing.List[Child] - ) + _stateobject_attributes = dict(x=list[Child]) class TDict(TObject): - _stateobject_attributes = dict( - x=typing.Dict[str, Child] - ) + _stateobject_attributes = dict(x=dict[str, Child]) class TAny(TObject): - _stateobject_attributes = dict( - x=typing.Any - ) + _stateobject_attributes = dict(x=Any) class TSerializableChild(TObject): - _stateobject_attributes = dict( - x=Child - ) + _stateobject_attributes = dict(x=Child) def test_simple(): @@ -67,12 +55,8 @@ def test_simple(): def test_serializable_child(): child = Child(42) a = TSerializableChild(child) - assert a.get_state() == { - "x": {"x": 42} - } - a.set_state({ - "x": {"x": 43} - }) + assert a.get_state() == {"x": {"x": 42}} + a.set_state({"x": {"x": 43}}) assert a.x.x == 43 assert a.x is child b = a.copy() @@ -82,9 +66,7 @@ def test_serializable_child(): def test_tuple(): a = TTuple((42, Child(43))) - assert a.get_state() == { - "x": (42, {"x": 43}) - } + assert a.get_state() == {"x": (42, {"x": 43})} b = a.copy() a.set_state({"x": (44, {"x": 45})}) assert a.x == (44, Child(45)) @@ -110,9 +92,7 @@ def test_list(): def test_dict(): a = TDict({"foo": Child(42)}) - assert a.get_state() == { - "x": {"foo": {"x": 42}} - } + assert a.get_state() == {"x": {"foo": {"x": 42}}} b = a.copy() assert list(a.x.items()) == list(b.x.items()) assert a.x is not b.x @@ -132,7 +112,7 @@ def test_any(): def test_too_much_state(): a = Child(42) s = a.get_state() - s['foo'] = 'bar' + s["foo"] = "bar" with pytest.raises(RuntimeWarning): a.set_state(s) diff --git a/test/mitmproxy/test_taddons.py b/test/mitmproxy/test_taddons.py index 49c18498aa..6f0a76b9ea 100644 --- a/test/mitmproxy/test_taddons.py +++ b/test/mitmproxy/test_taddons.py @@ -1,12 +1,9 @@ import io -import pytest - from mitmproxy.test import taddons from mitmproxy import ctx -@pytest.mark.asyncio async def test_recordingmaster(): with taddons.context() as tctx: assert not tctx.master.has_log("nonexistent") @@ -15,7 +12,6 @@ async def test_recordingmaster(): await tctx.master.await_log("foo", level="error") -@pytest.mark.asyncio async def test_dumplog(): with taddons.context() as tctx: ctx.log.info("testing") @@ -27,9 +23,5 @@ async def test_dumplog(): def test_load_script(tdata): with taddons.context() as tctx: - s = tctx.script( - tdata.path( - "mitmproxy/data/addonscripts/recorder/recorder.py" - ) - ) + s = tctx.script(tdata.path("mitmproxy/data/addonscripts/recorder/recorder.py")) assert s diff --git a/test/mitmproxy/test_tcp.py b/test/mitmproxy/test_tcp.py index dce6493c00..13001c8625 100644 --- a/test/mitmproxy/test_tcp.py +++ b/test/mitmproxy/test_tcp.py @@ -6,7 +6,6 @@ class TestTCPFlow: - def test_copy(self): f = tflow.ttcpflow() f.get_state() @@ -31,7 +30,7 @@ def test_copy(self): b = m2.get_state() assert a == b - m = tcp.TCPMessage(False, 'foo') + m = tcp.TCPMessage(False, "foo") m.set_state(f.messages[0].get_state()) assert m.timestamp == f.messages[0].timestamp @@ -55,5 +54,5 @@ def test_match(self): def test_repr(self): f = tflow.ttcpflow() - assert 'TCPFlow' in repr(f) - assert '-> ' in repr(f.messages[0]) + assert "TCPFlow" in repr(f) + assert "-> " in repr(f.messages[0]) diff --git a/test/mitmproxy/test_tls.py b/test/mitmproxy/test_tls.py new file mode 100644 index 0000000000..5cfc0cf86c --- /dev/null +++ b/test/mitmproxy/test_tls.py @@ -0,0 +1,72 @@ +from mitmproxy import tls + + +CLIENT_HELLO_NO_EXTENSIONS = bytes.fromhex( + "03015658a756ab2c2bff55f636814deac086b7ca56b65058c7893ffc6074f5245f70205658a75475103a152637" + "78e1bb6d22e8bbd5b6b0a3a59760ad354e91ba20d353001a0035002f000a000500040009000300060008006000" + "61006200640100" +) +FULL_CLIENT_HELLO_NO_EXTENSIONS = ( + b"\x16\x03\x03\x00\x65" # record layer + b"\x01\x00\x00\x61" + CLIENT_HELLO_NO_EXTENSIONS # handshake header +) + + +class TestClientHello: + def test_no_extensions(self): + c = tls.ClientHello(CLIENT_HELLO_NO_EXTENSIONS) + assert repr(c) + assert c.sni is None + assert c.cipher_suites == [53, 47, 10, 5, 4, 9, 3, 6, 8, 96, 97, 98, 100] + assert c.alpn_protocols == [] + assert c.extensions == [] + assert c.raw_bytes(False) == CLIENT_HELLO_NO_EXTENSIONS + assert c.raw_bytes(True) == FULL_CLIENT_HELLO_NO_EXTENSIONS + + def test_extensions(self): + data = bytes.fromhex( + "03033b70638d2523e1cba15f8364868295305e9c52aceabda4b5147210abc783e6e1000022c02bc02fc02cc030" + "cca9cca8cc14cc13c009c013c00ac014009c009d002f0035000a0100006cff0100010000000010000e00000b65" + "78616d706c652e636f6d0017000000230000000d00120010060106030501050304010403020102030005000501" + "00000000001200000010000e000c02683208687474702f312e3175500000000b00020100000a00080006001d00" + "170018" + ) + c = tls.ClientHello(data) + assert repr(c) + assert c.sni == "example.com" + assert c.cipher_suites == [ + 49195, + 49199, + 49196, + 49200, + 52393, + 52392, + 52244, + 52243, + 49161, + 49171, + 49162, + 49172, + 156, + 157, + 47, + 53, + 10, + ] + assert c.alpn_protocols == [b"h2", b"http/1.1"] + assert c.extensions == [ + (65281, b"\x00"), + (0, b"\x00\x0e\x00\x00\x0bexample.com"), + (23, b""), + (35, b""), + ( + 13, + b"\x00\x10\x06\x01\x06\x03\x05\x01\x05\x03\x04\x01\x04\x03\x02\x01\x02\x03", + ), + (5, b"\x01\x00\x00\x00\x00"), + (18, b""), + (16, b"\x00\x0c\x02h2\x08http/1.1"), + (30032, b""), + (11, b"\x01\x00"), + (10, b"\x00\x06\x00\x1d\x00\x17\x00\x18"), + ] diff --git a/test/mitmproxy/test_types.py b/test/mitmproxy/test_types.py index e0f56995f9..04eaf4d1c9 100644 --- a/test/mitmproxy/test_types.py +++ b/test/mitmproxy/test_types.py @@ -1,6 +1,7 @@ +from collections.abc import Sequence + import pytest import os -import typing import contextlib import mitmproxy.exceptions @@ -85,7 +86,10 @@ def test_path(tdata, monkeypatch): assert b.parse(tctx.master.commands, mitmproxy.types.Path, "/bar") == "/bar" monkeypatch.setenv("HOME", "/home/test") monkeypatch.setenv("USERPROFILE", "/home/test") - assert b.parse(tctx.master.commands, mitmproxy.types.Path, "~/mitm") == "/home/test/mitm" + assert ( + b.parse(tctx.master.commands, mitmproxy.types.Path, "~/mitm") + == "/home/test/mitm" + ) assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, "foo") is True assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, "~/mitm") is True assert b.is_valid(tctx.master.commands, mitmproxy.types.Path, 3) is False @@ -93,17 +97,17 @@ def test_path(tdata, monkeypatch): def normPathOpts(prefix, match): ret = [] for s in b.completion(tctx.master.commands, mitmproxy.types.Path, match): - s = s[len(prefix):] + s = s[len(prefix) :] s = s.replace(os.sep, "/") ret.append(s) return ret cd = os.path.normpath(tdata.path("mitmproxy/completion")) - assert normPathOpts(cd, cd) == ['/aaa', '/aab', '/aac', '/bbb/'] - assert normPathOpts(cd, os.path.join(cd, "a")) == ['/aaa', '/aab', '/aac'] + assert normPathOpts(cd, cd) == ["/aaa", "/aab", "/aac", "/bbb/"] + assert normPathOpts(cd, os.path.join(cd, "a")) == ["/aaa", "/aab", "/aac"] with chdir(cd): - assert normPathOpts("", "./") == ['./aaa', './aab', './aac', './bbb/'] - assert normPathOpts("", "") == ['./aaa', './aab', './aac', './bbb/'] + assert normPathOpts("", "./") == ["./aaa", "./aab", "./aac", "./bbb/"] + assert normPathOpts("", "") == ["./aaa", "./aab", "./aac", "./bbb/"] assert b.completion( tctx.master.commands, mitmproxy.types.Path, "nonexistent" ) == ["nonexistent"] @@ -118,23 +122,32 @@ def test_cmd(): assert b.parse(tctx.master.commands, mitmproxy.types.Cmd, "cmd1") == "cmd1" with pytest.raises(mitmproxy.exceptions.TypeError): assert b.parse(tctx.master.commands, mitmproxy.types.Cmd, "foo") - assert len( - b.completion(tctx.master.commands, mitmproxy.types.Cmd, "") - ) == len(tctx.master.commands.commands.keys()) + assert len(b.completion(tctx.master.commands, mitmproxy.types.Cmd, "")) == len( + tctx.master.commands.commands.keys() + ) def test_cutspec(): with taddons.context() as tctx: b = mitmproxy.types._CutSpecType() - b.parse(tctx.master.commands, mitmproxy.types.CutSpec, "foo,bar") == ["foo", "bar"] + b.parse(tctx.master.commands, mitmproxy.types.CutSpec, "foo,bar") == [ + "foo", + "bar", + ] assert b.is_valid(tctx.master.commands, mitmproxy.types.CutSpec, 1) is False assert b.is_valid(tctx.master.commands, mitmproxy.types.CutSpec, "foo") is False - assert b.is_valid(tctx.master.commands, mitmproxy.types.CutSpec, "request.path") is True - - assert b.completion( - tctx.master.commands, mitmproxy.types.CutSpec, "request.p" - ) == b.valid_prefixes - ret = b.completion(tctx.master.commands, mitmproxy.types.CutSpec, "request.port,f") + assert ( + b.is_valid(tctx.master.commands, mitmproxy.types.CutSpec, "request.path") + is True + ) + + assert ( + b.completion(tctx.master.commands, mitmproxy.types.CutSpec, "request.p") + == b.valid_prefixes + ) + ret = b.completion( + tctx.master.commands, mitmproxy.types.CutSpec, "request.port,f" + ) assert ret[0].startswith("request.port,") assert len(ret) == len(b.valid_prefixes) @@ -142,8 +155,13 @@ def test_cutspec(): def test_marker(): with taddons.context() as tctx: b = mitmproxy.types._MarkerType() - assert b.parse(tctx.master.commands, mitmproxy.types.Marker, ":red_circle:") == ":red_circle:" - assert b.parse(tctx.master.commands, mitmproxy.types.Marker, "true") == ":default:" + assert ( + b.parse(tctx.master.commands, mitmproxy.types.Marker, ":red_circle:") + == ":red_circle:" + ) + assert ( + b.parse(tctx.master.commands, mitmproxy.types.Marker, "true") == ":default:" + ) assert b.parse(tctx.master.commands, mitmproxy.types.Marker, "false") == "" with pytest.raises(mitmproxy.exceptions.TypeError): @@ -151,9 +169,14 @@ def test_marker(): assert b.is_valid(tctx.master.commands, mitmproxy.types.Marker, "true") is True assert b.is_valid(tctx.master.commands, mitmproxy.types.Marker, "false") is True - assert b.is_valid(tctx.master.commands, mitmproxy.types.Marker, "bogus") is False + assert ( + b.is_valid(tctx.master.commands, mitmproxy.types.Marker, "bogus") is False + ) assert b.is_valid(tctx.master.commands, mitmproxy.types.Marker, "X") is True - assert b.is_valid(tctx.master.commands, mitmproxy.types.Marker, ":red_circle:") is True + assert ( + b.is_valid(tctx.master.commands, mitmproxy.types.Marker, ":red_circle:") + is True + ) ret = b.completion(tctx.master.commands, mitmproxy.types.Marker, ":smil") assert len(ret) > 10 @@ -169,21 +192,24 @@ def test_arg(): def test_strseq(): with taddons.context() as tctx: b = mitmproxy.types._StrSeqType() - assert b.completion(tctx.master.commands, typing.Sequence[str], "") == [] - assert b.parse(tctx.master.commands, typing.Sequence[str], "foo") == ["foo"] - assert b.parse(tctx.master.commands, typing.Sequence[str], "foo,bar") == ["foo", "bar"] - assert b.is_valid(tctx.master.commands, typing.Sequence[str], ["foo"]) is True - assert b.is_valid(tctx.master.commands, typing.Sequence[str], ["a", "b", 3]) is False - assert b.is_valid(tctx.master.commands, typing.Sequence[str], 1) is False - assert b.is_valid(tctx.master.commands, typing.Sequence[str], "foo") is False + assert b.completion(tctx.master.commands, Sequence[str], "") == [] + assert b.parse(tctx.master.commands, Sequence[str], "foo") == ["foo"] + assert b.parse(tctx.master.commands, Sequence[str], "foo,bar") == ["foo", "bar"] + assert b.is_valid(tctx.master.commands, Sequence[str], ["foo"]) is True + assert b.is_valid(tctx.master.commands, Sequence[str], ["a", "b", 3]) is False + assert b.is_valid(tctx.master.commands, Sequence[str], 1) is False + assert b.is_valid(tctx.master.commands, Sequence[str], "foo") is False class DummyConsole: @command.command("view.flows.resolve") - def resolve(self, spec: str) -> typing.Sequence[flow.Flow]: + def resolve(self, spec: str) -> Sequence[flow.Flow]: if spec == "err": raise mitmproxy.exceptions.CommandError() - n = int(spec) + try: + n = int(spec) + except ValueError: + n = 1 return [tflow.tflow(resp=True)] * n @command.command("cut") @@ -191,7 +217,7 @@ def cut(self, spec: str) -> mitmproxy.types.Data: return [["test"]] @command.command("options") - def options(self) -> typing.Sequence[str]: + def options(self) -> Sequence[str]: return ["one", "two", "three"] @@ -199,8 +225,11 @@ def test_flow(): with taddons.context() as tctx: tctx.master.addons.add(DummyConsole()) b = mitmproxy.types._FlowType() - assert len(b.completion(tctx.master.commands, flow.Flow, "")) == len(b.valid_prefixes) + assert len(b.completion(tctx.master.commands, flow.Flow, "")) == len( + b.valid_prefixes + ) assert b.parse(tctx.master.commands, flow.Flow, "1") + assert b.parse(tctx.master.commands, flow.Flow, "has space") assert b.is_valid(tctx.master.commands, flow.Flow, tflow.tflow()) is True assert b.is_valid(tctx.master.commands, flow.Flow, "xx") is False with pytest.raises(mitmproxy.exceptions.TypeError): @@ -215,17 +244,21 @@ def test_flows(): with taddons.context() as tctx: tctx.master.addons.add(DummyConsole()) b = mitmproxy.types._FlowsType() - assert len( - b.completion(tctx.master.commands, typing.Sequence[flow.Flow], "") - ) == len(b.valid_prefixes) - assert b.is_valid(tctx.master.commands, typing.Sequence[flow.Flow], [tflow.tflow()]) is True - assert b.is_valid(tctx.master.commands, typing.Sequence[flow.Flow], "xx") is False - assert b.is_valid(tctx.master.commands, typing.Sequence[flow.Flow], 0) is False - assert len(b.parse(tctx.master.commands, typing.Sequence[flow.Flow], "0")) == 0 - assert len(b.parse(tctx.master.commands, typing.Sequence[flow.Flow], "1")) == 1 - assert len(b.parse(tctx.master.commands, typing.Sequence[flow.Flow], "2")) == 2 + assert len(b.completion(tctx.master.commands, Sequence[flow.Flow], "")) == len( + b.valid_prefixes + ) + assert ( + b.is_valid(tctx.master.commands, Sequence[flow.Flow], [tflow.tflow()]) + is True + ) + assert b.is_valid(tctx.master.commands, Sequence[flow.Flow], "xx") is False + assert b.is_valid(tctx.master.commands, Sequence[flow.Flow], 0) is False + assert len(b.parse(tctx.master.commands, Sequence[flow.Flow], "0")) == 0 + assert len(b.parse(tctx.master.commands, Sequence[flow.Flow], "1")) == 1 + assert len(b.parse(tctx.master.commands, Sequence[flow.Flow], "2")) == 2 + assert len(b.parse(tctx.master.commands, Sequence[flow.Flow], "has space")) == 1 with pytest.raises(mitmproxy.exceptions.TypeError): - b.parse(tctx.master.commands, typing.Sequence[flow.Flow], "err") + b.parse(tctx.master.commands, Sequence[flow.Flow], "err") def test_data(): @@ -246,24 +279,36 @@ def test_choice(): with taddons.context() as tctx: tctx.master.addons.add(DummyConsole()) b = mitmproxy.types._ChoiceType() - assert b.is_valid( - tctx.master.commands, - mitmproxy.types.Choice("options"), - "one", - ) is True - assert b.is_valid( - tctx.master.commands, - mitmproxy.types.Choice("options"), - "invalid", - ) is False - assert b.is_valid( - tctx.master.commands, - mitmproxy.types.Choice("nonexistent"), - "invalid", - ) is False + assert ( + b.is_valid( + tctx.master.commands, + mitmproxy.types.Choice("options"), + "one", + ) + is True + ) + assert ( + b.is_valid( + tctx.master.commands, + mitmproxy.types.Choice("options"), + "invalid", + ) + is False + ) + assert ( + b.is_valid( + tctx.master.commands, + mitmproxy.types.Choice("nonexistent"), + "invalid", + ) + is False + ) comp = b.completion(tctx.master.commands, mitmproxy.types.Choice("options"), "") assert comp == ["one", "two", "three"] - assert b.parse(tctx.master.commands, mitmproxy.types.Choice("options"), "one") == "one" + assert ( + b.parse(tctx.master.commands, mitmproxy.types.Choice("options"), "one") + == "one" + ) with pytest.raises(mitmproxy.exceptions.TypeError): b.parse(tctx.master.commands, mitmproxy.types.Choice("options"), "invalid") diff --git a/test/mitmproxy/test_version.py b/test/mitmproxy/test_version.py index f6595c5277..1e72ab4ed4 100644 --- a/test/mitmproxy/test_version.py +++ b/test/mitmproxy/test_version.py @@ -10,7 +10,7 @@ def test_version(capsys): here = pathlib.Path(__file__).absolute().parent version_file = here / ".." / ".." / "mitmproxy" / "version.py" - runpy.run_path(str(version_file), run_name='__main__') + runpy.run_path(str(version_file), run_name="__main__") stdout, stderr = capsys.readouterr() assert len(stdout) > 0 assert stdout.strip() == version.VERSION @@ -19,7 +19,7 @@ def test_version(capsys): def test_get_version(): version.VERSION = "3.0.0rc2" - with mock.patch('subprocess.check_output') as m, mock.patch('subprocess.run') as m2: + with mock.patch("subprocess.check_output") as m, mock.patch("subprocess.run") as m2: m2.return_value = True m.return_value = b"tag-0-cafecafe" @@ -32,5 +32,5 @@ def test_get_version(): m.return_value = b"tag-2-cafecafe" assert version.get_dev_version() == "3.0.0rc2 (+2, commit cafecaf)" - m.side_effect = subprocess.CalledProcessError(-1, 'git describe --tags --long') + m.side_effect = subprocess.CalledProcessError(-1, "git describe --tags --long") assert version.get_dev_version() == "3.0.0rc2" diff --git a/test/mitmproxy/tools/console/conftest.py b/test/mitmproxy/tools/console/conftest.py new file mode 100644 index 0000000000..0be6eef146 --- /dev/null +++ b/test/mitmproxy/tools/console/conftest.py @@ -0,0 +1,47 @@ +import re +import sys + +import pytest + +from mitmproxy import options +from mitmproxy.tools.console import window +from mitmproxy.tools.console.master import ConsoleMaster + + +def tokenize(input: str) -> list[str]: + keys = [] + for i, k in enumerate(re.split("[<>]", input)): + if i % 2: + keys.append(k) + else: + keys.extend(k) + return keys + + +class ConsoleTestMaster(ConsoleMaster): + def type(self, input: str) -> None: + for key in tokenize(input): + 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() + + +@pytest.fixture +def console(monkeypatch) -> ConsoleTestMaster: + """Stupid workaround for https://youtrack.jetbrains.com/issue/PY-30279/""" + + +@pytest.fixture +async def console(monkeypatch) -> ConsoleTestMaster: # noqa + # monkeypatch.setattr(window.Screen, "get_cols_rows", lambda self: (120, 120)) + monkeypatch.setattr(window.Screen, "start", lambda *_: True) + monkeypatch.setattr(ConsoleTestMaster, "sig_call_in", lambda *_, **__: True) + monkeypatch.setattr(sys.stdout, "isatty", lambda: True) + + opts = options.Options() + m = ConsoleTestMaster(opts) + opts.server = False + opts.console_mouse = False + await m.running() + return m diff --git a/test/mitmproxy/tools/console/test_commander.py b/test/mitmproxy/tools/console/test_commander.py index a297fcf7a1..e77ae518a4 100644 --- a/test/mitmproxy/tools/console/test_commander.py +++ b/test/mitmproxy/tools/console/test_commander.py @@ -9,7 +9,7 @@ @pytest.fixture(autouse=True) def commander_tctx(tmpdir): # This runs before each test - dir_name = tmpdir.mkdir('mitmproxy').dirname + dir_name = tmpdir.mkdir("mitmproxy").dirname confdir = dir_name opts = options.Options() @@ -17,7 +17,7 @@ def commander_tctx(tmpdir): commander_tctx = taddons.context(options=opts) ch = command_history.CommandHistory() commander_tctx.master.addons.add(ch) - ch.configure('command_history') + ch.configure("command_history") yield commander_tctx @@ -33,21 +33,21 @@ def test_cycle(self): ["a", "b", "c"], ["a", "b", "c", "a"], ["c", "b", "a", "c"], - ["a", "c", "a", "c"] + ["a", "c", "a", "c"], ], [ "xxx", ["a", "b", "c"], ["xxx", "xxx", "xxx"], ["xxx", "xxx", "xxx"], - ["xxx", "xxx", "xxx"] + ["xxx", "xxx", "xxx"], ], [ "b", ["a", "b", "ba", "bb", "c"], ["b", "ba", "bb", "b"], ["bb", "ba", "b", "bb"], - ["b", "bb", "b", "bb"] + ["b", "bb", "b", "bb"], ], ] for start, opts, cycle, cycle_reverse, cycle_mix in tests: @@ -63,9 +63,8 @@ def test_cycle(self): class TestCommandEdit: - def test_open_command_bar(self, commander_tctx): - edit = commander.CommandEdit(commander_tctx.master, '') + edit = commander.CommandEdit(commander_tctx.master, "") try: edit.update() @@ -73,224 +72,223 @@ def test_open_command_bar(self, commander_tctx): pytest.faied("Unexpected IndexError") def test_insert(self, commander_tctx): - edit = commander.CommandEdit(commander_tctx.master, '') - edit.keypress(1, 'a') - assert edit.get_edit_text() == 'a' + edit = commander.CommandEdit(commander_tctx.master, "") + edit.keypress(1, "a") + assert edit.get_edit_text() == "a" # Don't let users type a space before starting a command # as a usability feature - edit = commander.CommandEdit(commander_tctx.master, '') - edit.keypress(1, ' ') - assert edit.get_edit_text() == '' + edit = commander.CommandEdit(commander_tctx.master, "") + edit.keypress(1, " ") + assert edit.get_edit_text() == "" def test_backspace(self, commander_tctx): - edit = commander.CommandEdit(commander_tctx.master, '') + edit = commander.CommandEdit(commander_tctx.master, "") - edit.keypress(1, 'a') - edit.keypress(1, 'b') - assert edit.get_edit_text() == 'ab' + edit.keypress(1, "a") + edit.keypress(1, "b") + assert edit.get_edit_text() == "ab" - edit.keypress(1, 'backspace') - assert edit.get_edit_text() == 'a' + edit.keypress(1, "backspace") + assert edit.get_edit_text() == "a" def test_left(self, commander_tctx): - edit = commander.CommandEdit(commander_tctx.master, '') + edit = commander.CommandEdit(commander_tctx.master, "") - edit.keypress(1, 'a') + edit.keypress(1, "a") assert edit.cbuf.cursor == 1 - edit.keypress(1, 'left') + edit.keypress(1, "left") assert edit.cbuf.cursor == 0 # Do it again to make sure it won't go negative - edit.keypress(1, 'left') + edit.keypress(1, "left") assert edit.cbuf.cursor == 0 def test_right(self, commander_tctx): - edit = commander.CommandEdit(commander_tctx.master, '') + edit = commander.CommandEdit(commander_tctx.master, "") - edit.keypress(1, 'a') + edit.keypress(1, "a") assert edit.cbuf.cursor == 1 # Make sure cursor won't go past the text - edit.keypress(1, 'right') + edit.keypress(1, "right") assert edit.cbuf.cursor == 1 # Make sure cursor goes left and then back right - edit.keypress(1, 'left') + edit.keypress(1, "left") assert edit.cbuf.cursor == 0 - edit.keypress(1, 'right') + edit.keypress(1, "right") assert edit.cbuf.cursor == 1 def test_up_and_down(self, commander_tctx): - edit = commander.CommandEdit(commander_tctx.master, '') + edit = commander.CommandEdit(commander_tctx.master, "") - commander_tctx.master.commands.execute('commands.history.clear') + commander_tctx.master.commands.execute("commands.history.clear") commander_tctx.master.commands.execute('commands.history.add "cmd1"') - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd1" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd1" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit = commander.CommandEdit(commander_tctx.master, '') + edit = commander.CommandEdit(commander_tctx.master, "") - commander_tctx.master.commands.execute('commands.history.clear') + commander_tctx.master.commands.execute("commands.history.clear") commander_tctx.master.commands.execute('commands.history.add "cmd1"') commander_tctx.master.commands.execute('commands.history.add "cmd2"') - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd2' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd2" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd1" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd1" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'cmd2' + edit.keypress(1, "down") + assert edit.get_edit_text() == "cmd2" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit.keypress(1, 'a') - edit.keypress(1, 'b') - edit.keypress(1, 'c') - assert edit.get_edit_text() == 'abc' + edit.keypress(1, "a") + edit.keypress(1, "b") + edit.keypress(1, "c") + assert edit.get_edit_text() == "abc" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'abc' + edit.keypress(1, "up") + assert edit.get_edit_text() == "abc" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'abc' + edit.keypress(1, "down") + assert edit.get_edit_text() == "abc" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'abc' + edit.keypress(1, "down") + assert edit.get_edit_text() == "abc" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'abc' + edit.keypress(1, "up") + assert edit.get_edit_text() == "abc" - edit = commander.CommandEdit(commander_tctx.master, '') + edit = commander.CommandEdit(commander_tctx.master, "") commander_tctx.master.commands.execute('commands.history.add "cmd3"') - edit.keypress(1, 'z') - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'z' + edit.keypress(1, "z") + edit.keypress(1, "up") + assert edit.get_edit_text() == "z" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'z' + edit.keypress(1, "down") + assert edit.get_edit_text() == "z" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'z' + edit.keypress(1, "down") + assert edit.get_edit_text() == "z" - edit.keypress(1, 'backspace') - assert edit.get_edit_text() == '' + edit.keypress(1, "backspace") + assert edit.get_edit_text() == "" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd3' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd3" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd2' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd2" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd1" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'cmd2' + edit.keypress(1, "down") + assert edit.get_edit_text() == "cmd2" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'cmd3' + edit.keypress(1, "down") + assert edit.get_edit_text() == "cmd3" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit.keypress(1, 'c') - assert edit.get_edit_text() == 'c' + edit.keypress(1, "c") + assert edit.get_edit_text() == "c" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd3' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd3" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd3' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd3" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd2' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd2" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'cmd3' + edit.keypress(1, "down") + assert edit.get_edit_text() == "cmd3" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd3' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd3" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd2' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd2" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd1" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd1" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd1" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'cmd2' + edit.keypress(1, "down") + assert edit.get_edit_text() == "cmd2" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'cmd3' + edit.keypress(1, "down") + assert edit.get_edit_text() == "cmd3" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" - edit.keypress(1, 'backspace') - assert edit.get_edit_text() == '' + edit.keypress(1, "backspace") + assert edit.get_edit_text() == "" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd3' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd3" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd2' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd2" - edit.keypress(1, 'up') - assert edit.get_edit_text() == 'cmd1' + edit.keypress(1, "up") + assert edit.get_edit_text() == "cmd1" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'cmd2' + edit.keypress(1, "down") + assert edit.get_edit_text() == "cmd2" - edit.keypress(1, 'down') - assert edit.get_edit_text() == 'cmd3' + edit.keypress(1, "down") + assert edit.get_edit_text() == "cmd3" - edit.keypress(1, 'down') - assert edit.get_edit_text() == '' + edit.keypress(1, "down") + assert edit.get_edit_text() == "" class TestCommandBuffer: - def test_backspace(self): tests = [ [("", 0), ("", 0)], @@ -348,14 +346,14 @@ def test_cycle_completion(self): cb.cycle_completion() ce = commander.CommandEdit(commander_tctx.master, "se") - ce.keypress(1, 'tab') + ce.keypress(1, "tab") ce.update() ret = ce.cbuf.render() assert ret == [ - ('commander_command', 'set'), - ('text', ' '), - ('commander_hint', 'option '), - ('commander_hint', 'value '), + ("commander_command", "set"), + ("text", " "), + ("commander_hint", "option "), + ("commander_hint", "*value "), ] def test_render(self): @@ -367,18 +365,18 @@ def test_render(self): cb.text = "set view_filter '~bq test'" ret = cb.render() assert ret == [ - ('commander_command', 'set'), - ('text', ' '), - ('text', 'view_filter'), - ('text', ' '), - ('text', "'~bq test'"), + ("commander_command", "set"), + ("text", " "), + ("text", "view_filter"), + ("text", " "), + ("text", "'~bq test'"), ] cb.text = "set" ret = cb.render() assert ret == [ - ('commander_command', 'set'), - ('text', ' '), - ('commander_hint', 'option '), - ('commander_hint', 'value '), + ("commander_command", "set"), + ("text", " "), + ("commander_hint", "option "), + ("commander_hint", "*value "), ] diff --git a/test/mitmproxy/tools/console/test_common.py b/test/mitmproxy/tools/console/test_common.py index 0ae897d2ef..b7461bad98 100644 --- a/test/mitmproxy/tools/console/test_common.py +++ b/test/mitmproxy/tools/console/test_common.py @@ -5,16 +5,12 @@ def test_format_flow(): - flows = [ - tflow.tflow(resp=True), - tflow.tflow(err=True), - tflow.ttcpflow(), - tflow.ttcpflow(err=True), - ] - for f in flows: + for f in tflow.tflows(): for render_mode in common.RenderMode: assert common.format_flow(f, render_mode=render_mode) - assert common.format_flow(f, render_mode=render_mode, hostheader=True, focused=False) + assert common.format_flow( + f, render_mode=render_mode, hostheader=True, focused=False + ) def test_format_keyvals(): @@ -26,13 +22,15 @@ def test_format_keyvals(): ] ) wrapped = urwid.Pile( - urwid.SimpleFocusListWalker( - common.format_keyvals([("foo", "bar")]) - ) + urwid.SimpleFocusListWalker(common.format_keyvals([("foo", "bar")])) ) assert wrapped.render((30,)) - assert common.format_keyvals( - [ - ("aa", wrapped) - ] - ) + assert common.format_keyvals([("aa", wrapped)]) + + +def test_truncated_text(): + urwid.set_encoding("utf8") + half_width_text = common.TruncatedText("Half-width", []) + full_width_text = common.TruncatedText("FULL-WIDTH", []) + assert half_width_text.render((10,)) + assert full_width_text.render((10,)) diff --git a/test/mitmproxy/tools/console/test_contentview.py b/test/mitmproxy/tools/console/test_contentview.py new file mode 100644 index 0000000000..9819ea9a2e --- /dev/null +++ b/test/mitmproxy/tools/console/test_contentview.py @@ -0,0 +1,39 @@ +from mitmproxy.test import tflow +from mitmproxy import contentviews +from mitmproxy.contentviews.base import format_text + + +class TContentView(contentviews.View): + name = "Test View" + + def __call__(self, data, **metadata): + return "TContentView", format_text("test_content") + + def render_priority(self, data, *, content_type=None, **metadata) -> float: + return 2 + + +async def test_contentview_flowview(console): + assert "Flows" in console.screen_contents() + flow = tflow.tflow() + flow.request.headers["content-type"] = "text/html" + await console.load_flow(flow) + assert ">>" in console.screen_contents() + console.type("") + assert "Flow Details" in console.screen_contents() + assert "XML" in console.screen_contents() + + view = TContentView() + contentviews.add(view) + assert "XML" not in console.screen_contents() + assert "TContentView" in console.screen_contents() + contentviews.remove(view) + assert "XML" in console.screen_contents() + assert "TContentView" not in console.screen_contents() + + console.type("q") + assert "Flows" in console.screen_contents() + contentviews.add(view) + console.type("") + assert "Flow Details" in console.screen_contents() + assert "TContentView" in console.screen_contents() diff --git a/test/mitmproxy/tools/console/test_defaultkeys.py b/test/mitmproxy/tools/console/test_defaultkeys.py index 389a8eefeb..b2c44411d1 100644 --- a/test/mitmproxy/tools/console/test_defaultkeys.py +++ b/test/mitmproxy/tools/console/test_defaultkeys.py @@ -1,5 +1,3 @@ -import pytest - import mitmproxy.types from mitmproxy import command from mitmproxy import ctx @@ -9,7 +7,6 @@ from mitmproxy.tools.console import master -@pytest.mark.asyncio async def test_commands_exist(): command_manager = command.CommandManager(ctx) @@ -24,10 +21,7 @@ async def test_commands_exist(): parsed, _ = command_manager.parse_partial(binding.command.strip()) cmd = parsed[0].value - args = [ - a.value for a in parsed[1:] - if a.type != mitmproxy.types.Space - ] + args = [a.value for a in parsed[1:] if a.type != mitmproxy.types.Space] assert cmd in m.commands.commands diff --git a/test/mitmproxy/tools/console/test_flowview.py b/test/mitmproxy/tools/console/test_flowview.py new file mode 100644 index 0000000000..89c6c5a5e1 --- /dev/null +++ b/test/mitmproxy/tools/console/test_flowview.py @@ -0,0 +1,8 @@ +from mitmproxy.test import tflow + + +async def test_flowview(monkeypatch, console): + for f in tflow.tflows(): + console.commands.call("view.clear") + await console.load_flow(f) + console.type("") diff --git a/test/mitmproxy/tools/console/test_integration.py b/test/mitmproxy/tools/console/test_integration.py index ed80bad2fd..5eaf32e993 100644 --- a/test/mitmproxy/tools/console/test_integration.py +++ b/test/mitmproxy/tools/console/test_integration.py @@ -1,56 +1,22 @@ -import re -import sys -from typing import List - -import pytest - -import mitmproxy.options -from mitmproxy import master -from mitmproxy.tools.console import window -from mitmproxy.tools.console.master import ConsoleMaster - - -def tokenize(input: str) -> List[str]: - keys = [] - for i, k in enumerate(re.split("[<>]", input)): - if i % 2: - keys.append(k) - else: - keys.extend(k) - return keys - - -class ConsoleTestMaster(ConsoleMaster): - def type(self, input: str) -> None: - for key in tokenize(input): - self.window.keypress(self.ui.get_cols_rows(), key) - - -@pytest.fixture -def console(monkeypatch): - monkeypatch.setattr(window.Screen, "get_cols_rows", lambda self: (120, 120)) - monkeypatch.setattr(master.Master, "run_loop", lambda *_: True) - monkeypatch.setattr(ConsoleTestMaster, "sig_call_in", lambda *_, **__: True) - monkeypatch.setattr(sys.stdout, "isatty", lambda: True) - - opts = mitmproxy.options.Options() - m = ConsoleTestMaster(opts) - m.run() - return m - - -@pytest.mark.asyncio def test_integration(tdata, console): - console.type(f":view.flows.load {tdata.path('mitmproxy/data/dumpfile-7.mitm')}") + console.type( + f":view.flows.load {tdata.path('mitmproxy/data/dumpfile-7.mitm')}" + ) console.type("") console.type("") # view second flow + assert "http://example.com/" in console.screen_contents() -@pytest.mark.asyncio def test_options_home_end(console): console.type("O") + assert "Options" in console.screen_contents() -@pytest.mark.asyncio def test_keybindings_home_end(console): console.type("K") + assert "Key Binding" in console.screen_contents() + + +def test_replay_count(console): + console.type(":replay.server.count") + assert "Data viewer" in console.screen_contents() diff --git a/test/mitmproxy/tools/console/test_keymap.py b/test/mitmproxy/tools/console/test_keymap.py index 1e5400be25..12e12f7c0d 100644 --- a/test/mitmproxy/tools/console/test_keymap.py +++ b/test/mitmproxy/tools/console/test_keymap.py @@ -80,17 +80,17 @@ def test_load_path(tmpdir): km = keymap.Keymap(tctx.master) tctx.master.keymap = km - with open(dst, 'wb') as f: + with open(dst, "wb") as f: f.write(b"\xff\xff\xff") with pytest.raises(keymap.KeyBindingError, match="expected UTF8"): kmc.load_path(km, dst) - with open(dst, 'w') as f: + with open(dst, "w") as f: f.write("'''") with pytest.raises(keymap.KeyBindingError): kmc.load_path(km, dst) - with open(dst, 'w') as f: + with open(dst, "w") as f: f.write( """ - key: key1 @@ -103,7 +103,7 @@ def test_load_path(tmpdir): with pytest.raises(keymap.KeyBindingError): kmc.load_path(km, dst) - with open(dst, 'w') as f: + with open(dst, "w") as f: f.write( """ - key: key1 @@ -115,9 +115,9 @@ def test_load_path(tmpdir): """ ) kmc.load_path(km, dst) - assert(km.get("chooser", "key1")) + assert km.get("chooser", "key1") - with open(dst, 'w') as f: + with open(dst, "w") as f: f.write( """ - key: key2 @@ -129,11 +129,11 @@ def test_load_path(tmpdir): """ ) kmc.load_path(km, dst) - assert(km.get("flowlist", "key2")) - assert(km.get("flowview", "key2")) + assert km.get("flowlist", "key2") + assert km.get("flowview", "key2") km.add("key123", "str", ["flowlist", "flowview"]) - with open(dst, 'w') as f: + with open(dst, "w") as f: f.write( """ - key: key123 @@ -142,9 +142,9 @@ def test_load_path(tmpdir): """ ) kmc.load_path(km, dst) - assert(km.get("flowlist", "key123")) - assert(km.get("flowview", "key123")) - assert(km.get("options", "key123")) + assert km.get("flowlist", "key123") + assert km.get("flowview", "key123") + assert km.get("options", "key123") def test_parse(): @@ -163,7 +163,9 @@ def test_parse(): nonexistent: bar """ ) - with pytest.raises(keymap.KeyBindingError, match="Missing required key attributes"): + with pytest.raises( + keymap.KeyBindingError, match="Missing required key attributes" + ): kmc.parse( """ - help: key1 @@ -184,8 +186,9 @@ def test_parse(): cmd: cmd """ ) - assert kmc.parse( - """ + assert ( + kmc.parse( + """ - key: key1 ctx: [one, two] help: one @@ -193,4 +196,13 @@ def test_parse(): foo bar foo bar """ - ) == [{"key": "key1", "ctx": ["one", "two"], "help": "one", "cmd": "foo bar foo bar\n"}] \ No newline at end of file + ) + == [ + { + "key": "key1", + "ctx": ["one", "two"], + "help": "one", + "cmd": "foo bar foo bar\n", + } + ] + ) diff --git a/test/mitmproxy/tools/console/test_master.py b/test/mitmproxy/tools/console/test_master.py index 6df8b57f83..e69de29bb2 100644 --- a/test/mitmproxy/tools/console/test_master.py +++ b/test/mitmproxy/tools/console/test_master.py @@ -1,26 +0,0 @@ -import urwid - -import pytest - -from mitmproxy import options, hooks -from mitmproxy.tools import console - -from ... import tservers - - -@pytest.mark.asyncio -class TestMaster(tservers.MasterTest): - def mkmaster(self, **opts): - o = options.Options(**opts) - m = console.master.ConsoleMaster(o) - m.addons.trigger(hooks.ConfigureHook(o.keys())) - return m - - async def test_basic(self): - m = self.mkmaster() - for i in (1, 2, 3): - try: - await self.dummy_cycle(m, 1, b"") - except urwid.ExitMainLoop: - pass - assert len(m.view) == i diff --git a/test/mitmproxy/tools/console/test_palettes.py b/test/mitmproxy/tools/console/test_palettes.py index 8581193b51..05d3c274d4 100644 --- a/test/mitmproxy/tools/console/test_palettes.py +++ b/test/mitmproxy/tools/console/test_palettes.py @@ -2,7 +2,6 @@ class TestPalette: - def test_helptext(self): for i in palettes.palettes.values(): assert i.palette(False) diff --git a/test/mitmproxy/tools/console/test_statusbar.py b/test/mitmproxy/tools/console/test_statusbar.py index 7ca78e263a..a54b983fe3 100644 --- a/test/mitmproxy/tools/console/test_statusbar.py +++ b/test/mitmproxy/tools/console/test_statusbar.py @@ -4,7 +4,7 @@ from mitmproxy.tools.console import statusbar, master -def test_statusbar(monkeypatch): +async def test_statusbar(monkeypatch): o = options.Options() m = master.ConsoleMaster(o) m.options.update( @@ -27,7 +27,7 @@ def test_statusbar(monkeypatch): mode="transparent", ) - m.options.update(view_order='url', console_focus_follow=True) + m.options.update(view_order="url", console_focus_follow=True) monkeypatch.setattr(m.addons.get("clientplayback"), "count", lambda: 42) monkeypatch.setattr(m.addons.get("serverplayback"), "count", lambda: 42) monkeypatch.setattr(statusbar.StatusBar, "refresh", lambda x: None) @@ -36,27 +36,32 @@ def test_statusbar(monkeypatch): assert bar.ib._w -@pytest.mark.parametrize("message,ready_message", [ - ("", [(None, ""), ("warn", "")]), - (("info", "Line fits into statusbar"), [("info", "Line fits into statusbar"), - ("warn", "")]), - ("Line doesn't fit into statusbar", [(None, "Line doesn'\u2026"), - ("warn", "(more in eventlog)")]), - (("alert", "Two lines.\nFirst fits"), [("alert", "Two lines."), - ("warn", "(more in eventlog)")]), - ("Two long lines\nFirst doesn't fit", [(None, "Two long li\u2026"), - ("warn", "(more in eventlog)")]) -]) +@pytest.mark.parametrize( + "message,ready_message", + [ + ("", [(None, ""), ("warn", "")]), + ( + ("info", "Line fits into statusbar"), + [("info", "Line fits into statusbar"), ("warn", "")], + ), + ( + "Line doesn't fit into statusbar", + [(None, "Line doesn'\u2026"), ("warn", "(more in eventlog)")], + ), + ( + ("alert", "Two lines.\nFirst fits"), + [("alert", "Two lines."), ("warn", "(more in eventlog)")], + ), + ( + "Two long lines\nFirst doesn't fit", + [(None, "Two long li\u2026"), ("warn", "(more in eventlog)")], + ), + ], +) def test_shorten_message(message, ready_message): - o = options.Options() - m = master.ConsoleMaster(o) - ab = statusbar.ActionBar(m) - assert ab.shorten_message(message, max_width=30) == ready_message + assert statusbar.ActionBar.shorten_message(message, max_width=30) == ready_message def test_shorten_message_narrow(): - o = options.Options() - m = master.ConsoleMaster(o) - ab = statusbar.ActionBar(m) - shorten_msg = ab.shorten_message("error", max_width=4) + shorten_msg = statusbar.ActionBar.shorten_message("error", max_width=4) assert shorten_msg == [(None, "\u2026"), ("warn", "(more in eventlog)")] diff --git a/test/mitmproxy/tools/test_cmdline.py b/test/mitmproxy/tools/test_cmdline.py index d644f594d5..d0b237d7e3 100644 --- a/test/mitmproxy/tools/test_cmdline.py +++ b/test/mitmproxy/tools/test_cmdline.py @@ -1,7 +1,7 @@ import argparse from mitmproxy import options -from mitmproxy.tools import cmdline, web, dump, console +from mitmproxy.tools import cmdline from mitmproxy.tools import main @@ -15,20 +15,17 @@ def test_common(): def test_mitmproxy(): opts = options.Options() - console.master.ConsoleMaster(opts) ap = cmdline.mitmproxy(opts) assert ap def test_mitmdump(): opts = options.Options() - dump.DumpMaster(opts) ap = cmdline.mitmdump(opts) assert ap def test_mitmweb(): opts = options.Options() - web.master.WebMaster(opts) ap = cmdline.mitmweb(opts) assert ap diff --git a/test/mitmproxy/tools/test_dump.py b/test/mitmproxy/tools/test_dump.py index 9cdaf83ef8..e84857a163 100644 --- a/test/mitmproxy/tools/test_dump.py +++ b/test/mitmproxy/tools/test_dump.py @@ -2,8 +2,6 @@ import pytest -from mitmproxy import controller -from mitmproxy import log from mitmproxy import options from mitmproxy.tools import dump @@ -14,23 +12,16 @@ def mkmaster(self, **opts): m = dump.DumpMaster(o, with_termlog=False, with_dumper=False) return m - def test_has_error(self): - m = self.mkmaster() - ent = log.LogEntry("foo", "error") - ent.reply = controller.DummyReply() - m.addons.trigger(log.AddLogHook(ent)) - assert m.errorcheck.has_errored - @pytest.mark.parametrize("termlog", [False, True]) - def test_addons_termlog(self, termlog): - with mock.patch('sys.stdout'): + async def test_addons_termlog(self, termlog): + with mock.patch("sys.stdout"): o = options.Options() m = dump.DumpMaster(o, with_termlog=termlog) - assert (m.addons.get('termlog') is not None) == termlog + assert (m.addons.get("termlog") is not None) == termlog @pytest.mark.parametrize("dumper", [False, True]) - def test_addons_dumper(self, dumper): - with mock.patch('sys.stdout'): + async def test_addons_dumper(self, dumper): + with mock.patch("sys.stdout"): o = options.Options() m = dump.DumpMaster(o, with_dumper=dumper) - assert (m.addons.get('dumper') is not None) == dumper + assert (m.addons.get("dumper") is not None) == dumper diff --git a/test/mitmproxy/tools/test_main.py b/test/mitmproxy/tools/test_main.py index 7572d36494..41437d89bb 100644 --- a/test/mitmproxy/tools/test_main.py +++ b/test/mitmproxy/tools/test_main.py @@ -8,16 +8,28 @@ def test_mitmweb(event_loop, tdata): asyncio.set_event_loop(event_loop) - main.mitmweb([ - "--no-web-open-browser", - "-s", tdata.path(shutdown_script), - "-q", "-p", "0", "--web-port", "0", - ]) + main.mitmweb( + [ + "--no-web-open-browser", + "-s", + tdata.path(shutdown_script), + "-q", + "-p", + "0", + "--web-port", + "0", + ] + ) def test_mitmdump(event_loop, tdata): asyncio.set_event_loop(event_loop) - main.mitmdump([ - "-s", tdata.path(shutdown_script), - "-q", "-p", "0", - ]) + main.mitmdump( + [ + "-s", + tdata.path(shutdown_script), + "-q", + "-p", + "0", + ] + ) diff --git a/test/mitmproxy/tools/web/test_app.py b/test/mitmproxy/tools/web/test_app.py index ae4ee04146..8cd88074fa 100644 --- a/test/mitmproxy/tools/web/test_app.py +++ b/test/mitmproxy/tools/web/test_app.py @@ -1,12 +1,12 @@ -import asyncio import io import gzip import json import logging import textwrap -import typing +from collections.abc import Sequence from contextlib import redirect_stdout from pathlib import Path +from typing import Optional from unittest import mock import pytest @@ -23,13 +23,13 @@ @pytest.fixture(scope="module") def no_tornado_logging(): - logging.getLogger('tornado.access').disabled = True - logging.getLogger('tornado.application').disabled = True - logging.getLogger('tornado.general').disabled = True + logging.getLogger("tornado.access").disabled = True + logging.getLogger("tornado.application").disabled = True + logging.getLogger("tornado.general").disabled = True yield - logging.getLogger('tornado.access').disabled = False - logging.getLogger('tornado.application').disabled = False - logging.getLogger('tornado.general').disabled = False + logging.getLogger("tornado.access").disabled = False + logging.getLogger("tornado.application").disabled = False + logging.getLogger("tornado.general").disabled = False def get_json(resp: httpclient.HTTPResponse): @@ -43,7 +43,9 @@ def test_generate_tflow_js(tdata): tf_http.server_conn.id = "f087e7b2-6d0a-41a8-a8f0-e1a4761395f8" tf_http.server_conn.certificate_list = [ certs.Cert.from_pem( - Path(tdata.path("mitmproxy/net/data/verificationcerts/self-signed.pem")).read_bytes() + Path( + tdata.path("mitmproxy/net/data/verificationcerts/self-signed.pem") + ).read_bytes() ) ] tf_http.request.trailers = Headers(trailer="qvalue") @@ -55,28 +57,44 @@ def test_generate_tflow_js(tdata): tf_tcp.client_conn.id = "8be32b99-a0b3-446e-93bc-b29982fe1322" tf_tcp.server_conn.id = "e33bb2cd-c07e-4214-9a8e-3a8f85f25200" + tf_dns = tflow.tdnsflow(resp=True, err=True) + tf_dns.id = "5434da94-1017-42fa-872d-a189508d48e4" + tf_dns.client_conn.id = "0b4cc0a3-6acb-4880-81c0-1644084126fc" + tf_dns.server_conn.id = "db5294af-c008-4098-a320-a94f901eaf2f" + # language=TypeScript content = ( "/** Auto-generated by test_app.py:test_generate_tflow_js */\n" - "import {HTTPFlow, TCPFlow} from '../../flow';\n" + "import {HTTPFlow, TCPFlow, DNSFlow} from '../../flow';\n" "export function THTTPFlow(): Required {\n" " return %s\n" "}\n" "export function TTCPFlow(): Required {\n" " return %s\n" - "}" % ( - textwrap.indent(json.dumps(app.flow_to_json(tf_http), indent=4, sort_keys=True), " "), - textwrap.indent(json.dumps(app.flow_to_json(tf_tcp), indent=4, sort_keys=True), " "), + "}\n" + "export function TDNSFlow(): Required {\n" + " return %s\n" + "}\n" + % ( + textwrap.indent( + json.dumps(app.flow_to_json(tf_http), indent=4, sort_keys=True), " " + ), + textwrap.indent( + json.dumps(app.flow_to_json(tf_tcp), indent=4, sort_keys=True), " " + ), + textwrap.indent( + json.dumps(app.flow_to_json(tf_dns), indent=4, sort_keys=True), " " + ), ) ) content = content.replace(": null", ": undefined") - (Path(__file__).parent / "../../../../web/src/js/__tests__/ducks/_tflow.ts").write_bytes( - content.encode() - ) + ( + Path(__file__).parent / "../../../../web/src/js/__tests__/ducks/_tflow.ts" + ).write_bytes(content.encode()) -def test_generate_options_js(): +async def test_generate_options_js(): o = options.Options() m = webmaster.WebMaster(o) opt: optmanager._Option @@ -88,9 +106,9 @@ def ts_type(t): return "string" if t == int: return "number" - if t == typing.Sequence[str]: + if t == Sequence[str]: return "string[]" - if t == typing.Optional[str]: + if t == Optional[str]: return "string | undefined" raise RuntimeError(t) @@ -107,24 +125,26 @@ def ts_type(t): print("") print("export const defaultState: OptionsState = {") for _, opt in sorted(m.options.items()): - print(f" {opt.name}: {json.dumps(opt.default)},".replace(": null", ": undefined")) + print( + f" {opt.name}: {json.dumps(opt.default)},".replace( + ": null", ": undefined" + ) + ) print("}") - (Path(__file__).parent / "../../../../web/src/js/ducks/_options_gen.ts").write_bytes( - s.getvalue().encode() - ) + ( + Path(__file__).parent / "../../../../web/src/js/ducks/_options_gen.ts" + ).write_bytes(s.getvalue().encode()) @pytest.mark.usefixtures("no_tornado_logging", "tdata") class TestApp(tornado.testing.AsyncHTTPTestCase): - def get_new_ioloop(self): - io_loop = tornado.platform.asyncio.AsyncIOLoop() - asyncio.set_event_loop(io_loop.asyncio_loop) - return io_loop - def get_app(self): - o = options.Options(http2=False) - m = webmaster.WebMaster(o, with_termlog=False) + async def make_master(): + o = options.Options(http2=False) + return webmaster.WebMaster(o, with_termlog=False) + + m = self.io_loop.asyncio_loop.run_until_complete(make_master()) f = tflow.tflow(resp=True) f.id = "42" f.request.content = b"foo\nbar" @@ -188,8 +208,7 @@ def test_resume(self): for f in self.view: f.intercept() - assert self.fetch( - "/flows/42/resume", method="POST").code == 200 + assert self.fetch("/flows/42/resume", method="POST").code == 200 assert sum(f.intercepted for f in self.view) >= 1 assert self.fetch("/flows/resume", method="POST").code == 200 assert all(not f.intercepted for f in self.view) @@ -250,8 +269,12 @@ def test_flow_update(self): assert f.response.text == "resp" upd = { - "request": {"trailers": [("foo", "baz")], }, - "response": {"trailers": [("foo", "baz")], }, + "request": { + "trailers": [("foo", "baz")], + }, + "response": { + "trailers": [("foo", "baz")], + }, } assert self.put_json("/flows/42", upd).code == 200 assert f.request.trailers["foo"] == "baz" @@ -262,12 +285,15 @@ def test_flow_update(self): assert self.put_json("/flows/42", {"request": {"foo": 42}}).code == 400 assert self.put_json("/flows/42", {"response": {"foo": 42}}).code == 400 assert self.fetch("/flows/42", method="PUT", body="{}").code == 400 - assert self.fetch( - "/flows/42", - method="PUT", - headers={"Content-Type": "application/json"}, - body="!!" - ).code == 400 + assert ( + self.fetch( + "/flows/42", + method="PUT", + headers={"Content-Type": "application/json"}, + body="!!", + ).code + == 400 + ) def test_flow_duplicate(self): resp = self.fetch("/flows/42/duplicate", method="POST") @@ -300,9 +326,10 @@ def test_flow_content(self): del f.response.headers["Content-Disposition"] f.request.path = "/foo/bar.jpg" - assert self.fetch( - "/flows/42/response/content.data" - ).headers["Content-Disposition"] == 'attachment; filename=bar.jpg' + assert ( + self.fetch("/flows/42/response/content.data").headers["Content-Disposition"] + == "attachment; filename=bar.jpg" + ) f.response.content = b"" r = self.fetch("/flows/42/response/content.data") @@ -317,7 +344,9 @@ def test_flow_content_returns_raw_content_when_decoding_fails(self): f.response.headers["Content-Encoding"] = "gzip" # replace gzip magic number with garbage - invalid_encoded_content = gzip.compress(b"Hello world!").replace(b"\x1f\x8b", b"\xff\xff") + invalid_encoded_content = gzip.compress(b"Hello world!").replace( + b"\x1f\x8b", b"\xff\xff" + ) f.response.raw_content = invalid_encoded_content r = self.fetch("/flows/42/response/content.data") @@ -327,11 +356,10 @@ def test_flow_content_returns_raw_content_when_decoding_fails(self): f.revert() def test_update_flow_content(self): - assert self.fetch( - "/flows/42/request/content.data", - method="POST", - body="new" - ).code == 200 + assert ( + self.fetch("/flows/42/request/content.data", method="POST", body="new").code + == 200 + ) f = self.view.get_by_id("42") assert f.request.content == b"new" assert f.modified() @@ -339,18 +367,23 @@ def test_update_flow_content(self): def test_update_flow_content_multipart(self): body = ( - b'--somefancyboundary\r\n' + b"--somefancyboundary\r\n" b'Content-Disposition: form-data; name="a"; filename="a.txt"\r\n' - b'\r\n' - b'such multipart. very wow.\r\n' - b'--somefancyboundary--\r\n' + b"\r\n" + b"such multipart. very wow.\r\n" + b"--somefancyboundary--\r\n" + ) + assert ( + self.fetch( + "/flows/42/request/content.data", + method="POST", + headers={ + "Content-Type": 'multipart/form-data; boundary="somefancyboundary"' + }, + body=body, + ).code + == 200 ) - assert self.fetch( - "/flows/42/request/content.data", - method="POST", - headers={"Content-Type": 'multipart/form-data; boundary="somefancyboundary"'}, - body=body - ).code == 200 f = self.view.get_by_id("42") assert f.request.content == b"such multipart. very wow." assert f.modified() @@ -358,30 +391,29 @@ def test_update_flow_content_multipart(self): def test_flow_contentview(self): assert get_json(self.fetch("/flows/42/request/content/raw")) == { - "lines": [ - [["text", "foo"]], - [["text", "bar"]] - ], - "description": "Raw" + "lines": [[["text", "foo"]], [["text", "bar"]]], + "description": "Raw", } assert get_json(self.fetch("/flows/42/request/content/raw?lines=1")) == { - "lines": [ - [["text", "foo"]] - ], - "description": "Raw" + "lines": [[["text", "foo"]]], + "description": "Raw", } assert self.fetch("/flows/42/messages/content/raw").code == 400 def test_flow_contentview_websocket(self): assert get_json(self.fetch("/flows/43/messages/content/raw?lines=2")) == [ - {'description': 'Raw', - 'from_client': True, - 'lines': [[['text', 'hello binary']]], - 'timestamp': 946681203}, - {'description': 'Raw', - 'from_client': True, - 'lines': [[['text', 'hello text']]], - 'timestamp': 946681204} + { + "description": "Raw", + "from_client": True, + "lines": [[["text", "hello binary"]]], + "timestamp": 946681203, + }, + { + "description": "Raw", + "from_client": True, + "lines": [[["text", "hello text"]]], + "timestamp": 946681204, + }, ] def test_commands(self): @@ -405,7 +437,7 @@ def test_events(self): def test_options(self): j = get_json(self.fetch("/options")) assert type(j) == dict - assert type(j['anticache']) == dict + assert type(j["anticache"]) == dict def test_option_update(self): assert self.put_json("/options", {"anticache": True}).code == 200 @@ -443,7 +475,7 @@ def test_websocket(self): "help": "Try to convince servers to send us un-compressed data.", "type": "bool", } - } + }, } ws_client.close() diff --git a/test/mitmproxy/tools/web/test_master.py b/test/mitmproxy/tools/web/test_master.py index 87dc59aa9b..e69de29bb2 100644 --- a/test/mitmproxy/tools/web/test_master.py +++ b/test/mitmproxy/tools/web/test_master.py @@ -1,19 +0,0 @@ -import pytest - -from mitmproxy import options -from mitmproxy.tools.web import master - -from ... import tservers - - -class TestWebMaster(tservers.MasterTest): - def mkmaster(self, **opts): - o = options.Options(**opts) - return master.WebMaster(o) - - @pytest.mark.asyncio - async def test_basic(self): - m = self.mkmaster() - for i in (1, 2, 3): - await self.dummy_cycle(m, 1, b"") - assert len(m.view) == i diff --git a/test/mitmproxy/tools/web/test_static_viewer.py b/test/mitmproxy/tools/web/test_static_viewer.py index 01eaae68e5..870cd4421d 100644 --- a/test/mitmproxy/tools/web/test_static_viewer.py +++ b/test/mitmproxy/tools/web/test_static_viewer.py @@ -1,6 +1,5 @@ import json from unittest import mock -import pytest from mitmproxy.test import taddons from mitmproxy.test import tflow @@ -13,60 +12,61 @@ def test_save_static(tmpdir): - tmpdir.mkdir('static') + tmpdir.mkdir("static") static_viewer.save_static(tmpdir) assert len(tmpdir.listdir()) == 2 - assert tmpdir.join('index.html').check(file=1) - assert tmpdir.join('static/static.js').read() == 'MITMWEB_STATIC = true;' + assert tmpdir.join("index.html").check(file=1) + assert tmpdir.join("static/static.js").read() == "MITMWEB_STATIC = true;" def test_save_filter_help(tmpdir): static_viewer.save_filter_help(tmpdir) - f = tmpdir.join('/filter-help.json') + f = tmpdir.join("/filter-help.json") assert f.check(file=1) assert f.read() == json.dumps(dict(commands=flowfilter.help)) def test_save_settings(tmpdir): static_viewer.save_settings(tmpdir) - f = tmpdir.join('/settings.json') + f = tmpdir.join("/settings.json") assert f.check(file=1) def test_save_flows(tmpdir): flows = [tflow.tflow(resp=False), tflow.tflow(resp=True)] static_viewer.save_flows(tmpdir, flows) - assert tmpdir.join('flows.json').check(file=1) - assert tmpdir.join('flows.json').read() == json.dumps([flow_to_json(f) for f in flows]) + assert tmpdir.join("flows.json").check(file=1) + assert tmpdir.join("flows.json").read() == json.dumps( + [flow_to_json(f) for f in flows] + ) -@mock.patch('mitmproxy.ctx.log') +@mock.patch("mitmproxy.ctx.log") def test_save_flows_content(ctx, tmpdir): flows = [tflow.tflow(resp=False), tflow.tflow(resp=True)] - with mock.patch('time.time', mock.Mock(side_effect=[1, 2, 2] * 4)): + with mock.patch("time.time", mock.Mock(side_effect=[1, 2, 2] * 4)): static_viewer.save_flows_content(tmpdir, flows) - flows_path = tmpdir.join('flows') + flows_path = tmpdir.join("flows") assert len(flows_path.listdir()) == len(flows) for p in flows_path.listdir(): - assert p.join('request').check(dir=1) - assert p.join('response').check(dir=1) - assert p.join('request/content.data').check(file=1) - assert p.join('request/content').check(dir=1) - assert p.join('response/content.data').check(file=1) - assert p.join('response/content').check(dir=1) - assert p.join('request/content/Auto.json').check(file=1) - assert p.join('response/content/Auto.json').check(file=1) + assert p.join("request").check(dir=1) + assert p.join("response").check(dir=1) + assert p.join("request/content.data").check(file=1) + assert p.join("request/content").check(dir=1) + assert p.join("response/content.data").check(file=1) + assert p.join("response/content").check(dir=1) + assert p.join("request/content/Auto.json").check(file=1) + assert p.join("response/content/Auto.json").check(file=1) -@pytest.mark.asyncio async def test_static_viewer(tmpdir): s = static_viewer.StaticViewer() rf = readfile.ReadFile() sa = save.Save() with taddons.context(rf) as tctx: - sa.save([tflow.tflow(resp=True)], str(tmpdir.join('foo'))) + sa.save([tflow.tflow(resp=True)], str(tmpdir.join("foo"))) tctx.master.addons.add(s) - tctx.configure(s, web_static_viewer=str(tmpdir), rfile=str(tmpdir.join('foo'))) - assert tmpdir.join('index.html').check(file=1) - assert tmpdir.join('static').check(dir=1) - assert tmpdir.join('flows').check(dir=1) + tctx.configure(s, web_static_viewer=str(tmpdir), rfile=str(tmpdir.join("foo"))) + assert tmpdir.join("index.html").check(file=1) + assert tmpdir.join("static").check(dir=1) + assert tmpdir.join("flows").check(dir=1) diff --git a/test/mitmproxy/tservers.py b/test/mitmproxy/tservers.py deleted file mode 100644 index 6105f12f3b..0000000000 --- a/test/mitmproxy/tservers.py +++ /dev/null @@ -1,33 +0,0 @@ -from unittest import mock - -from mitmproxy import controller -from mitmproxy import eventsequence -from mitmproxy import io -from mitmproxy.proxy import server_hooks -from mitmproxy.test import tflow -from mitmproxy.test import tutils - - -class MasterTest: - - async def cycle(self, master, content): - f = tflow.tflow(req=tutils.treq(content=content)) - layer = mock.Mock("mitmproxy.proxy.protocol.base.Layer") - layer.client_conn = f.client_conn - layer.reply = controller.DummyReply() - await master.addons.handle_lifecycle(server_hooks.ClientConnectedHook(layer)) - for e in eventsequence.iterate(f): - await master.addons.handle_lifecycle(e) - await master.addons.handle_lifecycle(server_hooks.ClientDisconnectedHook(layer)) - return f - - async def dummy_cycle(self, master, n, content): - for i in range(n): - await self.cycle(master, content) - await master._shutdown() - - def flowfile(self, path): - with open(path, "wb") as f: - fw = io.FlowWriter(f) - t = tflow.tflow(resp=True) - fw.add(t) diff --git a/test/mitmproxy/utils/test_arg_check.py b/test/mitmproxy/utils/test_arg_check.py index 62a3921ca6..97102f49b9 100644 --- a/test/mitmproxy/utils/test_arg_check.py +++ b/test/mitmproxy/utils/test_arg_check.py @@ -7,32 +7,52 @@ from mitmproxy.utils import arg_check -@pytest.mark.parametrize('arg, output', [ - (["-T"], "-T is deprecated, please use --mode transparent instead"), - (["-U"], "-U is deprecated, please use --mode upstream:SPEC instead"), - (["--confdir"], "--confdir is deprecated.\n" - "Please use `--set confdir=value` instead.\n" - "To show all options and their default values use --options"), - (["--palette"], "--palette is deprecated.\n" - "Please use `--set console_palette=value` instead.\n" - "To show all options and their default values use --options"), - (["--wfile"], "--wfile is deprecated.\n" - "Please use `--save-stream-file` instead."), - (["--eventlog"], "--eventlog has been removed."), - (["--nonanonymous"], '--nonanonymous is deprecated.\n' - 'Please use `--proxyauth SPEC` instead.\n' - 'SPEC Format: "username:pass", "any" to accept any user/pass combination,\n' - '"@path" to use an Apache htpasswd file, or\n' - '"ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" ' - 'for LDAP authentication.'), - (["--replacements"], "--replacements is deprecated.\n" - "Please use `--modify-body` or `--modify-headers` instead."), - (["--underscore_option"], "--underscore_option uses underscores, please use hyphens --underscore-option") -]) +@pytest.mark.parametrize( + "arg, output", + [ + (["-T"], "-T is deprecated, please use --mode transparent instead"), + (["-U"], "-U is deprecated, please use --mode upstream:SPEC instead"), + ( + ["--confdir"], + "--confdir is deprecated.\n" + "Please use `--set confdir=value` instead.\n" + "To show all options and their default values use --options", + ), + ( + ["--palette"], + "--palette is deprecated.\n" + "Please use `--set console_palette=value` instead.\n" + "To show all options and their default values use --options", + ), + ( + ["--wfile"], + "--wfile is deprecated.\n" "Please use `--save-stream-file` instead.", + ), + (["--eventlog"], "--eventlog has been removed."), + ( + ["--nonanonymous"], + "--nonanonymous is deprecated.\n" + "Please use `--proxyauth SPEC` instead.\n" + 'SPEC Format: "username:pass", "any" to accept any user/pass combination,\n' + '"@path" to use an Apache htpasswd file, or\n' + '"ldap[s]:url_server_ldap:dn_auth:password:dn_subtree" ' + "for LDAP authentication.", + ), + ( + ["--replacements"], + "--replacements is deprecated.\n" + "Please use `--modify-body` or `--modify-headers` instead.", + ), + ( + ["--underscore_option"], + "--underscore_option uses underscores, please use hyphens --underscore-option", + ), + ], +) def test_check_args(arg, output): f = io.StringIO() with contextlib.redirect_stdout(f): - with mock.patch('sys.argv') as m: + with mock.patch("sys.argv") as m: m.__getitem__.return_value = arg arg_check.check() assert f.getvalue().strip() == output diff --git a/test/mitmproxy/utils/test_asyncio_utils.py b/test/mitmproxy/utils/test_asyncio_utils.py index 5446027ae7..1b59da8b62 100644 --- a/test/mitmproxy/utils/test_asyncio_utils.py +++ b/test/mitmproxy/utils/test_asyncio_utils.py @@ -1,45 +1,17 @@ import asyncio -import pytest - from mitmproxy.utils import asyncio_utils async def ttask(): - asyncio_utils.set_task_debug_info( - asyncio.current_task(), - name="newname", - ) + asyncio_utils.set_current_task_debug_info(name="newname") await asyncio.sleep(999) -@pytest.mark.asyncio async def test_simple(): - task = asyncio_utils.create_task( - ttask(), - name="ttask", - client=("127.0.0.1", 42313) - ) + task = asyncio_utils.create_task(ttask(), name="ttask", client=("127.0.0.1", 42313)) assert asyncio_utils.task_repr(task) == "127.0.0.1:42313: ttask (age: 0s)" await asyncio.sleep(0) assert "newname" in asyncio_utils.task_repr(task) - asyncio_utils.cancel_task(task, "bye") - await asyncio.sleep(0) - assert task.cancelled() - - -def test_closed_loop(): - # Crude test for line coverage. - # This should eventually go, see the description in asyncio_utils.create_task for details. - asyncio_utils.create_task( - ttask(), - name="ttask", - ) - t = ttask() - with pytest.raises(RuntimeError): - asyncio_utils.create_task( - t, - name="ttask", - ignore_closed_loop=False, - ) - t.close() # suppress "not awaited" warning \ No newline at end of file + delattr(task, "created") + assert asyncio_utils.task_repr(task) diff --git a/test/mitmproxy/utils/test_debug.py b/test/mitmproxy/utils/test_debug.py index 88a338093d..a61bff8682 100644 --- a/test/mitmproxy/utils/test_debug.py +++ b/test/mitmproxy/utils/test_debug.py @@ -9,27 +9,26 @@ @pytest.mark.parametrize("precompiled", [True, False]) def test_dump_system_info_precompiled(precompiled): sys.frozen = None - with mock.patch.object(sys, 'frozen', precompiled): + with mock.patch.object(sys, "frozen", precompiled): assert ("binary" in debug.dump_system_info()) == precompiled def test_dump_info(): cs = io.StringIO() - debug.dump_info(None, None, file=cs, testing=True) + debug.dump_info(None, None, file=cs) assert cs.getvalue() assert "Tasks" not in cs.getvalue() -@pytest.mark.asyncio async def test_dump_info_async(): cs = io.StringIO() - debug.dump_info(None, None, file=cs, testing=True) + debug.dump_info(None, None, file=cs) assert "Tasks" in cs.getvalue() def test_dump_stacks(): cs = io.StringIO() - debug.dump_stacks(None, None, file=cs, testing=True) + debug.dump_stacks(None, None, file=cs) assert cs.getvalue() diff --git a/test/mitmproxy/utils/test_human.py b/test/mitmproxy/utils/test_human.py index 91e123f55f..944740611f 100644 --- a/test/mitmproxy/utils/test_human.py +++ b/test/mitmproxy/utils/test_human.py @@ -16,8 +16,8 @@ def test_parse_size(): assert human.parse_size("0b") == 0 assert human.parse_size("1") == 1 assert human.parse_size("1k") == 1024 - assert human.parse_size("1m") == 1024**2 - assert human.parse_size("1g") == 1024**3 + assert human.parse_size("1m") == 1024 ** 2 + assert human.parse_size("1g") == 1024 ** 3 with pytest.raises(ValueError): human.parse_size("1f") with pytest.raises(ValueError): @@ -53,7 +53,10 @@ def test_pretty_duration(): def test_format_address(): assert human.format_address(("::1", "54010", "0", "0")) == "[::1]:54010" - assert human.format_address(("::ffff:127.0.0.1", "54010", "0", "0")) == "127.0.0.1:54010" + assert ( + human.format_address(("::ffff:127.0.0.1", "54010", "0", "0")) + == "127.0.0.1:54010" + ) assert human.format_address(("127.0.0.1", "54010")) == "127.0.0.1:54010" assert human.format_address(("example.com", "54010")) == "example.com:54010" assert human.format_address(("::", "8080")) == "*:8080" diff --git a/test/mitmproxy/utils/test_sliding_window.py b/test/mitmproxy/utils/test_sliding_window.py index 23c760324d..706ec394b9 100644 --- a/test/mitmproxy/utils/test_sliding_window.py +++ b/test/mitmproxy/utils/test_sliding_window.py @@ -9,7 +9,7 @@ def test_simple(): (1000, 1001, 1002, 1003), (1001, 1002, 1003, 1004), (1002, 1003, 1004, None), - (1003, 1004, None, None) + (1003, 1004, None, None), ] diff --git a/test/mitmproxy/utils/test_strutils.py b/test/mitmproxy/utils/test_strutils.py index 3a928ff719..3459a673f3 100644 --- a/test/mitmproxy/utils/test_strutils.py +++ b/test/mitmproxy/utils/test_strutils.py @@ -27,14 +27,14 @@ def test_escape_control_characters(): assert strutils.escape_control_characters("\nne", False) == ".ne" assert strutils.escape_control_characters("\u2605") == "\u2605" assert ( - strutils.escape_control_characters(bytes(bytearray(range(128))).decode()) == - '.........\t\n..\r.................. !"#$%&\'()*+,-./0123456789:;<' - '=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~.' + strutils.escape_control_characters(bytes(bytearray(range(128))).decode()) + == ".........\t\n..\r.................. !\"#$%&'()*+,-./0123456789:;<" + "=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~." ) assert ( - strutils.escape_control_characters(bytes(bytearray(range(128))).decode(), False) == - '................................ !"#$%&\'()*+,-./0123456789:;<' - '=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~.' + strutils.escape_control_characters(bytes(bytearray(range(128))).decode(), False) + == "................................ !\"#$%&'()*+,-./0123456789:;<" + "=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~." ) with pytest.raises(ValueError): @@ -45,7 +45,7 @@ def test_bytes_to_escaped_str(): assert strutils.bytes_to_escaped_str(b"foo") == "foo" assert strutils.bytes_to_escaped_str(b"\b") == r"\x08" assert strutils.bytes_to_escaped_str(br"&!?=\)") == r"&!?=\\)" - assert strutils.bytes_to_escaped_str(b'\xc3\xbc') == r"\xc3\xbc" + assert strutils.bytes_to_escaped_str(b"\xc3\xbc") == r"\xc3\xbc" assert strutils.bytes_to_escaped_str(b"'") == r"'" assert strutils.bytes_to_escaped_str(b'"') == r'"' @@ -58,7 +58,9 @@ def test_bytes_to_escaped_str(): assert strutils.bytes_to_escaped_str(b"\n", True) == "\n" assert strutils.bytes_to_escaped_str(b"\\n", True) == "\\ \\ n".replace(" ", "") assert strutils.bytes_to_escaped_str(b"\\\n", True) == "\\ \\ \n".replace(" ", "") - assert strutils.bytes_to_escaped_str(b"\\\\n", True) == "\\ \\ \\ \\ n".replace(" ", "") + assert strutils.bytes_to_escaped_str(b"\\\\n", True) == "\\ \\ \\ \\ n".replace( + " ", "" + ) with pytest.raises(ValueError): strutils.bytes_to_escaped_str("such unicode") @@ -70,7 +72,7 @@ def test_escaped_str_to_bytes(): assert strutils.escaped_str_to_bytes("&!?=\\\\)") == br"&!?=\)" assert strutils.escaped_str_to_bytes("\\x08") == b"\b" assert strutils.escaped_str_to_bytes("&!?=\\\\)") == br"&!?=\)" - assert strutils.escaped_str_to_bytes("\u00fc") == b'\xc3\xbc' + assert strutils.escaped_str_to_bytes("\u00fc") == b"\xc3\xbc" with pytest.raises(ValueError): strutils.escaped_str_to_bytes(b"very byte") @@ -101,29 +103,37 @@ def test_hexdump(): ESCAPE_QUOTES = [ "'" + strutils.SINGLELINE_CONTENT + strutils.NO_ESCAPE + "'", - '"' + strutils.SINGLELINE_CONTENT + strutils.NO_ESCAPE + '"' + '"' + strutils.SINGLELINE_CONTENT + strutils.NO_ESCAPE + '"', ] def test_split_special_areas(): assert strutils.split_special_areas("foo", ESCAPE_QUOTES) == ["foo"] - assert strutils.split_special_areas("foo 'bar' baz", ESCAPE_QUOTES) == ["foo ", "'bar'", " baz"] - assert strutils.split_special_areas( - """foo 'b\\'a"r' baz""", - ESCAPE_QUOTES - ) == ["foo ", "'b\\'a\"r'", " baz"] + assert strutils.split_special_areas("foo 'bar' baz", ESCAPE_QUOTES) == [ + "foo ", + "'bar'", + " baz", + ] + assert strutils.split_special_areas("""foo 'b\\'a"r' baz""", ESCAPE_QUOTES) == [ + "foo ", + "'b\\'a\"r'", + " baz", + ] assert strutils.split_special_areas( - "foo\n/*bar\nbaz*/\nqux", - [r'/\*[\s\S]+?\*/'] + "foo\n/*bar\nbaz*/\nqux", [r"/\*[\s\S]+?\*/"] ) == ["foo\n", "/*bar\nbaz*/", "\nqux"] - assert strutils.split_special_areas( - "foo\n//bar\nbaz", - [r'//.+$'] - ) == ["foo\n", "//bar", "\nbaz"] + assert strutils.split_special_areas("foo\n//bar\nbaz", [r"//.+$"]) == [ + "foo\n", + "//bar", + "\nbaz", + ] def test_escape_special_areas(): - assert strutils.escape_special_areas('foo "bar" baz', ESCAPE_QUOTES, "*") == 'foo "bar" baz' + assert ( + strutils.escape_special_areas('foo "bar" baz', ESCAPE_QUOTES, "*") + == 'foo "bar" baz' + ) esc = strutils.escape_special_areas('foo "b*r" b*z', ESCAPE_QUOTES, "*") assert esc == 'foo "b\ue02ar" b*z' assert strutils.unescape_special_areas(esc) == 'foo "b*r" b*z' diff --git a/test/mitmproxy/utils/test_typecheck.py b/test/mitmproxy/utils/test_typecheck.py index 55bbcc7138..347e39f303 100644 --- a/test/mitmproxy/utils/test_typecheck.py +++ b/test/mitmproxy/utils/test_typecheck.py @@ -1,5 +1,8 @@ import io import typing +from collections.abc import Sequence +from typing import Any, Optional, TextIO, Union + import pytest from mitmproxy.utils import typecheck @@ -27,57 +30,58 @@ def test_check_option_type(): def test_check_union(): - typecheck.check_option_type("foo", 42, typing.Union[int, str]) - typecheck.check_option_type("foo", "42", typing.Union[int, str]) + typecheck.check_option_type("foo", 42, Union[int, str]) + typecheck.check_option_type("foo", "42", Union[int, str]) with pytest.raises(TypeError): - typecheck.check_option_type("foo", [], typing.Union[int, str]) + typecheck.check_option_type("foo", [], Union[int, str]) def test_check_tuple(): - typecheck.check_option_type("foo", (42, "42"), typing.Tuple[int, str]) + typecheck.check_option_type("foo", (42, "42"), tuple[int, str]) with pytest.raises(TypeError): - typecheck.check_option_type("foo", None, typing.Tuple[int, str]) + typecheck.check_option_type("foo", None, tuple[int, str]) with pytest.raises(TypeError): - typecheck.check_option_type("foo", (), typing.Tuple[int, str]) + typecheck.check_option_type("foo", (), tuple[int, str]) with pytest.raises(TypeError): - typecheck.check_option_type("foo", (42, 42), typing.Tuple[int, str]) + typecheck.check_option_type("foo", (42, 42), tuple[int, str]) with pytest.raises(TypeError): - typecheck.check_option_type("foo", ("42", 42), typing.Tuple[int, str]) + typecheck.check_option_type("foo", ("42", 42), tuple[int, str]) def test_check_sequence(): - typecheck.check_option_type("foo", [10], typing.Sequence[int]) + typecheck.check_option_type("foo", [10], Sequence[int]) with pytest.raises(TypeError): - typecheck.check_option_type("foo", ["foo"], typing.Sequence[int]) + typecheck.check_option_type("foo", ["foo"], Sequence[int]) with pytest.raises(TypeError): - typecheck.check_option_type("foo", [10, "foo"], typing.Sequence[int]) + typecheck.check_option_type("foo", [10, "foo"], Sequence[int]) with pytest.raises(TypeError): - typecheck.check_option_type("foo", [b"foo"], typing.Sequence[str]) + typecheck.check_option_type("foo", [b"foo"], Sequence[str]) with pytest.raises(TypeError): - typecheck.check_option_type("foo", "foo", typing.Sequence[str]) + typecheck.check_option_type("foo", "foo", Sequence[str]) def test_check_io(): - typecheck.check_option_type("foo", io.StringIO(), typing.IO[str]) + typecheck.check_option_type("foo", io.StringIO(), TextIO) with pytest.raises(TypeError): - typecheck.check_option_type("foo", "foo", typing.IO[str]) + typecheck.check_option_type("foo", "foo", TextIO) def test_check_any(): - typecheck.check_option_type("foo", 42, typing.Any) - typecheck.check_option_type("foo", object(), typing.Any) - typecheck.check_option_type("foo", None, typing.Any) + typecheck.check_option_type("foo", 42, Any) + typecheck.check_option_type("foo", object(), Any) + typecheck.check_option_type("foo", None, Any) def test_typesec_to_str(): - assert(typecheck.typespec_to_str(str)) == "str" - assert(typecheck.typespec_to_str(typing.Sequence[str])) == "sequence of str" - assert(typecheck.typespec_to_str(typing.Optional[str])) == "optional str" - assert(typecheck.typespec_to_str(typing.Optional[int])) == "optional int" + assert (typecheck.typespec_to_str(str)) == "str" + assert (typecheck.typespec_to_str(Sequence[str])) == "sequence of str" + assert (typecheck.typespec_to_str(Optional[str])) == "optional str" + assert (typecheck.typespec_to_str(Optional[int])) == "optional int" with pytest.raises(NotImplementedError): typecheck.typespec_to_str(dict) -def test_mapping_types(): - # this is not covered by check_option_type, but still belongs in this module - assert (str, int) == typecheck.mapping_types(typing.Mapping[str, int]) +def test_typing_aliases(): + assert (typecheck.typespec_to_str(typing.Sequence[str])) == "sequence of str" + typecheck.check_option_type("foo", [10], typing.Sequence[int]) + typecheck.check_option_type("foo", (42, "42"), typing.Tuple[int, str]) diff --git a/test/mitmproxy/utils/test_vt_codes.py b/test/mitmproxy/utils/test_vt_codes.py new file mode 100644 index 0000000000..d8ac9cc423 --- /dev/null +++ b/test/mitmproxy/utils/test_vt_codes.py @@ -0,0 +1,7 @@ +import io + +from mitmproxy.utils.vt_codes import ensure_supported + + +def test_simple(): + assert not ensure_supported(io.StringIO()) diff --git a/test/release/test_cibuild.py b/test/release/test_cibuild.py index 26a2c33484..c1ec0aed25 100644 --- a/test/release/test_cibuild.py +++ b/test/release/test_cibuild.py @@ -195,16 +195,19 @@ def test_buildenviron_windows(tmp_path): assert (tmp_path / "arch").exists() -@pytest.mark.parametrize("version, tag, ok", [ - ("3.0.0.dev", "", True), # regular snapshot - ("3.0.0.dev", "v3.0.0", False), # forgot to remove ".dev" on bump - ("3.0.0", "", False), # forgot to re-add ".dev" - ("3.0.0", "v4.0.0", False), # version mismatch - ("3.0.0", "v3.0.0", True), # regular release - ("3.0.0.rc1", "v3.0.0.rc1", False), # non-canonical. - ("3.0.0.dev", "anyname", True), # tagged test/dev release - ("3.0.0", "3.0.0", False), # tagged, but without v prefix -]) +@pytest.mark.parametrize( + "version, tag, ok", + [ + ("3.0.0.dev", "", True), # regular snapshot + ("3.0.0.dev", "v3.0.0", False), # forgot to remove ".dev" on bump + ("3.0.0", "", False), # forgot to re-add ".dev" + ("3.0.0", "v4.0.0", False), # version mismatch + ("3.0.0", "v3.0.0", True), # regular release + ("3.0.0.rc1", "v3.0.0.rc1", False), # non-canonical. + ("3.0.0.dev", "anyname", True), # tagged test/dev release + ("3.0.0", "3.0.0", False), # tagged, but without v prefix + ], +) def test_buildenviron_check_version(version, tag, ok, tmpdir): tmpdir.mkdir("mitmproxy").join("version.py").write(f'VERSION = "{version}"') diff --git a/tox.ini b/tox.ini index 3fb39b90b5..db3d7c6ec3 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ commands = [testenv:flake8] deps = - flake8>=3.8.4,<4 + flake8>=3.8.4,<4.1 flake8-tidy-imports>=4.2.0,<5 commands = flake8 --jobs 8 mitmproxy examples test release {posargs} @@ -29,17 +29,17 @@ commands = [testenv:mypy] deps = - mypy==0.910 - types-certifi==2020.4.0 - types-Flask==1.1.3 - types-Werkzeug==1.0.5 - types-requests==2.25.9 - types-cryptography==3.3.5 - types-pyOpenSSL==20.0.6 + mypy==0.961 + types-certifi==2021.10.8.3 + types-Flask==1.1.6 + types-Werkzeug==1.0.9 + types-requests==2.28.0 + types-cryptography==3.3.21 + types-pyOpenSSL==22.0.4 commands = mypy {posargs} - + [testenv:individual_coverage] commands = python ./test/individual_coverage.py {posargs} diff --git a/web/README.md b/web/README.md index 1de6f9e013..0c08dbda79 100644 --- a/web/README.md +++ b/web/README.md @@ -4,6 +4,7 @@ - Run `node --version` to make sure that you have at least Node.js 14 or above. If you are on **Ubuntu <= 20.04**, you need to [upgrade](https://github.com/nodesource/distributions/blob/master/README.md#installation-instructions). +- Run `cd mitmproxy/web` to change to the directory with package.json - Run `npm install` to install dependencies - Run `npm start` to start live-compilation - Run `mitmweb` after activating your Python virtualenv (see [`../CONTRIBUTING.md`](../CONTRIBUTING.md)). diff --git a/web/src/css/flowdetail.less b/web/src/css/flowdetail.less index 37a2bb6a3a..ec38bb5f01 100644 --- a/web/src/css/flowdetail.less +++ b/web/src/css/flowdetail.less @@ -94,7 +94,7 @@ } } -.flow-detail table { +.connection-table, .timing-table, .certificate-table { td:nth-child(2) { font-family: @font-family-monospace; width: 70%; @@ -189,3 +189,9 @@ dl.cert-attributes { flex: 0 0 calc(100% - 2em); } } + +.dns-request table, .dns-response table { + th, td { + padding-right: 1rem; + } +} diff --git a/web/src/css/sprites.less b/web/src/css/sprites.less index ccd400ca7c..0750116998 100644 --- a/web/src/css/sprites.less +++ b/web/src/css/sprites.less @@ -52,3 +52,7 @@ .resource-icon-tcp { background-image: url(images/resourceTcpIcon.png); } + +.resource-icon-dns { + background-image: url(images/resourceDnsIcon.png); +} diff --git a/web/src/images/resourceDnsIcon.png b/web/src/images/resourceDnsIcon.png new file mode 100644 index 0000000000..31dee2765e Binary files /dev/null and b/web/src/images/resourceDnsIcon.png differ diff --git a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap index 923d67e120..9f54436191 100644 --- a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap +++ b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowColumnsSpec.tsx.snap @@ -177,7 +177,7 @@ exports[`should render columns: method 1`] = ` - GET + WSS diff --git a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.tsx.snap b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.tsx.snap index e42d25fa1b..323d075f58 100644 --- a/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.tsx.snap +++ b/web/src/js/__tests__/components/FlowTable/__snapshots__/FlowRowSpec.tsx.snap @@ -34,7 +34,7 @@ exports[`FlowRow 1`] = ` - GET + WSS { expect(asFragment()).toMatchSnapshot(); fireEvent.click(screen.getByText("Error")); + expect(asFragment()).toMatchSnapshot(); + + store.dispatch(flowActions.select(store.getState().flows.list[3].id)); + + fireEvent.click(screen.getByText("Request")); + expect(asFragment()).toMatchSnapshot(); + + fireEvent.click(screen.getByText("Response")); + expect(asFragment()).toMatchSnapshot(); + + fireEvent.click(screen.getByText("Error")); + expect(asFragment()).toMatchSnapshot(); }); diff --git a/web/src/js/__tests__/components/Header/__snapshots__/MainMenuSpec.tsx.snap b/web/src/js/__tests__/components/Header/__snapshots__/MainMenuSpec.tsx.snap index c35509af67..b5164c65cc 100644 --- a/web/src/js/__tests__/components/Header/__snapshots__/MainMenuSpec.tsx.snap +++ b/web/src/js/__tests__/components/Header/__snapshots__/MainMenuSpec.tsx.snap @@ -26,7 +26,7 @@ exports[`MainMenu 1`] = ` class="form-control" placeholder="Search" type="text" - value="~u /second | ~tcp" + value="~u /second | ~tcp | ~dns" />
`; + +exports[`FlowView 8`] = ` + +
+ +
+
+ error +
+ + 1999-12-31 23:00:07.000 + +
+
+
+
+
+`; + +exports[`FlowView 9`] = ` + +
+ +
+
+
+ QUERY  +
+
+
+ Recursive Question +
+ + + + + + + + + + + + + + + +
+ Name + + Type + + Class +
+ dns.google + + A + + IN +
+
+
+ Answer +
+ — +
+
+ Authority +
+ — +
+
+ Additional +
+ — +
+
+
+`; + +exports[`FlowView 10`] = ` + +
+ +
+
+
+ NOERROR  +
+
+
+ Recursive Question +
+ + + + + + + + + + + + + + + +
+ Name + + Type + + Class +
+ dns.google + + A + + IN +
+
+
+ Recursive Answer +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ Name + + Type + + Class + + TTL + + Data +
+ dns.google + + A + + IN + + 32 + + 8.8.8.8 +
+ dns.google + + A + + IN + + 32 + + 8.8.4.4 +
+
+
+ Authority +
+ — +
+
+ Additional +
+ — +
+
+
+`; + +exports[`FlowView 11`] = ` + +
+ +
+
+ error +
+ + 1999-12-31 23:00:07.000 + +
+
+
+
+
+`; diff --git a/web/src/js/__tests__/ducks/_tflow.ts b/web/src/js/__tests__/ducks/_tflow.ts index 072d78e4a4..6cafa28777 100644 --- a/web/src/js/__tests__/ducks/_tflow.ts +++ b/web/src/js/__tests__/ducks/_tflow.ts @@ -1,5 +1,5 @@ /** Auto-generated by test_app.py:test_generate_tflow_js */ -import {HTTPFlow, TCPFlow} from '../../flow'; +import {HTTPFlow, TCPFlow, DNSFlow} from '../../flow'; export function THTTPFlow(): Required { return { "client_conn": { @@ -145,6 +145,7 @@ export function THTTPFlow(): Required { "tls_established": true, "tls_version": "TLSv1.2" }, + "timestamp_created": 946681200, "type": "http", "websocket": { "close_code": 1000, @@ -221,6 +222,129 @@ export function TTCPFlow(): Required { "tls_established": true, "tls_version": "TLSv1.2" }, + "timestamp_created": 946681200, "type": "tcp" } -} \ No newline at end of file +} +export function TDNSFlow(): Required { + return { + "client_conn": { + "alpn": "http/1.1", + "cert": undefined, + "cipher": "cipher", + "id": "0b4cc0a3-6acb-4880-81c0-1644084126fc", + "peername": [ + "127.0.0.1", + 22 + ], + "sni": "address", + "sockname": [ + "", + 0 + ], + "timestamp_end": 946681206, + "timestamp_start": 946681200, + "timestamp_tls_setup": 946681201, + "tls_established": true, + "tls_version": "TLSv1.2" + }, + "comment": "", + "error": { + "msg": "error", + "timestamp": 946681207.0 + }, + "id": "5434da94-1017-42fa-872d-a189508d48e4", + "intercepted": false, + "is_replay": undefined, + "marked": "", + "modified": false, + "request": { + "additionals": [], + "answers": [], + "authoritative_answer": false, + "authorities": [], + "id": 42, + "op_code": "QUERY", + "query": true, + "questions": [ + { + "class": "IN", + "name": "dns.google", + "type": "A" + } + ], + "recursion_available": false, + "recursion_desired": true, + "response_code": "NOERROR", + "size": 0, + "status_code": 200, + "timestamp": 946681200, + "truncation": false + }, + "response": { + "additionals": [], + "answers": [ + { + "class": "IN", + "data": "8.8.8.8", + "name": "dns.google", + "ttl": 32, + "type": "A" + }, + { + "class": "IN", + "data": "8.8.4.4", + "name": "dns.google", + "ttl": 32, + "type": "A" + } + ], + "authoritative_answer": false, + "authorities": [], + "id": 42, + "op_code": "QUERY", + "query": false, + "questions": [ + { + "class": "IN", + "name": "dns.google", + "type": "A" + } + ], + "recursion_available": true, + "recursion_desired": true, + "response_code": "NOERROR", + "size": 8, + "status_code": 200, + "timestamp": 946681201, + "truncation": false + }, + "server_conn": { + "address": [ + "address", + 22 + ], + "alpn": undefined, + "cert": undefined, + "cipher": undefined, + "id": "db5294af-c008-4098-a320-a94f901eaf2f", + "peername": [ + "192.168.0.1", + 22 + ], + "sni": "address", + "sockname": [ + "address", + 22 + ], + "timestamp_end": 946681205, + "timestamp_start": 946681202, + "timestamp_tcp_setup": 946681203, + "timestamp_tls_setup": 946681204, + "tls_established": true, + "tls_version": "TLSv1.2" + }, + "timestamp_created": 946681200, + "type": "dns" + } +} diff --git a/web/src/js/__tests__/ducks/tutils.ts b/web/src/js/__tests__/ducks/tutils.ts index ec8aaab294..7a72cb2f82 100644 --- a/web/src/js/__tests__/ducks/tutils.ts +++ b/web/src/js/__tests__/ducks/tutils.ts @@ -1,9 +1,9 @@ import thunk from 'redux-thunk' import configureStore, {MockStoreCreator, MockStoreEnhanced} from 'redux-mock-store' import {ConnectionState} from '../../ducks/connection' -import {THTTPFlow, TTCPFlow} from './_tflow' +import {TDNSFlow, THTTPFlow, TTCPFlow} from './_tflow' import {AppDispatch, RootState} from "../../ducks"; -import {HTTPFlow, TCPFlow} from "../../flow"; +import {DNSFlow, HTTPFlow, TCPFlow} from "../../flow"; import {defaultState as defaultConf} from "../../ducks/conf" import {defaultState as defaultOptions} from "../../ducks/options" @@ -11,13 +11,14 @@ const mockStoreCreator: MockStoreCreator = configureStor export {THTTPFlow as TFlow, TTCPFlow} +const tflow0: HTTPFlow = THTTPFlow(); const tflow1: HTTPFlow = THTTPFlow(); -const tflow2: HTTPFlow = THTTPFlow(); -const tflow3: TCPFlow = TTCPFlow(); -tflow1.modified = true -tflow1.intercepted = true -tflow2.id = "flow2"; -tflow2.request.path = "/second"; +const tflow2: TCPFlow = TTCPFlow(); +const tflow3: DNSFlow = TDNSFlow(); +tflow0.modified = true +tflow0.intercepted = true +tflow1.id = "flow2"; +tflow1.request.path = "/second"; export const testState: RootState = { conf: defaultConf, @@ -71,18 +72,32 @@ export const testState: RootState = { }, options: defaultOptions, flows: { - selected: [tflow2.id], - byId: {[tflow1.id]: tflow1, [tflow2.id]: tflow2, [tflow3.id]: tflow3}, - filter: '~u /second | ~tcp', + selected: [tflow1.id], + byId: { + [tflow0.id]: tflow0, + [tflow1.id]: tflow1, + [tflow2.id]: tflow2, + [tflow3.id]: tflow3, + }, + filter: '~u /second | ~tcp | ~dns', highlight: '~u /path', sort: { desc: true, column: "path" }, - view: [tflow2, tflow3], - list: [tflow1, tflow2, tflow3], - listIndex: {[tflow1.id]: 0, [tflow2.id]: 1, [tflow3.id]: 2}, - viewIndex: {[tflow2.id]: 0, [tflow3.id]: 1}, + view: [tflow1, tflow2, tflow3], + list: [tflow0, tflow1, tflow2, tflow3], + listIndex: { + [tflow0.id]: 0, + [tflow1.id]: 1, + [tflow2.id]: 2, + [tflow3.id]: 3 + }, + viewIndex: { + [tflow1.id]: 0, + [tflow2.id]: 1, + [tflow3.id]: 2, + }, }, connection: { state: ConnectionState.ESTABLISHED diff --git a/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx b/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx index 9f15ab4a40..514cfb98cf 100644 --- a/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx +++ b/web/src/js/__tests__/ducks/ui/keyboardSpec.tsx @@ -2,7 +2,7 @@ import reduceFlows, * as flowsActions from "../../../ducks/flows"; import {onKeyDown} from '../../../ducks/ui/keyboard' import * as UIActions from '../../../ducks/ui/flow' import * as modalActions from '../../../ducks/ui/modal' -import {fetchApi} from '../../../utils' +import {fetchApi, runCommand} from '../../../utils' import {TStore} from "../tutils"; jest.mock('../../../utils') @@ -102,6 +102,11 @@ describe('onKeyDown', () => { }) + it('should handle create action', () => { + store.dispatch(createKeyEvent("n")) + expect(runCommand).toBeCalledWith('view.flows.create', "get", "https://example.com/") + }) + it('should handle duplicate action', () => { store.dispatch(createKeyEvent("D")) expect(fetchApi).toBeCalledWith('/flows/1/duplicate', {method: 'POST'}) diff --git a/web/src/js/__tests__/flow/utilsSpec.tsx b/web/src/js/__tests__/flow/utilsSpec.tsx index 717b0f5620..0d63081b65 100644 --- a/web/src/js/__tests__/flow/utilsSpec.tsx +++ b/web/src/js/__tests__/flow/utilsSpec.tsx @@ -1,5 +1,7 @@ import * as utils from '../../flow/utils' -import {TFlow} from "../ducks/tutils"; +import {TFlow, TTCPFlow} from "../ducks/tutils"; +import {TDNSFlow, THTTPFlow} from "../ducks/_tflow"; +import {HTTPFlow} from "../../flow"; describe('MessageUtils', () => { it('should be possible to get first header', () => { @@ -67,3 +69,24 @@ describe('isValidHttpVersion', () => { expect(utils.isValidHttpVersion("HTTP//1")).toBeFalsy() }) }) + +it('should be possible to get a start time', () => { + expect(utils.startTime(THTTPFlow())).toEqual(946681200); + expect(utils.startTime(TTCPFlow())).toEqual(946681200); + expect(utils.startTime(TDNSFlow())).toEqual(946681200); +}) + +it('should be possible to get an end time', () => { + let f: HTTPFlow = THTTPFlow(); + expect(utils.endTime(f)).toEqual(946681205); + f.websocket = undefined; + expect(utils.endTime(f)).toEqual(946681203); + expect(utils.endTime(TTCPFlow())).toEqual(946681205); + expect(utils.endTime(TDNSFlow())).toEqual(946681201); +}) + +it('should be possible to get a total size', () => { + expect(utils.getTotalSize(THTTPFlow())).toEqual(43); + expect(utils.getTotalSize(TTCPFlow())).toEqual(12); + expect(utils.getTotalSize(TDNSFlow())).toEqual(8); +}) diff --git a/web/src/js/components/FlowTable/FlowColumns.tsx b/web/src/js/components/FlowTable/FlowColumns.tsx index 419e195186..5e0cce01cd 100644 --- a/web/src/js/components/FlowTable/FlowColumns.tsx +++ b/web/src/js/components/FlowTable/FlowColumns.tsx @@ -37,8 +37,8 @@ icon.headerName = '' icon.sortKey = flow => getIcon(flow) const getIcon = (flow: Flow): string => { - if (flow.type === "tcp") { - return "resource-icon-tcp" + if (flow.type === "tcp" || flow.type === "dns") { + return `resource-icon-${flow.type}` } if (flow.websocket) { return 'resource-icon-websocket' @@ -77,6 +77,8 @@ const mainPath = (flow: Flow): string => { return RequestUtils.pretty_url(flow.request) case "tcp": return `${flow.client_conn.peername.join(':')} ↔ ${flow.server_conn?.address?.join(':')}` + case "dns": + return `${flow.request.questions.map(q => `${q.name} ${q.type}`).join(", ")} = ${(flow.response?.answers.map(q => q.data).join(", ") ?? "...") || "?"}` } } @@ -106,18 +108,20 @@ export const path: FlowColumn = ({flow}) => { path.headerName = 'Path' path.sortKey = flow => mainPath(flow) -export const method: FlowColumn = ({flow}) => { - return ( - {flow.type === "http" ? flow.request.method : flow.type.toUpperCase()} - ) -}; +export const method: FlowColumn = ({flow}) => {method.sortKey(flow)} method.headerName = 'Method' -method.sortKey = flow => flow.type === "http" ? flow.request.method : flow.type.toUpperCase() +method.sortKey = flow => { + switch (flow.type) { + case "http": return flow.websocket ? (flow.client_conn.tls_established ? "WSS" : "WS") : flow.request.method + case "dns": return flow.request.op_code + default: return flow.type.toUpperCase() + } +} export const status: FlowColumn = ({flow}) => { - let color = 'darkred'; + let color = 'darkred' - if (flow.type !== "http" || !flow.response) + if ((flow.type !== "http" && flow.type != "dns") || !flow.response) return if (100 <= flow.response.status_code && flow.response.status_code < 200) { @@ -127,17 +131,23 @@ export const status: FlowColumn = ({flow}) => { } else if (300 <= flow.response.status_code && flow.response.status_code < 400) { color = 'lightblue' } else if (400 <= flow.response.status_code && flow.response.status_code < 500) { - color = 'lightred' + color = 'red' } else if (500 <= flow.response.status_code && flow.response.status_code < 600) { - color = 'lightred' + color = 'red' } return ( - {flow.response.status_code} + {status.sortKey(flow)} ) } status.headerName = 'Status' -status.sortKey = flow => flow.type === "http" && flow.response && flow.response.status_code +status.sortKey = flow => { + switch (flow.type) { + case "http": return flow.response?.status_code + case "dns": return flow.response?.response_code + default: return undefined + } +} export const size: FlowColumn = ({flow}) => { return ( diff --git a/web/src/js/components/FlowView.tsx b/web/src/js/components/FlowView.tsx index 70f63894ac..53df8b8ae1 100644 --- a/web/src/js/components/FlowView.tsx +++ b/web/src/js/components/FlowView.tsx @@ -1,6 +1,7 @@ import * as React from "react" import {FunctionComponent} from "react" import {Request, Response} from './FlowView/HttpMessages' +import {Request as DnsRequest, Response as DnsResponse} from './FlowView/DnsMessages' import Connection from './FlowView/Connection' import Error from "./FlowView/Error" import Timing from "./FlowView/Timing" @@ -24,6 +25,8 @@ export const allTabs: { [name: string]: FunctionComponent & { displayN timing: Timing, websocket: WebSocket, messages: TcpMessages, + dnsrequest: DnsRequest, + dnsresponse: DnsResponse, } export function tabsForFlow(flow: Flow): string[] { @@ -35,6 +38,9 @@ export function tabsForFlow(flow: Flow): string[] { case "tcp": tabs = ["messages"] break + case "dns": + tabs = ['request', 'response'].filter(k => flow[k]).map(s => "dns" + s) + break } if (flow.error) diff --git a/web/src/js/components/FlowView/DnsMessages.tsx b/web/src/js/components/FlowView/DnsMessages.tsx new file mode 100644 index 0000000000..2eb6818f32 --- /dev/null +++ b/web/src/js/components/FlowView/DnsMessages.tsx @@ -0,0 +1,108 @@ +import * as React from "react" + +import {useAppSelector} from "../../ducks"; +import {DNSFlow, DNSMessage, DNSResourceRecord} from '../../flow' + +const Summary: React.FC<{ + message: DNSMessage +}> = ({message}) => ( +
+ {message.query ? message.op_code : message.response_code} +   + {message.truncation ? "(Truncated)" : ""} +
+) + +const Questions: React.FC<{ + message: DNSMessage +}> = ({message}) => ( + <> +
{message.recursion_desired ? "Recursive " : ""}Question
+ + + + + + + + + + {message.questions.map(question => ( + + + + + + ))} + +
NameTypeClass
{question.name}{question.type}{question.class}
+ +) + +const ResourceRecords: React.FC<{ + name: string + values: DNSResourceRecord[] +}> = ({name, values}) => ( + <> +
{name}
+ {values.length > 0 + ? + + + + + + + + + + + {values.map(rr => ( + + + + + + + + ))} + +
NameTypeClassTTLData
{rr.name}{rr.type}{rr.class}{rr.ttl}{rr.data}
+ : "—" + } + +) + +const Message: React.FC<{ + type: "request" | "response" + message: DNSMessage +}> = ({type, message}) => ( +
+
+ +
+ +
+ +
+ +
+ +
+) + +export function Request() { + const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as DNSFlow; + return ; +} + +Request.displayName = "Request" + +export function Response() { + const flow = useAppSelector(state => state.flows.byId[state.flows.selected[0]]) as DNSFlow & { response: DNSMessage } + return ; +} + +Response.displayName = "Response" diff --git a/web/src/js/components/Footer.tsx b/web/src/js/components/Footer.tsx index d170ac3c96..4df2683b26 100644 --- a/web/src/js/components/Footer.tsx +++ b/web/src/js/components/Footer.tsx @@ -6,7 +6,7 @@ import {useAppSelector} from "../ducks"; export default function Footer() { const version = useAppSelector(state => state.conf.version); let { - mode, intercept, showhost, upstream_cert, rawtcp, http2, websocket, anticache, anticomp, + mode, intercept, showhost, upstream_cert, rawtcp, dns_server, http2, websocket, anticache, anticomp, stickyauth, stickycookie, stream_large_bodies, listen_host, listen_port, server, ssl_insecure } = useAppSelector(state => state.options); @@ -30,6 +30,9 @@ export default function Footer() { {!rawtcp && ( no-raw-tcp )} + {dns_server && ( + dns-server + )} {!http2 && ( no-http2 )} diff --git a/web/src/js/components/Header/FlowMenu.tsx b/web/src/js/components/Header/FlowMenu.tsx index da9d86c678..9d6e6cceb1 100644 --- a/web/src/js/components/Header/FlowMenu.tsx +++ b/web/src/js/components/Header/FlowMenu.tsx @@ -120,7 +120,7 @@ function DownloadButton({flow}: { flow: Flow }) { function ExportButton({flow}: { flow: Flow }) { return 1} - disabled={flow.type === "tcp"}>Export▾ + disabled={flow.type !== "http"}>Export▾ } options={{"placement": "bottom-start"}}> copy(flow, "raw_request")}>Copy raw request copy(flow, "raw_response")}>Copy raw response diff --git a/web/src/js/components/Modal/Modal.jsx b/web/src/js/components/Modal/Modal.jsx deleted file mode 100644 index 64b61fcce8..0000000000 --- a/web/src/js/components/Modal/Modal.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import * as React from "react" -import ModalList from './ModalList' -import { useAppSelector } from "../../ducks"; - - -export default function PureModal() { - const activeModal = useAppSelector(state => state.ui.modal.activeModal) - const ActiveModal = ModalList.find(m => m.name === activeModal ) - - return( - activeModal ? :
- ) -} diff --git a/web/src/js/components/Modal/Modal.tsx b/web/src/js/components/Modal/Modal.tsx new file mode 100644 index 0000000000..d9a475eb91 --- /dev/null +++ b/web/src/js/components/Modal/Modal.tsx @@ -0,0 +1,13 @@ +import * as React from "react" +import ModalList from './ModalList' +import { useAppSelector } from "../../ducks"; + + +export default function PureModal() { + const activeModal : string = useAppSelector(state => state.ui.modal.activeModal) + const ActiveModal:(() => JSX.Element) | undefined= ModalList.find(m => m.name === activeModal ) + + return( + activeModal&&ActiveModal!==undefined ? :
+ ) +} diff --git a/web/src/js/components/Modal/ModalLayout.jsx b/web/src/js/components/Modal/ModalLayout.tsx similarity index 67% rename from web/src/js/components/Modal/ModalLayout.jsx rename to web/src/js/components/Modal/ModalLayout.tsx index 387f7ded97..dd21aed081 100644 --- a/web/src/js/components/Modal/ModalLayout.jsx +++ b/web/src/js/components/Modal/ModalLayout.tsx @@ -1,10 +1,14 @@ import * as React from "react" -export default function ModalLayout ({ children }) { +type ModalLayoutProps = { + children: React.ReactNode, +} + +export default function ModalLayout ({ children}: ModalLayoutProps ) { return (
-