from __future__ import annotations import asyncio import dataclasses import fnmatch import functools import json import sys import threading import traceback import warnings from types import TracebackType from typing import ( Any, AnyStr, Awaitable, Callable, Dict, List, Mapping, MutableMapping, Optional, Sequence, Tuple, Type, TypeVar, Union, overload, ) if sys.version_info >= (3, 8): from typing import Literal, TypedDict else: from typing_extensions import Literal, TypedDict if sys.version_info >= (3, 11): from typing import NotRequired else: from typing_extensions import NotRequired import _frida _device_manager = None _Cancellable = _frida.Cancellable ProcessTarget = Union[int, str] Spawn = _frida.Spawn @dataclasses.dataclass class RPCResult: finished: bool = False value: Any = None error: Optional[Exception] = None def get_device_manager() -> "DeviceManager": """ Get or create a singleton DeviceManager that let you manage all the devices """ global _device_manager if _device_manager is None: _device_manager = DeviceManager(_frida.DeviceManager()) return _device_manager def _filter_missing_kwargs(d: MutableMapping[Any, Any]) -> None: for key in list(d.keys()): if d[key] is None: d.pop(key) R = TypeVar("R") def cancellable(f: Callable[..., R]) -> Callable[..., R]: @functools.wraps(f) def wrapper(*args: Any, **kwargs: Any) -> R: cancellable = kwargs.pop("cancellable", None) if cancellable is not None: with cancellable: return f(*args, **kwargs) return f(*args, **kwargs) return wrapper class IOStream: """ Frida's own implementation of an input/output stream """ def __init__(self, impl: _frida.IOStream) -> None: self._impl = impl def __repr__(self) -> str: return repr(self._impl) @property def is_closed(self) -> bool: """ Query whether the stream is closed """ return self._impl.is_closed() @cancellable def close(self) -> None: """ Close the stream. """ self._impl.close() @cancellable def read(self, count: int) -> bytes: """ Read up to the specified number of bytes from the stream """ return self._impl.read(count) @cancellable def read_all(self, count: int) -> bytes: """ Read exactly the specified number of bytes from the stream """ return self._impl.read_all(count) @cancellable def write(self, data: bytes) -> int: """ Write as much as possible of the provided data to the stream """ return self._impl.write(data) @cancellable def write_all(self, data: bytes) -> None: """ Write all of the provided data to the stream """ self._impl.write_all(data) class PortalMembership: def __init__(self, impl: _frida.PortalMembership) -> None: self._impl = impl @cancellable def terminate(self) -> None: """ Terminate the membership """ self._impl.terminate() class ScriptExportsSync: """ Proxy object that expose all the RPC exports of a script as attributes on this class A method named exampleMethod in a script will be called with instance.example_method on this object """ def __init__(self, script: "Script") -> None: self._script = script def __getattr__(self, name: str) -> Callable[..., Any]: script = self._script js_name = _to_camel_case(name) def method(*args: Any, **kwargs: Any) -> Any: return script._rpc_request("call", js_name, args, **kwargs) return method def __dir__(self) -> List[str]: return self._script.list_exports_sync() ScriptExports = ScriptExportsSync class ScriptExportsAsync: """ Proxy object that expose all the RPC exports of a script as attributes on this class A method named exampleMethod in a script will be called with instance.example_method on this object """ def __init__(self, script: "Script") -> None: self._script = script def __getattr__(self, name: str) -> Callable[..., Awaitable[Any]]: script = self._script js_name = _to_camel_case(name) async def method(*args: Any, **kwargs: Any) -> Any: return await script._rpc_request_async("call", js_name, args, **kwargs) return method def __dir__(self) -> List[str]: return self._script.list_exports_sync() class ScriptErrorMessage(TypedDict): type: Literal["error"] description: str stack: NotRequired[str] fileName: NotRequired[str] lineNumber: NotRequired[int] columnNumber: NotRequired[int] class ScriptPayloadMessage(TypedDict): type: Literal["send"] payload: NotRequired[Any] ScriptMessage = Union[ScriptPayloadMessage, ScriptErrorMessage] ScriptMessageCallback = Callable[[ScriptMessage, Optional[bytes]], None] ScriptDestroyedCallback = Callable[[], None] class RPCException(Exception): """ Wraps remote errors from the script RPC """ def __str__(self) -> str: return str(self.args[2]) if len(self.args) >= 3 else str(self.args[0]) class Script: def __init__(self, impl: _frida.Script) -> None: self.exports_sync = ScriptExportsSync(self) self.exports_async = ScriptExportsAsync(self) self._impl = impl self._on_message_callbacks: List[ScriptMessageCallback] = [] self._log_handler: Callable[[str, str], None] = self.default_log_handler self._pending: Dict[ int, Callable[[Optional[Any], Optional[Union[RPCException, _frida.InvalidOperationError]]], None] ] = {} self._next_request_id = 1 self._cond = threading.Condition() impl.on("destroyed", self._on_destroyed) impl.on("message", self._on_message) @property def exports(self) -> ScriptExportsSync: """ The old way of retrieving the synchronous exports caller """ warnings.warn( "Script.exports will become asynchronous in the future, use the explicit Script.exports_sync instead", DeprecationWarning, stacklevel=2, ) return self.exports_sync def __repr__(self) -> str: return repr(self._impl) @property def is_destroyed(self) -> bool: """ Query whether the script has been destroyed """ return self._impl.is_destroyed() @cancellable def load(self) -> None: """ Load the script. """ self._impl.load() @cancellable def unload(self) -> None: """ Unload the script """ self._impl.unload() @cancellable def eternalize(self) -> None: """ Eternalize the script """ self._impl.eternalize() def post(self, message: Any, data: Optional[AnyStr] = None) -> None: """ Post a JSON-encoded message to the script """ raw_message = json.dumps(message) kwargs = {"data": data} _filter_missing_kwargs(kwargs) self._impl.post(raw_message, **kwargs) @cancellable def enable_debugger(self, port: Optional[int] = None) -> None: """ Enable the Node.js compatible script debugger """ kwargs = {"port": port} _filter_missing_kwargs(kwargs) self._impl.enable_debugger(**kwargs) @cancellable def disable_debugger(self) -> None: """ Disable the Node.js compatible script debugger """ self._impl.disable_debugger() @overload def on(self, signal: Literal["destroyed"], callback: ScriptDestroyedCallback) -> None: ... @overload def on(self, signal: Literal["message"], callback: ScriptMessageCallback) -> None: ... @overload def on(self, signal: str, callback: Callable[..., Any]) -> None: ... def on(self, signal: str, callback: Callable[..., Any]) -> None: """ Add a signal handler """ if signal == "message": self._on_message_callbacks.append(callback) else: self._impl.on(signal, callback) @overload def off(self, signal: Literal["destroyed"], callback: ScriptDestroyedCallback) -> None: ... @overload def off(self, signal: Literal["message"], callback: ScriptMessageCallback) -> None: ... @overload def off(self, signal: str, callback: Callable[..., Any]) -> None: ... def off(self, signal: str, callback: Callable[..., Any]) -> None: """ Remove a signal handler """ if signal == "message": self._on_message_callbacks.remove(callback) else: self._impl.off(signal, callback) def get_log_handler(self) -> Callable[[str, str], None]: """ Get the method that handles the script logs """ return self._log_handler def set_log_handler(self, handler: Callable[[str, str], None]) -> None: """ Set the method that handles the script logs :param handler: a callable that accepts two parameters: 1. the log level name 2. the log message """ self._log_handler = handler def default_log_handler(self, level: str, text: str) -> None: """ The default implementation of the log handler, prints the message to stdout or stderr, depending on the level """ if level == "info": print(text, file=sys.stdout) else: print(text, file=sys.stderr) async def list_exports_async(self) -> List[str]: """ Asynchronously list all the exported attributes from the script's rpc """ result = await self._rpc_request_async("list") assert isinstance(result, list) return result def list_exports_sync(self) -> List[str]: """ List all the exported attributes from the script's rpc """ result = self._rpc_request("list") assert isinstance(result, list) return result def list_exports(self) -> List[str]: """ List all the exported attributes from the script's rpc """ warnings.warn( "Script.list_exports will become asynchronous in the future, use the explicit Script.list_exports_sync instead", DeprecationWarning, stacklevel=2, ) return self.list_exports_sync() def _rpc_request_async(self, *args: Any) -> asyncio.Future[Any]: loop = asyncio.get_event_loop() future: asyncio.Future[Any] = asyncio.Future() def on_complete(value: Any, error: Optional[Union[RPCException, _frida.InvalidOperationError]]) -> None: if error is not None: loop.call_soon_threadsafe(future.set_exception, error) else: loop.call_soon_threadsafe(future.set_result, value) request_id = self._append_pending(on_complete) if not self.is_destroyed: self._send_rpc_call(request_id, *args) else: self._on_destroyed() return future @cancellable def _rpc_request(self, *args: Any) -> Any: result = RPCResult() def on_complete(value: Any, error: Optional[Union[RPCException, _frida.InvalidOperationError]]) -> None: with self._cond: result.finished = True result.value = value result.error = error self._cond.notify_all() def on_cancelled() -> None: self._pending.pop(request_id, None) on_complete(None, None) request_id = self._append_pending(on_complete) if not self.is_destroyed: self._send_rpc_call(request_id, *args) cancellable = Cancellable.get_current() cancel_handler = cancellable.connect(on_cancelled) try: with self._cond: while not result.finished: self._cond.wait() finally: cancellable.disconnect(cancel_handler) cancellable.raise_if_cancelled() else: self._on_destroyed() if result.error is not None: raise result.error return result.value def _append_pending( self, callback: Callable[[Any, Optional[Union[RPCException, _frida.InvalidOperationError]]], None] ) -> int: with self._cond: request_id = self._next_request_id self._next_request_id += 1 self._pending[request_id] = callback return request_id def _send_rpc_call(self, request_id: int, *args: Any) -> None: message = ["frida:rpc", request_id] message.extend(args) self.post(message) def _on_rpc_message(self, request_id: int, operation: str, params: List[Any], data: Optional[Any]) -> None: if operation in ("ok", "error"): callback = self._pending.pop(request_id, None) if callback is None: return value = None error = None if operation == "ok": value = params[0] if data is None else data else: error = RPCException(*params[0:3]) callback(value, error) def _on_destroyed(self) -> None: while True: next_pending = None with self._cond: pending_ids = list(self._pending.keys()) if len(pending_ids) > 0: next_pending = self._pending.pop(pending_ids[0]) if next_pending is None: break next_pending(None, _frida.InvalidOperationError("script has been destroyed")) def _on_message(self, raw_message: str, data: Optional[bytes]) -> None: message = json.loads(raw_message) mtype = message["type"] payload = message.get("payload", None) if mtype == "log": level = message["level"] text = payload self._log_handler(level, text) elif mtype == "send" and isinstance(payload, list) and len(payload) > 0 and payload[0] == "frida:rpc": request_id = payload[1] operation = payload[2] params = payload[3:] self._on_rpc_message(request_id, operation, params, data) else: for callback in self._on_message_callbacks[:]: try: callback(message, data) except: traceback.print_exc() SessionDetachedCallback = Callable[ [ Literal[ "application-requested", "process-replaced", "process-terminated", "connection-terminated", "device-lost" ], Optional[_frida.Crash], ], None, ] class Session: def __init__(self, impl: _frida.Session) -> None: self._impl = impl def __repr__(self) -> str: return repr(self._impl) @property def is_detached(self) -> bool: """ Query whether the session is detached """ return self._impl.is_detached() @cancellable def detach(self) -> None: """ Detach session from the process """ self._impl.detach() @cancellable def resume(self) -> None: """ Resume session after network error """ self._impl.resume() @cancellable def enable_child_gating(self) -> None: """ Enable child gating """ self._impl.enable_child_gating() @cancellable def disable_child_gating(self) -> None: """ Disable child gating """ self._impl.disable_child_gating() @cancellable def create_script( self, source: str, name: Optional[str] = None, snapshot: Optional[bytes] = None, runtime: Optional[str] = None ) -> Script: """ Create a new script """ kwargs = {"name": name, "snapshot": snapshot, "runtime": runtime} _filter_missing_kwargs(kwargs) return Script(self._impl.create_script(source, **kwargs)) # type: ignore @cancellable def create_script_from_bytes( self, data: bytes, name: Optional[str] = None, snapshot: Optional[bytes] = None, runtime: Optional[str] = None ) -> Script: """ Create a new script from bytecode """ kwargs = {"name": name, "snapshot": snapshot, "runtime": runtime} _filter_missing_kwargs(kwargs) return Script(self._impl.create_script_from_bytes(data, **kwargs)) # type: ignore @cancellable def compile_script(self, source: str, name: Optional[str] = None, runtime: Optional[str] = None) -> bytes: """ Compile script source code to bytecode """ kwargs = {"name": name, "runtime": runtime} _filter_missing_kwargs(kwargs) return self._impl.compile_script(source, **kwargs) @cancellable def snapshot_script(self, embed_script: str, warmup_script: Optional[str], runtime: Optional[str] = None) -> bytes: """ Evaluate script and snapshot the resulting VM state """ kwargs = {"warmup_script": warmup_script, "runtime": runtime} _filter_missing_kwargs(kwargs) return self._impl.snapshot_script(embed_script, **kwargs) @cancellable def setup_peer_connection( self, stun_server: Optional[str] = None, relays: Optional[Sequence[_frida.Relay]] = None ) -> None: """ Set up a peer connection with the target process """ kwargs = {"stun_server": stun_server, "relays": relays} _filter_missing_kwargs(kwargs) self._impl.setup_peer_connection(**kwargs) # type: ignore @cancellable def join_portal( self, address: str, certificate: Optional[str] = None, token: Optional[str] = None, acl: Union[None, List[str], Tuple[str]] = None, ) -> PortalMembership: """ Join a portal """ kwargs: Dict[str, Any] = {"certificate": certificate, "token": token, "acl": acl} _filter_missing_kwargs(kwargs) return PortalMembership(self._impl.join_portal(address, **kwargs)) @overload def on( self, signal: Literal["detached"], callback: SessionDetachedCallback, ) -> None: ... @overload def on(self, signal: str, callback: Callable[..., Any]) -> None: ... def on(self, signal: str, callback: Callable[..., Any]) -> None: """ Add a signal handler """ self._impl.on(signal, callback) @overload def off( self, signal: Literal["detached"], callback: SessionDetachedCallback, ) -> None: ... @overload def off(self, signal: str, callback: Callable[..., Any]) -> None: ... def off(self, signal: str, callback: Callable[..., Any]) -> None: """ Remove a signal handler """ self._impl.off(signal, callback) BusDetachedCallback = Callable[[], None] BusMessageCallback = Callable[[Mapping[Any, Any], Optional[bytes]], None] class Bus: def __init__(self, impl: _frida.Bus) -> None: self._impl = impl self._on_message_callbacks: List[Callable[..., Any]] = [] impl.on("message", self._on_message) @cancellable def attach(self) -> None: """ Attach to the bus """ self._impl.attach() def post(self, message: Any, data: Optional[Union[str, bytes]] = None) -> None: """ Post a JSON-encoded message to the bus """ raw_message = json.dumps(message) kwargs = {"data": data} _filter_missing_kwargs(kwargs) self._impl.post(raw_message, **kwargs) @overload def on(self, signal: Literal["detached"], callback: BusDetachedCallback) -> None: ... @overload def on(self, signal: Literal["message"], callback: BusMessageCallback) -> None: ... @overload def on(self, signal: str, callback: Callable[..., Any]) -> None: ... def on(self, signal: str, callback: Callable[..., Any]) -> None: """ Add a signal handler """ if signal == "message": self._on_message_callbacks.append(callback) else: self._impl.on(signal, callback) @overload def off(self, signal: Literal["detached"], callback: BusDetachedCallback) -> None: ... @overload def off(self, signal: Literal["message"], callback: BusMessageCallback) -> None: ... @overload def off(self, signal: str, callback: Callable[..., Any]) -> None: ... def off(self, signal: str, callback: Callable[..., Any]) -> None: """ Remove a signal handler """ if signal == "message": self._on_message_callbacks.remove(callback) else: self._impl.off(signal, callback) def _on_message(self, raw_message: str, data: Any) -> None: message = json.loads(raw_message) for callback in self._on_message_callbacks[:]: try: callback(message, data) except: traceback.print_exc() DeviceSpawnAddedCallback = Callable[[_frida.Spawn], None] DeviceSpawnRemovedCallback = Callable[[_frida.Spawn], None] DeviceChildAddedCallback = Callable[[_frida.Child], None] DeviceChildRemovedCallback = Callable[[_frida.Child], None] DeviceProcessCrashedCallback = Callable[[_frida.Crash], None] DeviceOutputCallback = Callable[[int, int, bytes], None] DeviceUninjectedCallback = Callable[[int], None] DeviceLostCallback = Callable[[], None] class Device: """ Represents a device that Frida connects to """ def __init__(self, device: _frida.Device) -> None: assert device.bus is not None self.id = device.id self.name = device.name self.icon = device.icon self.type = device.type self.bus = Bus(device.bus) self._impl = device def __repr__(self) -> str: return repr(self._impl) @property def is_lost(self) -> bool: """ Query whether the device has been lost """ return self._impl.is_lost() @cancellable def query_system_parameters(self) -> Dict[str, Any]: """ Returns a dictionary of information about the host system """ return self._impl.query_system_parameters() @cancellable def get_frontmost_application(self, scope: Optional[str] = None) -> Optional[_frida.Application]: """ Get details about the frontmost application """ kwargs = {"scope": scope} _filter_missing_kwargs(kwargs) return self._impl.get_frontmost_application(**kwargs) @cancellable def enumerate_applications( self, identifiers: Optional[Sequence[str]] = None, scope: Optional[str] = None ) -> List[_frida.Application]: """ Enumerate applications """ kwargs = {"identifiers": identifiers, "scope": scope} _filter_missing_kwargs(kwargs) return self._impl.enumerate_applications(**kwargs) # type: ignore @cancellable def enumerate_processes( self, pids: Optional[Sequence[int]] = None, scope: Optional[str] = None ) -> List[_frida.Process]: """ Enumerate processes """ kwargs = {"pids": pids, "scope": scope} _filter_missing_kwargs(kwargs) return self._impl.enumerate_processes(**kwargs) # type: ignore @cancellable def get_process(self, process_name: str) -> _frida.Process: """ Get the process with the given name :raises ProcessNotFoundError: if the process was not found or there were more than one process with the given name """ process_name_lc = process_name.lower() matching = [ process for process in self._impl.enumerate_processes() if fnmatch.fnmatchcase(process.name.lower(), process_name_lc) ] if len(matching) == 1: return matching[0] elif len(matching) > 1: matches_list = ", ".join([f"{process.name} (pid: {process.pid})" for process in matching]) raise _frida.ProcessNotFoundError(f"ambiguous name; it matches: {matches_list}") else: raise _frida.ProcessNotFoundError(f"unable to find process with name '{process_name}'") @cancellable def enable_spawn_gating(self) -> None: """ Enable spawn gating """ self._impl.enable_spawn_gating() @cancellable def disable_spawn_gating(self) -> None: """ Disable spawn gating """ self._impl.disable_spawn_gating() @cancellable def enumerate_pending_spawn(self) -> List[_frida.Spawn]: """ Enumerate pending spawn """ return self._impl.enumerate_pending_spawn() @cancellable def enumerate_pending_children(self) -> List[_frida.Child]: """ Enumerate pending children """ return self._impl.enumerate_pending_children() @cancellable def spawn( self, program: Union[str, List[Union[str, bytes]], Tuple[Union[str, bytes]]], argv: Union[None, List[Union[str, bytes]], Tuple[Union[str, bytes]]] = None, envp: Optional[Dict[str, str]] = None, env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None, stdio: Optional[str] = None, **kwargs: Any, ) -> int: """ Spawn a process into an attachable state """ if not isinstance(program, str): argv = program if isinstance(argv[0], bytes): program = argv[0].decode() else: program = argv[0] if len(argv) == 1: argv = None kwargs = {"argv": argv, "envp": envp, "env": env, "cwd": cwd, "stdio": stdio, "aux": kwargs} _filter_missing_kwargs(kwargs) return self._impl.spawn(program, **kwargs) @cancellable def input(self, target: ProcessTarget, data: bytes) -> None: """ Input data on stdin of a spawned process :param target: the PID or name of the process """ self._impl.input(self._pid_of(target), data) @cancellable def resume(self, target: ProcessTarget) -> None: """ Resume a process from the attachable state :param target: the PID or name of the process """ self._impl.resume(self._pid_of(target)) @cancellable def kill(self, target: ProcessTarget) -> None: """ Kill a process :param target: the PID or name of the process """ self._impl.kill(self._pid_of(target)) @cancellable def attach( self, target: ProcessTarget, realm: Optional[str] = None, persist_timeout: Optional[int] = None, ) -> Session: """ Attach to a process :param target: the PID or name of the process """ kwargs = {"realm": realm, "persist_timeout": persist_timeout} _filter_missing_kwargs(kwargs) return Session(self._impl.attach(self._pid_of(target), **kwargs)) # type: ignore @cancellable def inject_library_file(self, target: ProcessTarget, path: str, entrypoint: str, data: str) -> int: """ Inject a library file to a process :param target: the PID or name of the process """ return self._impl.inject_library_file(self._pid_of(target), path, entrypoint, data) @cancellable def inject_library_blob(self, target: ProcessTarget, blob: bytes, entrypoint: str, data: str) -> int: """ Inject a library blob to a process :param target: the PID or name of the process """ return self._impl.inject_library_blob(self._pid_of(target), blob, entrypoint, data) @cancellable def open_channel(self, address: str) -> IOStream: """ Open a device-specific communication channel """ return IOStream(self._impl.open_channel(address)) @cancellable def get_bus(self) -> Bus: """ Get the message bus of the device """ return self.bus @overload def on(self, signal: Literal["spawn-added"], callback: DeviceSpawnAddedCallback) -> None: ... @overload def on(self, signal: Literal["spawn-removed"], callback: DeviceSpawnRemovedCallback) -> None: ... @overload def on(self, signal: Literal["child-added"], callback: DeviceChildAddedCallback) -> None: ... @overload def on(self, signal: Literal["child-removed"], callback: DeviceChildRemovedCallback) -> None: ... @overload def on(self, signal: Literal["process-crashed"], callback: DeviceProcessCrashedCallback) -> None: ... @overload def on(self, signal: Literal["output"], callback: DeviceOutputCallback) -> None: ... @overload def on(self, signal: Literal["uninjected"], callback: DeviceUninjectedCallback) -> None: ... @overload def on(self, signal: Literal["lost"], callback: DeviceLostCallback) -> None: ... @overload def on(self, signal: str, callback: Callable[..., Any]) -> None: ... def on(self, signal: str, callback: Callable[..., Any]) -> None: """ Add a signal handler """ self._impl.on(signal, callback) @overload def off(self, signal: Literal["spawn-added"], callback: DeviceSpawnAddedCallback) -> None: ... @overload def off(self, signal: Literal["spawn-removed"], callback: DeviceSpawnRemovedCallback) -> None: ... @overload def off(self, signal: Literal["child-added"], callback: DeviceChildAddedCallback) -> None: ... @overload def off(self, signal: Literal["child-removed"], callback: DeviceChildRemovedCallback) -> None: ... @overload def off(self, signal: Literal["process-crashed"], callback: DeviceProcessCrashedCallback) -> None: ... @overload def off(self, signal: Literal["output"], callback: DeviceOutputCallback) -> None: ... @overload def off(self, signal: Literal["uninjected"], callback: DeviceUninjectedCallback) -> None: ... @overload def off(self, signal: Literal["lost"], callback: DeviceLostCallback) -> None: ... @overload def off(self, signal: str, callback: Callable[..., Any]) -> None: ... def off(self, signal: str, callback: Callable[..., Any]) -> None: """ Remove a signal handler """ self._impl.off(signal, callback) def _pid_of(self, target: ProcessTarget) -> int: if isinstance(target, str): return self.get_process(target).pid else: return target DeviceManagerAddedCallback = Callable[[_frida.Device], None] DeviceManagerRemovedCallback = Callable[[_frida.Device], None] DeviceManagerChangedCallback = Callable[[], None] class DeviceManager: def __init__(self, impl: _frida.DeviceManager) -> None: self._impl = impl def __repr__(self) -> str: return repr(self._impl) def get_local_device(self) -> Device: """ Get the local device """ return self.get_device_matching(lambda d: d.type == "local", timeout=0) def get_remote_device(self) -> Device: """ Get the first remote device in the devices list """ return self.get_device_matching(lambda d: d.type == "remote", timeout=0) def get_usb_device(self, timeout: int = 0) -> Device: """ Get the first device connected over USB in the devices list """ return self.get_device_matching(lambda d: d.type == "usb", timeout) def get_device(self, id: Optional[str], timeout: int = 0) -> Device: """ Get a device by its id """ return self.get_device_matching(lambda d: d.id == id, timeout) @cancellable def get_device_matching(self, predicate: Callable[[Device], bool], timeout: int = 0) -> Device: """ Get device matching predicate :param predicate: a function to filter the devices :param timeout: operation timeout in seconds """ if timeout < 0: raw_timeout = -1 elif timeout == 0: raw_timeout = 0 else: raw_timeout = int(timeout * 1000.0) return Device(self._impl.get_device_matching(lambda d: predicate(Device(d)), raw_timeout)) @cancellable def enumerate_devices(self) -> List[Device]: """ Enumerate devices """ return [Device(device) for device in self._impl.enumerate_devices()] @cancellable def add_remote_device( self, address: str, certificate: Optional[str] = None, origin: Optional[str] = None, token: Optional[str] = None, keepalive_interval: Optional[int] = None, ) -> Device: """ Add a remote device """ kwargs: Dict[str, Any] = { "certificate": certificate, "origin": origin, "token": token, "keepalive_interval": keepalive_interval, } _filter_missing_kwargs(kwargs) return Device(self._impl.add_remote_device(address, **kwargs)) @cancellable def remove_remote_device(self, address: str) -> None: """ Remove a remote device """ self._impl.remove_remote_device(address=address) @overload def on(self, signal: Literal["added"], callback: DeviceManagerAddedCallback) -> None: ... @overload def on(self, signal: Literal["removed"], callback: DeviceManagerRemovedCallback) -> None: ... @overload def on(self, signal: Literal["changed"], callback: DeviceManagerChangedCallback) -> None: ... @overload def on(self, signal: str, callback: Callable[..., Any]) -> None: ... def on(self, signal: str, callback: Callable[..., Any]) -> None: """ Add a signal handler """ self._impl.on(signal, callback) @overload def off(self, signal: Literal["added"], callback: DeviceManagerAddedCallback) -> None: ... @overload def off(self, signal: Literal["removed"], callback: DeviceManagerRemovedCallback) -> None: ... @overload def off(self, signal: Literal["changed"], callback: DeviceManagerChangedCallback) -> None: ... @overload def off(self, signal: str, callback: Callable[..., Any]) -> None: ... def off(self, signal: str, callback: Callable[..., Any]) -> None: """ Remove a signal handler """ self._impl.off(signal, callback) class EndpointParameters: def __init__( self, address: Optional[str] = None, port: Optional[int] = None, certificate: Optional[str] = None, origin: Optional[str] = None, authentication: Optional[Tuple[str, Union[str, Callable[[str], Any]]]] = None, asset_root: Optional[str] = None, ): kwargs: Dict[str, Any] = {"address": address, "port": port, "certificate": certificate, "origin": origin} if asset_root is not None: kwargs["asset_root"] = str(asset_root) _filter_missing_kwargs(kwargs) if authentication is not None: (auth_scheme, auth_data) = authentication if auth_scheme == "token": kwargs["auth_token"] = auth_data elif auth_scheme == "callback": if not callable(auth_data): raise ValueError( "Authentication data must provide a Callable if the authentication scheme is callback" ) kwargs["auth_callback"] = make_auth_callback(auth_data) else: raise ValueError("invalid authentication scheme") self._impl = _frida.EndpointParameters(**kwargs) PortalServiceNodeJoinedCallback = Callable[[int, _frida.Application], None] PortalServiceNodeLeftCallback = Callable[[int, _frida.Application], None] PortalServiceNodeConnectedCallback = Callable[[int, Tuple[str, int]], None] PortalServiceNodeDisconnectedCallback = Callable[[int, Tuple[str, int]], None] PortalServiceControllerConnectedCallback = Callable[[int, Tuple[str, int]], None] PortalServiceControllerDisconnectedCallback = Callable[[int, Tuple[str, int]], None] PortalServiceAuthenticatedCallback = Callable[[int, Mapping[Any, Any]], None] PortalServiceSubscribeCallback = Callable[[int], None] PortalServiceMessageCallback = Callable[[int, Mapping[Any, Any], Optional[bytes]], None] class PortalService: def __init__( self, cluster_params: EndpointParameters = EndpointParameters(), control_params: Optional[EndpointParameters] = None, ) -> None: args = [cluster_params._impl] if control_params is not None: args.append(control_params._impl) impl = _frida.PortalService(*args) self.device = impl.device self._impl = impl self._on_authenticated_callbacks: List[PortalServiceAuthenticatedCallback] = [] self._on_message_callbacks: List[PortalServiceMessageCallback] = [] impl.on("authenticated", self._on_authenticated) impl.on("message", self._on_message) @cancellable def start(self) -> None: """ Start listening for incoming connections :raises InvalidOperationError: if the service isn't stopped :raises AddressInUseError: if the given address is already in use """ self._impl.start() @cancellable def stop(self) -> None: """ Stop listening for incoming connections, and kick any connected clients :raises InvalidOperationError: if the service is already stopped """ self._impl.stop() def post(self, connection_id: int, message: Any, data: Optional[Union[str, bytes]] = None) -> None: """ Post a message to a specific control channel. """ raw_message = json.dumps(message) kwargs = {"data": data} _filter_missing_kwargs(kwargs) self._impl.post(connection_id, raw_message, **kwargs) def narrowcast(self, tag: str, message: Any, data: Optional[Union[str, bytes]] = None) -> None: """ Post a message to control channels with a specific tag """ raw_message = json.dumps(message) kwargs = {"data": data} _filter_missing_kwargs(kwargs) self._impl.narrowcast(tag, raw_message, **kwargs) def broadcast(self, message: Any, data: Optional[Union[str, bytes]] = None) -> None: """ Broadcast a message to all control channels """ raw_message = json.dumps(message) kwargs = {"data": data} _filter_missing_kwargs(kwargs) self._impl.broadcast(raw_message, **kwargs) def enumerate_tags(self, connection_id: int) -> List[str]: """ Enumerate tags of a specific connection """ return self._impl.enumerate_tags(connection_id) def tag(self, connection_id: int, tag: str) -> None: """ Tag a specific control channel """ self._impl.tag(connection_id, tag) def untag(self, connection_id: int, tag: str) -> None: """ Untag a specific control channel """ self._impl.untag(connection_id, tag) @overload def on(self, signal: Literal["node-joined"], callback: PortalServiceNodeJoinedCallback) -> None: ... @overload def on(self, signal: Literal["node-left"], callback: PortalServiceNodeLeftCallback) -> None: ... @overload def on(self, signal: Literal["controller-connected"], callback: PortalServiceControllerConnectedCallback) -> None: ... @overload def on( self, signal: Literal["controller-disconnected"], callback: PortalServiceControllerDisconnectedCallback ) -> None: ... @overload def on(self, signal: Literal["node-connected"], callback: PortalServiceNodeConnectedCallback) -> None: ... @overload def on(self, signal: Literal["node-disconnected"], callback: PortalServiceNodeDisconnectedCallback) -> None: ... @overload def on(self, signal: Literal["authenticated"], callback: PortalServiceAuthenticatedCallback) -> None: ... @overload def on(self, signal: Literal["subscribe"], callback: PortalServiceSubscribeCallback) -> None: ... @overload def on(self, signal: Literal["message"], callback: PortalServiceMessageCallback) -> None: ... @overload def on(self, signal: str, callback: Callable[..., Any]) -> None: ... def on(self, signal: str, callback: Callable[..., Any]) -> None: """ Add a signal handler """ if signal == "authenticated": self._on_authenticated_callbacks.append(callback) elif signal == "message": self._on_message_callbacks.append(callback) else: self._impl.on(signal, callback) def off(self, signal: str, callback: Callable[..., Any]) -> None: """ Remove a signal handler """ if signal == "authenticated": self._on_authenticated_callbacks.remove(callback) elif signal == "message": self._on_message_callbacks.remove(callback) else: self._impl.off(signal, callback) def _on_authenticated(self, connection_id: int, raw_session_info: str) -> None: session_info = json.loads(raw_session_info) for callback in self._on_authenticated_callbacks[:]: try: callback(connection_id, session_info) except: traceback.print_exc() def _on_message(self, connection_id: int, raw_message: str, data: Optional[bytes]) -> None: message = json.loads(raw_message) for callback in self._on_message_callbacks[:]: try: callback(connection_id, message, data) except: traceback.print_exc() class CompilerDiagnosticFile(TypedDict): path: str line: int character: int class CompilerDiagnostic(TypedDict): category: str code: int file: NotRequired[CompilerDiagnosticFile] text: str CompilerStartingCallback = Callable[[], None] CompilerFinishedCallback = Callable[[], None] CompilerOutputCallback = Callable[[str], None] CompilerDiagnosticsCallback = Callable[[List[CompilerDiagnostic]], None] class Compiler: def __init__(self) -> None: self._impl = _frida.Compiler(get_device_manager()._impl) def __repr__(self) -> str: return repr(self._impl) @cancellable def build( self, entrypoint: str, project_root: Optional[str] = None, source_maps: Optional[str] = None, compression: Optional[str] = None, ) -> str: kwargs = {"project_root": project_root, "source_maps": source_maps, "compression": compression} _filter_missing_kwargs(kwargs) return self._impl.build(entrypoint, **kwargs) @cancellable def watch( self, entrypoint: str, project_root: Optional[str] = None, source_maps: Optional[str] = None, compression: Optional[str] = None, ) -> None: kwargs = {"project_root": project_root, "source_maps": source_maps, "compression": compression} _filter_missing_kwargs(kwargs) return self._impl.watch(entrypoint, **kwargs) @overload def on(self, signal: Literal["starting"], callback: CompilerStartingCallback) -> None: ... @overload def on(self, signal: Literal["finished"], callback: CompilerFinishedCallback) -> None: ... @overload def on(self, signal: Literal["output"], callback: CompilerOutputCallback) -> None: ... @overload def on(self, signal: Literal["diagnostics"], callback: CompilerDiagnosticsCallback) -> None: ... @overload def on(self, signal: str, callback: Callable[..., Any]) -> None: ... def on(self, signal: str, callback: Callable[..., Any]) -> None: self._impl.on(signal, callback) @overload def off(self, signal: Literal["starting"], callback: CompilerStartingCallback) -> None: ... @overload def off(self, signal: Literal["finished"], callback: CompilerFinishedCallback) -> None: ... @overload def off(self, signal: Literal["output"], callback: CompilerOutputCallback) -> None: ... @overload def off(self, signal: Literal["diagnostics"], callback: CompilerDiagnosticsCallback) -> None: ... @overload def off(self, signal: str, callback: Callable[..., Any]) -> None: ... def off(self, signal: str, callback: Callable[..., Any]) -> None: self._impl.off(signal, callback) class CancellablePollFD: def __init__(self, cancellable: _Cancellable) -> None: self.handle = cancellable.get_fd() self._cancellable: Optional[_Cancellable] = cancellable def __del__(self) -> None: self.release() def release(self) -> None: if self._cancellable is not None: if self.handle != -1: self._cancellable.release_fd() self.handle = -1 self._cancellable = None def __repr__(self) -> str: return repr(self.handle) def __enter__(self) -> int: return self.handle def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], trace: Optional[TracebackType], ) -> None: self.release() class Cancellable: def __init__(self) -> None: self._impl = _Cancellable() def __repr__(self) -> str: return repr(self._impl) @property def is_cancelled(self) -> bool: """ Query whether cancellable has been cancelled """ return self._impl.is_cancelled() def raise_if_cancelled(self) -> None: """ Raise an exception if cancelled :raises OperationCancelledError: """ self._impl.raise_if_cancelled() def get_pollfd(self) -> CancellablePollFD: return CancellablePollFD(self._impl) @classmethod def get_current(cls) -> _frida.Cancellable: """ Get the top cancellable from the stack """ return _Cancellable.get_current() def __enter__(self) -> None: self._impl.push_current() def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], trace: Optional[TracebackType], ) -> None: self._impl.pop_current() def connect(self, callback: Callable[..., Any]) -> int: """ Register notification callback :returns: the created handler id """ return self._impl.connect(callback) def disconnect(self, handler_id: int) -> None: """ Unregister notification callback. """ self._impl.disconnect(handler_id) def cancel(self) -> None: """ Set cancellable to cancelled """ self._impl.cancel() def make_auth_callback(callback: Callable[[str], Any]) -> Callable[[Any], str]: """ Wraps authenticated callbacks with JSON marshaling """ def authenticate(token: str) -> str: session_info = callback(token) return json.dumps(session_info) return authenticate def _to_camel_case(name: str) -> str: result = "" uppercase_next = False for c in name: if c == "_": uppercase_next = True elif uppercase_next: result += c.upper() uppercase_next = False else: result += c.lower() return result