Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
view.py750 linesDownload Raw Back to addons
1"""2The View:3 4- Keeps track of a store of flows5- Maintains a filtered, ordered view onto that list of flows6- Exposes a number of signals so the view can be monitored7- Tracks focus within the view8- Exposes a settings store for flows that automatically expires if the flow is9  removed from the store.10"""11 12import collections13import logging14import re15from collections.abc import Iterator16from collections.abc import MutableMapping17from collections.abc import Sequence18from typing import Any19from typing import Optional20 21import sortedcontainers22 23import mitmproxy.flow24from mitmproxy import command25from mitmproxy import connection26from mitmproxy import ctx27from mitmproxy import dns28from mitmproxy import exceptions29from mitmproxy import flowfilter30from mitmproxy import hooks31from mitmproxy import http32from mitmproxy import io33from mitmproxy import tcp34from mitmproxy import udp35from mitmproxy.log import ALERT36from mitmproxy.utils import human37from mitmproxy.utils import signals38 39# The underlying sorted list implementation expects the sort key to be stable40# for the lifetime of the object. However, if we sort by size, for instance,41# the sort order changes as the flow progresses through its lifecycle. We42# address this through two means:43#44# - Let order keys cache the sort value by flow ID.45#46# - Add a facility to refresh items in the list by removing and re-adding them47# when they are updated.48 49 50class _OrderKey:51    def __init__(self, view):52        self.view = view53 54    def generate(self, f: mitmproxy.flow.Flow) -> Any:  # pragma: no cover55        pass56 57    def refresh(self, f):58        k = self._key()59        old = self.view.settings[f][k]60        new = self.generate(f)61        if old != new:62            self.view._view.remove(f)63            self.view.settings[f][k] = new64            self.view._view.add(f)65            self.view.sig_view_refresh.send()66 67    def _key(self):68        return "_order_%s" % id(self)69 70    def __call__(self, f):71        if f.id in self.view._store:72            k = self._key()73            s = self.view.settings[f]74            if k in s:75                return s[k]76            val = self.generate(f)77            s[k] = val78            return val79        else:80            return self.generate(f)81 82 83class OrderRequestStart(_OrderKey):84    def generate(self, f: mitmproxy.flow.Flow) -> float:85        return f.timestamp_created86 87 88class OrderRequestMethod(_OrderKey):89    def generate(self, f: mitmproxy.flow.Flow) -> str:90        if isinstance(f, http.HTTPFlow):91            return f.request.method92        elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)):93            return f.type.upper()94        elif isinstance(f, dns.DNSFlow):95            return dns.op_codes.to_str(f.request.op_code)96        else:97            raise NotImplementedError()98 99 100class OrderRequestURL(_OrderKey):101    def generate(self, f: mitmproxy.flow.Flow) -> str:102        if isinstance(f, http.HTTPFlow):103            return f.request.url104        elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)):105            return human.format_address(f.server_conn.address)106        elif isinstance(f, dns.DNSFlow):107            return f.request.questions[0].name if f.request.questions else ""108        else:109            raise NotImplementedError()110 111 112class OrderKeySize(_OrderKey):113    def generate(self, f: mitmproxy.flow.Flow) -> int:114        if isinstance(f, http.HTTPFlow):115            size = 0116            if f.request.raw_content:117                size += len(f.request.raw_content)118            if f.response and f.response.raw_content:119                size += len(f.response.raw_content)120            return size121        elif isinstance(f, (tcp.TCPFlow, udp.UDPFlow)):122            size = 0123            for message in f.messages:124                size += len(message.content)125            return size126        elif isinstance(f, dns.DNSFlow):127            return f.response.size if f.response else 0128        else:129            raise NotImplementedError()130 131 132orders = [133    ("t", "time"),134    ("m", "method"),135    ("u", "url"),136    ("z", "size"),137]138 139 140def _signal_with_flow(flow: mitmproxy.flow.Flow) -> None: ...141 142 143def _sig_view_remove(flow: mitmproxy.flow.Flow, index: int) -> None: ...144 145 146class View(collections.abc.Sequence):147    def __init__(self) -> None:148        super().__init__()149        self._store: collections.OrderedDict[str, mitmproxy.flow.Flow] = (150            collections.OrderedDict()151        )152        self.filter = flowfilter.match_all153        # Should we show only marked flows?154        self.show_marked = False155 156        self.default_order = OrderRequestStart(self)157        self.orders = dict(158            time=OrderRequestStart(self),159            method=OrderRequestMethod(self),160            url=OrderRequestURL(self),161            size=OrderKeySize(self),162        )163        self.order_key: _OrderKey = self.default_order164        self.order_reversed = False165        self.focus_follow = False166 167        self._view = sortedcontainers.SortedListWithKey(key=self.order_key)168 169        # The sig_view* signals broadcast events that affect the view. That is,170        # an update to a flow in the store but not in the view does not trigger171        # a signal. All signals are called after the view has been updated.172        self.sig_view_update = signals.SyncSignal(_signal_with_flow)173        self.sig_view_add = signals.SyncSignal(_signal_with_flow)174        self.sig_view_remove = signals.SyncSignal(_sig_view_remove)175        # Signals that the view should be refreshed completely176        self.sig_view_refresh = signals.SyncSignal(lambda: None)177 178        # The sig_store* signals broadcast events that affect the underlying179        # store. If a flow is removed from just the view, sig_view_remove is180        # triggered. If it is removed from the store while it is also in the181        # view, both sig_store_remove and sig_view_remove are triggered.182        self.sig_store_remove = signals.SyncSignal(_signal_with_flow)183        # Signals that the store should be refreshed completely184        self.sig_store_refresh = signals.SyncSignal(lambda: None)185 186        self.focus = Focus(self)187        self.settings = Settings(self)188 189    def load(self, loader):190        loader.add_option(191            "view_filter", Optional[str], None, "Limit the view to matching flows."192        )193        loader.add_option(194            "view_order",195            str,196            "time",197            "Flow sort order.",198            choices=list(map(lambda c: c[1], orders)),199        )200        loader.add_option(201            "view_order_reversed", bool, False, "Reverse the sorting order."202        )203        loader.add_option(204            "console_focus_follow", bool, False, "Focus follows new flows."205        )206 207    def store_count(self):208        return len(self._store)209 210    def _rev(self, idx: int) -> int:211        """212        Reverses an index, if needed213        """214        if self.order_reversed:215            if idx < 0:216                idx = -idx - 1217            else:218                idx = len(self._view) - idx - 1219                if idx < 0:220                    raise IndexError221        return idx222 223    def __len__(self):224        return len(self._view)225 226    def __getitem__(self, offset) -> Any:227        return self._view[self._rev(offset)]228 229    # Reflect some methods to the efficient underlying implementation230 231    def _bisect(self, f: mitmproxy.flow.Flow) -> int:232        v = self._view.bisect_right(f)233        return self._rev(v - 1) + 1234 235    def index(236        self, f: mitmproxy.flow.Flow, start: int = 0, stop: int | None = None237    ) -> int:238        return self._rev(self._view.index(f, start, stop))239 240    def __contains__(self, f: Any) -> bool:241        return self._view.__contains__(f)242 243    def _order_key_name(self):244        return "_order_%s" % id(self.order_key)245 246    def _base_add(self, f):247        self.settings[f][self._order_key_name()] = self.order_key(f)248        self._view.add(f)249 250    def _refilter(self):251        self._view.clear()252        for i in self._store.values():253            if self.show_marked and not i.marked:254                continue255            if self.filter(i):256                self._base_add(i)257        self.sig_view_refresh.send()258 259    """ View API """260 261    # Focus262    @command.command("view.focus.go")263    def go(self, offset: int) -> None:264        """265        Go to a specified offset. Positive offests are from the beginning of266        the view, negative from the end of the view, so that 0 is the first267        flow, -1 is the last flow.268        """269        if len(self) == 0:270            return271        if offset < 0:272            offset = len(self) + offset273        if offset < 0:274            offset = 0275        if offset > len(self) - 1:276            offset = len(self) - 1277        self.focus.flow = self[offset]278 279    @command.command("view.focus.next")280    def focus_next(self) -> None:281        """282        Set focus to the next flow.283        """284        if self.focus.index is not None:285            idx = self.focus.index + 1286            if self.inbounds(idx):287                self.focus.flow = self[idx]288        else:289            pass290 291    @command.command("view.focus.prev")292    def focus_prev(self) -> None:293        """294        Set focus to the previous flow.295        """296        if self.focus.index is not None:297            idx = self.focus.index - 1298            if self.inbounds(idx):299                self.focus.flow = self[idx]300        else:301            pass302 303    # Order304    @command.command("view.order.options")305    def order_options(self) -> Sequence[str]:306        """307        Choices supported by the view_order option.308        """309        return list(sorted(self.orders.keys()))310 311    @command.command("view.order.reverse")312    def set_reversed(self, boolean: bool) -> None:313        self.order_reversed = boolean314        self.sig_view_refresh.send()315 316    @command.command("view.order.set")317    def set_order(self, order_key: str) -> None:318        """319        Sets the current view order.320        """321        if order_key not in self.orders:322            raise exceptions.CommandError("Unknown flow order: %s" % order_key)323        key = self.orders[order_key]324        self.order_key = key325        newview = sortedcontainers.SortedListWithKey(key=key)326        newview.update(self._view)327        self._view = newview328 329    @command.command("view.order")330    def get_order(self) -> str:331        """332        Returns the current view order.333        """334        order = ""335        for k in self.orders.keys():336            if self.order_key == self.orders[k]:337                order = k338        return order339 340    # Filter341    @command.command("view.filter.set")342    def set_filter_cmd(self, filter_expr: str) -> None:343        """344        Sets the current view filter.345        """346        filt = None347        if filter_expr:348            try:349                filt = flowfilter.parse(filter_expr)350            except ValueError as e:351                raise exceptions.CommandError(str(e)) from e352        self.set_filter(filt)353 354    def set_filter(self, flt: flowfilter.TFilter | None):355        self.filter = flt or flowfilter.match_all356        self._refilter()357 358    # View Updates359    @command.command("view.clear")360    def clear(self) -> None:361        """362        Clears both the store and view.363        """364        self._store.clear()365        self._view.clear()366        self.sig_view_refresh.send()367        self.sig_store_refresh.send()368 369    @command.command("view.clear_unmarked")370    def clear_not_marked(self) -> None:371        """372        Clears only the unmarked flows.373        """374        for flow in self._store.copy().values():375            if not flow.marked:376                self._store.pop(flow.id)377 378        self._refilter()379        self.sig_store_refresh.send()380 381    # View Settings382    @command.command("view.settings.getval")383    def getvalue(self, flow: mitmproxy.flow.Flow, key: str, default: str) -> str:384        """385        Get a value from the settings store for the specified flow.386        """387        return self.settings[flow].get(key, default)388 389    @command.command("view.settings.setval.toggle")390    def setvalue_toggle(self, flows: Sequence[mitmproxy.flow.Flow], key: str) -> None:391        """392        Toggle a boolean value in the settings store, setting the value to393        the string "true" or "false".394        """395        updated = []396        for f in flows:397            current = self.settings[f].get(key, "false")398            self.settings[f][key] = "false" if current == "true" else "true"399            updated.append(f)400        ctx.master.addons.trigger(hooks.UpdateHook(updated))401 402    @command.command("view.settings.setval")403    def setvalue(404        self, flows: Sequence[mitmproxy.flow.Flow], key: str, value: str405    ) -> None:406        """407        Set a value in the settings store for the specified flows.408        """409        updated = []410        for f in flows:411            self.settings[f][key] = value412            updated.append(f)413        ctx.master.addons.trigger(hooks.UpdateHook(updated))414 415    # Flows416    @command.command("view.flows.duplicate")417    def duplicate(self, flows: Sequence[mitmproxy.flow.Flow]) -> None:418        """419        Duplicates the specified flows, and sets the focus to the first420        duplicate.421        """422        dups = [f.copy() for f in flows]423        if dups:424            self.add(dups)425            self.focus.flow = dups[0]426            logging.log(ALERT, "Duplicated %s flows" % len(dups))427 428    @command.command("view.flows.remove")429    def remove(self, flows: Sequence[mitmproxy.flow.Flow]) -> None:430        """431        Removes the flow from the underlying store and the view.432        """433        for f in flows:434            if f.id in self._store:435                if f.killable:436                    f.kill()437                if f in self._view:438                    # We manually pass the index here because multiple flows may have the same439                    # sorting key, and we cannot reconstruct the index from that.440                    idx = self._view.index(f)441                    self._view.remove(f)442                    self.sig_view_remove.send(flow=f, index=idx)443                del self._store[f.id]444                self.sig_store_remove.send(flow=f)445        if len(flows) > 1:446            logging.log(ALERT, "Removed %s flows" % len(flows))447 448    @command.command("view.flows.resolve")449    def resolve(self, flow_spec: str) -> Sequence[mitmproxy.flow.Flow]:450        """451        Resolve a flow list specification to an actual list of flows.452        """453        if flow_spec == "@all":454            return [i for i in self._store.values()]455        if flow_spec == "@focus":456            return [self.focus.flow] if self.focus.flow else []457        elif flow_spec == "@shown":458            return [i for i in self]459        elif flow_spec == "@hidden":460            return [i for i in self._store.values() if i not in self._view]461        elif flow_spec == "@marked":462            return [i for i in self._store.values() if i.marked]463        elif flow_spec == "@unmarked":464            return [i for i in self._store.values() if not i.marked]465        elif re.match(r"@[0-9a-f\-,]{36,}", flow_spec):466            ids = flow_spec[1:].split(",")467            return [i for i in self._store.values() if i.id in ids]468        else:469            try:470                filt = flowfilter.parse(flow_spec)471            except ValueError as e:472                raise exceptions.CommandError(str(e)) from e473            return [i for i in self._store.values() if filt(i)]474 475    @command.command("view.flows.create")476    def create(self, method: str, url: str) -> None:477        try:478            req = http.Request.make(method.upper(), url)479        except ValueError as e:480            raise exceptions.CommandError("Invalid URL: %s" % e)481 482        c = connection.Client(483            peername=("", 0),484            sockname=("", 0),485            timestamp_start=req.timestamp_start - 0.0001,486        )487        s = connection.Server(address=(req.host, req.port))488 489        f = http.HTTPFlow(c, s)490        f.request = req491        f.request.headers["Host"] = req.host492        self.add([f])493 494    @command.command("view.flows.load")495    def load_file(self, path: mitmproxy.types.Path) -> None:496        """497        Load flows into the view, without processing them with addons.498        """499        try:500            with open(path, "rb") as f:501                for i in io.FlowReader(f).stream():502                    # Do this to get a new ID, so we can load the same file N times and503                    # get new flows each time. It would be more efficient to just have a504                    # .newid() method or something.505                    self.add([i.copy()])506        except OSError as e:507            logging.error(e.strerror)508        except exceptions.FlowReadException as e:509            logging.error(str(e))510 511    def add(self, flows: Sequence[mitmproxy.flow.Flow]) -> None:512        """513        Adds a flow to the state. If the flow already exists, it is514        ignored.515        """516        for f in flows:517            if f.id not in self._store:518                self._store[f.id] = f519                if self.filter(f):520                    self._base_add(f)521                    if self.focus_follow:522                        self.focus.flow = f523                    self.sig_view_add.send(flow=f)524 525    def get_by_id(self, flow_id: str) -> mitmproxy.flow.Flow | None:526        """527        Get flow with the given id from the store.528        Returns None if the flow is not found.529        """530        return self._store.get(flow_id)531 532    # View Properties533    @command.command("view.properties.length")534    def get_length(self) -> int:535        """536        Returns view length.537        """538        return len(self)539 540    @command.command("view.properties.marked")541    def get_marked(self) -> bool:542        """543        Returns true if view is in marked mode.544        """545        return self.show_marked546 547    @command.command("view.properties.marked.toggle")548    def toggle_marked(self) -> None:549        """550        Toggle whether to show marked views only.551        """552        self.show_marked = not self.show_marked553        self._refilter()554 555    @command.command("view.properties.inbounds")556    def inbounds(self, index: int) -> bool:557        """558        Is this 0 <= index < len(self)?559        """560        return 0 <= index < len(self)561 562    # Event handlers563    def configure(self, updated):564        if "view_filter" in updated:565            filt = None566            if ctx.options.view_filter:567                try:568                    filt = flowfilter.parse(ctx.options.view_filter)569                except ValueError as e:570                    raise exceptions.OptionsError(str(e)) from e571            self.set_filter(filt)572        if "view_order" in updated:573            if ctx.options.view_order not in self.orders:574                raise exceptions.OptionsError(575                    "Unknown flow order: %s" % ctx.options.view_order576                )577            self.set_order(ctx.options.view_order)578        if "view_order_reversed" in updated:579            self.set_reversed(ctx.options.view_order_reversed)580        if "console_focus_follow" in updated:581            self.focus_follow = ctx.options.console_focus_follow582 583    def requestheaders(self, f):584        self.add([f])585 586    def error(self, f):587        self.update([f])588 589    def response(self, f):590        self.update([f])591 592    def intercept(self, f):593        self.update([f])594 595    def resume(self, f):596        self.update([f])597 598    def kill(self, f):599        self.update([f])600 601    def tcp_start(self, f):602        self.add([f])603 604    def tcp_message(self, f):605        self.update([f])606 607    def tcp_error(self, f):608        self.update([f])609 610    def tcp_end(self, f):611        self.update([f])612 613    def udp_start(self, f):614        self.add([f])615 616    def udp_message(self, f):617        self.update([f])618 619    def udp_error(self, f):620        self.update([f])621 622    def udp_end(self, f):623        self.update([f])624 625    def dns_request(self, f):626        self.add([f])627 628    def dns_response(self, f):629        self.update([f])630 631    def dns_error(self, f):632        self.update([f])633 634    def update(self, flows: Sequence[mitmproxy.flow.Flow]) -> None:635        """636        Updates a list of flows. If flow is not in the state, it's ignored.637        """638        for f in flows:639            if f.id in self._store:640                if self.filter(f):641                    if f not in self._view:642                        self._base_add(f)643                        if self.focus_follow:644                            self.focus.flow = f645                        self.sig_view_add.send(flow=f)646                    else:647                        # This is a tad complicated. The sortedcontainers648                        # implementation assumes that the order key is stable. If649                        # it changes mid-way Very Bad Things happen. We detect when650                        # this happens, and re-fresh the item.651                        self.order_key.refresh(f)652                        self.sig_view_update.send(flow=f)653                else:654                    try:655                        idx = self._view.index(f)656                    except ValueError:657                        pass  # The value was not in the view658                    else:659                        self._view.remove(f)660                        self.sig_view_remove.send(flow=f, index=idx)661 662 663class Focus:664    """665    Tracks a focus element within a View.666    """667 668    def __init__(self, v: View) -> None:669        self.view = v670        self._flow: mitmproxy.flow.Flow | None = None671        self.sig_change = signals.SyncSignal(lambda: None)672        if len(self.view):673            self.flow = self.view[0]674        v.sig_view_add.connect(self._sig_view_add)675        v.sig_view_remove.connect(self._sig_view_remove)676        v.sig_view_refresh.connect(self._sig_view_refresh)677 678    @property679    def flow(self) -> mitmproxy.flow.Flow | None:680        return self._flow681 682    @flow.setter683    def flow(self, f: mitmproxy.flow.Flow | None):684        if f is not None and f not in self.view:685            raise ValueError("Attempt to set focus to flow not in view")686        self._flow = f687        self.sig_change.send()688 689    @property690    def index(self) -> int | None:691        if self.flow:692            return self.view.index(self.flow)693        return None694 695    @index.setter696    def index(self, idx):697        if idx < 0 or idx > len(self.view) - 1:698            raise ValueError("Index out of view bounds")699        self.flow = self.view[idx]700 701    def _nearest(self, f, v):702        return min(v._bisect(f), len(v) - 1)703 704    def _sig_view_remove(self, flow, index):705        if len(self.view) == 0:706            self.flow = None707        elif flow is self.flow:708            self.index = min(index, len(self.view) - 1)709 710    def _sig_view_refresh(self):711        if len(self.view) == 0:712            self.flow = None713        elif self.flow is None:714            self.flow = self.view[0]715        elif self.flow not in self.view:716            self.flow = self.view[self._nearest(self.flow, self.view)]717 718    def _sig_view_add(self, flow):719        # We only have to act if we don't have a focus element720        if not self.flow:721            self.flow = flow722 723 724class Settings(collections.abc.Mapping):725    def __init__(self, view: View) -> None:726        self.view = view727        self._values: MutableMapping[str, dict] = {}728        view.sig_store_remove.connect(self._sig_store_remove)729        view.sig_store_refresh.connect(self._sig_store_refresh)730 731    def __iter__(self) -> Iterator:732        return iter(self._values)733 734    def __len__(self) -> int:735        return len(self._values)736 737    def __getitem__(self, f: mitmproxy.flow.Flow) -> dict:738        if f.id not in self.view._store:739            raise KeyError740        return self._values.setdefault(f.id, {})741 742    def _sig_store_remove(self, flow):743        if flow.id in self._values:744            del self._values[flow.id]745 746    def _sig_store_refresh(self):747        for fid in list(self._values.keys()):748            if fid not in self.view._store:749                del self._values[fid]750 
codekingpro/portable-devtools · Team Ai