module.exports = plugin( 'sasl', { start() { this.SASL = new SASLFactory() this.streamFeature = { name: 'sasl', priority: 1000, match, restart: true, run: (entity, features) => { return this.gotFeatures(features) }, } this.plugins['stream-features'].add(this.streamFeature) }, stop() { delete this.SASL this.plugins['stream-features'].remove(this.streamFeature) delete this.streamFeature delete this.mech }, use(...args) { this.SASL.use(...args) }, gotFeatures(features) { const offered = getMechanismNames(features) const usable = this.getUsableMechanisms(offered) // FIXME const available = this.getAvailableMechanisms() return Promise.resolve(this.getMechanism(usable)).then(mech => { this.mech = mech return this.handleMechanism(mech, features) }) }, handleMechanism(mech, features) { this.entity._status('authenticate') if (mech === 'ANONYMOUS') { return this.authenticate(mech, {}, features) } return this.entity.delegate( 'authenticate', (username, password) => { return this.authenticate(mech, {username, password}, features) }, mech ) }, getAvailableMechanisms() { return this.SASL._mechs.map(({name}) => name) }, getUsableMechanisms(mechs) { const supported = this.getAvailableMechanisms() return mechs.filter(mech => { return supported.indexOf(mech) > -1 }) }, getMechanism(usable) { return usable[0] // FIXME prefer SHA-1, ... maybe order usable, available, ... by preferred? }, findMechanism(name) { return this.SASL.create([name]) }, authenticate(mechname, credentials) { const mech = this.findMechanism(mechname) if (!mech) { return Promise.reject(new Error('no compatible mechanism')) } const {domain} = this.entity.options const creds = Object.assign( { username: null, password: null, server: domain, host: domain, realm: domain, serviceType: 'xmpp', serviceName: domain, }, credentials ) this.entity._status('authenticating') return new Promise((resolve, reject) => { const handler = element => { if (element.attrs.xmlns !== NS) { return } if (element.name === 'challenge') { mech.challenge(decode(element.text())) const resp = mech.response(creds) this.entity.send( xml( 'response', {xmlns: NS, mechanism: mech.name}, typeof resp === 'string' ? encode(resp) : '' ) ) return } if (element.name === 'failure') { reject( new SASLError( element.children[0].name, element.getChildText('text') || '', element ) ) } else if (element.name === 'success') { resolve() this.entity._status('authenticated') } this.entity.removeListener('nonza', handler) } this.entity.on('nonza', handler) if (mech.clientFirst) { this.entity.send( xml( 'auth', {xmlns: NS, mechanism: mech.name}, encode(mech.response(creds)) ) ) } }) }, }, [streamFeatures] )
module.exports = plugin('stream-features', { start() { this.features = [] const {entity} = this this.handler = el => { if (el.name !== 'stream:features') { return } const streamFeatures = this.selectFeatures(el) if (streamFeatures.length === 0) { return } function iterate(c) { const feature = streamFeatures[c] return feature .run(entity, el) .then(() => { if (feature.restart) { return entity.restart() } else if (c === streamFeatures.length - 1) { if (entity.jid) entity._status('online', entity.jid) } else { iterate(c + 1) } }) .catch(err => entity.emit('error', err)) } return iterate(0) } entity.on('nonza', this.handler) }, stop() { delete this.features this.entity.off('nonza', this.handler) delete this.handler }, selectFeatures(el) { return this.features .filter(f => f.match(el, this.entity) && typeof f.priority === 'number') .sort((a, b) => { return a.priority < b.priority }) }, add({name, priority, run, match, restart}) { this.features.push({name, priority, run, match, restart}) }, })
module.exports = plugin('stream-features', { start() { this.features = [] this.negotiated = [] const {entity} = this this.handler = el => { if (el.name !== 'stream:features') { return } const streamFeatures = this.selectFeatures(el) if (streamFeatures.length === 0) { return } const features = streamFeatures.map(feature => { return { name: feature.name, run: (...args) => { return feature.run(entity, el, ...args).then(() => { if (feature.restart) { return entity.restart() } else if (entity.jid) { entity._status('online', entity.jid) } else { this.onStreamFeatures(features, el) } }) .catch(err => entity.emit('error', err)) }, } }) this.onStreamFeatures(features, el) } entity.on('nonza', this.handler) }, stop() { delete this.features delete this.negotiated this.entity.off('nonza', this.handler) delete this.handler }, selectFeatures(el) { return this.features .filter(f => f.match(el, this.entity) && this.negotiated.indexOf(f) === -1 && typeof f.priority === 'number') .sort((a, b) => { return a.priority < b.priority }) }, onStreamFeatures(features) { const feature = features.shift() feature.run() }, add({name, priority, run, match, restart}) { this.features.push({name, priority, run, match, restart}) }, })
'use strict' const plugin = require('@xmpp/plugin') const callee = require('./callee') const caller = require('./caller') module.exports = plugin('ping', { NS_PING: 'urn:xmpp:ping', ping(...args) { return this.plugins['ping-caller'].ping(...args) }, }, [callee, caller])
module.exports = plugin('reconnect', { delay: 1000, reconnect() { const {entity} = this this.emit('reconnecting') return delay(this.delay).then(() => { entity.start(entity.startOptions) .then(() => { this.emit('reconnected') }) .catch(err => { this.emit('error', err) this.reconnect() }) }) }, enable() { this.entity.on('close', () => this.reconnect()) }, start() { const {entity} = this if (entity.jid) { this.enable() } else { entity.once('online', () => this.enable()) } }, stop() { this.entity.removeListener('online', this.enable) this.entity.removeListener('close', this.onClose) clearTimeout(this._timeout) }, })