From 90e3d2335b8d0ee4e37a9d1eea02233292916202 Mon Sep 17 00:00:00 2001 From: GodSaveTheDoge <51802433+GodSaveTheDoge@users.noreply.github.com> Date: Sat, 28 Aug 2021 00:18:06 +0200 Subject: [PATCH] First commit Made a first proof of concept with the ability to override things --- README.md | 50 +++++++++++++++++++++++++++++++++++++++++++ hijack.py | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+) create mode 100644 README.md create mode 100644 hijack.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..81c9cf3 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +This allows to override things. + +Some examples: +```python +$ cat target.py +import sys + +print(sys.executable) +$ python target.py +/usr/bin/python +$ cat patch.py +from hijack import BinLaden + +patcher = BinLaden() +patcher.override("sys.executable", "nope") +patcher.run("target.py") +$ python patch.py +nope +$ +``` + +This could be useful to add some functionality or act as a decorator: +```python +$ cat target.py +import requests + +try: + r = requests.get("fsf.org") + print(r.status_code, r.reason) +except Exception as e: + print(e) +$ python target.py +Invalid URL 'fsf.org': No schema supplied. Perhaps you meant http://fsf.org? +$ cat patch.py +from hijack import BinLaden + +patcher = BinLaden() +old_get = patcher.import_("requests").get + +@patcher.override("requests.get") +def get(url, *a, **kw): + if not url.startswith("http"): + url = "https://" + url + return old_get(url, *a, **kw) + +patcher.run("target.py") +$ python patch.py +200 OK +$ +``` diff --git a/hijack.py b/hijack.py new file mode 100644 index 0000000..b8b1447 --- /dev/null +++ b/hijack.py @@ -0,0 +1,63 @@ +import sys +import runpy +import types +# TIL: module.__builtins__ is just an implementation detail +import builtins + +# If you don't know what the qualified name is: +# https://docs.python.org/3/glossary.html#term-qualified-name + +class BinLaden: + def __init__(self): + # "Things" yet to override + # Once they have been overriden thre is no need to keep em around + # since sys.modules is a thing. + # If you want to have "selective" override keeping the original module + # might be necessary to avoid reimporting every time + # This dict maps qualified name -> "thing" + self.to_override = {} + self.hijack() + + def __import__(self, name, globals_=None, locals_=None, fromlist=(), level=0): + module = self.import_(name, globals_, locals_, fromlist, level) + + for qualname, obj in self.to_override.copy().items(): + if not qualname[0] == module.__name__: + continue + + foo = module + for name in qualname[1:-1]: + if not hasattr(foo, name): + break + foo = getattr(module, name) + else: + if hasattr(foo, qualname[-1]): + setattr(foo, qualname[-1], obj) + self.to_override.pop(qualname) + + return module + + def hijack(self): + self.import_ = builtins.__import__ + builtins.__import__ = self.__import__ + + def restore(self): + builtins.__import__ = self.import_ + + def run(self, file): + runpy.run_path(file, run_name="__main__") + + def runmodule(self, path): + sys.modules.pop("__main__") + runpy.run_module(path, run_name="__main__") + + def override(self, qualname, obj=None): + if obj: + self.to_override[tuple(qualname.split("."))] = obj + return + + def _(fun): + self.to_override[tuple(qualname.split("."))] = fun + return fun + + return _