From 25c01f294df39d191d8fc31fd43069afb663a950 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 6 Aug 2026 15:49:51 +0100 Subject: [PATCH 1/2] Remove unused imports --- tests/mitmproxy_addons/callback.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/mitmproxy_addons/callback.py b/tests/mitmproxy_addons/callback.py index 8631580..a159c40 100644 --- a/tests/mitmproxy_addons/callback.py +++ b/tests/mitmproxy_addons/callback.py @@ -1,5 +1,4 @@ from typing import Optional -import asyncio import aiohttp import json @@ -7,8 +6,6 @@ from mitmproxy import ctx, flowfilter from mitmproxy.http import Response from controller import MITM_DOMAIN_NAME -from urllib.request import urlopen, Request -from urllib.error import HTTPError, URLError from datetime import datetime # See README.md for information about this addon From 0b0b2c1a15bb578f723349179c8474f0eda141c1 Mon Sep 17 00:00:00 2001 From: Richard van der Hoff Date: Thu, 6 Aug 2026 16:45:09 +0100 Subject: [PATCH 2/2] mitmproxy: change config atomically Use an `attrs` class to store the config, and change it atomically. --- tests/mitmproxy_addons/__init__.py | 1 + tests/mitmproxy_addons/callback.py | 69 +++++++++++++++++++----------- 2 files changed, 46 insertions(+), 24 deletions(-) diff --git a/tests/mitmproxy_addons/__init__.py b/tests/mitmproxy_addons/__init__.py index 3a1be27..3e8dc10 100644 --- a/tests/mitmproxy_addons/__init__.py +++ b/tests/mitmproxy_addons/__init__.py @@ -9,6 +9,7 @@ def install(package): subprocess.check_call([sys.executable, "-m", "pip", "install", package]) install("aiohttp") +install("attrs") from callback import Callback from controller import MITM_DOMAIN_NAME, app diff --git a/tests/mitmproxy_addons/callback.py b/tests/mitmproxy_addons/callback.py index a159c40..ae1a553 100644 --- a/tests/mitmproxy_addons/callback.py +++ b/tests/mitmproxy_addons/callback.py @@ -1,7 +1,8 @@ -from typing import Optional -import aiohttp import json +import aiohttp +import attrs + import mitmproxy from mitmproxy import ctx, flowfilter from mitmproxy.http import Response @@ -10,17 +11,13 @@ # See README.md for information about this addon class Callback: + _config: "Config" + def __init__(self): self.reset() - self.matchall = flowfilter.parse(".") - self.filter: Optional[flowfilter.TFilter] = self.matchall def reset(self): - self.config = { - "callback_request_url": "", - "callback_response_url": "", - "filter": None, - } + self._config = Config.from_config_dict({}) def load(self, loader): loader.add_option( @@ -41,21 +38,19 @@ def configure(self, updates): if ctx.options.callback is None: self.reset() return - self.config = ctx.options.callback - new_filter = self.config.get('filter', None) - print(f"callback req_url={self.config.get('callback_request_url')} res_url={self.config.get('callback_response_url')} filter={new_filter}") - if new_filter: - self.filter = flowfilter.parse(new_filter) - else: - self.filter = self.matchall + + new_config = Config.from_config_dict(ctx.options.callback) + print(f"callback config={new_config}") + self._config = new_config async def request(self, flow): # always ignore the controller if flow.request.pretty_host == MITM_DOMAIN_NAME: return - if self.config.get("callback_request_url", "") == "": - return # ignore requests if we aren't told a url - if not flowfilter.match(self.filter, flow): + config = self._config + if config.callback_request_url == "": + return # ignore requests if we aren't told a url + if not flowfilter.match(config.filter, flow): return # ignore requests which don't match the filter try: # e.g GET requests have no req body req_body = flow.request.json() @@ -68,15 +63,16 @@ async def request(self, flow): "url": flow.request.url, "request_body": req_body, } - await self.send_callback(flow, self.config["callback_request_url"], callback_body) + await self.send_callback(flow, config.callback_request_url, callback_body) async def response(self, flow): # always ignore the controller if flow.request.pretty_host == MITM_DOMAIN_NAME: return - if self.config.get("callback_response_url","") == "": - return # ignore responses if we aren't told a url - if flowfilter.match(self.filter, flow): + config = self._config + if config.callback_response_url == "": + return # ignore responses if we aren't told a url + if flowfilter.match(config.filter, flow): try: # e.g GET requests have no req body req_body = flow.request.json() except: @@ -94,7 +90,7 @@ async def response(self, flow): "request_body": req_body, "response_body": res_body, } - await self.send_callback(flow, self.config["callback_response_url"], callback_body) + await self.send_callback(flow, config.callback_response_url, callback_body) async def send_callback(self, flow: mitmproxy.http.HTTPFlow, url: str, body: dict): try: @@ -136,3 +132,28 @@ async def send_callback(self, flow: mitmproxy.http.HTTPFlow, url: str, body: dic except Exception as error: print(f"ERR: callback for {flow.request.url} returned {error}") print(f"ERR: callback, provided request body was {body}") + + +@attrs.define +class Config: + callback_request_url: str + callback_response_url: str + filter: flowfilter.TFilter = attrs.field( + # When stringifying a Config, use the pattern of the filter. + repr=lambda v: repr(v.pattern), + ) + + @staticmethod + def from_config_dict(config_dict: dict) -> "Config": + """Parse the config dict sent over HTTP from the application into a new Config object.""" + callback_request_url = config_dict.get("callback_request_url", "") + callback_response_url = config_dict.get("callback_response_url", "") + + filter_string = config_dict.get('filter') + if isinstance(filter_string, str): + parsed_filter = flowfilter.parse(filter_string) + else: + # match-all, if no filter supplied + parsed_filter = flowfilter.parse(".") + + return Config(callback_request_url, callback_response_url, parsed_filter)