├── TB4hooky ├── Utils │ ├── __init__.py │ ├── LinkDealer.py │ └── ImportUtil.py ├── hooky │ ├── __init__.py │ ├── CacheObject.py │ ├── Serializer.py │ └── Hooker.py ├── netLoader │ ├── __init__.py │ ├── PackageLoader.py │ ├── AdapterConnectionRemote.py │ ├── ComplexImport.py │ ├── DaynamicImport.py │ ├── SourceCodeManager.py │ ├── ModuleLoader.py │ ├── RemoteControl.py │ └── MetaFinder.py ├── typeInspect │ ├── __init__.py │ └── Inspect.py ├── __init__.py └── Exception │ ├── SubclassError.py │ ├── __init__.py │ └── GlobalExceptionHook.py ├── .gitignore ├── remote_test.py ├── requirements.txt ├── setup.py ├── LICENSE ├── doc └── API reference.md └── README.MD /TB4hooky/Utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /TB4hooky/hooky/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /TB4hooky/netLoader/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /TB4hooky/typeInspect/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | dist 3 | MANIFEST.in 4 | TB4hooky.egg-info 5 | test.py 6 | remote_test.py 7 | .idea 8 | -------------------------------------------------------------------------------- /TB4hooky/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 8:26 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | -------------------------------------------------------------------------------- /TB4hooky/Exception/SubclassError.py: -------------------------------------------------------------------------------- 1 | class SubclassError(Exception): 2 | def __init__(self, msg): 3 | super().__init__(msg) 4 | -------------------------------------------------------------------------------- /TB4hooky/Exception/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 8:26 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | -------------------------------------------------------------------------------- /remote_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 3:41 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | from TB4hooky.hooky.Serializer import ServerSerialize, REMOTE_FUNCTION_MAPPING 9 | 10 | -------------------------------------------------------------------------------- /TB4hooky/Exception/GlobalExceptionHook.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 7:06 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | import sys 9 | 10 | 11 | class GlobalException(object): 12 | 13 | def __init__(self, exc_handle): 14 | sys.excepthook = exc_handle 15 | 16 | @classmethod 17 | def register_global_exception(cls, exc_handle): 18 | cls(exc_handle) 19 | 20 | -------------------------------------------------------------------------------- /TB4hooky/typeInspect/Inspect.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | 3 | 4 | class Inspect(object): 5 | """Inspect function type""" 6 | @staticmethod 7 | def check_function_type(_obj): 8 | if not inspect.isfunction(_obj) and not inspect.ismethod(_obj): 9 | raise TypeError(f"Error type is {type(_obj)} should be 'function'.") 10 | 11 | @staticmethod 12 | def check_string_type(_obj): 13 | if not isinstance(_obj, str): 14 | raise TypeError(f"Error type is {type(_obj)} should be 'str' because you use the remote hook.") 15 | 16 | -------------------------------------------------------------------------------- /TB4hooky/netLoader/PackageLoader.py: -------------------------------------------------------------------------------- 1 | from TB4hooky.netLoader.ModuleLoader import RemoteModuleLoader 2 | from types import ModuleType 3 | 4 | 5 | class RemotePackageLoader(RemoteModuleLoader): 6 | def load_module(self, fullname: str) -> ModuleType: 7 | mod = super(RemoteModuleLoader, self).load_module(fullname) 8 | mod.__path__ = [self._endpoint] 9 | mod.__package__ = fullname 10 | 11 | return mod 12 | 13 | def get_filename(self, fullname: str) -> str: 14 | return f"{self._endpoint}/__init__.py" 15 | 16 | def is_package(self, fullname: str) -> bool: 17 | return True 18 | -------------------------------------------------------------------------------- /TB4hooky/hooky/CacheObject.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 6:23 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | import base64 9 | 10 | 11 | class CacheMinx: 12 | __slots__ = () 13 | 14 | def __setitem__(self, key, value): 15 | if key in ("co_lnotab", "co_codestring"): 16 | value = base64.b64decode(value.encode()) 17 | if key in ("co_argcount", "co_kwonlyargcount", "co_nlocals", 18 | "co_stacksize", "co_flags", "co_firstlineno"): 19 | value = int(value) 20 | if key in ("co_consts", "co_names", "co_varnames", "co_freevars"): 21 | value = tuple(value) 22 | return super().__setitem__(key, value) 23 | 24 | 25 | class Cache(CacheMinx, dict): 26 | ... 27 | -------------------------------------------------------------------------------- /TB4hooky/netLoader/AdapterConnectionRemote.py: -------------------------------------------------------------------------------- 1 | from TB4hooky.netLoader.RemoteControl import ConnectionRemote, ConnectionRemotePackage 2 | 3 | 4 | class AdapterConnectionRemote(ConnectionRemote): 5 | """ 6 | For modify the method 'get_remote_f' should 7 | override method '_get_remote_f' and '__exit__', '__enter__' method 8 | you can save the IO object(example save the process buffer object) in 'self._session' 9 | or implement you own protocol. 10 | """ 11 | pass 12 | 13 | 14 | class AdapterConnectionRemotePackage(ConnectionRemotePackage): 15 | """ 16 | For modify the method 'get_remote_package' should 17 | override method '_get_remote_package' and '__exit__', '__enter__' method 18 | you can save the IO object(example save the process buffer object) in 'self._session' 19 | or implement you own protocol. 20 | """ 21 | pass 22 | 23 | -------------------------------------------------------------------------------- /TB4hooky/hooky/Serializer.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 3:32 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | from TB4hooky.hooky.Hooker import CodeHooker 9 | 10 | __all__ = [ 11 | 'REMOTE_FUNCTION_MAPPING', 12 | 'ServerSerialize', 13 | ] 14 | 15 | REMOTE_FUNCTION_MAPPING = {} 16 | 17 | 18 | class ServerSerialize(object): 19 | def __init__(self, count): 20 | self.count = count 21 | 22 | def __call__(self, func): 23 | def recv_args(): 24 | # func callable校验 25 | assert callable(func) 26 | func_serialize = CodeHooker.serialize_init(func) 27 | serialize_result = func_serialize.serialize_func(self.count) 28 | if func.__name__ in REMOTE_FUNCTION_MAPPING: 29 | return func 30 | REMOTE_FUNCTION_MAPPING[func.__name__] = serialize_result 31 | return func 32 | return recv_args() 33 | 34 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.8.6 2 | aiosignal==1.3.1 3 | async-timeout==4.0.3 4 | asynctest==0.13.0 5 | attrs==23.2.0 6 | bleach==6.0.0 7 | charset-normalizer==3.3.2 8 | click==8.1.7 9 | colorama==0.4.6 10 | docutils==0.20.1 11 | frozenlist==1.3.3 12 | fsspec==2023.1.0 13 | idna==3.6 14 | importlib-metadata==6.7.0 15 | importlib-resources==5.12.0 16 | itsdangerous==2.1.2 17 | jaraco.classes==3.2.3 18 | Jinja2==3.1.3 19 | keyring==24.1.1 20 | loguru==0.7.2 21 | markdown-it-py==2.2.0 22 | MarkupSafe==2.1.5 23 | mdurl==0.1.2 24 | more-itertools==9.1.0 25 | multidict==6.0.5 26 | pkginfo==1.10.0 27 | Pygments==2.17.2 28 | pywin32-ctypes==0.2.2 29 | readme-renderer==37.3 30 | requests==2.31.0 31 | requests-toolbelt==1.0.0 32 | rfc3986==2.0.0 33 | rich==13.7.1 34 | six==1.16.0 35 | tqdm==4.66.2 36 | twine==4.0.2 37 | typing_extensions==4.7.1 38 | urllib3==2.0.7 39 | webencodings==0.5.1 40 | Werkzeug==2.2.3 41 | win32-setctime==1.1.0 42 | wincertstore==0.2 43 | yarl==1.9.4 44 | zipp==3.15.0 45 | certifi -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 7:48 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | import setuptools 9 | 10 | with open("README.MD", 'r', encoding='utf-8') as des: 11 | long_description = des.read() 12 | 13 | with open("requirements.txt", 'r', encoding='utf-8') as pkg: 14 | pkgs = pkg.readlines() 15 | 16 | setuptools.setup( 17 | name='TB4hooky', 18 | version='0.1.10-alpha', 19 | author='Fake77', 20 | author_email="yankail520@gmail.com", 21 | description='Python hook framework support Local hook and Remote hook', 22 | long_description=long_description, 23 | long_description_content_type="text/markdown; charset=UTF-8;", 24 | url='https://github.com/fateofdate/TB4hooky', 25 | include_package_data=False, 26 | install_requires=[pkg.replace("\n", '') for pkg in pkgs], 27 | 28 | packages=setuptools.find_packages(), 29 | classifiers=[ 30 | "Programming Language :: Python :: 3", 31 | "License :: OSI Approved :: MIT License", 32 | 'Operating System :: OS Independent' 33 | ], 34 | python_requires=">=3.7" 35 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 YK L 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /TB4hooky/netLoader/ComplexImport.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 0:32 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | from TB4hooky.Utils.ImportUtil import FileHandle 9 | from importlib import import_module 10 | import time 11 | 12 | 13 | class AddressInjection(object): 14 | ADDRESS = None 15 | 16 | @staticmethod 17 | def register_backup_imp_server(_f): 18 | AddressInjection.ADDRESS = _f 19 | 20 | def __call__(self, func): 21 | 22 | def arg(*args, **kwargs): 23 | result = func(*args, **kwargs, remote_addr=self.ADDRESS) 24 | return result 25 | 26 | return arg 27 | 28 | 29 | @AddressInjection() 30 | def handle_exception(excType, excValue, tb, remote_addr): 31 | if excType is ModuleNotFoundError or ImportError: 32 | module_name = str(excValue).split("'")[-2] 33 | try: 34 | FileHandle(remote_addr, module_name).start_remote_import() 35 | time.sleep(0.5) 36 | globals()[module_name] = import_module(module_name) 37 | 38 | except Exception as e: 39 | print(e) 40 | return 41 | 42 | else: 43 | raise excType(excValue, tb) 44 | -------------------------------------------------------------------------------- /TB4hooky/netLoader/DaynamicImport.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import importlib 3 | import time 4 | from loguru import logger 5 | from TB4hooky.netLoader.MetaFinder import RemoteMetaFinder 6 | from TB4hooky.netLoader.SourceCodeManager import SourceCodeManager 7 | from TB4hooky.netLoader.ComplexImport import FileHandle 8 | from TB4hooky.Utils.LinkDealer import LinksDealer 9 | 10 | import sys 11 | 12 | _meta = {} 13 | _scm = SourceCodeManager() 14 | _endpoint = "" 15 | 16 | 17 | def register(endpoint: str) -> None: 18 | global _endpoint 19 | _endpoint = endpoint 20 | if not hasattr(_meta, endpoint): 21 | finder = RemoteMetaFinder(endpoint, _scm) 22 | _meta[endpoint] = finder 23 | sys.meta_path.append(finder) 24 | 25 | def remote_import(_pkg: str): 26 | if _endpoint: 27 | loop = asyncio.get_event_loop() 28 | links = LinksDealer.get_link(_endpoint) 29 | for link in links: 30 | if link.startswith(_pkg.split('.')[0]): 31 | logger.success(f"Find pkg {link.replace('/', '')}!") 32 | FileHandle(_endpoint, link, loop).start_remote_import() 33 | time.sleep(0.2) 34 | print(FileHandle.get_install_path()) 35 | sys.path.append(FileHandle.get_install_path() + '\\' + _pkg.split('.')[0]) 36 | pkg = importlib.import_module(_pkg) 37 | return pkg 38 | 39 | 40 | def unload(endpoint: str) -> None: 41 | if hasattr(_meta, endpoint): 42 | finder = _meta.pop(endpoint) 43 | sys.meta_path.remove(finder) 44 | -------------------------------------------------------------------------------- /TB4hooky/netLoader/SourceCodeManager.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 8:08 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | from TB4hooky.netLoader.AdapterConnectionRemote import AdapterConnectionRemotePackage as ConnectionRemotePackage 9 | 10 | __all__ = [ 11 | "SourceCodeManager", 12 | ] 13 | 14 | 15 | class SourceCodeManager: 16 | def __init__(self) -> None: 17 | self._sources = {} 18 | self._complied_codes = {} 19 | 20 | def get_source_code(self, filename: str) -> str: 21 | source = self._sources.get(filename) 22 | if not source: 23 | with ConnectionRemotePackage() as conn: 24 | session = conn.get_remote_package(filename) 25 | if session.status_code == 200: 26 | source = session.text 27 | # print(filename) 28 | else: 29 | # print(filename + 'i') 30 | session = conn.get_remote_package(filename + 'i') 31 | source = session.text 32 | self._sources[filename] = source 33 | return source 34 | 35 | def get_complied_code(self, filename: str): 36 | source = self.get_source_code(filename) 37 | complied_code = self._complied_codes.get(filename) 38 | if not complied_code: 39 | complied_code = compile(source, filename, 'exec') 40 | self._complied_codes[filename] = complied_code 41 | return complied_code 42 | 43 | def clear(self) -> None: 44 | self._sources.clear() 45 | self._complied_codes.clear() 46 | -------------------------------------------------------------------------------- /TB4hooky/netLoader/ModuleLoader.py: -------------------------------------------------------------------------------- 1 | from importlib.abc import ( 2 | SourceLoader, 3 | ) 4 | import sys 5 | from types import ModuleType 6 | from TB4hooky.netLoader.SourceCodeManager import SourceCodeManager 7 | 8 | 9 | class RemoteModuleLoader(SourceLoader): 10 | def __init__(self, endpoint: str, source_code_manager: SourceCodeManager) -> None: 11 | self._endpoint = endpoint 12 | self._source_code_manager = source_code_manager 13 | 14 | def load_module(self, fullname: str) -> ModuleType: 15 | code = self.get_code(fullname) 16 | mod = sys.modules.setdefault(fullname, ModuleType(fullname)) 17 | mod.__file__ = self.get_filename(fullname) 18 | mod.__loader__ = self 19 | mod.__package__ = fullname.rpartition('.')[0] 20 | exec(code, mod.__dict__) 21 | return mod 22 | 23 | def get_code(self, fullname: str) -> str: 24 | filename = self.get_filename(fullname) 25 | return self._source_code_manager.get_complied_code(filename) 26 | 27 | def get_data(self, path: str) -> bytes: ... 28 | 29 | def get_filename(self, fullname: str) -> str: 30 | basename = fullname.split('.')[-1] 31 | return f"{self._endpoint}/{basename}.py" 32 | 33 | def get_source(self, fullname: str) -> str: 34 | filename = self.get_filename(fullname) 35 | try: 36 | return self._source_code_manager.get_source_code(fullname, filename) 37 | except Exception as _: 38 | raise ImportError(f"Can't import {filename}.") 39 | 40 | def is_package(self, fullname: str) -> bool: 41 | return False 42 | -------------------------------------------------------------------------------- /TB4hooky/Utils/LinkDealer.py: -------------------------------------------------------------------------------- 1 | from html.parser import HTMLParser 2 | import requests 3 | 4 | 5 | class LinksDealer(object): 6 | 7 | @staticmethod 8 | def _get_links(addr): 9 | links = set() 10 | 11 | class LinksParser(HTMLParser): 12 | def handle_starttag(self, tag, attrs): 13 | if tag == 'a': 14 | attrs = dict(attrs) 15 | links.add(attrs.get('href').rstrip('/')) 16 | 17 | 18 | try: 19 | resp = requests.get(addr) 20 | content = resp.text 21 | parser = LinksParser() 22 | parser.feed(content) 23 | except Exception as _: 24 | pass 25 | return links 26 | 27 | @staticmethod 28 | def get_link(addr): 29 | return LinksDealer._get_links(addr) 30 | 31 | @staticmethod 32 | def _filter_links(links): 33 | return links.startswith(("http://", "https://")) 34 | 35 | @staticmethod 36 | def filter_links(links): 37 | return LinksDealer._filter_links(links) 38 | 39 | @staticmethod 40 | def _get_filehandle_links(addr): 41 | links = set() 42 | 43 | class LinksParser(HTMLParser): 44 | def handle_starttag(self, tag, attrs): 45 | if tag == 'a': 46 | attrs = dict(attrs) 47 | links.add(attrs.get('href')) 48 | 49 | try: 50 | resp = requests.get(addr) 51 | content = resp.text 52 | parser = LinksParser() 53 | parser.feed(content) 54 | except Exception as _: 55 | pass 56 | return links 57 | 58 | @staticmethod 59 | def get_filehandle_links(addr): 60 | return LinksDealer._get_filehandle_links(addr) -------------------------------------------------------------------------------- /TB4hooky/netLoader/RemoteControl.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class BaseConnection(object): 5 | def __init__(self): ... 6 | 7 | def __enter__(self): 8 | raise NotImplementedError("Subclasses must implement __enter__") 9 | 10 | def __exit__(self, exc_type, exc_val, *args, **kwargs): 11 | raise NotImplementedError("Subclasses must implement __exit__") 12 | 13 | def get_remote_f(self, remote_addr, *args, **kwargs): 14 | raise NotImplementedError("Subclasses must implement 'get_remote_f'.") 15 | 16 | def get_remote_package(self, remote_addr, *args, **kwargs): ... 17 | 18 | 19 | class ConnectionRemote(BaseConnection): 20 | def __init__(self, *args, **kwargs): 21 | super().__init__() 22 | self._session: requests.Session = None 23 | 24 | def __enter__(self): 25 | self._session = requests.Session() 26 | return self 27 | 28 | def __exit__(self, exc_type, exc_val, *args): 29 | self._session.close() 30 | 31 | def _get_remote_f(self, remote_addr, *args, **kwargs): 32 | resp = self._session.get(remote_addr) 33 | return resp 34 | 35 | def get_remote_f(self, remote_addr, *args, **kwargs): 36 | result = self._get_remote_f(remote_addr, *args, **kwargs) 37 | return result 38 | 39 | def get_remote_package(self, remote_addr, *args, **kwargs): ... 40 | 41 | 42 | class ConnectionRemotePackage(BaseConnection): 43 | def __init__(self, *args, **kwargs): 44 | super().__init__() 45 | self._session: requests.Session = None 46 | 47 | def __enter__(self): 48 | self._session = requests.Session() 49 | return self 50 | 51 | def __exit__(self, exc_type, exc_val, *args, **kwargs): 52 | self._session.close() 53 | 54 | def get_remote_f(self, remote_addr, *args, **kwargs): ... 55 | 56 | def _get_remote_package(self, remote_addr, *args, **kwargs) -> requests.Response: 57 | result = self._session.get(remote_addr, *args, **kwargs) 58 | return result 59 | 60 | def get_remote_package(self, remote_addr, *args, **kwargs) -> requests.Response: 61 | return self._get_remote_package(remote_addr, *args, **kwargs) 62 | -------------------------------------------------------------------------------- /TB4hooky/netLoader/MetaFinder.py: -------------------------------------------------------------------------------- 1 | from importlib.abc import MetaPathFinder 2 | from TB4hooky.netLoader.SourceCodeManager import SourceCodeManager 3 | from TB4hooky.Utils.LinkDealer import LinksDealer 4 | from TB4hooky.netLoader.PackageLoader import RemotePackageLoader 5 | from TB4hooky.netLoader.ModuleLoader import RemoteModuleLoader 6 | 7 | 8 | class RemoteMetaFinder(MetaPathFinder): 9 | def __init__(self, endpoint: str, source_code_manager: SourceCodeManager) -> None: 10 | self._endpoint = endpoint 11 | self._nodes = {} 12 | self._source_code_manager = source_code_manager 13 | 14 | def _is_module(self, filename: str, endpoint: str) -> bool: 15 | return filename in self._nodes[endpoint] 16 | 17 | def _is_package(self, basename: str, endpoint) -> bool: 18 | return basename in self._nodes[endpoint] 19 | 20 | def _store_node(self, node: str) -> None: 21 | if not hasattr(self._nodes, node): 22 | self._nodes[node] = LinksDealer.get_link(node) 23 | 24 | def _get_node(self, path: str): 25 | if path is None: 26 | return self._endpoint 27 | else: 28 | if not path[0].startswith(self._endpoint): 29 | return None 30 | return path[0] 31 | 32 | def find_module(self, fullname: str, path: str = None): 33 | # 1. 获取当前节点 34 | current_code = self._get_node(path) 35 | # 如果节点不存在中止流程 36 | if not current_code: 37 | return None 38 | # 1.1 缓存节点 39 | self._store_node(current_code) 40 | # 2. 获取当前的basename (模块名称) 41 | basename = fullname.split(".")[-1] 42 | # 3. 处理package 43 | if self._is_package(basename, current_code): 44 | child_location = f"{current_code}/{basename}" 45 | loader = RemotePackageLoader(child_location, self._source_code_manager) 46 | try: 47 | loader.load_module(fullname) 48 | self._nodes[child_location] = LinksDealer.get_link(child_location) 49 | except Exception as _: 50 | loader = None 51 | return loader 52 | # 4. 处理module 53 | filename = f"{basename}.py" 54 | if self._is_module(filename, current_code): 55 | return RemoteModuleLoader(current_code, self._source_code_manager) 56 | else: 57 | return None 58 | 59 | def invalidate_caches(self) -> None: 60 | self._nodes.clear() 61 | self._source_code_manager.clear() 62 | -------------------------------------------------------------------------------- /TB4hooky/Utils/ImportUtil.py: -------------------------------------------------------------------------------- 1 | """ 2 | coding: utf-8 3 | @Software: PyCharm 4 | @Time: 0:41 5 | @Author: Fake77 6 | @Module Name: 7 | """ 8 | import sys 9 | from TB4hooky.Utils.LinkDealer import LinksDealer 10 | import os 11 | from loguru import logger 12 | from aiohttp import ClientSession 13 | import asyncio 14 | import aiohttp 15 | from tqdm import tqdm 16 | 17 | 18 | class FileHandle(object): 19 | """ 20 | package = FileHandle("http://127.0.0.1:9000", 'torch') 21 | package.start_remote_import() 22 | 23 | """ 24 | 25 | def __init__(self, _endpoint: str, package: str, loop): 26 | # 判断是否为dist-info 包文件 27 | if package.endswith("dist-info"): 28 | package = package 29 | else: 30 | package = package.replace(".", '/') 31 | # 生成根路径 32 | self._root_path = _endpoint 33 | # 包路径 34 | self._endpoint = _endpoint + '/' + package 35 | self._package = package 36 | # 初始化链接库 37 | self._links = LinksDealer.get_filehandle_links(_endpoint + '/' + package) 38 | # 获取当前安装目录 39 | self._install_path = FileHandle.get_install_path() 40 | # 如果获取不到安装目录则在当前文件夹下创建安装目录 41 | if self._install_path is None: 42 | self._install_path = '\\site-packages' 43 | # 下载文件列表 44 | self._files = [] 45 | # 事件循环 46 | self._event_loop = loop 47 | # 设置并发信号量 48 | self.semaphore = asyncio.Semaphore(100) 49 | # 设置重发列表 50 | self._retry = [] 51 | 52 | @staticmethod 53 | def get_install_path(): 54 | """ 55 | 获取安装路径 一般为 site-packages 56 | """ 57 | for path in sys.path: 58 | if path.split('\\')[-1] == 'site-packages': 59 | return path 60 | 61 | @staticmethod 62 | def is_directory(path): 63 | """ 64 | 判断是否为文件 65 | directory/ 目录 -> True 66 | hello.txt 文件 -> False 67 | """ 68 | if path[-1] == '/': 69 | return True 70 | return False 71 | 72 | async def download_file(self, path: str, pbar: tqdm): 73 | """ 74 | 下载文件 75 | :param: path: url 76 | :param: pbar: tqdm object 77 | """ 78 | async with self.semaphore: 79 | try: 80 | async with ClientSession() as session: 81 | async with session.get(path) as content: 82 | filepath = self._install_path + path[len(self._root_path):].replace("/", '\\') 83 | 84 | directory_path = "\\".join(filepath.split('\\')[:-1]) 85 | 86 | if not os.path.exists(directory_path): 87 | os.makedirs(directory_path) 88 | 89 | with open(filepath, 'wb') as f: 90 | f.write(await content.read()) 91 | pbar.update(1) 92 | except aiohttp.client_exceptions.ClientOSError: 93 | self._retry.append(path) 94 | 95 | def get_all_file(self, endpoint: str): 96 | """ 97 | 遍历当前endpoint目录下的全部文件 98 | """ 99 | # 递归获取该包的全部文件 100 | for sub_path in LinksDealer.get_filehandle_links(endpoint): 101 | if endpoint[-1] != '/': 102 | path = endpoint + '/' + sub_path 103 | else: 104 | path = endpoint + sub_path 105 | if self.is_directory(path): 106 | self.get_all_file(path) 107 | else: 108 | self._files.append(path) 109 | 110 | async def main(self, link_lst, pbar): 111 | tasks = [] 112 | for url in link_lst: 113 | tasks.append(asyncio.create_task(self.download_file(url, pbar))) 114 | await asyncio.wait(tasks) 115 | 116 | def start_remote_import(self): 117 | # 提交远程文件路径到self._files 118 | self.get_all_file(self._endpoint) 119 | 120 | # 判断远程包是否存在 121 | if len(self._files) == 0: 122 | raise ImportError(f"Not such Package {self._package} in {self._endpoint}.") 123 | 124 | logger.info(f"remote import {self._package} from {self._endpoint} total file: {len(self._files)}") 125 | 126 | # fetch 远程包 127 | with tqdm(total=len(self._files)) as pbar: 128 | self._event_loop.run_until_complete(self.main(self._files, pbar)) 129 | if len(self._retry) != 0: 130 | self._event_loop.run_until_complete(self.main(self._retry, pbar)) 131 | 132 | -------------------------------------------------------------------------------- /doc/API reference.md: -------------------------------------------------------------------------------- 1 | ### [netLoader](https://github.com/fateofdate/TB4hooky/tree/main/TB4hooky/netLoader) 2 | > 负责远程导入和网络交互的组件包 3 | ##### 基类 4 | 5 | * ```class``` **BaseConnection** 6 | >**Code source: [TB4hooky/netLoader/RemoteControl.py](https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/netLoader/RemoteControl.py)** 7 | 8 | 9 | 10 | >* **instance method** ```__init__()->None``` 11 | >初始化方法,实现的时候需要重写方法可以初始化一些,有关远程调用的对象 12 | >example 13 | >```python 14 | >class ImplementBaseConnection(BaseConnection): 15 | > def __init__(): 16 | > # 初始化基于http的session对象 17 | > self.session = requests.Session() 18 | > 19 | >``` 20 | >* **instance method** ```__enter__()->None``` 21 | >BaseConnection 的子类实需要实现python ```context protocol``` 以确保IO流的正确运行,以及调用结束后的正确关闭. 22 | >* **instance method** ```__exit__(exc_type, exc_cal, *args, **kwargs)->None``` 23 | >BaseConnection 的子类需要实现此方法,以将在 ```__init__```方法中存入的IO流对象正确的关闭. 24 | >* **instance method** ```get_remote_f(self, remote_addr: str, *args, **kwargs)->Any``` 25 | >BaseConnection的子类需要实现此方法,用于获取远程函数信息. 26 | >* **instance method** ```get_remote_package(self, remote_addr: str, *args, **kwargs)->Any``` 27 | >BaseConnection的子类需要实现此方法,用于获取远程包信息 28 | 29 | 30 | 31 | ##### netLoader 对象 32 | 33 | * ```class``` **ConnectionRemote** 34 | 35 | >**Code source: [TB4hooky/netLoader/RemoteControl.py](https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/netLoader/RemoteControl.py)** 36 | 37 | 38 | 39 | >* **instance method** ```get_remote_f(self, remote_addr, *args, **kwargs)->requests.Response``` 40 | > 41 | > ```remote_addr```为远程```url```地址, 基于HTTP协议实现的远程字节码获取方法,返回一个```requests.Response```对象. 42 | > 43 | >* **instance method** ```get_remote_package(self, remote_addr, *args, **kwargs) -> Any``` 44 | > 45 | > ConnectionRemote类中不实现,由ConnectionRemotePackage子类实现。 46 | 47 | * ```class``` **ConnectionRemotePackage** 48 | 49 | > **Code source: [TB4hooky/netLoader/RemoteControl.py](https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/netLoader/RemoteControl.py)** 50 | 51 | 52 | 53 | > * **instance method** ```def get_remote_package(self, remote_addr, *args, **kwargs) ->requests.Response:``` 54 | > 55 | > ```remote_addr```为远程```url```地址, 基于HTTP协议实现的远程包获取方法,返回一个```requests.Response```对象. 56 | 57 | 58 | 59 | ### [netLoader.AdapterConnectionRemote][https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/netLoader/AdapterConnectionRemote.py] 60 | 61 | > **Code source: [TB4hooky/netLoader/AdapterConnectionRemote.py][https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/netLoader/AdapterConnectionRemote.py]** 62 | 63 | * ```class``` **AdapterConnectionRemote(ConnectionRemote)** 64 | 65 | > 如果需要自定义传输协议可以在此处重写```__exit__```, ```__enter__```,``` __init__```,``` get_remote_f``` 方法 ,具体参考**BaseConnection** **reference:  BaseConnection** 66 | > 67 | > ``` 68 | > For modify the method 'get_remote_f' should 69 | > override method '_get_remote_f' and '__exit__', '__enter__' method 70 | > you can save the IO object(example save the process buffer object) in 'self._session' 71 | > or implement you own protocol. 72 | > ``` 73 | 74 | * ```class``` **AdapterConnectionRemotePackage(ConnectionRemotePackage)** 75 | 76 | > 如果需要自定义远程包传输的协议,可以在此处重写```__exit__```, ```__enter__```,``` __init__```,``` get_remote_f``` 方法 。 77 | 78 | 79 | 80 | ### [netLoader.DaynamicImport][https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/netLoader/DaynamicImport.py] 81 | 82 | > **Code source: [TB4hook/netLoader/DaynamicImport][https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/netLoader/DaynamicImport.py]** 83 | 84 | 85 | 86 | > **function** ```register(endpoint: str) -> None:``` 87 | > 88 | > ```endpoint```: 远程服务器地址. 89 | > 90 | > 注册包导入服务器远程服务器 91 | > 92 | > 93 | > 94 | > **function** ```def unload(endpoint: str) -> None:``` 95 | > 96 | > ```endpoint```: 远程服务器地址. 97 | > 98 | > 从```sys.metapath```中卸载指定远程服务器 99 | 100 | ### [hooky][https://github.com/fateofdate/TB4hooky/tree/main/TB4hooky/hooky] 101 | 102 | > 主要HOOK 逻辑实现 103 | 104 | ##### 基类 105 | 106 | * ```abstract class``` **MetaCodeHooker** 107 | 108 | > **Code source: [TB4hooky/hooky/Hooker.py][https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/hooky/Hooker.py]** 109 | 110 | > hooker的核心方法抽象类 MetaCodeHooker 111 | > 112 | > 1. local hook 113 | > 2. remote hook 114 | > 115 | > 如果要实现CodeHooker应该实现抽象类中以下的方法: 116 | > 117 | > 118 | > 119 | > **staticmethod abstact method** ```def extract_co_code(*args, **kwargs): ...``` 120 | > 121 | > 主要用于提取```co_code```对象 122 | > 123 | > 124 | > 125 | > **abstract method** ```def swap_code_info(self, *args, **kwargs): ...``` 126 | > 127 | > 用于交换```_f```和```_hook_obj```的```co_code```属性 128 | 129 | ### [hooky.Hooker.CodeHooker][https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/hooky/Hooker.py] 130 | 131 | * ```class``` **CodeHooker** 132 | 133 | > HOOK的核心类, 当我们需要在远程端调用的时候需要对函数进行序列化时可以使用```serialize_init(cls, func)-> CodeHooker```获得用于序列化函数的对象 134 | > 135 | > #### Local hooker example 136 | > 137 | > ```python 138 | > from TB4hook.hooky.Hooker import CodeHooker 139 | > 140 | > def test(a): 141 | > print("it's test.", a) 142 | > 143 | > @CodeHooker(_f=test) 144 | > def a(): ... 145 | > 146 | > if __name__ == "__main__": 147 | > a("hello world") 148 | > 149 | > # $> it's test.hello world 150 | > ``` 151 | > 152 | > #### Remote hooker example 153 | > 154 | > ```python 155 | > from TB4hook.hooky.Hooker import CodeHooker 156 | > 157 | > ##### remote server runing at http://127.0.0.1:8000 158 | > # web api ''/test' 159 | > def test(a, b): 160 | > print(a, b) 161 | > 162 | > serialize = CodeHooker.serialize_init(test) 163 | > # set count and get serialize result 164 | > serialize_result = serialize.serialize_func(3) 165 | > 166 | > # here is web server for return the serialize result ... 167 | > 168 | > ##### client 169 | > 170 | > @CodeHooker(_f="http://127.0.0.1:8000/test", remote=True) 171 | > def client(): ... 172 | > 173 | > if __name__ == "__main__": 174 | > client(1, 2) 175 | > 176 | > # $> 12 177 | > ``` 178 | > 179 | > 180 | > 181 | > **class method** ```serialize_init(cls, func) ->CodeHooker``` 182 | > 183 | > ```func```: 远程调用时需要序列化的函数,或者静态方法,类方法,不可以是实例方法, 如果要用到实例的方法需要在函数内部导入```import```,并且在所有导入前方注册```register```然后导入包后进行实例化再使用,可以参考DaynamicImpoer.register方法。 184 | > 185 | > 接收```func```获取可生成序列化字符串的```CodeHooker```对象。 186 | > 187 | > 188 | > 189 | > **staticmethod method** ```def serialize_func(self, count: int) -> str:``` 190 | > 191 | > ```count```: 设置远程端缓存数,即设置其声明周期。 192 | > 193 | > **返回值**: 序列化后的字符串 194 | 195 | ### [hooky.Hooker.InstanceHooker][https://github.com/fateofdate/TB4hooky/blob/main/TB4hooky/hooky/Hooker.py] 196 | 197 | * ```class``` **InstanceHooker** 198 | 199 | > 实例方法的hooker 200 | > 201 | > #### Instance method hook example 202 | > 203 | > ```python 204 | > # 导入InstanceCodeHooker 205 | > from hooky.Hooker import InstanceCodeHooker 206 | > 207 | > class LocalMethod(object): 208 | > 209 | > def __init__(self): 210 | > self.flag = 'hello world' 211 | > 212 | > def sign(self, flag): 213 | > print(self.flag) 214 | > self.flag = flag 215 | > 216 | > def chg(self, flag): 217 | > print(self.flag) 218 | > self.flag = flag 219 | > 220 | > 221 | > ins = LocalMethod() 222 | > 223 | > @InstanceCodeHooker(_f='sign', instance=ins) 224 | > def sign():... 225 | > 226 | > @InstanceCodeHooker(_f='chg', instance=ins) 227 | > def another_sign():... 228 | > 229 | > # 调用方法 230 | > sign("hello") 231 | > another_sign("world") 232 | > print(ins.flag) 233 | > 234 | > # $> hello world 235 | > # hello 236 | > # world 237 | > ``` 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /TB4hooky/hooky/Hooker.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import sys 4 | import types 5 | 6 | LOGGER = True 7 | 8 | from TB4hooky.netLoader.AdapterConnectionRemote import AdapterConnectionRemote as ConnectionRemote 9 | from abc import ABCMeta, abstractmethod 10 | from TB4hooky.typeInspect.Inspect import Inspect 11 | from TB4hooky.hooky.CacheObject import Cache 12 | from loguru import logger 13 | 14 | 15 | DEFAULT_CACHE_COUNT = 64 16 | 17 | 18 | 19 | def unable_logger(): 20 | global LOGGER 21 | LOGGER = False 22 | 23 | 24 | class MetaCodeHooker(metaclass=ABCMeta): 25 | @staticmethod 26 | @abstractmethod 27 | def extract_co_code(*args, **kwargs): ... 28 | 29 | @abstractmethod 30 | def swap_code_info(self, *args, **kwargs): ... 31 | 32 | @abstractmethod 33 | def swap_remote_code_info(self, *args, **kwargs): ... 34 | 35 | 36 | class BaseCodeHooker(object): 37 | """ BaseCodeHooker """ 38 | 39 | def __init__(self, _f, remote: bool = False, *args, **kwargs): 40 | if not ((3, 7) < sys.version_info < (3, 8)): 41 | raise NotImplemented("Not implemented yet") 42 | # 远程hook 43 | self._remote = remote 44 | # 实现函数对象 45 | self._f = _f 46 | # 被hook对象 47 | self._hook_obj = None 48 | # 缓存 49 | self._cache = None 50 | # 缓存计数器 51 | self._cache_count = None 52 | # 当前被hook的函数 53 | # remote func argument 54 | self.arg = args 55 | self.kwarg = kwargs 56 | 57 | # extract implement hook function 'co_code' byte code 58 | @staticmethod 59 | def extract_co_code(func): 60 | return BaseCodeHooker._extract_co_code(func) 61 | 62 | @staticmethod 63 | def _extract_co_code(func): 64 | Inspect.check_function_type(func) 65 | return func.__code__.co_code 66 | 67 | def _swap_code_info(self): 68 | Inspect.check_function_type(self._f) 69 | Inspect.check_function_type(self._hook_obj) 70 | _f_code = self._f.__code__ 71 | if not self._cache: 72 | self._cache = BaseCodeHooker.extract_co_code(self._f) 73 | self._hook_obj.__code__ = types.CodeType( 74 | _f_code.co_argcount, 75 | _f_code.co_kwonlyargcount, 76 | _f_code.co_nlocals, 77 | _f_code.co_stacksize, 78 | _f_code.co_flags, 79 | self._cache, 80 | _f_code.co_consts, 81 | _f_code.co_names, 82 | _f_code.co_varnames, 83 | _f_code.co_filename, 84 | _f_code.co_name, 85 | _f_code.co_firstlineno, 86 | _f_code.co_lnotab, 87 | _f_code.co_freevars 88 | ) 89 | 90 | def swap_code_info(self): 91 | self._swap_code_info() 92 | 93 | def swap_remote_code_info(self, host): 94 | pass 95 | 96 | 97 | class CodeHooker(BaseCodeHooker): 98 | def __init__(self, _f, remote: bool = None): 99 | if remote: 100 | self._cache_count = DEFAULT_CACHE_COUNT 101 | super().__init__(_f, remote) 102 | 103 | def __call__(self, func): 104 | self._hook_obj = func 105 | 106 | def arg_recv(*args, **kwargs): 107 | if not self._remote: 108 | if LOGGER: 109 | logger.success(f"Hook mod [Local], Local function" 110 | f" from [{self._f.__name__}] to Hook object [{self._hook_obj.__name__}]") 111 | 112 | self.swap_code_info() 113 | else: 114 | if LOGGER: 115 | logger.success(f"Hook mod [Remote], Remote code " 116 | f"from host [{self._f}] to Hook object [{self._hook_obj.__name__}]") 117 | 118 | self.swap_remote_code_info(self._f) 119 | return self._hook_obj(*args, **kwargs) 120 | 121 | return arg_recv 122 | 123 | @classmethod 124 | def serialize_init(cls, func): 125 | return cls(func, remote=False) 126 | 127 | def _serialize_func(self, count: int) -> str: 128 | _f_struct = {"cache_count": count, 129 | "co_argcount": self._f.__code__.co_argcount, 130 | 'co_kwonlyargcount': self._f.__code__.co_kwonlyargcount, 131 | 'co_nlocals': self._f.__code__.co_nlocals, 132 | 'co_stacksize': self._f.__code__.co_stacksize, 133 | 'co_flags': self._f.__code__.co_flags, 134 | 'co_codestring': base64.b64encode(self._f.__code__.co_code).decode(), 135 | 'co_consts': self._f.__code__.co_consts, 136 | 'co_names': self._f.__code__.co_names, 137 | 'co_varnames': self._f.__code__.co_varnames, 138 | 'co_filename': self._f.__code__.co_filename, 139 | 'co_name': self._f.__code__.co_name, 140 | 'co_firstlineno': self._f.__code__.co_firstlineno, 141 | 'co_lnotab': base64.b64encode(self._f.__code__.co_lnotab).decode(), 142 | 'co_freevars': self._f.__code__.co_freevars} 143 | return json.dumps(_f_struct) 144 | 145 | def serialize_func(self, count: int) -> str: 146 | return self._serialize_func(count) 147 | 148 | def _set_cache(self, serialize: dict): 149 | for key in serialize.keys(): 150 | value = serialize[key] 151 | if key == "cache_count": 152 | self._cache_count = int(value) 153 | self._cache[key] = value 154 | 155 | def swap_remote_code_info(self, host, *args, **kwargs): 156 | self._swap_remote_code_info(host) 157 | 158 | def _set_hook_code(self): 159 | self._hook_obj.__code__ = types.CodeType( 160 | self._cache['co_argcount'], 161 | self._cache['co_kwonlyargcount'], 162 | self._cache['co_nlocals'], 163 | self._cache['co_stacksize'], 164 | self._cache['co_flags'], 165 | self._cache['co_codestring'], 166 | self._cache['co_consts'], 167 | self._cache['co_names'], 168 | self._cache['co_varnames'], 169 | self._cache['co_filename'], 170 | self._cache['co_name'], 171 | self._cache['co_firstlineno'], 172 | self._cache['co_lnotab'], 173 | self._cache['co_freevars'] 174 | ) 175 | 176 | def _swap_remote_code_info(self, host, *args, **kwargs): 177 | Inspect.check_string_type(host) 178 | if not self._cache_count: 179 | 180 | # remote cache 181 | with ConnectionRemote() as conn: 182 | remote_content = conn.get_remote_f(host, *args, **kwargs) 183 | 184 | # 判断分发服务器是否在线 185 | if 0 < (remote_content.status_code - 200) < 10: 186 | raise Exception(f"Remote code request Error status " 187 | f"code: {remote_content.status_code} remote host: {host}.") 188 | remote_content = remote_content.json() 189 | 190 | # 取出RPC 填充 cache 191 | self._cache = Cache() 192 | self._set_cache(remote_content) 193 | self._cache.pop("cache_count") 194 | 195 | self._cache_count -= 1 196 | self._set_hook_code() 197 | 198 | 199 | class InstanceCodeHooker(CodeHooker): 200 | _INSTANCE = {} 201 | 202 | def __init__(self, _f, instance): 203 | super().__init__(_f, remote=False) 204 | # 保证注册过的instance 205 | if instance not in self._INSTANCE.values(): 206 | self._instance = instance 207 | self._INSTANCE[hash(instance)] = instance 208 | else: 209 | self._instance = self._INSTANCE[hash(instance)] 210 | Inspect.check_string_type(_f) 211 | self._f = getattr(instance, _f) 212 | 213 | def __call__(self, func): 214 | self._hook_obj = func 215 | 216 | def arg_recv(*args, **kwargs): 217 | if logger: 218 | logger.success(f"Hook mod [Local], Local function" 219 | f" from [{self._instance.__class__.__name__}().{self._f.__name__}] to Hook object [{self._hook_obj.__name__}]") 220 | 221 | self.swap_code_info() 222 | 223 | return self._hook_obj(self._instance, *args, **kwargs) 224 | 225 | return arg_recv 226 | 227 | def swap_remote_code_info(self, host, *args, **kwargs): 228 | raise NotImplemented("Error 'InstanceCodeHooker' " 229 | "not support remote hook should use 'CodeHooker'.") 230 | 231 | def _swap_remote_code_info(self, host, *args, **kwargs): 232 | raise NotImplemented("Error 'InstanceCodeHooker' " 233 | "not support remote hook should use 'CodeHooker'.") 234 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # TB4hooky 2 | ![PyPI - Version](https://img.shields.io/pypi/v/TB4hooky) 3 | ![](https://img.shields.io/badge/python-v3.7.16-1E90FF.svg) ![GitHub repo file or directory count](https://img.shields.io/github/directory-file-count/fateofdate/TB4hooky) ![GitHub Repo stars](https://img.shields.io/github/stars/fateofdate/TB4hooky) ![GitHub watchers](https://img.shields.io/github/watchers/fateofdate/TB4hooky) ![GitHub forks](https://img.shields.io/github/forks/fateofdate/TB4hooky) 4 | 5 | ### 简介 6 | > TB4hooky 是一个由python编写的代码hook框架,支持```remote hook```和```local hook``` 7 | > 支持自定义序列化协议,支持远程导入包模块,开箱即用。 8 | ### 使用场景 9 | > 1. 持续集成, 对于经常更新的核心算法,核心函数等等可以进行```remote hook``` 并且将核心代码放在远程服务器上,当远程代码变动或者更新时,本地函数将在缓存次数耗尽后,自动拉取远程服务器的代码进行热更新。 10 | > 2. 核心算法保护,以及动态校验算法,即利用```remote hook``` 机制去实现核心校验函数,本地并不实现核心函数,并且将远程服务器上的实现代码进行动态变化,就可以做到动态算法校验, 从而实现提高逆向难度的效果。 11 | > 3. 接口稳定性,依赖倒置,使得编码依赖于抽象而不依赖于底层实现,与实现解耦合,利用```local hook```机制对于接口类进行hook, 即可以在不使用继承,组合的情况下对实现类方法,并且接口类的抽象具有稳定性,不会因为底层代码的变化影响到,高层的设计。 12 | > 4. 分布式计算,通过```remote hook```机制中心服务器下发运算任务给计算节点,计算节点不停的从中心服务器去抓取算法字节码。 13 | >5. 量化交易,当多个交易机需要同步热更新控制机上的交易策略时可以通过```remote hook```机制实时运行新的策略。 14 | ### 安装 15 | ```pip install TB4hooky``` 16 | ### 动态导入原理 17 | >```mermaid 18 | > graph TB 19 | > BulitinImport(Builtin importer) --> |find| bReturn(return pkg) 20 | > BulitinImport(Builtin importer) -.not find.-> frozenImport(Frozen importer) 21 | > frozenImport(Frozen importer) --> |find| fReturn(return pkg) 22 | > frozenImport(Frozen importer) -.not find.->CustomImporter(Custom Importer) 23 | > CustomImporter(Custom Importer) ==req==> RemoteImportServer[Remote import server] 24 | > RemoteImportServer[Remote import server] ==resp==> CustomImporter(Custom Importer) 25 | > CustomImporter(Custom Importer) --> |find| creturn(return pkg) 26 | > CustomImporter(Custom Importer) -.not find.-> sysexcept(Sys.ExceptionGlobalHook) 27 | > sysexcept(Sys.ExceptionGlobalHook) --> handle(Exception handle funcion) 28 | > handle(Exception handle funcion) --> filehandle(FileHandle object) 29 | > filehandle(FileHandle object) ==req==> RemoteImportServer[Remote import server] 30 | > RemoteImportServer[Remote import server] ==resp==> filehandle(FileHandle object) 31 | > filehandle(FileHandle object) ==resp==>handle(Exception handle funcion) 32 | > handle(Exception handle funcion) --> |find| freturn(return pkg) 33 | > handle(Exception handle funcion) -.not find.-> raiseImprotError(raise ImportError) 34 | > ``` 35 | 36 | ### 快速入门 37 | > ```local hook``` 38 | > 将本地实现函数的字节码替换到hook的函数中使其实现本地函数的功能 39 | ### 普通函数的hook 40 | > ```python 41 | > from hooky.Hooker import CodeHooker 42 | > 43 | > def local_hook(arg1, arg2): 44 | > print("hello world\n") 45 | > print("arg1:", arg1) 46 | > print("arg2:", arg2) 47 | > 48 | > @CodeHooker(_f=local_hook) 49 | > def target_function(): ... 50 | > 51 | > if __name__ == "__main__": 52 | > target_function("Create by", "Tuzi") 53 | > ``` 54 | > **运行结果** 55 | > ``` 56 | > $> hello world 57 | > Create by Tuzi 58 | > ``` 59 | 60 | ### 类方法的HOOK 61 | >```python 62 | > from hooky.Hooker import CodeHooker 63 | > 64 | ># 接口实现类 65 | >class Trait(object): 66 | > @staticmethod 67 | > def local_sign(*args, **kwargs): 68 | > print("123456") 69 | > 70 | > @staticmethod 71 | > def local_init(cls, *args, **kwargs): 72 | > print('class method verify') 73 | > return cls() 74 | > 75 | ># 接口类 76 | >class Spider(object): 77 | > @classmethod 78 | > @CodeHooker(_f=Trait.local_init) 79 | > def local_init(cls): ... 80 | > 81 | > 82 | > @CodeHooker(_f=Trait.local_sign) 83 | > def local_sign(self): ... 84 | > 85 | > def use(self): 86 | > print("use load") 87 | > self.local_sign() 88 | > 89 | > @staticmethod 90 | > @CodeHooker(_f=Trait.local_sign) 91 | > def llocal_sign(*args, **kwargs): ... 92 | ># classmethod hook 93 | >spider = Spider.local_init() 94 | ># method hook 95 | >spider.local_sign() 96 | ># normal call instance method 97 | >spider.use() 98 | ># staticmethod hook 99 | >Spider.llocal_sign() 100 | > 101 | > 102 | > ``` 103 | > **运行结果** 104 | > ``` 105 | > $> class method verify 106 | > 123456 107 | > 123456 108 | > use load 109 | > 123456 110 | >``` 111 | ### 远程hook 112 | > ```remote hook``` 将远程服务器上的方法字节码拉到本地后进行hook。 113 | > 114 | >这里我们先定义一个```远程服务端```,这里我们使用Flask 搭建 115 | > ```python 116 | >from flask import Flask 117 | >from hooky.Hooker import CodeHooker 118 | >from netLoader.DaynamicImport import register 119 | > 120 | >app = Flask(__name__) 121 | > 122 | ># 这里我们定义一个远程包导入服务器并且安装模块 123 | >register("http://127.0.0.1:9000") 124 | > 125 | > 126 | ># 实现一个远程函数 127 | > 128 | >def sign(flags): 129 | > print(f"It's a sign function, flag is {flags}") 130 | > 131 | ># 实例化序列化类 132 | >serializer = CodeHooker.serialize_init(sign) 133 | > 134 | ># 设置远程缓存数为3并且获取反序列化结果 135 | >serialize_result = serializer.serialize_func(3) 136 | > 137 | > 138 | ># 定义一个路由用于接收请求 139 | >@app.route("/sign") 140 | >def get_sign(): 141 | > return serialize_result 142 | > 143 | ># 启动服务 144 | >app.run(debug=True, host="0.0.0.0", port=8000) 145 | > 146 | >``` 147 | > 现在我们开始进行```remote hook``` 148 | > 149 | >```python 150 | ># 导入CodeHooker 151 | >from hooky.Hooker import CodeHooker 152 | > 153 | ># remote 地址为我们启动的服务器地址并且为sign接口 154 | >@CodeHooker(_f="http://127.0.0.1:8000/sign", remote=True) 155 | >def local_sign(): ... 156 | > 157 | > 158 | >local_sign('hello world') 159 | > 160 | >``` 161 | > **运行结果** 162 | >``` 163 | >$> It's a sign function, flag is hello world 164 | >``` 165 | > 远程hook同样可以进行```classmethod``` 和 ```staticmethod``` 的hook同```local hook``` 一样,这里就不再演示了, 不过值得注意的是,```remote hook```的时候远程服务端不可以直接在函数内部进行实例化,我们拥有如下的解决方案, 166 | 需要利用到远程导入库,此时需要再准备一台服务端作为包导入端并且启动web服务,并且在客户端与服务端安装远程钩子, 可以将写好的类代码放到包导入端中,然后在服务端的函数中进行导入后使用。 167 | > 168 | >```python 169 | ># 方式一 170 | >from netLoader.DaynamicImport import( 171 | > register, 172 | > ) 173 | > 174 | ># 注册远程包导入服务器地址 175 | >register("http://127.0.0.1:9000") 176 | > 177 | > 178 | >""" 179 | > 客户端代码或者服务端代码 180 | > example: 181 | > # 服务端 182 | > def sign(): 183 | > import fib # 这是自己实现的类 184 | > f = fin() # 实例化 185 | > f.count() # 调用实例化方法 186 | ># 实例化序列化类 187 | >serializer = CodeHooker.serialize_init(sign) 188 | > 189 | ># 设置远程缓存数为3并且获取反序列化结果 190 | >serialize_result = serializer.serialize_func(3) 191 | > 192 | ># 后面的步骤就和上面一样了,定义接口返回序列化以后的值即可 193 | >""" 194 | > 195 | ># 卸载钩子 196 | >unload("http://127.0.0.1:9000") 197 | > 198 | ># 方式二 199 | >str = """ 200 | >class Sign(): 201 | > def sign(self): 202 | > print("it's sign method in class Sign.") 203 | > 204 | >""" 205 | ># 远程端定义如下函数 206 | >def sign(): 207 | > cp_obj = complie(str, "", "exec") 208 | > exec(cp_obj) 209 | > 210 | ># 不过这种方式作者认为不够优雅还是建议使用第一种远程导入的模块. 211 | >``` 212 | >### 远程端示例(v0.1.9a 支持) 213 | > ```python 214 | > from flask import Flask 215 | > from TB4hooky.hooky.Serialize import ServerSerialize, REMOTE_FUNCTION_MAPPING 216 | > app = Flask(__name__) 217 | > 218 | > # 新增语法糖 v0.1.9a ServerSerialize, 219 | > # 将会把函数对象注册到 REMOTE_FUNCTION_MAPPING 中 220 | > @ServerSerialize(count=3) 221 | > def add(a, b): 222 | > return a + b 223 | > 224 | > @app.route("/add") 225 | > def get_add(): 226 | > # 可以直接在接口中直接返回以该函数名为键的值 227 | > return REMOTE_FUNCTION_MAPPING['add'] 228 | > 229 | > app.run("127.0.0.1", port=8000, debug=True) 230 | >``` 231 | > 232 | ### 本地实例方法HOOK 233 | > 上面我们用```local hook```hook 类方法,实例方法,静态方法,但是实现函数上我们用,静态函数,函数等进行的实现,现在我们将引入一个新的对象,```InstanceCodeHooker```来帮助我们进行实例方法实现hook对象的修改。 234 | > 235 | >```python 236 | ># 导入InstanceCodeHooker 237 | >from hooky.Hooker import InstanceCodeHooker 238 | > 239 | >class LocalMethod(object): 240 | > 241 | > def __init__(self): 242 | > self.flag = 'hello world' 243 | > 244 | > def sign(self, flag): 245 | > print(self.flag) 246 | > self.flag = flag 247 | > 248 | > def chg(self, flag): 249 | > print(self.flag) 250 | > self.flag = flag 251 | > 252 | > 253 | >ins = LocalMethod() 254 | > 255 | >@InstanceCodeHooker(_f='sign', instance=ins) 256 | >def sign():... 257 | > 258 | >@InstanceCodeHooker(_f='chg', instance=ins) 259 | >def another_sign():... 260 | > 261 | ># 调用方法 262 | >sign("hello") 263 | >another_sign("world") 264 | >print(ins.flag) 265 | >``` 266 | >**运行结果** 267 | >``` 268 | >$> hello world 269 | > hello 270 | > world 271 | >``` 272 | ### 远程导入包 273 | > 由于python包导入机制过于复杂,导致在导入一些比较大的库的时候,```hook_meta```没法很好的兼容其导入方式,所以添加了辅助函数 ```remote_import ```,来尽量调和该问题可以尝试通过```try except```来捕捉相关异常然后通过 274 | > ```remote_import```来显示的导入该包,该函数接收一个字符串,在这之前需要进行注册```register```,如果在这之前没有注册则什么都不会执行. 275 | > 276 | > ```python 277 | > # 导入netLoader 278 | > from TB4hooky.netLoader.DaynamicImport import register, remote_import, unload 279 | > # 注册包中心 280 | > register("http://127.0.0.1:9000") 281 | > 282 | > # 尝试导入 283 | > try: 284 | > import torch 285 | > except Exception as _: 286 | > # 显示导入 287 | > remote_import('torch') 288 | > # 卸载钩子 289 | > unload("http://127.0.0.1:9000") 290 | >``` 291 | 292 | >#### 更新日志 293 | > * 28.03.2024 - v0.1.5 - alpha 294 | > 295 | > * \- remote/local hook 296 | > * 29.03.2024 - v0.1.6a 297 | > * \- fix bug remote import 298 | > * 29.03.2024 - v0.1.7a 299 | > * \- fix bug remote import 300 | > * 01.04.2024 - v0.1.8a 301 | > * \- add remote import API ```remote_import``` 302 | > * 01.04.2024 - v0.1.8b 303 | > * 02.04.2024 - v0.1.9a 304 | > * add TB4hooky.hooky.Serialize.ServerSerialize 语法糖方便服务端的序列化 305 | > * 03.04.2024 - v0.1.10a0 306 | > * fix bug Serialize 307 | > * 04.04.2024 - v0.1.11 308 | > * fix bug ServerSerialize 309 | >
310 | ### Create by Tuzi --------------------------------------------------------------------------------