Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
flowview.py498 linesDownload Raw Back to console
1import sys2from functools import lru_cache3 4import urwid5 6import mitmproxy.flow7import mitmproxy.tools.console.master8import mitmproxy_rs.syntax_highlight9from mitmproxy import contentviews10from mitmproxy import ctx11from mitmproxy import dns12from mitmproxy import http13from mitmproxy import tcp14from mitmproxy import udp15from mitmproxy.dns import DNSMessage16from mitmproxy.tools.console import common17from mitmproxy.tools.console import flowdetailview18from mitmproxy.tools.console import layoutwidget19from mitmproxy.tools.console import searchable20from mitmproxy.tools.console import tabs21from mitmproxy.utils import strutils22 23 24class FlowViewHeader(urwid.WidgetWrap):25    def __init__(26        self,27        master: "mitmproxy.tools.console.master.ConsoleMaster",28    ) -> None:29        self.master = master30        self.focus_changed()31 32    def focus_changed(self):33        cols, _ = self.master.ui.get_cols_rows()34        if self.master.view.focus.flow:35            self._w = common.format_flow(36                self.master.view.focus.flow,37                render_mode=common.RenderMode.DETAILVIEW,38                hostheader=self.master.options.showhost,39            )40        else:41            self._w = urwid.Pile([])42 43 44class FlowDetails(tabs.Tabs):45    def __init__(self, master):46        self.master = master47        super().__init__([])48        self.show()49        self.last_displayed_body = None50        self.last_displayed_websocket_messages = None51        contentviews.registry.on_change.connect(self.contentview_changed)52 53    @property54    def view(self):55        return self.master.view56 57    @property58    def flow(self) -> mitmproxy.flow.Flow:59        return self.master.view.focus.flow60 61    def contentview_changed(self, view):62        # this is called when a contentview addon is live-reloaded.63        # we clear our cache and then rerender64        self._get_content_view.cache_clear()65        if self.master.window.current_window("flowview"):66            self.show()67 68    def focus_changed(self):69        f = self.flow70        if f:71            if isinstance(f, http.HTTPFlow):72                if f.websocket:73                    self.tabs = [74                        (self.tab_http_request, self.view_request),75                        (self.tab_http_response, self.view_response),76                        (self.tab_websocket_messages, self.view_websocket_messages),77                        (self.tab_details, self.view_details),78                    ]79                else:80                    self.tabs = [81                        (self.tab_http_request, self.view_request),82                        (self.tab_http_response, self.view_response),83                        (self.tab_details, self.view_details),84                    ]85            elif isinstance(f, tcp.TCPFlow):86                self.tabs = [87                    (self.tab_tcp_stream, self.view_message_stream),88                    (self.tab_details, self.view_details),89                ]90            elif isinstance(f, udp.UDPFlow):91                self.tabs = [92                    (self.tab_udp_stream, self.view_message_stream),93                    (self.tab_details, self.view_details),94                ]95            elif isinstance(f, dns.DNSFlow):96                self.tabs = [97                    (self.tab_dns_request, self.view_dns_request),98                    (self.tab_dns_response, self.view_dns_response),99                    (self.tab_details, self.view_details),100                ]101            self.show()102        else:103            # Get the top window from the focus stack (the currently active view).104            # If it's NOT the "flowlist", it's safe to pop back to the previous view.105            if self.master.window.focus_stack().stack[-1] != "flowlist":106                self.master.window.pop()107            # If it is the "flowlist", we’re already at the main view with no flows to show.108            # Popping now would close the last window and prompt app exit, so we remain on the empty flow list screen instead.109 110    def tab_http_request(self):111        flow = self.flow112        assert isinstance(flow, http.HTTPFlow)113        if self.flow.intercepted and not flow.response:114            return "Request intercepted"115        else:116            return "Request"117 118    def tab_http_response(self):119        flow = self.flow120        assert isinstance(flow, http.HTTPFlow)121 122        # there is no good way to detect what part of the flow is intercepted,123        # so we apply some heuristics to see if it's the HTTP response.124        websocket_started = flow.websocket and len(flow.websocket.messages) != 0125        response_is_intercepted = (126            self.flow.intercepted and flow.response and not websocket_started127        )128        if response_is_intercepted:129            return "Response intercepted"130        else:131            return "Response"132 133    def tab_dns_request(self) -> str:134        flow = self.flow135        assert isinstance(flow, dns.DNSFlow)136        if self.flow.intercepted and not flow.response:137            return "Request intercepted"138        else:139            return "Request"140 141    def tab_dns_response(self) -> str:142        flow = self.flow143        assert isinstance(flow, dns.DNSFlow)144        if self.flow.intercepted and flow.response:145            return "Response intercepted"146        else:147            return "Response"148 149    def tab_tcp_stream(self):150        return "TCP Stream"151 152    def tab_udp_stream(self):153        return "UDP Stream"154 155    def tab_websocket_messages(self):156        flow = self.flow157        assert isinstance(flow, http.HTTPFlow)158        assert flow.websocket159 160        if self.flow.intercepted and len(flow.websocket.messages) != 0:161            return "WebSocket Messages intercepted"162        else:163            return "WebSocket Messages"164 165    def tab_details(self):166        return "Detail"167 168    def view_request(self):169        flow = self.flow170        assert isinstance(flow, http.HTTPFlow)171        return self.conn_text(flow.request)172 173    def view_response(self):174        flow = self.flow175        assert isinstance(flow, http.HTTPFlow)176        return self.conn_text(flow.response)177 178    def view_dns_request(self):179        flow = self.flow180        assert isinstance(flow, dns.DNSFlow)181        return self.dns_message_text("request", flow.request)182 183    def view_dns_response(self):184        flow = self.flow185        assert isinstance(flow, dns.DNSFlow)186        return self.dns_message_text("response", flow.response)187 188    def _contentview_status_bar(self, description: str, viewmode: str):189        cols = [190            urwid.Text(191                [192                    ("heading", description),193                ]194            ),195            urwid.Text(196                [197                    " ",198                    ("heading", "["),199                    ("heading_key", "m"),200                    ("heading", (":%s]" % viewmode)),201                ],202                align="right",203            ),204        ]205        contentview_status_bar = urwid.AttrMap(urwid.Columns(cols), "heading")206        return contentview_status_bar207 208    FROM_CLIENT_MARKER = ("from_client", f"{common.SYMBOL_FROM_CLIENT} ")209    TO_CLIENT_MARKER = ("to_client", f"{common.SYMBOL_TO_CLIENT} ")210 211    def view_websocket_messages(self):212        flow = self.flow213        assert isinstance(flow, http.HTTPFlow)214        assert flow.websocket is not None215 216        if not flow.websocket.messages:217            return searchable.Searchable([urwid.Text(("highlight", "No messages."))])218 219        viewmode = self.master.commands.call("console.flowview.mode")220 221        widget_lines = []222        for m in flow.websocket.messages:223            pretty = contentviews.prettify_message(m, flow, viewmode)224            chunks = mitmproxy_rs.syntax_highlight.highlight(225                pretty.text,226                language=pretty.syntax_highlight,227            )228            if m.from_client:229                marker = self.FROM_CLIENT_MARKER230            else:231                marker = self.TO_CLIENT_MARKER232            widget_lines.append(urwid.Text([marker, *chunks]))233 234        if flow.websocket.closed_by_client is not None:235            widget_lines.append(236                urwid.Text(237                    [238                        (239                            self.FROM_CLIENT_MARKER240                            if flow.websocket.closed_by_client241                            else self.TO_CLIENT_MARKER242                        ),243                        (244                            "alert"245                            if flow.websocket.close_code in (1000, 1001, 1005)246                            else "error",247                            f"Connection closed: {flow.websocket.close_code} {flow.websocket.close_reason}",248                        ),249                    ]250                )251            )252 253        if flow.intercepted:254            markup = widget_lines[-1].get_text()[0]255            widget_lines[-1].set_text(("intercept", markup))256 257        widget_lines.insert(258            0, self._contentview_status_bar(viewmode.capitalize(), viewmode)259        )260 261        if (last_view := self.last_displayed_websocket_messages) is not None:262            last_view.walker[:] = widget_lines263            view = last_view264        else:265            view = searchable.Searchable(widget_lines)266            self.last_displayed_websocket_messages = view267 268        return view269 270    def view_message_stream(self) -> urwid.Widget:271        flow = self.flow272        assert isinstance(flow, (tcp.TCPFlow, udp.UDPFlow))273 274        if not flow.messages:275            return searchable.Searchable([urwid.Text(("highlight", "No messages."))])276 277        viewmode = self.master.commands.call("console.flowview.mode")278 279        widget_lines = []280        for m in flow.messages:281            if m.from_client:282                marker = self.FROM_CLIENT_MARKER283            else:284                marker = self.TO_CLIENT_MARKER285            pretty = contentviews.prettify_message(m, flow, viewmode)286            chunks = mitmproxy_rs.syntax_highlight.highlight(287                pretty.text,288                language=pretty.syntax_highlight,289            )290 291            widget_lines.append(urwid.Text([marker, *chunks]))292 293        if flow.intercepted:294            markup = widget_lines[-1].get_text()[0]295            widget_lines[-1].set_text(("intercept", markup))296 297        widget_lines.insert(298            0, self._contentview_status_bar(viewmode.capitalize(), viewmode)299        )300 301        return searchable.Searchable(widget_lines)302 303    def view_details(self):304        return flowdetailview.flowdetails(self.view, self.flow)305 306    def content_view(307        self, viewmode: str, message: http.Message308    ) -> tuple[str, list[urwid.Text]]:309        if message.raw_content is None:310            return "", [urwid.Text([("error", "[content missing]")])]311 312        if message.raw_content == b"":313            if isinstance(message, http.Request):314                query = getattr(message, "query", "")315                if not query:316                    # No body and no query params317                    return "", [urwid.Text("No request content")]318                # else: there are query params -> fall through to render them319            else:320                return "", [urwid.Text("No content")]321 322        full = self.master.commands.execute(323            "view.settings.getval @focus fullcontents false"324        )325 326        if full == "true":327            limit = sys.maxsize328        else:329            limit = ctx.options.content_view_lines_cutoff330 331        flow_modify_cache_invalidation = hash(332            (333                message.raw_content,334                message.headers.fields,335                getattr(message, "path", None),336            )337        )338        # we need to pass the message off-band because it's not hashable339        self._get_content_view_message = message340        return self._get_content_view(viewmode, limit, flow_modify_cache_invalidation)341 342    @lru_cache(maxsize=200)343    def _get_content_view(344        self, viewmode: str, max_lines: int, _345    ) -> tuple[str, list[urwid.Text]]:346        message: http.Message = self._get_content_view_message347        self._get_content_view_message = None  # type: ignore[assignment]348 349        pretty = contentviews.prettify_message(message, self.flow, viewmode)350        cut_off = strutils.cut_after_n_lines(pretty.text, max_lines)351 352        chunks = mitmproxy_rs.syntax_highlight.highlight(353            cut_off,354            language=pretty.syntax_highlight,355        )356 357        text_objects = [urwid.Text(chunks)]358        if len(cut_off) < len(pretty.text):359            text_objects.append(360                urwid.Text(361                    [362                        (363                            "highlight",364                            "Stopped displaying data after %d lines. Press "365                            % max_lines,366                        ),367                        ("key", "f"),368                        ("highlight", " to load all data."),369                    ]370                )371            )372 373        return f"{pretty.view_name} {pretty.description}", text_objects374 375    def conn_text(self, conn):376        if conn:377            hdrs = []378            for k, v in conn.headers.fields:379                # This will always force an ascii representation of headers. For example, if the server sends a380                #381                #     X-Authors: Made with ❤ in Hamburg382                #383                # header, mitmproxy will display the following:384                #385                #     X-Authors: Made with \xe2\x9d\xa4 in Hamburg.386                #387                # The alternative would be to just use the header's UTF-8 representation and maybe388                # do `str.replace("\t", "\\t")` to exempt tabs from urwid's special characters escaping [1].389                # That would in some terminals allow rendering UTF-8 characters, but the mapping390                # wouldn't be bijective, i.e. a user couldn't distinguish "\\t" and "\t".391                # Also, from a security perspective, a mitmproxy user couldn't be fooled by homoglyphs.392                #393                # 1) https://github.com/mitmproxy/mitmproxy/issues/1833394                #    https://github.com/urwid/urwid/blob/6608ee2c9932d264abd1171468d833b7a4082e13/urwid/display_common.py#L35-L36,395 396                k = strutils.bytes_to_escaped_str(k) + ":"397                v = strutils.bytes_to_escaped_str(v)398                hdrs.append((k, v))399            txt = common.format_keyvals(hdrs, key_format="header")400            viewmode = self.master.commands.call("console.flowview.mode")401            msg, body = self.content_view(viewmode, conn)402 403            txt.append(self._contentview_status_bar(msg, viewmode))404            txt.extend(body)405        else:406            txt = [407                urwid.Text(""),408                urwid.Text(409                    [410                        ("highlight", "No response. Press "),411                        ("key", "e"),412                        ("highlight", " and edit any aspect to add one."),413                    ]414                ),415            ]416        return searchable.Searchable(txt)417 418    def dns_message_text(419        self, type: str, message: DNSMessage | None420    ) -> searchable.Searchable:421        """422        Alternative:423        if not message:424            return searchable.Searchable([urwid.Text(("highlight", f"No {typ}."))])425 426        viewmode = self.master.commands.call("console.flowview.mode")427        pretty = contentviews.prettify_message(message, flow, viewmode)428        chunks = mitmproxy_rs.syntax_highlight.highlight(429            pretty.text,430            language=pretty.syntax_highlight,431        )432 433        widget_lines = [434            self._contentview_status_bar(viewmode.capitalize(), viewmode),435            urwid.Text(chunks)436        ]437        return searchable.Searchable(widget_lines)438        """439        # Keep in sync with web/src/js/components/FlowView/DnsMessages.tsx440        if message:441 442            def rr_text(rr: dns.ResourceRecord):443                return urwid.Text(444                    f"  {rr.name} {dns.types.to_str(rr.type)} {dns.classes.to_str(rr.class_)} {rr.ttl} {rr}"445                )446 447            txt = []448            txt.append(449                urwid.Text(450                    "{recursive}Question".format(451                        recursive="Recursive " if message.recursion_desired else "",452                    )453                )454            )455            txt.extend(456                urwid.Text(457                    f"  {q.name} {dns.types.to_str(q.type)} {dns.classes.to_str(q.class_)}"458                )459                for q in message.questions460            )461            txt.append(urwid.Text(""))462            txt.append(463                urwid.Text(464                    "{authoritative}{recursive}Answer".format(465                        authoritative="Authoritative "466                        if message.authoritative_answer467                        else "",468                        recursive="Recursive " if message.recursion_available else "",469                    )470                )471            )472            txt.extend(map(rr_text, message.answers))473            txt.append(urwid.Text(""))474            txt.append(urwid.Text("Authority"))475            txt.extend(map(rr_text, message.authorities))476            txt.append(urwid.Text(""))477            txt.append(urwid.Text("Addition"))478            txt.extend(map(rr_text, message.additionals))479            return searchable.Searchable(txt)480        else:481            return searchable.Searchable([urwid.Text(("highlight", f"No {type}."))])482 483 484class FlowView(urwid.Frame, layoutwidget.LayoutWidget):485    keyctx = "flowview"486    title = "Flow Details"487 488    def __init__(self, master):489        super().__init__(490            FlowDetails(master),491            header=FlowViewHeader(master),492        )493        self.master = master494 495    def focus_changed(self, *args, **kwargs):496        self.body.focus_changed()497        self.header.focus_changed()498 
codekingpro/portable-devtools · Team Ai