2025-12-25 upload
This commit is contained in:
229
venv/Lib/site-packages/mitmproxy/addons/script.py
Normal file
229
venv/Lib/site-packages/mitmproxy/addons/script.py
Normal file
@@ -0,0 +1,229 @@
|
||||
import asyncio
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
from collections.abc import Sequence
|
||||
|
||||
import mitmproxy.types as mtypes
|
||||
from mitmproxy import addonmanager
|
||||
from mitmproxy import command
|
||||
from mitmproxy import ctx
|
||||
from mitmproxy import eventsequence
|
||||
from mitmproxy import exceptions
|
||||
from mitmproxy import flow
|
||||
from mitmproxy import hooks
|
||||
from mitmproxy.utils import asyncio_utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_script(path: str) -> types.ModuleType | None:
|
||||
fullname = "__mitmproxy_script__.{}".format(
|
||||
os.path.splitext(os.path.basename(path))[0]
|
||||
)
|
||||
# the fullname is not unique among scripts, so if there already is an existing script with said
|
||||
# fullname, remove it.
|
||||
sys.modules.pop(fullname, None)
|
||||
oldpath = sys.path
|
||||
sys.path.insert(0, os.path.dirname(path))
|
||||
|
||||
try:
|
||||
loader = importlib.machinery.SourceFileLoader(fullname, path)
|
||||
spec = importlib.util.spec_from_loader(fullname, loader=loader)
|
||||
assert spec
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
loader.exec_module(m)
|
||||
if not getattr(m, "name", None):
|
||||
m.name = path # type: ignore
|
||||
return m
|
||||
except ImportError as e:
|
||||
if getattr(sys, "frozen", False):
|
||||
e.msg += (
|
||||
f".\n"
|
||||
f"Note that mitmproxy's binaries include their own Python environment. "
|
||||
f"If your addon requires the installation of additional dependencies, "
|
||||
f"please install mitmproxy from PyPI "
|
||||
f"(https://docs.mitmproxy.org/stable/overview-installation/#installation-from-the-python-package-index-pypi)."
|
||||
)
|
||||
script_error_handler(path, e)
|
||||
return None
|
||||
except Exception as e:
|
||||
script_error_handler(path, e)
|
||||
return None
|
||||
finally:
|
||||
sys.path[:] = oldpath
|
||||
|
||||
|
||||
def script_error_handler(path: str, exc: Exception) -> None:
|
||||
"""
|
||||
Log errors during script loading.
|
||||
"""
|
||||
tback = exc.__traceback__
|
||||
tback = addonmanager.cut_traceback(
|
||||
tback, "invoke_addon_sync"
|
||||
) # we're calling configure() on load
|
||||
tback = addonmanager.cut_traceback(
|
||||
tback, "_call_with_frames_removed"
|
||||
) # module execution from importlib
|
||||
logger.error(f"error in script {path}", exc_info=(type(exc), exc, tback))
|
||||
|
||||
|
||||
ReloadInterval = 1
|
||||
|
||||
|
||||
class Script:
|
||||
"""
|
||||
An addon that manages a single script.
|
||||
"""
|
||||
|
||||
def __init__(self, path: str, reload: bool) -> None:
|
||||
self.name = "scriptmanager:" + path
|
||||
self.path = path
|
||||
self.fullpath = os.path.expanduser(path.strip("'\" "))
|
||||
self.ns: types.ModuleType | None = None
|
||||
self.is_running = False
|
||||
|
||||
if not os.path.isfile(self.fullpath):
|
||||
raise exceptions.OptionsError(f"No such script: {self.fullpath}")
|
||||
|
||||
self.reloadtask = None
|
||||
if reload:
|
||||
self.reloadtask = asyncio_utils.create_task(
|
||||
self.watcher(),
|
||||
name=f"script watcher for {path}",
|
||||
keep_ref=False,
|
||||
)
|
||||
else:
|
||||
self.loadscript()
|
||||
|
||||
def running(self):
|
||||
self.is_running = True
|
||||
|
||||
def done(self):
|
||||
if self.reloadtask:
|
||||
self.reloadtask.cancel()
|
||||
|
||||
@property
|
||||
def addons(self):
|
||||
return [self.ns] if self.ns else []
|
||||
|
||||
def loadscript(self):
|
||||
logger.info("Loading script %s" % self.path)
|
||||
if self.ns:
|
||||
ctx.master.addons.remove(self.ns)
|
||||
self.ns = None
|
||||
with addonmanager.safecall():
|
||||
ns = load_script(self.fullpath)
|
||||
ctx.master.addons.register(ns)
|
||||
self.ns = ns
|
||||
if self.ns:
|
||||
try:
|
||||
ctx.master.addons.invoke_addon_sync(
|
||||
self.ns, hooks.ConfigureHook(ctx.options.keys())
|
||||
)
|
||||
except Exception as e:
|
||||
script_error_handler(self.fullpath, e)
|
||||
if self.is_running:
|
||||
# We're already running, so we call that on the addon now.
|
||||
ctx.master.addons.invoke_addon_sync(self.ns, hooks.RunningHook())
|
||||
|
||||
async def watcher(self):
|
||||
# Script loading is terminally confused at the moment.
|
||||
# This here is a stopgap workaround to defer loading.
|
||||
await asyncio.sleep(0)
|
||||
last_mtime = 0.0
|
||||
while True:
|
||||
try:
|
||||
mtime = os.stat(self.fullpath).st_mtime
|
||||
except FileNotFoundError:
|
||||
logger.info("Removing script %s" % self.path)
|
||||
scripts = list(ctx.options.scripts)
|
||||
scripts.remove(self.path)
|
||||
ctx.options.update(scripts=scripts)
|
||||
return
|
||||
if mtime > last_mtime:
|
||||
self.loadscript()
|
||||
last_mtime = mtime
|
||||
await asyncio.sleep(ReloadInterval)
|
||||
|
||||
|
||||
class ScriptLoader:
|
||||
"""
|
||||
An addon that manages loading scripts from options.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_running = False
|
||||
self.addons = []
|
||||
|
||||
def load(self, loader):
|
||||
loader.add_option("scripts", Sequence[str], [], "Execute a script.")
|
||||
|
||||
def running(self):
|
||||
self.is_running = True
|
||||
|
||||
@command.command("script.run")
|
||||
def script_run(self, flows: Sequence[flow.Flow], path: mtypes.Path) -> None:
|
||||
"""
|
||||
Run a script on the specified flows. The script is configured with
|
||||
the current options and all lifecycle events for each flow are
|
||||
simulated. Note that the load event is not invoked.
|
||||
"""
|
||||
if not os.path.isfile(path):
|
||||
logger.error("No such script: %s" % path)
|
||||
return
|
||||
mod = load_script(path)
|
||||
if mod:
|
||||
with addonmanager.safecall():
|
||||
ctx.master.addons.invoke_addon_sync(
|
||||
mod,
|
||||
hooks.ConfigureHook(ctx.options.keys()),
|
||||
)
|
||||
ctx.master.addons.invoke_addon_sync(mod, hooks.RunningHook())
|
||||
for f in flows:
|
||||
for evt in eventsequence.iterate(f):
|
||||
ctx.master.addons.invoke_addon_sync(mod, evt)
|
||||
|
||||
def configure(self, updated):
|
||||
if "scripts" in updated:
|
||||
for s in ctx.options.scripts:
|
||||
if ctx.options.scripts.count(s) > 1:
|
||||
raise exceptions.OptionsError("Duplicate script")
|
||||
|
||||
for a in self.addons[:]:
|
||||
if a.path not in ctx.options.scripts:
|
||||
logger.info("Un-loading script: %s" % a.path)
|
||||
ctx.master.addons.remove(a)
|
||||
self.addons.remove(a)
|
||||
|
||||
# The machinations below are to ensure that:
|
||||
# - Scripts remain in the same order
|
||||
# - Scripts are not initialized un-necessarily. If only a
|
||||
# script's order in the script list has changed, it is just
|
||||
# moved.
|
||||
|
||||
current = {}
|
||||
for a in self.addons:
|
||||
current[a.path] = a
|
||||
|
||||
ordered = []
|
||||
newscripts = []
|
||||
for s in ctx.options.scripts:
|
||||
if s in current:
|
||||
ordered.append(current[s])
|
||||
else:
|
||||
sc = Script(s, True)
|
||||
ordered.append(sc)
|
||||
newscripts.append(sc)
|
||||
|
||||
self.addons = ordered
|
||||
|
||||
for s in newscripts:
|
||||
ctx.master.addons.register(s)
|
||||
if self.is_running:
|
||||
# If we're already running, we configure and tell the addon
|
||||
# we're up and running.
|
||||
ctx.master.addons.invoke_addon_sync(s, hooks.RunningHook())
|
||||
Reference in New Issue
Block a user