UNPKG

@fnlb-project/stanza

Version:

Modern XMPP in the browser, with a JSON API

295 lines (294 loc) 10.8 kB
import { priorityQueue } from 'async'; import { JingleAction, JingleErrorCondition, JingleReasonCondition, JingleSessionRole, StanzaErrorCondition } from '../Constants'; import { uuid } from '../Utils'; const badRequest = { condition: StanzaErrorCondition.BadRequest }; const unsupportedInfo = { condition: StanzaErrorCondition.FeatureNotImplemented, jingleError: JingleErrorCondition.UnsupportedInfo, type: 'modify' }; export default class JingleSession { constructor(opts) { this.parent = opts.parent; this.sid = opts.sid || uuid(); this.peerID = opts.peerID; this.role = opts.initiator ? JingleSessionRole.Initiator : JingleSessionRole.Responder; this._sessionState = 'starting'; this._connectionState = 'starting'; // We track the intial pending description types in case // of the need for a tie-breaker. this.pendingApplicationTypes = opts.applicationTypes || []; this.pendingAction = undefined; // Here is where we'll ensure that all actions are processed // in order, even if a particular action requires async handling. this.processingQueue = priorityQueue(async (task, next) => { if (this.state === 'ended') { // Don't process anything once the session has been ended if (task.type === 'local' && task.reject) { task.reject(new Error('Session ended')); } if (next) { next(); } return; } if (task.type === 'local') { this._log('debug', 'Processing local action:', task.name); try { const res = await task.handler(); task.resolve(res); } catch (err) { task.reject(err); } if (next) { next(); } return; } const { action, changes, cb } = task; this._log('debug', 'Processing remote action:', action); return new Promise(resolve => { const done = (err, result) => { cb(err, result); if (next) { next(); } resolve(); }; switch (action) { case JingleAction.ContentAccept: return this.onContentAccept(changes, done); case JingleAction.ContentAdd: return this.onContentAdd(changes, done); case JingleAction.ContentModify: return this.onContentModify(changes, done); case JingleAction.ContentReject: return this.onContentReject(changes, done); case JingleAction.ContentRemove: return this.onContentRemove(changes, done); case JingleAction.DescriptionInfo: return this.onDescriptionInfo(changes, done); case JingleAction.SecurityInfo: return this.onSecurityInfo(changes, done); case JingleAction.SessionAccept: return this.onSessionAccept(changes, done); case JingleAction.SessionInfo: return this.onSessionInfo(changes, done); case JingleAction.SessionInitiate: return this.onSessionInitiate(changes, done); case JingleAction.SessionTerminate: return this.onSessionTerminate(changes, done); case JingleAction.TransportAccept: return this.onTransportAccept(changes, done); case JingleAction.TransportInfo: return this.onTransportInfo(changes, done); case JingleAction.TransportReject: return this.onTransportReject(changes, done); case JingleAction.TransportReplace: return this.onTransportReplace(changes, done); default: this._log('error', 'Invalid or unsupported action: ' + action); done({ condition: StanzaErrorCondition.BadRequest }); } }); }, 1); } get isInitiator() { return this.role === JingleSessionRole.Initiator; } get peerRole() { return this.isInitiator ? JingleSessionRole.Responder : JingleSessionRole.Initiator; } get state() { return this._sessionState; } set state(value) { if (value !== this._sessionState) { this._log('info', 'Changing session state to: ' + value); this._sessionState = value; if (this.parent) { this.parent.emit('sessionState', this, value); } } } get connectionState() { return this._connectionState; } set connectionState(value) { if (value !== this._connectionState) { this._log('info', 'Changing connection state to: ' + value); this._connectionState = value; if (this.parent) { this.parent.emit('connectionState', this, value); } } } send(action, data) { data = data || {}; data.sid = this.sid; data.action = action; const requirePending = new Set([ JingleAction.ContentAccept, JingleAction.ContentAdd, JingleAction.ContentModify, JingleAction.ContentReject, JingleAction.ContentRemove, JingleAction.SessionAccept, JingleAction.SessionInitiate, JingleAction.TransportAccept, JingleAction.TransportReject, JingleAction.TransportReplace ]); if (requirePending.has(action)) { this.pendingAction = action; } else { this.pendingAction = undefined; } this.parent.signal(this, { id: uuid(), jingle: data, to: this.peerID, type: 'set' }); } processLocal(name, handler) { return new Promise((resolve, reject) => { this.processingQueue.push({ handler, name, reject, resolve, type: 'local' }, 1 // Process local requests first ); }); } process(action, changes, cb) { this.processingQueue.push({ action, cb, changes, type: 'remote' }, 2 // Process remote requests second ); } start(_opts, _next) { this._log('error', 'Can not start base sessions'); this.end('unsupported-applications', true); } accept(_opts, _next) { this._log('error', 'Can not accept base sessions'); this.end('unsupported-applications'); } cancel() { this.end('cancel'); } decline() { this.end('decline'); } end(reason = 'success', silent = false) { this.state = 'ended'; this.processingQueue.kill(); if (typeof reason === 'string') { reason = { condition: reason }; } if (!silent) { this.send('session-terminate', { reason }); } this.parent.emit('terminated', this, reason); this.parent.forgetSession(this); } _log(level, message, ...data) { if (this.parent) { message = this.sid + ': ' + message; this.parent.emit('log', level, message, ...data); this.parent.emit('log:' + level, message, ...data); } } onSessionInitiate(changes, cb) { cb(); } onSessionAccept(changes, cb) { cb(); } onSessionTerminate(changes, cb) { this.end(changes.reason, true); cb(); } // It is mandatory to reply to a session-info action with // an unsupported-info error if the info isn't recognized. // // However, a session-info action with no associated payload // is acceptable (works like a ping). onSessionInfo(changes, cb) { if (!changes.info) { cb(); } else { cb(unsupportedInfo); } } // It is mandatory to reply to a security-info action with // an unsupported-info error if the info isn't recognized. onSecurityInfo(changes, cb) { cb(unsupportedInfo); } // It is mandatory to reply to a description-info action with // an unsupported-info error if the info isn't recognized. onDescriptionInfo(changes, cb) { cb(unsupportedInfo); } // It is mandatory to reply to a transport-info action with // an unsupported-info error if the info isn't recognized. onTransportInfo(changes, cb) { cb(unsupportedInfo); } // It is mandatory to reply to a content-add action with either // a content-accept or content-reject. onContentAdd(changes, cb) { // Allow ack for the content-add to be sent. cb(); this.send(JingleAction.ContentReject, { reason: { condition: JingleReasonCondition.FailedApplication, text: 'content-add is not supported' } }); } onContentAccept(changes, cb) { cb(badRequest); } onContentReject(changes, cb) { cb(badRequest); } onContentModify(changes, cb) { cb(badRequest); } onContentRemove(changes, cb) { cb(badRequest); } // It is mandatory to reply to a transport-add action with either // a transport-accept or transport-reject. onTransportReplace(changes, cb) { // Allow ack for the transport-replace be sent. cb(); this.send(JingleAction.TransportReject, { reason: { condition: JingleReasonCondition.FailedTransport, text: 'transport-replace is not supported' } }); } onTransportAccept(changes, cb) { cb(badRequest); } onTransportReject(changes, cb) { cb(badRequest); } }