152 lines
5.1 KiB
Python
152 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import hmac
|
|
import logging
|
|
import secrets
|
|
import webbrowser
|
|
from collections.abc import Sequence
|
|
from typing import TYPE_CHECKING
|
|
|
|
import argon2
|
|
|
|
from mitmproxy import ctx
|
|
from mitmproxy import exceptions
|
|
from mitmproxy.tools.web.web_columns import AVAILABLE_WEB_COLUMNS
|
|
|
|
if TYPE_CHECKING:
|
|
from mitmproxy.tools.web.master import WebMaster
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class WebAuth:
|
|
_password: str
|
|
_hasher: argon2.PasswordHasher
|
|
|
|
def __init__(self):
|
|
self._password = secrets.token_hex(16)
|
|
self._hasher = argon2.PasswordHasher()
|
|
|
|
def load(self, loader):
|
|
loader.add_option(
|
|
"web_password",
|
|
str,
|
|
"",
|
|
"Password to protect the mitmweb user interface. "
|
|
"Values starting with `$` are interpreted as an argon2 hash, "
|
|
"everything else is considered a plaintext password. "
|
|
"If no password is provided, a random token is generated on startup."
|
|
"For automated calls, you can pass the password as token query parameter"
|
|
"or as `Authorization: Bearer ...` header.",
|
|
)
|
|
|
|
def configure(self, updated) -> None:
|
|
if "web_password" in updated:
|
|
if ctx.options.web_password.startswith("$"):
|
|
try:
|
|
argon2.extract_parameters(ctx.options.web_password)
|
|
except argon2.exceptions.InvalidHashError:
|
|
raise exceptions.OptionsError(
|
|
"`web_password` starts with `$`, but it's not a valid argon2 hash."
|
|
)
|
|
elif ctx.options.web_password:
|
|
logger.warning(
|
|
"Using a plaintext password to protect the mitmweb user interface. "
|
|
"Consider using an argon2 hash for `web_password` instead."
|
|
)
|
|
self._password = ctx.options.web_password or secrets.token_hex(16)
|
|
|
|
@property
|
|
def web_url(self) -> str:
|
|
if ctx.options.web_password:
|
|
auth = "" # We don't want to print plaintext passwords (and it doesn't work for argon2 anyhow).
|
|
else:
|
|
auth = f"?token={self._password}"
|
|
web_host = ctx.options.web_host
|
|
if ":" in web_host: # ipv6
|
|
web_host = f"[{web_host}]"
|
|
# noinspection HttpUrlsUsage
|
|
return f"http://{web_host}:{ctx.options.web_port}/{auth}"
|
|
|
|
@staticmethod
|
|
def auth_cookie_name() -> str:
|
|
return f"mitmproxy-auth-{ctx.options.web_port}"
|
|
|
|
def is_valid_password(self, password: str) -> bool:
|
|
if self._password.startswith("$"):
|
|
try:
|
|
return self._hasher.verify(self._password, password)
|
|
except argon2.exceptions.VerificationError:
|
|
return False
|
|
else:
|
|
return hmac.compare_digest(
|
|
self._password,
|
|
password,
|
|
)
|
|
|
|
|
|
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_columns",
|
|
Sequence[str],
|
|
["tls", "icon", "path", "method", "status", "size", "time"],
|
|
f"Columns to show in the flow list. Can be one of the following: {', '.join(AVAILABLE_WEB_COLUMNS)}",
|
|
)
|
|
|
|
def running(self):
|
|
if hasattr(ctx.options, "web_open_browser") and ctx.options.web_open_browser:
|
|
master: WebMaster = ctx.master # type: ignore
|
|
success = open_browser(master.web_url)
|
|
if not success:
|
|
logger.info(
|
|
f"No web browser found. Please open a browser and point it to {master.web_url}",
|
|
)
|
|
if not success and not ctx.options.web_password:
|
|
logger.info(
|
|
f"You can configure a fixed authentication token by setting the `web_password` option "
|
|
f"(https://docs.mitmproxy.org/stable/concepts-options/#web_password).",
|
|
)
|
|
|
|
|
|
def open_browser(url: str) -> bool:
|
|
"""
|
|
Open a URL in a browser window.
|
|
In contrast to webbrowser.open, we limit the list of suitable browsers.
|
|
This gracefully degrades to a no-op on headless servers, where webbrowser.open
|
|
would otherwise open lynx.
|
|
|
|
Returns:
|
|
True, if a browser has been opened
|
|
False, if no suitable browser has been found.
|
|
"""
|
|
browsers = (
|
|
"windows-default",
|
|
"macosx",
|
|
"wslview %s",
|
|
"gio",
|
|
"x-www-browser",
|
|
"gnome-open %s",
|
|
"xdg-open",
|
|
"google-chrome",
|
|
"chrome",
|
|
"chromium",
|
|
"chromium-browser",
|
|
"firefox",
|
|
"opera",
|
|
"safari",
|
|
)
|
|
for browser in browsers:
|
|
try:
|
|
b = webbrowser.get(browser)
|
|
except webbrowser.Error:
|
|
pass
|
|
else:
|
|
if b.open(url):
|
|
return True
|
|
return False
|