@twilio/plugin-microvisor
Version:
Interact with your Twilio Microvisor devices
147 lines (146 loc) • 4.87 kB
JavaScript
"use strict";
const EventEmitter = require('events');
const { URL } = require('url');
const WebSocket = require('ws');
const { TwilioCliError } = require('@twilio/cli-core').services.error;
const PING_INTERVAL_MS = 300 * 1000;
const PING_TIMEOUT_MS = 60 * 1000;
class DeviceOfflineError extends Error {
constructor() {
super('Device is not online');
}
}
class Tunnel extends EventEmitter {
constructor(opts) {
super();
if (opts.tokenUrl) {
this.tokenUrl = opts.tokenUrl;
}
else {
if (!opts.deviceSid) {
throw new Error('Tunnel requires opts.tokenUrl or opts.deviceSid');
}
const tokenBaseUrl = opts.tokenBaseUrl || 'https://microvisor.twilio.com';
const tokenPath = opts.tokenPath || `v1/Devices/${opts.deviceSid}/DebugToken`;
this.tokenUrl = new URL(tokenPath, tokenBaseUrl);
}
if (opts.tunnelUrl) {
this.tunnelUrl = opts.tunnelUrl;
}
else {
const tunnelBaseUrl = opts.tunnelBaseUrl || 'wss://microvisor-debug.us1.twilio.com';
const tunnelPath = opts.tunnelPath || '/debug';
this.tunnelUrl = new URL(tunnelPath, tunnelBaseUrl);
}
if (!opts.client) {
throw new Error('Tunnel requires opts.client');
}
this.client = opts.client;
this.init();
}
async init() {
if (this.cancelInit) {
return;
}
const token = await this.getToken();
if (this.cancelInit) {
return;
}
var options = { headers: { Authorization: `Bearer ${token}` } };
if (process.env.HTTP_PROXY) {
const HttpsProxyAgent = require('https-proxy-agent');
options.agent = new HttpsProxyAgent(process.env.HTTP_PROXY);
}
this.ws = new WebSocket(this.tunnelUrl, options);
this.ws.on('open', () => this.onOpen());
this.ws.on('unexpected-response', (req, res) => this.onUnexpectedResponse(req, res));
this.ws.on('error', err => this.onError(err));
}
async getToken() {
if (this.cancelInit) {
return;
}
try {
var response = await this.client.request({
method: 'POST',
uri: this.tokenUrl
});
return response.body.token;
}
catch (error) {
if (error instanceof TwilioCliError && error.exitCode === 20404) {
this.emit('error', new TwilioCliError('Unknown device sid', error.exitCode, error.data));
}
else {
this.emit('error', error);
}
}
}
onOpen() {
this.pingInterval = setInterval(() => this.onPingInterval(), PING_INTERVAL_MS);
this.ws.on('pong', () => this.onPong());
this.ws.on('close', (code, reason) => this.onClose(code, reason));
this.ws.on('message', message => this.onMessage(message));
this.emit('open');
}
onMessage(message) {
this.emit('data', message);
}
onClose(code, reason) {
this.cleanup();
this.emit('close', code, reason);
}
onPingInterval() {
this.ws.ping();
this.pingTimeout = setTimeout(() => this.onPingTimeout(), PING_TIMEOUT_MS);
}
onPong() {
if (this.pingTimeout) {
clearTimeout(this.pingTimeout);
this.pingTimeout = null;
}
}
onPingTimeout() {
this.onError(new Error('Ping timeout'));
this.ws.removeAllListeners('close');
this.ws.terminate();
}
onUnexpectedResponse(_req, res) {
// remove the error listener, because it'll trigger when we close the websocket
this.ws.removeAllListeners('error');
// and add a no-op listener to prevent the unhandled event from throwing an exception
this.ws.addListener('error', () => { });
var error = res.statusCode === 404 ?
new DeviceOfflineError() :
new Error(`Error response from server: ${res.statusCode} - ${res.statusMessage}`);
this.emit('error', error);
// now close the websocket
this.ws.close();
}
onError(err) {
this.cleanup();
this.emit('error', err);
}
write(data) {
this.ws.send(data);
}
close() {
this.cancelInit = true;
this.cleanup();
if (this.ws) {
this.ws.close();
}
}
cleanup() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
delete this.pingInterval;
}
if (this.pingTimeout) {
clearTimeout(this.pingTimeout);
this.pingTimeout = null;
}
}
}
Tunnel.DeviceOfflineError = DeviceOfflineError;
module.exports = Tunnel;