@fnlb-project/stanza
Version:
Modern XMPP in the browser, with a JSON API
373 lines (372 loc) • 14.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const events_1 = require("events");
const Constants_1 = require("../Constants");
const Namespaces_1 = require("../Namespaces");
const platform_1 = require("../platform");
const Utils_1 = require("../Utils");
const FileTransferSession_1 = tslib_1.__importDefault(require("./FileTransferSession"));
const MediaSession_1 = tslib_1.__importDefault(require("./MediaSession"));
const Session_1 = tslib_1.__importDefault(require("./Session"));
const MAX_RELAY_BANDWIDTH = 768 * 1024; // maximum bandwidth used via TURN.
function isICEServer(val) {
return !val.type && (val.urls || val.url);
}
class SessionManager extends events_1.EventEmitter {
constructor(conf = {}) {
super();
conf = conf || {};
this.selfID = conf.selfID;
this.sessions = {};
this.peers = {};
this.iceServers = conf.iceServers || [];
this.prepareSession =
conf.prepareSession ||
(opts => {
if (!this.config.hasRTCPeerConnection) {
return;
}
if (opts.applicationTypes.indexOf(Namespaces_1.NS_JINGLE_RTP_1) >= 0) {
return new MediaSession_1.default(opts);
}
if (opts.applicationTypes.indexOf(Namespaces_1.NS_JINGLE_FILE_TRANSFER_5) >= 0) {
return new FileTransferSession_1.default(opts);
}
});
this.performTieBreak =
conf.performTieBreak ||
((sess, req) => {
const applicationTypes = (req.jingle.contents || []).map(content => {
if (content.application) {
return content.application.applicationType;
}
});
const intersection = (sess.pendingApplicationTypes || []).filter(appType => applicationTypes.includes(appType));
return intersection.length > 0;
});
this.createPeerConnection =
conf.createPeerConnection ||
((session, opts) => {
if (platform_1.RTCPeerConnection) {
return new platform_1.RTCPeerConnection(opts);
}
});
this.config = {
debug: false,
hasRTCPeerConnection: !!platform_1.RTCPeerConnection,
peerConnectionConfig: {
bundlePolicy: conf.bundlePolicy || 'balanced',
iceTransportPolicy: conf.iceTransportPolicy || 'all',
rtcpMuxPolicy: conf.rtcpMuxPolicy || 'require',
sdpSemantics: conf.sdpSemantics
},
peerConnectionConstraints: {
optional: [{ DtlsSrtpKeyAgreement: true }, { RtpDataChannels: false }]
},
...conf
};
}
addICEServer(server) {
if (typeof server === 'string') {
this.iceServers.push({ urls: server });
return;
}
if (isICEServer(server)) {
this.iceServers.push(server);
return;
}
let host = server.host || '';
if (host.indexOf(':') >= 0) {
host = `[${host}]`;
}
let uri = `${server.type}:${host}`;
if (server.port) {
uri += `:${server.port}`;
}
if (server.transport) {
uri += `?transport=${server.transport}`;
}
if (server.type === 'turn' || server.type === 'turns') {
this.iceServers.push({
credential: server.password,
urls: [uri],
username: server.username
});
}
else if (server.type === 'stun' || server.type === 'stuns') {
this.iceServers.push({ urls: [uri] });
}
}
resetICEServers() {
this.iceServers = [];
}
addSession(session) {
session.parent = this;
const sid = session.sid;
const peer = session.peerID;
this.sessions[sid] = session;
if (!this.peers[peer]) {
this.peers[peer] = [];
}
this.peers[peer].push(session);
this.emit('createdSession', session);
return session;
}
forgetSession(session) {
const peers = this.peers[session.peerID] || [];
if (peers.length) {
peers.splice(peers.indexOf(session), 1);
}
delete this.sessions[session.sid];
}
createMediaSession(peer, sid, stream) {
const session = new MediaSession_1.default({
config: this.config.peerConnectionConfig,
constraints: this.config.peerConnectionConstraints,
iceServers: this.iceServers,
initiator: true,
maxRelayBandwidth: MAX_RELAY_BANDWIDTH,
parent: this,
peerID: peer,
sid,
stream
});
this.addSession(session);
return session;
}
createFileTransferSession(peer, sid) {
const session = new FileTransferSession_1.default({
config: this.config.peerConnectionConfig,
constraints: this.config.peerConnectionConstraints,
iceServers: this.iceServers,
initiator: true,
maxRelayBandwidth: MAX_RELAY_BANDWIDTH,
parent: this,
peerID: peer,
sid
});
this.addSession(session);
return session;
}
endPeerSessions(peer, reason, silent = false) {
const sessions = this.peers[peer] || [];
delete this.peers[peer];
for (const session of sessions) {
session.end(reason || 'gone', silent);
}
}
endAllSessions(reason, silent = false) {
for (const peer of Object.keys(this.peers)) {
this.endPeerSessions(peer, reason, silent);
}
}
process(req) {
// Extract the request metadata that we need to verify
const sid = req.jingle ? req.jingle.sid : undefined;
let session = sid ? this.sessions[sid] : undefined;
const rid = req.id;
const sender = req.from;
if (!sender) {
return;
}
if (req.type === 'error') {
this._log('error', 'Received error response', req);
if (session && req.error && req.error.jingleError === 'unknown-session') {
return session.end('gone', true);
}
const isTieBreak = req.error && req.error.jingleError === 'tie-break';
if (session && session.state === 'pending' && isTieBreak) {
return session.end('alternative-session', true);
}
else {
if (session) {
session.pendingAction = undefined;
}
return;
}
}
if (req.type === 'result') {
if (session) {
session.pendingAction = undefined;
}
return;
}
const action = req.jingle.action;
const contents = req.jingle.contents || [];
const applicationTypes = contents.map(content => {
return content.application ? content.application.applicationType : undefined;
});
const transportTypes = contents.map(content => {
return content.transport ? content.transport.transportType : undefined;
});
// Now verify that we are allowed to actually process the
// requested action
if (action !== Constants_1.JingleAction.SessionInitiate) {
// Can't modify a session that we don't have.
if (!session) {
if (action === 'session-terminate') {
this.emit('send', {
id: rid,
to: sender,
type: 'result'
});
return;
}
this._log('error', 'Unknown session', sid);
return this._sendError(sender, rid, {
condition: 'item-not-found',
jingleError: 'unknown-session'
});
}
// Check if someone is trying to hijack a session.
if (session.peerID !== sender || session.state === 'ended') {
this._log('error', 'Session has ended, or action has wrong sender');
return this._sendError(sender, rid, {
condition: 'item-not-found',
jingleError: 'unknown-session'
});
}
// Can't accept a session twice
if (action === 'session-accept' && session.state !== 'pending') {
this._log('error', 'Tried to accept session twice', sid);
return this._sendError(sender, rid, {
condition: 'unexpected-request',
jingleError: 'out-of-order'
});
}
// Can't process two requests at once, need to tie break
if (action !== 'session-terminate' && action === session.pendingAction) {
this._log('error', 'Tie break during pending request');
if (session.isInitiator) {
return this._sendError(sender, rid, {
condition: 'conflict',
jingleError: 'tie-break'
});
}
}
}
else if (session) {
// Don't accept a new session if we already have one.
if (session.peerID !== sender) {
this._log('error', 'Duplicate sid from new sender');
return this._sendError(sender, rid, {
condition: 'service-unavailable'
});
}
// Check if we need to have a tie breaker because both parties
// happened to pick the same random sid.
if (session.state === 'pending') {
if (this.selfID &&
this.selfID > session.peerID &&
this.performTieBreak(session, req)) {
this._log('error', 'Tie break new session because of duplicate sids');
return this._sendError(sender, rid, {
condition: 'conflict',
jingleError: 'tie-break'
});
}
}
else {
// The other side is just doing it wrong.
this._log('error', 'Someone is doing this wrong');
return this._sendError(sender, rid, {
condition: 'unexpected-request',
jingleError: 'out-of-order'
});
}
}
else if (this.peers[sender] && this.peers[sender].length) {
// Check if we need to have a tie breaker because we already have
// a different session with this peer that is using the requested
// content application types.
for (let i = 0, len = this.peers[sender].length; i < len; i++) {
const sess = this.peers[sender][i];
if (sess &&
sess.state === 'pending' &&
sid &&
(0, Utils_1.octetCompare)(sess.sid, sid) > 0 &&
this.performTieBreak(sess, req)) {
this._log('info', 'Tie break session-initiate');
return this._sendError(sender, rid, {
condition: 'conflict',
jingleError: 'tie-break'
});
}
}
}
// We've now weeded out invalid requests, so we can process the action now.
if (action === 'session-initiate') {
if (!contents.length) {
return this._sendError(sender, rid, {
condition: 'bad-request'
});
}
session = this._createIncomingSession({
applicationTypes,
config: this.config.peerConnectionConfig,
constraints: this.config.peerConnectionConstraints,
iceServers: this.iceServers,
initiator: false,
parent: this,
peerID: sender,
sid,
transportTypes
}, req);
}
session.process(action, req.jingle, (err) => {
if (err) {
this._log('error', 'Could not process request', req, err);
this._sendError(sender, rid, err);
}
else {
this.emit('send', {
id: rid,
to: sender,
type: 'result'
});
// Wait for the initial action to be processed before emitting
// the session for the user to accept/reject.
if (action === 'session-initiate') {
this.emit('incoming', session);
}
}
});
}
signal(session, data) {
const action = data.jingle && data.jingle.action;
if (session.isInitiator && action === Constants_1.JingleAction.SessionInitiate) {
this.emit('outgoing', session);
}
this.emit('send', data);
}
_createIncomingSession(meta, req) {
let session;
if (this.prepareSession) {
session = this.prepareSession(meta, req);
}
// Fallback to a generic session type, which can
// only be used to end the session.
if (!session) {
session = new Session_1.default(meta);
}
this.addSession(session);
return session;
}
_sendError(to, id, data) {
if (!data.type) {
data.type = 'cancel';
}
this.emit('send', {
error: data,
id,
to,
type: 'error'
});
}
_log(level, message, ...args) {
this.emit('log', level, message, ...args);
this.emit('log:' + level, message, ...args);
}
}
exports.default = SessionManager;