Files
frida-node/lib/script.ts
T

353 lines
9.6 KiB
TypeScript

import { Cancellable } from "./cancellable";
import { Signals, Signal, SignalHandler, SignalAdapter } from "./signals";
import { inspect } from "util";
export class Script {
destroyed: Signal<ScriptDestroyedHandler>;
message: Signal<ScriptMessageHandler>;
private exportsProxy: any;
private logHandlerImpl: ScriptLogHandler = log;
constructor(private impl: any) {
const services = new ScriptServices(this, impl.signals);
const rpcController: RpcController = services;
this.exportsProxy = new ScriptExportsProxy(rpcController);
const signals: Signals = services;
this.destroyed = new Signal<ScriptDestroyedHandler>(signals, "destroyed");
this.message = new Signal<ScriptMessageHandler>(signals, "message");
}
get isDestroyed(): boolean {
return this.impl.isDestroyed;
}
get exports(): ScriptExports {
return this.exportsProxy;
}
get logHandler(): ScriptLogHandler {
return this.logHandlerImpl;
}
set logHandler(handler: ScriptLogHandler) {
this.logHandlerImpl = handler;
}
get defaultLogHandler(): ScriptLogHandler {
return log;
}
load(cancellable?: Cancellable): Promise<void> {
return this.impl.load(cancellable);
}
unload(cancellable?: Cancellable): Promise<void> {
return this.impl.unload(cancellable);
}
eternalize(cancellable?: Cancellable): Promise<void> {
return this.impl.eternalize(cancellable);
}
post(message: any, data: Buffer | null = null): void {
this.impl.post(message, data);
}
enableDebugger(options: EnableDebuggerOptions = {}, cancellable?: Cancellable): Promise<void> {
const { port = 0 } = options;
return this.impl.enableDebugger(port, cancellable);
}
disableDebugger(cancellable?: Cancellable): Promise<void> {
return this.impl.disableDebugger(cancellable);
}
[inspect.custom](depth, options) {
return "Script {}";
}
}
export interface ScriptOptions {
name?: string;
snapshot?: Buffer;
runtime?: ScriptRuntime;
}
export interface SnapshotOptions {
warmupScript?: string;
runtime?: ScriptRuntime;
}
export enum ScriptRuntime {
Default = "default",
QJS = "qjs",
V8 = "v8"
}
export type ScriptDestroyedHandler = () => void;
export type ScriptMessageHandler = (message: Message, data: Buffer | null) => void;
export type ScriptLogHandler = (level: LogLevel, text: string) => void;
export type Message = SendMessage | ErrorMessage;
export enum MessageType {
Send = "send",
Error = "error"
}
export interface SendMessage {
type: MessageType.Send;
payload: any;
}
export interface ErrorMessage {
type: MessageType.Error;
description: string;
stack?: string;
fileName?: string;
lineNumber?: number;
columnNumber?: number;
}
interface LogMessage {
type: "log";
level: LogLevel;
payload: string;
}
export interface ScriptExports {
[name: string]: (...args: any[]) => Promise<any>;
}
export enum LogLevel {
Info = "info",
Warning = "warning",
Error = "error"
}
export interface EnableDebuggerOptions {
port?: number;
}
class ScriptServices extends SignalAdapter implements RpcController {
private pendingRequests: { [id: string]: (error: Error | null, result?: any) => void } = {};
private nextRequestId: number = 1;
constructor(private script: Script, signals: Signals) {
super(signals);
this.signals.connect("destroyed", this.onDestroyed);
this.signals.connect("message", this.onMessage);
}
protected getProxy(name: string, userHandler: SignalHandler): SignalHandler | null {
if (name === "message") {
return (message, data) => {
if (!isInternalMessage(message)) {
userHandler(message, data);
}
};
}
return null;
}
private onDestroyed = () => {
this.signals.disconnect("destroyed", this.onDestroyed);
this.signals.disconnect("message", this.onMessage);
}
private onMessage = (message: Message, data: Buffer | null) => {
if (message.type === MessageType.Send && isRpcSendMessage(message)) {
const [ , id, operation, ...params ] = message.payload;
this.onRpcMessage(id, operation, params, data);
} else if (isLogMessage(message)) {
const opaqueMessage: any = message;
const logMessage: LogMessage = opaqueMessage;
this.script.logHandler(logMessage.level, logMessage.payload);
}
}
request(operation: string, params: any[], cancellable?: Cancellable): Promise<any> {
return new Promise((resolve, reject) => {
const id = this.nextRequestId++;
const complete = (error: Error | null, result?: any): void => {
if (cancellable !== undefined) {
cancellable.cancelled.disconnect(onOperationCancelled);
}
this.signals.disconnect("destroyed", onScriptDestroyed);
delete this.pendingRequests[id];
if (error === null) {
resolve(result);
} else {
reject(error);
}
};
function onScriptDestroyed(): void {
complete(new Error("Script is destroyed"));
}
function onOperationCancelled(): void {
complete(new Error("Operation was cancelled"));
}
this.pendingRequests[id] = complete;
this.script.post(["frida:rpc", id, operation].concat(params));
this.signals.connect("destroyed", onScriptDestroyed);
if (cancellable !== undefined) {
cancellable.cancelled.connect(onOperationCancelled);
if (cancellable.isCancelled) {
onOperationCancelled();
return;
}
}
if (this.script.isDestroyed) {
onScriptDestroyed();
}
});
}
onRpcMessage(id: number, operation: RpcOperation, params: any[], data: Buffer | null) {
if (operation === RpcOperation.Ok || operation === RpcOperation.Error) {
const callback = this.pendingRequests[id];
if (callback === undefined) {
return;
}
let value = null;
let error = null;
if (operation === RpcOperation.Ok) {
value = (data !== null) ? data : params[0];
} else {
const [message, name, stack, rawErr] = params;
error = new Error(message);
error.name = name;
error.stack = stack;
Object.assign(error, rawErr);
}
callback(error, value);
}
}
}
function ScriptExportsProxy(rpcController: RpcController): void {
return new Proxy(this, {
has(target, property) {
return !isReservedMethodName(property);;
},
get(target, property, receiver) {
if (property in target) {
return target[property];
}
if (property === inspect.custom) {
return inspectProxy;
}
if (isReservedMethodName(property)) {
return undefined;
}
return (...args: any[]): Promise<any> => {
let cancellable: Cancellable | undefined;
if (args[args.length - 1] instanceof Cancellable) {
cancellable = args.pop();
}
return rpcController.request("call", [property, args], cancellable);
};
},
set(target, property, value, receiver) {
target[property] = value;
return true;
},
ownKeys(target) {
return Object.getOwnPropertyNames(target);
},
getOwnPropertyDescriptor(target, property) {
if (property in target) {
return Object.getOwnPropertyDescriptor(target, property);
}
if (isReservedMethodName(property)) {
return undefined;
}
return {
writable: true,
configurable: true,
enumerable: true
};
},
});
}
function inspectProxy() {
return "ScriptExportsProxy {}";
}
interface RpcController {
request(operation: string, params: any[], cancellable?: Cancellable): Promise<any>;
}
enum RpcOperation {
Ok = "ok",
Error = "error"
}
function isInternalMessage(message: Message): boolean {
return isRpcMessage(message) || isLogMessage(message);
}
function isRpcMessage(message: Message): boolean {
return message.type === MessageType.Send && isRpcSendMessage(message);
}
function isRpcSendMessage(message: SendMessage): boolean {
const payload = message.payload;
if (!Array.isArray(payload)) {
return false;
}
return payload[0] === "frida:rpc";
}
function isLogMessage(message: Message): boolean {
return message.type as string === "log";
}
function log(level: LogLevel, text: string): void {
switch (level) {
case LogLevel.Info:
console.log(text);
break;
case LogLevel.Warning:
console.warn(text);
break;
case LogLevel.Error:
console.error(text);
break;
}
}
const reservedMethodNames = new Set<string>([
"then",
"catch",
"finally",
]);
function isReservedMethodName(name: string | number | symbol): boolean {
return reservedMethodNames.has(name.toString());
}