UNPKG

@duncte123/obs-websocket-js

Version:
180 lines (156 loc) 5.44 kB
const WebSocket = require('isomorphic-ws'); const EventEmitter = require('events'); const hash = require('./util/authenticationHashing'); const Status = require('./Status'); const debug = require('debug')('obs-websocket-js:Socket'); const logAmbiguousError = require('./util/logAmbiguousError'); const camelCaseKeys = require('./util/camelCaseKeys'); class Socket extends EventEmitter { constructor() { super(); this._connected = false; this._socket = undefined; const originalEmit = this.emit; this.emit = function (...args) { debug('[emit] %s err: %o data: %o', ...args); originalEmit.apply(this, args); }; } async connect(args = {}) { args = args || {}; const address = args.address || 'localhost:4444'; if (this._socket) { try { // Blindly try to close the socket. // Don't care if its already closed. // We just don't want any sockets to leak. this._socket.close(); } catch (error) { // These errors are probably safe to ignore, but debug log them just in case. debug('Failed to close previous WebSocket:', error.message); } } // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { try { await this._connect(address, Boolean(args.secure)); await this._authenticate(args.password); resolve(); } catch (err) { this._socket.close(); this._connected = false; logAmbiguousError(debug, 'Connection failed:', err); reject(err); } }); } /** * Opens a WebSocket connection to an obs-websocket server, but does not attempt any authentication. * * @param {String} address url without ws:// or wss:// prefix. * @param {Boolean} secure whether to us ws:// or wss:// * @returns {Promise} * @private * @return {Promise} on attempted creation of WebSocket connection. */ async _connect(address, secure) { return new Promise((resolve, reject) => { let settled = false; debug('Attempting to connect to: %s (secure: %s)', address, secure); this._socket = new WebSocket((secure ? 'wss://' : 'ws://') + address); // We only handle the initial connection error. // Beyond that, the consumer is responsible for adding their own generic `error` event listener. // FIXME: Unsure how best to expose additional information about the WebSocket error. this._socket.onerror = err => { if (settled) { logAmbiguousError(debug, 'Unknown Socket Error', err); this.emit('error', err); return; } settled = true; logAmbiguousError(debug, 'Websocket Connection failed:', err); reject(Status.CONNECTION_ERROR); }; this._socket.onopen = () => { if (settled) { return; } this._connected = true; settled = true; debug('Connection opened: %s', address); this.emit('ConnectionOpened'); resolve(); }; // Looks like this should be bound. We don't technically cancel the connection when the authentication fails. this._socket.onclose = () => { this._connected = false; debug('Connection closed: %s', address); this.emit('ConnectionClosed'); }; // This handler must be present before we can call _authenticate. this._socket.onmessage = msg => { debug('[OnMessage]: %o', msg); const message = camelCaseKeys(JSON.parse(msg.data)); let err; let data; if (message.status === 'error') { err = message; } else { data = message; } // Emit the message with ID if available, otherwise try to find a non-messageId driven event. if (message.messageId) { this.emit(`obs:internal:message:id-${message.messageId}`, err, data); } else if (message.updateType) { this.emit(message.updateType, data); } else { logAmbiguousError(debug, 'Unrecognized Socket Message:', message); this.emit('message', message); } }; }); } /** * Authenticates to an obs-websocket server. Must already have an active connection before calling this method. * * @param {String} [password=''] authentication string. * @private * @return {Promise} on resolution of authentication call. */ async _authenticate(password = '') { if (!this._connected) { throw Status.NOT_CONNECTED; } const auth = await this.send('GetAuthRequired'); if (!auth.authRequired) { debug('Authentication not Required'); this.emit('AuthenticationSuccess'); return Status.AUTH_NOT_REQUIRED; } try { await this.send('Authenticate', { auth: hash(auth.salt, auth.challenge, password) }); } catch (e) { debug('Authentication Failure %o', e); this.emit('AuthenticationFailure'); throw e; } debug('Authentication Success'); this.emit('AuthenticationSuccess'); } /** * Close and disconnect the WebSocket connection. * FIXME: this should support a callback and return a Promise to match the connect method. * * @function * @category request */ disconnect() { debug('Disconnect requested.'); if (this._socket) { this._socket.close(); } } } module.exports = Socket;