UNPKG

@twilio/plugin-microvisor

Version:

Interact with your Twilio Microvisor devices

147 lines (146 loc) 4.87 kB
"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;