web-ext-run
Version:
A tool to open and run web extensions
251 lines (248 loc) • 6.89 kB
JavaScript
import net from 'net';
import EventEmitter from 'events';
import domain from 'domain';
export const DEFAULT_PORT = 6000;
export const DEFAULT_HOST = '127.0.0.1';
const UNSOLICITED_EVENTS = new Set(['tabNavigated', 'styleApplied', 'propertyChange', 'networkEventUpdate', 'networkEvent', 'propertyChange', 'newMutations', 'frameUpdate', 'tabListChanged']);
// Parse RDP packets: BYTE_LENGTH + ':' + DATA.
export function parseRDPMessage(data) {
const str = data.toString();
const sepIdx = str.indexOf(':');
if (sepIdx < 1) {
return {
data
};
}
const byteLen = parseInt(str.slice(0, sepIdx));
if (isNaN(byteLen)) {
const error = new Error('Error parsing RDP message length');
return {
data,
error,
fatal: true
};
}
if (data.length - (sepIdx + 1) < byteLen) {
// Can't parse yet, will retry once more data has been received.
return {
data
};
}
data = data.slice(sepIdx + 1);
const msg = data.slice(0, byteLen);
data = data.slice(byteLen);
try {
return {
data,
rdpMessage: JSON.parse(msg.toString())
};
} catch (error) {
return {
data,
error,
fatal: false
};
}
}
export async function connectToFirefox(port) {
const client = new FirefoxRDPClient();
return client.connect(port).then(() => client);
}
export default class FirefoxRDPClient extends EventEmitter {
_incoming;
_pending;
_active;
_rdpConnection;
_onData;
_onError;
_onEnd;
_onTimeout;
constructor() {
super();
this._incoming = Buffer.alloc(0);
this._pending = [];
this._active = new Map();
this._onData = (...args) => this.onData(...args);
this._onError = (...args) => this.onError(...args);
this._onEnd = (...args) => this.onEnd(...args);
this._onTimeout = (...args) => this.onTimeout(...args);
}
connect(port) {
return new Promise((resolve, reject) => {
// Create a domain to wrap the errors that may be triggered
// by creating the client connection (e.g. ECONNREFUSED)
// so that we can reject the promise returned instead of
// exiting the entire process.
const d = domain.create();
d.once('error', reject);
d.run(() => {
const conn = net.createConnection({
port,
host: DEFAULT_HOST
});
this._rdpConnection = conn;
conn.on('data', this._onData);
conn.on('error', this._onError);
conn.on('end', this._onEnd);
conn.on('timeout', this._onTimeout);
// Resolve once the expected initial root message
// has been received.
this._expectReply('root', {
resolve,
reject
});
});
});
}
disconnect() {
if (!this._rdpConnection) {
return;
}
const conn = this._rdpConnection;
conn.off('data', this._onData);
conn.off('error', this._onError);
conn.off('end', this._onEnd);
conn.off('timeout', this._onTimeout);
conn.end();
this._rejectAllRequests(new Error('RDP connection closed'));
}
_rejectAllRequests(error) {
for (const activeDeferred of this._active.values()) {
activeDeferred.reject(error);
}
this._active.clear();
for (const {
deferred
} of this._pending) {
deferred.reject(error);
}
this._pending = [];
}
async request(requestProps) {
let request;
if (typeof requestProps === 'string') {
request = {
to: 'root',
type: requestProps
};
} else {
request = requestProps;
}
if (request.to == null) {
throw new Error(`Unexpected RDP request without target actor: ${request.type}`);
}
return new Promise((resolve, reject) => {
const deferred = {
resolve,
reject
};
this._pending.push({
request,
deferred
});
this._flushPendingRequests();
});
}
_flushPendingRequests() {
this._pending = this._pending.filter(({
request,
deferred
}) => {
if (this._active.has(request.to)) {
// Keep in the pending requests until there are no requests
// active on the target RDP actor.
return true;
}
const conn = this._rdpConnection;
if (!conn) {
throw new Error('RDP connection closed');
}
try {
let str = JSON.stringify(request);
str = `${Buffer.from(str).length}:${str}`;
conn.write(str);
this._expectReply(request.to, deferred);
} catch (err) {
deferred.reject(err);
}
// Remove the pending request from the queue.
return false;
});
}
_expectReply(targetActor, deferred) {
if (this._active.has(targetActor)) {
throw new Error(`${targetActor} does already have an active request`);
}
this._active.set(targetActor, deferred);
}
_handleMessage(rdpData) {
if (rdpData.from == null) {
if (rdpData.error) {
this.emit('rdp-error', rdpData);
return;
}
this.emit('error', new Error(`Received an RDP message without a sender actor: ${JSON.stringify(rdpData)}`));
return;
}
if (UNSOLICITED_EVENTS.has(rdpData.type)) {
this.emit('unsolicited-event', rdpData);
return;
}
if (this._active.has(rdpData.from)) {
const deferred = this._active.get(rdpData.from);
this._active.delete(rdpData.from);
if (rdpData.error) {
deferred?.reject(rdpData);
} else {
deferred?.resolve(rdpData);
}
this._flushPendingRequests();
return;
}
this.emit('error', new Error(`Unexpected RDP message received: ${JSON.stringify(rdpData)}`));
}
_readMessage() {
const {
data,
rdpMessage,
error,
fatal
} = parseRDPMessage(this._incoming);
this._incoming = data;
if (error) {
this.emit('error', new Error(`Error parsing RDP packet: ${String(error)}`));
// Disconnect automatically on a fatal error.
if (fatal) {
this.disconnect();
}
// Caller can parse the next message if the error wasn't fatal
// (e.g. the RDP packet that couldn't be parsed has been already
// removed from the incoming data buffer).
return !fatal;
}
if (!rdpMessage) {
// Caller will need to wait more data to parse the next message.
return false;
}
this._handleMessage(rdpMessage);
// Caller can try to parse the next message from the remaining data.
return true;
}
onData(data) {
this._incoming = Buffer.concat([this._incoming, data]);
while (this._readMessage()) {
// Keep parsing and handling messages until readMessage
// returns false.
}
}
onError(error) {
this.emit('error', error);
}
onEnd() {
this.emit('end');
}
onTimeout() {
this.emit('timeout');
}
}
//# sourceMappingURL=rdp-client.js.map