UNPKG

frida-remote-stream

Version:

Create an outbound stream over a message transport

217 lines (216 loc) 6.27 kB
import EventEmitter from "events"; import { Readable, Writable } from "stream"; export class Controller { constructor() { this.events = new EventEmitter(); this.sources = new Map(); this.nextEndpointId = 1; this.requests = new Map(); this.nextRequestId = 1; this.onCreate = (payload) => { const endpoint = payload.endpoint; const source = new Source(endpoint); this.sources.set(endpoint.id, source); this.events.emit("stream", source); }; this.onFinish = (payload) => { const id = payload.endpoint.id; const source = this.sources.get(id); if (source === undefined) { throw new Error("Invalid endpoint ID"); } this.sources.delete(id); source.push(null); }; this.onWrite = (payload, data) => { const id = payload.endpoint.id; const source = this.sources.get(id); if (source === undefined) { throw new Error("Invalid endpoint ID"); } if (data === null) { throw new Error("Invalid request: missing data"); } return source.deliver(data); }; this.handlers = { ".create": this.onCreate, ".finish": this.onFinish, ".write": this.onWrite }; } open(label, details = {}) { const endpoint = { id: this.nextEndpointId++, label, details }; return new Sink(this, endpoint); } receive(packet) { const stanza = packet.stanza; const { id, name, payload } = stanza; const type = name[0]; if (type === ".") { this.onRequest(id, name, payload, packet.data); } else if (type === "+") { this.onNotification(id, name, payload); } else { throw new Error("Unknown stanza: " + name); } } _request(name, payload, data) { return new Promise((resolve, reject) => { const id = this.nextRequestId++; this.requests.set(id, { resolve: resolve, reject: reject }); const stanza = { id, name, payload }; this.events.emit("send", { stanza, data }); }); } onRequest(id, name, payload, data) { const handler = this.handlers[name]; if (handler === undefined) { throw new Error(`Invalid request: ${name}`); } let result; try { result = handler(payload, data); } catch (e) { this.reject(id, e); return; } if (result instanceof Promise) { result .then(value => this.resolve(id, value)) .catch(error => this.reject(id, error)); } else { this.resolve(id, result); } } resolve(id, value) { const stanza = { id: id, name: "+result", payload: value }; this.events.emit("send", { stanza, data: null }); } reject(id, error) { const stanza = { id: id, name: "+error", payload: { message: error.toString() } }; this.events.emit("send", { stanza, data: null }); } onNotification(id, name, payload) { const request = this.requests.get(id); if (request === undefined) { throw new Error("Invalid request ID"); } this.requests.delete(id); if (name === "+result") { request.resolve(payload); } else if (name === "+error") { const response = payload; request.reject(new Error(response.message)); } else { throw new Error("Unknown notification: " + name); } } } export default Controller; class Source extends Readable { constructor({ label, details }) { super(); this.onReadComplete = null; this.delivery = null; this.label = label; this.details = details; } _read(size) { if (this.onReadComplete !== null) { return; } this.onReadComplete = chunk => { this.onReadComplete = null; if (chunk.length === 0) { this.push(null); return false; } if (this.push(chunk)) { this._read(size); } return true; }; this.tryComplete(); } deliver(chunk) { return new Promise((resolve, reject) => { if (this.delivery !== null) { throw new Error("Protocol violation"); } this.delivery = { chunk: chunk, resolve: resolve, reject: reject }; this.tryComplete(); }); } tryComplete() { const { onReadComplete, delivery } = this; if (onReadComplete === null || delivery === null) { return; } this.onReadComplete = null; this.delivery = null; if (onReadComplete(delivery.chunk)) { delivery.resolve(); } else { delivery.reject(new Error("Stream closed")); } } } class Sink extends Writable { constructor(controller, endpoint) { super(); this.controller = controller; this.endpoint = endpoint; this.controller._request(".create", { endpoint: this.endpoint }, null); this.once("finish", this._onFinish.bind(this)); } _write(chunk, encoding, callback) { this.controller._request(".write", { endpoint: this.endpoint }, chunk) .then(_ => callback()) .catch(error => callback(error)); } _onFinish() { this.controller._request(".finish", { endpoint: this.endpoint }, null); } }