pythonmitmproxy

How to Dynamically Route and Authenticate Upstream Proxies in mitmproxy Based on URL?


Hello Stack Overflow community,

I am working on a project using mitmproxy and I'm facing a challenge where I need to dynamically route requests to different upstream proxies based on the URL, along with handling authentication for these proxies. I would appreciate any guidance or suggestions on how to implement this.

Requirements:

  1. Dynamic Proxy Routing:
  1. Authentication for Each Proxy:
  1. Implementation in an Addon

My Attempts/Research: I've looked into the documentation but haven't found a clear way to change the upstream proxy dynamically based on the request URL, especially when it comes to incorporating authentication for different proxies.

Questions:

Any code examples, documentation references, or insights into how to approach this in mitmproxy would be extremely helpful.

Thank you in advance for your help!

below is the code I tried but not satisfied

import base64
from mitmproxy import http

class DynamicUpstreamProxy:
    def __init__(self):
        self.proxy_A = ("upstream-proxy-A.com", 8081)
        self.proxy_B = ("upstream-proxy-B.com", 8082)
        self.proxy_A_auth = self.encode_credentials("usernameA", "passwordA")
        self.proxy_B_auth = self.encode_credentials("usernameB", "passwordB")

    def encode_credentials(self, username, password):
        credentials = f"{username}:{password}"
        encoded_credentials = base64.b64encode(credentials.encode()).decode()
        return f"Basic {encoded_credentials}"

    def request(self, flow: http.HTTPFlow):
        url = flow.request.pretty_url

        if url.startswith("https://example.com/123"):
            # Upstream Proxy A
            flow.live.change_upstream_proxy_server(self.proxy_A)
            flow.request.headers["Proxy-Authorization"] = self.proxy_A_auth

        elif url.startswith("https://example.com/456"):
            #  Upstream Proxy B
            flow.live.change_upstream_proxy_server(self.proxy_B)
            flow.request.headers["Proxy-Authorization"] = self.proxy_B_auth

addons = [
    DynamicUpstreamProxy()
]

then run addon

mitmproxy -s my_upstream_addon.py

Solution

  • How about something like the below? This routes each request to an upstream proxy based on the value of a custom header called "X-Upstream-Proxy" or no upstream if the header does not exist (tested with mitmproxy v10.1.3).

    Regarding authentication with the upstream proxy server, I haven't tested this but I assume an upstream proxy value of "http://user:pass@proxy-hostname:8080" or similar should work.

    This code can be easily modified to run as a command line add-on to mitmproxy, take a look at a relevant example here: https://github.com/mitmproxy/mitmproxy/blob/main/examples/contrib/change_upstream_proxy.py

    import asyncio
    
    from urllib.parse import urlparse
    
    from mitmproxy.addons.proxyserver import Proxyserver
    from mitmproxy.options import Options
    from mitmproxy.tools.dump import DumpMaster
    from mitmproxy.http import HTTPFlow
    from mitmproxy.connection import Server
    from mitmproxy.net.server_spec import ServerSpec
    
    UPSTREAM_PROXY_HEADER = 'X-Upstream-Proxy'
    
    
    def get_upstream_proxy(flow: HTTPFlow) -> tuple[str, tuple[str, int]] | None:
        upstream_proxy = flow.request.headers.get(UPSTREAM_PROXY_HEADER)
        if upstream_proxy is not None:
            parsed_upstream_proxy = urlparse(upstream_proxy)
    
            if parsed_upstream_proxy.scheme not in ('http', 'https'):
                return None
    
            del flow.request.headers[UPSTREAM_PROXY_HEADER]
            return parsed_upstream_proxy.scheme, (parsed_upstream_proxy.hostname, parsed_upstream_proxy.port)
    
        return None
    
    
    class DynamicUpstreamProxy:
    
        def request(self, flow: HTTPFlow) -> None:
            upstream_proxy = get_upstream_proxy(flow)
    
            print(flow.request)
    
            if upstream_proxy is not None:
                has_proxy_changed = upstream_proxy != flow.server_conn.via
                server_connection_already_open = flow.server_conn.timestamp_start is not None
    
                if has_proxy_changed and server_connection_already_open:
                    # server_conn already refers to an existing connection (which cannot be modified),
                    # so we need to replace it with a new server connection object.
                    flow.server_conn = Server(address=flow.server_conn.address)
    
                flow.server_conn.via = ServerSpec(upstream_proxy)
            else:
                flow.server_conn.via = None
            
    if __name__ == '__main__':
        options = Options(listen_host='127.0.0.1', listen_port=8080, http2=True, mode=['upstream:http://dummy:8888/'])
        m = DumpMaster(options, with_termlog=True, with_dumper=False, loop=asyncio.get_event_loop())
    
        m.server = Proxyserver()
        m.addons.add(DynamicUpstreamProxy())
        asyncio.run(m.run())