frida-remote-stream
Version:
Create an outbound stream over a message transport
217 lines (216 loc) • 6.27 kB
JavaScript
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);
}
}