Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
export.py233 linesDownload Raw Back to addons
1import logging2import shlex3from collections.abc import Callable4from collections.abc import Sequence5 6import pyperclip7 8import mitmproxy.types9from mitmproxy import command10from mitmproxy import ctx11from mitmproxy import exceptions12from mitmproxy import flow13from mitmproxy import http14from mitmproxy.net.http.http1 import assemble15from mitmproxy.utils import strutils16 17 18def cleanup_request(f: flow.Flow) -> http.Request:19    if not getattr(f, "request", None):20        raise exceptions.CommandError("Can't export flow with no request.")21    assert isinstance(f, http.HTTPFlow)22    request = f.request.copy()23    request.decode(strict=False)24    return request25 26 27def pop_headers(request: http.Request) -> None:28    """Remove some headers that are redundant for curl/httpie export."""29    request.headers.pop("content-length", None)30 31    if request.headers.get("host", "") == request.host:32        request.headers.pop("host")33    if request.headers.get(":authority", "") == request.host:34        request.headers.pop(":authority")35 36 37def cleanup_response(f: flow.Flow) -> http.Response:38    if not getattr(f, "response", None):39        raise exceptions.CommandError("Can't export flow with no response.")40    assert isinstance(f, http.HTTPFlow)41    response = f.response.copy()  # type: ignore42    response.decode(strict=False)43    return response44 45 46def request_content_for_console(request: http.Request) -> str:47    try:48        text = request.get_text(strict=True)49        assert text50    except ValueError:51        # shlex.quote doesn't support a bytes object52        # see https://github.com/python/cpython/pull/1087153        raise exceptions.CommandError("Request content must be valid unicode")54    escape_control_chars = {chr(i): f"\\x{i:02x}" for i in range(32)}55    escaped_text = "".join(escape_control_chars.get(x, x) for x in text)56    if any(char in escape_control_chars for char in text):57        # Escaped chars need to be unescaped by the shell to be properly inperpreted by curl and httpie58        return f'"$(printf {shlex.quote(escaped_text)})"'59 60    return shlex.quote(escaped_text)61 62 63def curl_command(f: flow.Flow) -> str:64    request = cleanup_request(f)65    pop_headers(request)66 67    args = ["curl"]68 69    server_addr = f.server_conn.peername[0] if f.server_conn.peername else None70 71    if (72        ctx.options.export_preserve_original_ip73        and server_addr74        and request.pretty_host != server_addr75    ):76        resolve = f"{request.pretty_host}:{request.port}:[{server_addr}]"77        args.append("--resolve")78        args.append(resolve)79 80    for k, v in request.headers.items(multi=True):81        if k.lower() == "accept-encoding":82            args.append("--compressed")83        else:84            args += ["-H", f"{k}: {v}"]85 86    if request.method != "GET":87        if not request.content:88            # curl will not calculate content-length if there is no content89            # some server/verb combinations require content-length headers90            # (ex. nginx and POST)91            args += ["-H", "content-length: 0"]92 93        args += ["-X", request.method]94 95    args.append(request.pretty_url)96 97    command = " ".join(shlex.quote(arg) for arg in args)98    if request.content:99        command += f" -d {request_content_for_console(request)}"100    return command101 102 103def httpie_command(f: flow.Flow) -> str:104    request = cleanup_request(f)105    pop_headers(request)106 107    # TODO: Once https://github.com/httpie/httpie/issues/414 is implemented, we108    # should ensure we always connect to the IP address specified in the flow,109    # similar to how it's done in curl_command.110    url = request.pretty_url111 112    args = ["http", request.method, url]113    for k, v in request.headers.items(multi=True):114        args.append(f"{k}: {v}")115    cmd = " ".join(shlex.quote(arg) for arg in args)116    if request.content:117        cmd += " <<< " + request_content_for_console(request)118    return cmd119 120 121def raw_request(f: flow.Flow) -> bytes:122    request = cleanup_request(f)123    if request.raw_content is None:124        raise exceptions.CommandError("Request content missing.")125    return assemble.assemble_request(request)126 127 128def raw_response(f: flow.Flow) -> bytes:129    response = cleanup_response(f)130    if response.raw_content is None:131        raise exceptions.CommandError("Response content missing.")132    return assemble.assemble_response(response)133 134 135def raw(f: flow.Flow, separator=b"\r\n\r\n") -> bytes:136    """Return either the request or response if only one exists, otherwise return both"""137    request_present = (138        isinstance(f, http.HTTPFlow) and f.request and f.request.raw_content is not None139    )140    response_present = (141        isinstance(f, http.HTTPFlow)142        and f.response143        and f.response.raw_content is not None144    )145 146    if request_present and response_present:147        parts = [raw_request(f), raw_response(f)]148        if isinstance(f, http.HTTPFlow) and f.websocket:149            parts.append(f.websocket._get_formatted_messages())150        return separator.join(parts)151    elif request_present:152        return raw_request(f)153    elif response_present:154        return raw_response(f)155    else:156        raise exceptions.CommandError("Can't export flow with no request or response.")157 158 159formats: dict[str, Callable[[flow.Flow], str | bytes]] = dict(160    curl=curl_command,161    httpie=httpie_command,162    raw=raw,163    raw_request=raw_request,164    raw_response=raw_response,165)166 167 168class Export:169    def load(self, loader):170        loader.add_option(171            "export_preserve_original_ip",172            bool,173            False,174            """175            When exporting a request as an external command, make an effort to176            connect to the same IP as in the original request. This helps with177            reproducibility in cases where the behaviour depends on the178            particular host we are connecting to. Currently this only affects179            curl exports.180            """,181        )182 183    @command.command("export.formats")184    def formats(self) -> Sequence[str]:185        """186        Return a list of the supported export formats.187        """188        return list(sorted(formats.keys()))189 190    @command.command("export.file")191    def file(self, format: str, flow: flow.Flow, path: mitmproxy.types.Path) -> None:192        """193        Export a flow to path.194        """195        if format not in formats:196            raise exceptions.CommandError("No such export format: %s" % format)197        v = formats[format](flow)198        try:199            with open(path, "wb") as fp:200                if isinstance(v, bytes):201                    fp.write(v)202                else:203                    fp.write(v.encode("utf-8", "surrogateescape"))204        except OSError as e:205            logging.error(str(e))206 207    @command.command("export.clip")208    def clip(self, format: str, f: flow.Flow) -> None:209        """210        Export a flow to the system clipboard.211        """212        content = self.export_str(format, f)213        try:214            pyperclip.copy(content)215        except pyperclip.PyperclipException as e:216            logging.error(str(e))217 218    @command.command("export")219    def export_str(self, format: str, f: flow.Flow) -> str:220        """221        Export a flow and return the result.222        """223        if format not in formats:224            raise exceptions.CommandError("No such export format: %s" % format)225 226        content = formats[format](f)227        # The individual formatters may return surrogate-escaped UTF-8, but that may blow up in later steps.228        # For example, pyperclip on macOS does not like surrogates.229        # To fix this, We first surrogate-encode and then backslash-decode.230        content = strutils.always_bytes(content, "utf8", "surrogateescape")231        content = strutils.always_str(content, "utf8", "backslashreplace")232        return content233 
codekingpro/portable-devtools · Team Ai