Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
app.py940 linesDownload Raw Back to web
1from __future__ import annotations2 3import asyncio4import functools5import hashlib6import json7import logging8import mimetypes9import os.path10import re11import secrets12import sys13from collections.abc import Callable14from collections.abc import Sequence15from io import BytesIO16from typing import Any17from typing import Awaitable18from typing import ClassVar19from typing import Concatenate20from typing import Literal21from typing import Optional22 23import tornado.escape24import tornado.web25import tornado.websocket26 27import mitmproxy.flow28import mitmproxy.tools.web.master29import mitmproxy_rs30from mitmproxy import certs31from mitmproxy import command32from mitmproxy import contentviews33from mitmproxy import flowfilter34from mitmproxy import http35from mitmproxy import io36from mitmproxy import log37from mitmproxy import optmanager38from mitmproxy import version39from mitmproxy.dns import DNSFlow40from mitmproxy.http import HTTPFlow41from mitmproxy.tcp import TCPFlow42from mitmproxy.tcp import TCPMessage43from mitmproxy.tools.web.webaddons import WebAuth44from mitmproxy.udp import UDPFlow45from mitmproxy.udp import UDPMessage46from mitmproxy.utils import asyncio_utils47from mitmproxy.utils.emoji import emoji48from mitmproxy.utils.strutils import always_str49from mitmproxy.utils.strutils import cut_after_n_lines50from mitmproxy.websocket import WebSocketMessage51 52# Fix for Windows systems where .js files may have text/plain MIME type in registry.53# Modern browsers with ES6 module scripts require proper JavaScript MIME types.54# See: https://github.com/mitmproxy/mitmproxy/issues/797155mimetypes.add_type("text/javascript", ".js")56 57TRANSPARENT_PNG = (58    b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08"59    b"\x04\x00\x00\x00\xb5\x1c\x0c\x02\x00\x00\x00\x0bIDATx\xdac\xfc\xff\x07"60    b"\x00\x02\x00\x01\xfc\xa8Q\rh\x00\x00\x00\x00IEND\xaeB`\x82"61)62 63logger = logging.getLogger(__name__)64 65 66def cert_to_json(certs: Sequence[certs.Cert]) -> dict | None:67    if not certs:68        return None69    cert = certs[0]70    return {71        "keyinfo": cert.keyinfo,72        "sha256": cert.fingerprint().hex(),73        "notbefore": int(cert.notbefore.timestamp()),74        "notafter": int(cert.notafter.timestamp()),75        "serial": str(cert.serial),76        "subject": cert.subject,77        "issuer": cert.issuer,78        "altnames": [str(x.value) for x in cert.altnames],79    }80 81 82def flow_to_json(flow: mitmproxy.flow.Flow) -> dict:83    """84    Remove flow message content and cert to save transmission space.85    Args:86        flow: The original flow.87    Sync with web/src/flow.ts.88    """89    f = {90        "id": flow.id,91        "intercepted": flow.intercepted,92        "is_replay": flow.is_replay,93        "type": flow.type,94        "modified": flow.modified(),95        "marked": emoji.get(flow.marked, "🔴") if flow.marked else "",96        "comment": flow.comment,97        "timestamp_created": flow.timestamp_created,98    }99 100    if flow.client_conn:101        f["client_conn"] = {102            "id": flow.client_conn.id,103            "peername": flow.client_conn.peername,104            "sockname": flow.client_conn.sockname,105            "tls_established": flow.client_conn.tls_established,106            "cert": cert_to_json(flow.client_conn.certificate_list),107            "sni": flow.client_conn.sni,108            "cipher": flow.client_conn.cipher,109            "alpn": always_str(flow.client_conn.alpn, "ascii", "backslashreplace"),110            "tls_version": flow.client_conn.tls_version,111            "timestamp_start": flow.client_conn.timestamp_start,112            "timestamp_tls_setup": flow.client_conn.timestamp_tls_setup,113            "timestamp_end": flow.client_conn.timestamp_end,114        }115 116    if flow.server_conn:117        f["server_conn"] = {118            "id": flow.server_conn.id,119            "peername": flow.server_conn.peername,120            "sockname": flow.server_conn.sockname,121            "address": flow.server_conn.address,122            "tls_established": flow.server_conn.tls_established,123            "cert": cert_to_json(flow.server_conn.certificate_list),124            "sni": flow.server_conn.sni,125            "cipher": flow.server_conn.cipher,126            "alpn": always_str(flow.server_conn.alpn, "ascii", "backslashreplace"),127            "tls_version": flow.server_conn.tls_version,128            "timestamp_start": flow.server_conn.timestamp_start,129            "timestamp_tcp_setup": flow.server_conn.timestamp_tcp_setup,130            "timestamp_tls_setup": flow.server_conn.timestamp_tls_setup,131            "timestamp_end": flow.server_conn.timestamp_end,132        }133    if flow.error:134        f["error"] = flow.error.get_state()135 136    if isinstance(flow, HTTPFlow):137        content_length: int | None138        content_hash: str | None139 140        if flow.request.raw_content is not None:141            content_length = len(flow.request.raw_content)142            content_hash = hashlib.sha256(flow.request.raw_content).hexdigest()143        else:144            content_length = None145            content_hash = None146        f["request"] = {147            "method": flow.request.method,148            "scheme": flow.request.scheme,149            "host": flow.request.host,150            "port": flow.request.port,151            "path": flow.request.path,152            "http_version": flow.request.http_version,153            "headers": tuple(flow.request.headers.items(True)),154            "contentLength": content_length,155            "contentHash": content_hash,156            "timestamp_start": flow.request.timestamp_start,157            "timestamp_end": flow.request.timestamp_end,158            "pretty_host": flow.request.pretty_host,159        }160        if flow.response:161            if flow.response.raw_content is not None:162                content_length = len(flow.response.raw_content)163                content_hash = hashlib.sha256(flow.response.raw_content).hexdigest()164            else:165                content_length = None166                content_hash = None167            f["response"] = {168                "http_version": flow.response.http_version,169                "status_code": flow.response.status_code,170                "reason": flow.response.reason,171                "headers": tuple(flow.response.headers.items(True)),172                "contentLength": content_length,173                "contentHash": content_hash,174                "timestamp_start": flow.response.timestamp_start,175                "timestamp_end": flow.response.timestamp_end,176            }177            if flow.response.data.trailers:178                f["response"]["trailers"] = tuple(179                    flow.response.data.trailers.items(True)180                )181 182        if flow.websocket:183            f["websocket"] = {184                "messages_meta": {185                    "contentLength": sum(186                        len(x.content) for x in flow.websocket.messages187                    ),188                    "count": len(flow.websocket.messages),189                    "timestamp_last": flow.websocket.messages[-1].timestamp190                    if flow.websocket.messages191                    else None,192                },193                "closed_by_client": flow.websocket.closed_by_client,194                "close_code": flow.websocket.close_code,195                "close_reason": flow.websocket.close_reason,196                "timestamp_end": flow.websocket.timestamp_end,197            }198    elif isinstance(flow, (TCPFlow, UDPFlow)):199        f["messages_meta"] = {200            "contentLength": sum(len(x.content) for x in flow.messages),201            "count": len(flow.messages),202            "timestamp_last": flow.messages[-1].timestamp if flow.messages else None,203        }204    elif isinstance(flow, DNSFlow):205        f["request"] = flow.request.to_json()206        if flow.response:207            f["response"] = flow.response.to_json()208 209    return f210 211 212def logentry_to_json(e: log.LogEntry) -> dict:213    return {214        "id": id(e),  # we just need some kind of id.215        "message": e.msg,216        "level": e.level,217    }218 219 220class APIError(tornado.web.HTTPError):221    pass222 223 224class AuthRequestHandler(tornado.web.RequestHandler):225    AUTH_COOKIE_VALUE = b"y"226 227    def __init_subclass__(cls, **kwargs):228        """Automatically wrap all request handlers with `_require_auth`."""229        for method in cls.SUPPORTED_METHODS:230            method = method.lower()231            fn = getattr(cls, method)232            if fn is not tornado.web.RequestHandler._unimplemented_method:233                setattr(cls, method, AuthRequestHandler._require_auth(fn))234 235    def auth_fail(self, invalid_password: bool) -> None:236        """237        Will be called when returning a 403.238        May write a login form as the response.239        """240 241    @staticmethod242    def _require_auth[**P, R](243        fn: Callable[Concatenate[AuthRequestHandler, P], R],244    ) -> Callable[Concatenate[AuthRequestHandler, P], R | None]:245        @functools.wraps(fn)246        def wrapper(247            self: AuthRequestHandler, *args: P.args, **kwargs: P.kwargs248        ) -> R | None:249            if not self.current_user:250                password = ""251                if auth_header := self.request.headers.get("Authorization"):252                    auth_scheme, _, auth_params = auth_header.partition(" ")253                    if auth_scheme == "Bearer":254                        password = auth_params255 256                if not password:257                    password = self.get_argument("token", default="")258 259                if not self.settings["is_valid_password"](password):260                    self.set_status(403)261                    self.auth_fail(bool(password))262                    return None263                self.set_signed_cookie(264                    self.settings["auth_cookie_name"](),265                    self.AUTH_COOKIE_VALUE,266                    expires_days=400,267                    httponly=True,268                    samesite="Strict",269                )270            return fn(self, *args, **kwargs)271 272        return wrapper273 274    def get_current_user(self) -> bool:275        return (276            self.get_signed_cookie(self.settings["auth_cookie_name"](), min_version=2)277            == self.AUTH_COOKIE_VALUE278        )279 280 281class RequestHandler(AuthRequestHandler):282    application: Application283 284    def prepare(self):285        if (286            self.request.method not in ("GET", "HEAD", "OPTIONS")287            and "Sec-Fetch-Site" in self.request.headers288            and self.request.headers["Sec-Fetch-Site"] not in ("same-origin", "none")289        ):290            raise tornado.httpclient.HTTPError(403)291 292    def write(self, chunk: str | bytes | dict | list):293        # Writing arrays on the top level is ok nowadays.294        # http://flask.pocoo.org/docs/0.11/security/#json-security295        if isinstance(chunk, list):296            chunk = tornado.escape.json_encode(chunk)297            self.set_header("Content-Type", "application/json; charset=UTF-8")298        super().write(chunk)299 300    def set_default_headers(self):301        super().set_default_headers()302        self.set_header("Server", version.MITMPROXY)303        self.set_header("X-Frame-Options", "DENY")304        self.add_header("X-XSS-Protection", "1; mode=block")305        self.add_header("X-Content-Type-Options", "nosniff")306        self.add_header(307            "Content-Security-Policy",308            "default-src 'self'; "309            "connect-src 'self' ws:; "310            "img-src 'self' data:; "311            "style-src   'self' 'unsafe-inline'",312        )313 314    @property315    def json(self):316        if not self.request.headers.get("Content-Type", "").startswith(317            "application/json"318        ):319            raise APIError(400, "Invalid Content-Type, expected application/json.")320        try:321            return json.loads(self.request.body.decode())322        except Exception as e:323            raise APIError(400, f"Malformed JSON: {e}")324 325    @property326    def filecontents(self):327        """328        Accept either a multipart/form file upload or just take the plain request body.329 330        """331        if self.request.files:332            return next(iter(self.request.files.values()))[0].body333        else:334            return self.request.body335 336    @property337    def view(self) -> mitmproxy.addons.view.View:338        return self.application.master.view339 340    @property341    def master(self) -> mitmproxy.tools.web.master.WebMaster:342        return self.application.master343 344    @property345    def flow(self) -> mitmproxy.flow.Flow:346        flow_id = str(self.path_kwargs["flow_id"])347        # FIXME: Add a facility to addon.view to safely access the store348        flow = self.view.get_by_id(flow_id)349        if flow:350            return flow351        else:352            raise APIError(404, "Flow not found.")353 354    def write_error(self, status_code: int, **kwargs):355        if "exc_info" in kwargs and isinstance(kwargs["exc_info"][1], APIError):356            self.finish(kwargs["exc_info"][1].log_message)357        else:358            super().write_error(status_code, **kwargs)359 360 361class IndexHandler(RequestHandler):362    def auth_fail(self, invalid_password: bool) -> None:363        self.render("login.html", invalid_password=invalid_password)364 365    def get(self):366        self.render("../index.html")367 368    post = get  # login form369 370 371class FilterHelp(RequestHandler):372    def get(self):373        self.write(dict(commands=flowfilter.help))374 375 376class WebSocketEventBroadcaster(tornado.websocket.WebSocketHandler, AuthRequestHandler):377    # raise an error if inherited class doesn't specify its own instance.378    connections: ClassVar[set[WebSocketEventBroadcaster]]379 380    _send_queue: asyncio.Queue[bytes]381    _send_task: asyncio.Task[None]382 383    def prepare(self) -> Optional[Awaitable[None]]:384        token = self.xsrf_token  # https://github.com/tornadoweb/tornado/issues/645385        assert token386        return None387 388    def open(self, *args, **kwargs):389        self.connections.add(self)390        self._send_queue = asyncio.Queue()391        # Python 3.13+: use _send_queue.shutdown() and we can use keep_ref=True here.392        self._send_task = asyncio_utils.create_task(393            self.send_task(),394            name="WebSocket send task",395            keep_ref=False,396        )397 398    def on_close(self):399        self.connections.discard(self)400        self._send_task.cancel()401 402    @classmethod403    def broadcast(cls, **kwargs):404        message = cls._json_dumps(kwargs)405        for conn in cls.connections:406            conn.send(message)407 408    def send(self, message: bytes):409        self._send_queue.put_nowait(message)410 411    async def send_task(self):412        while True:413            message = await self._send_queue.get()414            try:415                await self.write_message(message)416            except tornado.websocket.WebSocketClosedError:417                self.on_close()418 419    @staticmethod420    def _json_dumps(d):421        return json.dumps(d, ensure_ascii=False).encode("utf8", "surrogateescape")422 423 424class ClientConnection(WebSocketEventBroadcaster):425    connections: ClassVar[set[ClientConnection]] = set()  # type: ignore426    application: Application427 428    def __init__(self, application: Application, request, **kwargs):429        super().__init__(application, request, **kwargs)430        self.filters: dict[str, flowfilter.TFilter] = {}  # filters per connection431 432    @classmethod433    def broadcast_flow_reset(cls) -> None:434        for conn in cls.connections:435            conn.send(cls._json_dumps({"type": "flows/reset"}))436            for name, expr in conn.filters.copy().items():437                conn.update_filter(name, expr.pattern)438 439    @classmethod440    def broadcast_flow(441        cls,442        type: Literal["flows/add", "flows/update"],443        f: mitmproxy.flow.Flow,444    ) -> None:445        flow_json = flow_to_json(f)446        for conn in cls.connections:447            conn._broadcast_flow(type, f, flow_json)448 449    def _broadcast_flow(450        self,451        type: Literal["flows/add", "flows/update"],452        f: mitmproxy.flow.Flow,453        flow_json: dict,  # Passing the flow_json dictionary to avoid recalculating it for each client454    ) -> None:455        filters = {name: bool(expr(f)) for name, expr in self.filters.items()}456        message = self._json_dumps(457            {458                "type": type,459                "payload": {460                    "flow": flow_json,461                    "matching_filters": filters,462                },463            },464        )465        self.send(message)466 467    def update_filter(self, name: str, expr: str) -> None:468        if expr:469            filt = flowfilter.parse(expr)470            self.filters[name] = filt471            matching_flow_ids = [f.id for f in self.application.master.view if filt(f)]472        else:473            self.filters.pop(name, None)474            matching_flow_ids = None475 476        message = self._json_dumps(477            {478                "type": "flows/filterUpdate",479                "payload": {480                    "name": name,481                    "matching_flow_ids": matching_flow_ids,482                },483            },484        )485        self.send(message=message)486 487    async def on_message(self, message: str | bytes):488        try:489            data = json.loads(message)490            match data["type"]:491                case "flows/updateFilter":492                    self.update_filter(data["payload"]["name"], data["payload"]["expr"])493                case other:494                    raise ValueError(f"Unsupported command: {other}")495        except Exception as e:496            logger.error(f"Error processing message from {self}: {e}")497            self.close(code=1011, reason="Internal server error.")498 499 500class Flows(RequestHandler):501    def get(self):502        self.write([flow_to_json(f) for f in self.view])503 504 505class DumpFlows(RequestHandler):506    def get(self) -> None:507        self.set_header("Content-Disposition", "attachment; filename=flows")508        self.set_header("Content-Type", "application/octet-stream")509 510        match: Callable[[mitmproxy.flow.Flow], bool]511        try:512            match = flowfilter.parse(self.request.arguments["filter"][0].decode())513        except ValueError:  # thrown py flowfilter.parse if filter is invalid514            raise APIError(400, f"Invalid filter argument / regex")515        except (516            KeyError,517            IndexError,518        ):  # Key+Index: ["filter"][0] can fail, if it's not set519 520            def match(_) -> bool:521                return True522 523        with BytesIO() as bio:524            fw = io.FlowWriter(bio)525            for f in self.view:526                if match(f):527                    fw.add(f)528            self.write(bio.getvalue())529 530    async def post(self):531        self.view.clear()532        bio = BytesIO(self.filecontents)533        for f in io.FlowReader(bio).stream():534            await self.master.load_flow(f)535        bio.close()536 537 538class ClearAll(RequestHandler):539    def post(self):540        self.view.clear()541        self.master.events.clear()542 543 544class ResumeFlows(RequestHandler):545    def post(self):546        for f in self.view:547            if not f.intercepted:548                continue549            f.resume()550            self.view.update([f])551 552 553class KillFlows(RequestHandler):554    def post(self):555        for f in self.view:556            if f.killable:557                f.kill()558                self.view.update([f])559 560 561class ResumeFlow(RequestHandler):562    def post(self, flow_id):563        self.flow.resume()564        self.view.update([self.flow])565 566 567class KillFlow(RequestHandler):568    def post(self, flow_id):569        if self.flow.killable:570            self.flow.kill()571            self.view.update([self.flow])572 573 574class FlowHandler(RequestHandler):575    def delete(self, flow_id):576        if self.flow.killable:577            self.flow.kill()578        self.view.remove([self.flow])579 580    def put(self, flow_id) -> None:581        flow: mitmproxy.flow.Flow = self.flow582        flow.backup()583        try:584            for a, b in self.json.items():585                if a == "request" and hasattr(flow, "request"):586                    request: mitmproxy.http.Request = flow.request587                    for k, v in b.items():588                        if k in ["method", "scheme", "host", "path", "http_version"]:589                            setattr(request, k, str(v))590                        elif k == "port":591                            request.port = int(v)592                        elif k == "headers":593                            request.headers.clear()594                            for header in v:595                                request.headers.add(*header)596                        elif k == "trailers":597                            if request.trailers is not None:598                                request.trailers.clear()599                            else:600                                request.trailers = mitmproxy.http.Headers()601                            for trailer in v:602                                request.trailers.add(*trailer)603                        elif k == "content":604                            request.text = v605                        else:606                            raise APIError(400, f"Unknown update request.{k}: {v}")607 608                elif a == "response" and hasattr(flow, "response"):609                    response: mitmproxy.http.Response = flow.response610                    for k, v in b.items():611                        if k in ["msg", "http_version"]:612                            setattr(response, k, str(v))613                        elif k == "code":614                            response.status_code = int(v)615                        elif k == "headers":616                            response.headers.clear()617                            for header in v:618                                response.headers.add(*header)619                        elif k == "trailers":620                            if response.trailers is not None:621                                response.trailers.clear()622                            else:623                                response.trailers = mitmproxy.http.Headers()624                            for trailer in v:625                                response.trailers.add(*trailer)626                        elif k == "content":627                            response.text = v628                        else:629                            raise APIError(400, f"Unknown update response.{k}: {v}")630                elif a == "marked":631                    flow.marked = b632                elif a == "comment":633                    flow.comment = b634                else:635                    raise APIError(400, f"Unknown update {a}: {b}")636        except APIError:637            flow.revert()638            raise639        self.view.update([flow])640 641 642class DuplicateFlow(RequestHandler):643    def post(self, flow_id):644        f = self.flow.copy()645        self.view.add([f])646        self.write(f.id)647 648 649class RevertFlow(RequestHandler):650    def post(self, flow_id):651        if self.flow.modified():652            self.flow.revert()653            self.view.update([self.flow])654 655 656class ReplayFlow(RequestHandler):657    def post(self, flow_id):658        self.master.commands.call("replay.client", [self.flow])659 660 661class FlowContent(RequestHandler):662    def post(self, flow_id, message):663        self.flow.backup()664        message = getattr(self.flow, message)665        message.content = self.filecontents666        self.view.update([self.flow])667 668    def get(self, flow_id, message):669        message = getattr(self.flow, message)670        assert isinstance(self.flow, HTTPFlow)671 672        original_cd = message.headers.get("Content-Disposition", None)673        filename = None674        if original_cd:675            if m := re.search(r'filename=([-\w" .()]+)', original_cd):676                filename = m.group(1)677        if not filename:678            filename = self.flow.request.path.split("?")[0].split("/")[-1]679 680        filename = re.sub(r'[^-\w" .()]', "", filename)681        cd = f"attachment; {filename=!s}"682        self.set_header("Content-Disposition", cd)683        self.set_header("Content-Type", "application/text")684        self.set_header("X-Content-Type-Options", "nosniff")685        self.set_header("X-Frame-Options", "DENY")686        self.write(message.get_content(strict=False))687 688 689class FlowContentView(RequestHandler):690    def message_to_json(691        self,692        view_name: str,693        message: http.Message | TCPMessage | UDPMessage | WebSocketMessage,694        flow: HTTPFlow | TCPFlow | UDPFlow,695        max_lines: int | None = None,696        from_client: bool | None = None,697        timestamp: float | None = None,698    ):699        if view_name and view_name.lower() == "auto":700            view_name = "auto"701        pretty = contentviews.prettify_message(message, flow, view_name=view_name)702        if max_lines:703            pretty.text = cut_after_n_lines(pretty.text, max_lines)704 705        ret: dict[str, Any] = dict(706            text=pretty.text,707            view_name=pretty.view_name,708            syntax_highlight=pretty.syntax_highlight,709            description=pretty.description,710        )711        if from_client is not None:712            ret["from_client"] = from_client713        if timestamp is not None:714            ret["timestamp"] = timestamp715        return ret716 717    def get(self, flow_id, message, content_view) -> None:718        flow = self.flow719        assert isinstance(flow, (HTTPFlow, TCPFlow, UDPFlow))720 721        if self.request.arguments.get("lines"):722            max_lines = int(self.request.arguments["lines"][0])723        else:724            max_lines = None725 726        if message == "messages":727            messages: list[TCPMessage] | list[UDPMessage] | list[WebSocketMessage]728            if isinstance(flow, HTTPFlow) and flow.websocket:729                messages = flow.websocket.messages730            elif isinstance(flow, (TCPFlow, UDPFlow)):731                messages = flow.messages732            else:733                raise APIError(400, f"This flow has no messages.")734            msgs = []735            for m in messages:736                d = self.message_to_json(737                    view_name=content_view,738                    message=m,739                    flow=flow,740                    max_lines=max_lines,741                    from_client=m.from_client,742                    timestamp=m.timestamp,743                )744                msgs.append(d)745                if max_lines:746                    max_lines -= d["text"].count("\n") + 1747                    assert max_lines is not None748                    if max_lines <= 0:749                        break750            self.write(msgs)751        else:752            message = getattr(self.flow, message)753            self.write(self.message_to_json(content_view, message, flow, max_lines))754 755 756class Commands(RequestHandler):757    def get(self) -> None:758        commands = {}759        for name, cmd in self.master.commands.commands.items():760            commands[name] = {761                "help": cmd.help,762                "parameters": [763                    {764                        "name": param.name,765                        "type": command.typename(param.type),766                        "kind": str(param.kind),767                    }768                    for param in cmd.parameters769                ],770                "return_type": command.typename(cmd.return_type)771                if cmd.return_type772                else None,773                "signature_help": cmd.signature_help(),774            }775        self.write(commands)776 777 778class ExecuteCommand(RequestHandler):779    def post(self, cmd: str):780        # TODO: We should parse query strings here, this API is painful.781        try:782            args = self.json["arguments"]783        except APIError:784            args = []785        try:786            result = self.master.commands.call_strings(cmd, args)787        except Exception as e:788            self.write({"error": str(e)})789        else:790            self.write(791                {792                    "value": result,793                    # "type": command.typename(type(result)) if result is not None else "none"794                }795            )796 797 798class Events(RequestHandler):799    def get(self):800        self.write([logentry_to_json(e) for e in self.master.events.data])801 802 803class Options(RequestHandler):804    def get(self):805        self.write(optmanager.dump_dicts(self.master.options))806 807    def put(self):808        update = self.json809        try:810            self.master.options.update(**update)811        except Exception as err:812            raise APIError(400, f"{err}")813 814 815class SaveOptions(RequestHandler):816    def post(self):817        # try:818        #     optmanager.save(self.master.options, CONFIG_PATH, True)819        # except Exception as err:820        #     raise APIError(400, "{}".format(err))821        pass822 823 824class State(RequestHandler):825    # Separate method for testability.826    @staticmethod827    def get_json(master: mitmproxy.tools.web.master.WebMaster):828        return {829            "version": version.VERSION,830            "contentViews": [831                v for v in contentviews.registry.available_views() if v != "query"832            ],833            "servers": {834                s.mode.full_spec: s.to_json() for s in master.proxyserver.servers835            },836            "platform": sys.platform,837            "localModeUnavailable": mitmproxy_rs.local.LocalRedirector.unavailable_reason(),838        }839 840    def get(self):841        self.write(State.get_json(self.master))842 843 844class ProcessList(RequestHandler):845    @staticmethod846    def get_json():847        processes = mitmproxy_rs.process_info.active_executables()848        return [849            {850                "is_visible": process.is_visible,851                "executable": str(process.executable),852                "is_system": process.is_system,853                "display_name": process.display_name,854            }855            for process in processes856        ]857 858    def get(self):859        self.write(ProcessList.get_json())860 861 862class ProcessImage(RequestHandler):863    def get(self):864        path = self.get_query_argument("path", None)865 866        if not path:867            raise APIError(400, "Missing 'path' parameter.")868 869        try:870            icon_bytes = mitmproxy_rs.process_info.executable_icon(path)871        except Exception:872            icon_bytes = TRANSPARENT_PNG873 874        self.set_header("Content-Type", "image/png")875        self.set_header("X-Content-Type-Options", "nosniff")876        self.set_header("Cache-Control", "max-age=604800")877        self.write(icon_bytes)878 879 880class GZipContentAndFlowFiles(tornado.web.GZipContentEncoding):881    CONTENT_TYPES = {882        "application/octet-stream",883        *tornado.web.GZipContentEncoding.CONTENT_TYPES,884    }885 886 887handlers = [888    (r"/", IndexHandler),889    (r"/filter-help(?:\.json)?", FilterHelp),890    (r"/updates", ClientConnection),891    (r"/commands(?:\.json)?", Commands),892    (r"/commands/(?P<cmd>[a-z.]+)", ExecuteCommand),893    (r"/events(?:\.json)?", Events),894    (r"/flows(?:\.json)?", Flows),895    (r"/flows/dump", DumpFlows),896    (r"/flows/resume", ResumeFlows),897    (r"/flows/kill", KillFlows),898    (r"/flows/(?P<flow_id>[0-9a-f\-]+)", FlowHandler),899    (r"/flows/(?P<flow_id>[0-9a-f\-]+)/resume", ResumeFlow),900    (r"/flows/(?P<flow_id>[0-9a-f\-]+)/kill", KillFlow),901    (r"/flows/(?P<flow_id>[0-9a-f\-]+)/duplicate", DuplicateFlow),902    (r"/flows/(?P<flow_id>[0-9a-f\-]+)/replay", ReplayFlow),903    (r"/flows/(?P<flow_id>[0-9a-f\-]+)/revert", RevertFlow),904    (r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response|messages)/content.data", FlowContent),905    (r"/flows/(?P<flow_id>[0-9a-f\-]+)/(?P<message>request|response|messages)/content/(?P<content_view>[0-9a-zA-Z\-\_%]+)(?:\.json)?", FlowContentView),906    (r"/clear", ClearAll),907    (r"/options(?:\.json)?", Options),908    (r"/options/save", SaveOptions),909    (r"/state(?:\.json)?", State),910    (r"/processes", ProcessList),911    (r"/executable-icon", ProcessImage),912]  # fmt: skip913 914 915class Application(tornado.web.Application):916    master: mitmproxy.tools.web.master.WebMaster917 918    def __init__(919        self, master: mitmproxy.tools.web.master.WebMaster, debug: bool920    ) -> None:921        self.master = master922        auth_addon: WebAuth = master.addons.get("webauth")923        super().__init__(924            handlers=handlers,  # type: ignore  # https://github.com/tornadoweb/tornado/pull/3455925            template_path=os.path.join(os.path.dirname(__file__), "templates"),926            static_path=os.path.join(os.path.dirname(__file__), "static"),927            xsrf_cookies=True,928            # https://github.com/mitmproxy/mitmproxy/issues/8194929            # 2026-05: We can move back to the default cookie name in a few years.930            xsrf_cookie_name="_mitmproxy_xsrf",931            xsrf_cookie_kwargs=dict(samesite="Strict"),932            cookie_secret=secrets.token_bytes(32),933            debug=debug,934            autoreload=False,935            transforms=[GZipContentAndFlowFiles],936            is_valid_password=auth_addon.is_valid_password,937            auth_cookie_name=auth_addon.auth_cookie_name,938            compiled_template_cache=False,  # Vite939        )940 
codekingpro/portable-devtools · Team Ai