UNPKG

turn-js

Version:

TURN (Traversal Using Relay NAT) library written entirely in JavaScript

691 lines (646 loc) 22.2 kB
'use strict' var merge = require('merge') var Q = require('q') var winston = require('winston-debug') var winstonWrapper = require('winston-meta-wrapper') var Attributes = require('./attributes') var ChannelData = require('./channel_data') var Packet = require('./packet') var StunClient = require('stun-js').StunClient var pjson = require('../package.json') var defaultSoftwareTag = pjson.name + ' v' + pjson.version var _log = winstonWrapper(winston) _log.addMeta({ module: 'turn:client' }) // Constructor class TurnClient extends StunClient { static CHANNEL_BINDING_LIFETIME = 600 static DEFAULT_ALLOCATION_LIFETIME = 600 static CREATE_PERMISSION_LIFETIME = 300 static DEFAULTS = { software: defaultSoftwareTag, lifetime: TurnClient.DEFAULT_ALLOCATION_LIFETIME, dontFragment: false } constructor(host, port, username, password, transport) { super(host, port, transport); // init this.username = username this.password = password // logging this._log = winstonWrapper(winston) this._log.addMeta({ module: 'turn:client' }) // register channel_data decoder this.decoders.push({ decoder: ChannelData.decode, listener: this.dispatchChannelDataPacket.bind(this) }) } /** TurnClient opertions */ // Execute allocation allocateP() { var self = this // send an allocate request without credentials return this.sendAllocateP() .then(function (allocateReply) { var errorCode = allocateReply.getAttribute(Attributes.ERROR_CODE) // check if the reply includes an error code attr if (errorCode) { // check of this is a 401 Unauthorized or a 438 Stale Nonce error if ([401, 438].indexOf(errorCode.code) !== -1) { // throw error if username and password are undefined if (self.username === undefined || self.password === undefined) { throw new Error('allocate error: unauthorized access, while username and/or password are undefined') } // create a new allocate request var args = {} self.nonce = args.nonce = allocateReply.getAttribute(Attributes.NONCE).value self.realm = args.realm = allocateReply.getAttribute(Attributes.REALM).value args.user = self.username args.pwd = self.password return self.sendAllocateP(args) } else { // throw an error if error code !== 401 self._log.error('allocate error: ' + errorCode.reason) self._log.error('allocate response: ' + JSON.stringify(allocateReply)) throw new Error('allocate error: ' + errorCode.reason) } } else { // process allocate reply in next call return Q.fcall(function () { return allocateReply }) } }) // let's process that allocate reply .then(function (allocateReply) { var errorCode = allocateReply.getAttribute(Attributes.ERROR_CODE) // check if the reply includes an error code attr if (errorCode) { throw new Error('allocate error: ' + errorCode.reason) } // store mapped address var mappedAddressAttr = allocateReply.getAttribute(Attributes.XOR_MAPPED_ADDRESS) if (!mappedAddressAttr) { mappedAddressAttr = allocateReply.getAttribute(Attributes.MAPPED_ADDRESS) } self.mappedAddress = { address: mappedAddressAttr.address, port: mappedAddressAttr.port } // store relayed address var relayedAddressAttr = allocateReply.getAttribute(Attributes.XOR_RELAYED_ADDRESS) self.relayedAddress = { address: relayedAddressAttr.address, port: relayedAddressAttr.port } // retrieve lifetime attr, if present var lifetimeAttr = allocateReply.getAttribute(Attributes.LIFETIME) // create and return result var result = { mappedAddress: self.mappedAddress, relayedAddress: self.relayedAddress } if (lifetimeAttr) { result.lifetime = lifetimeAttr.duration } return Q.fcall(function () { return result }) }) } allocate(onSuccess, onFailure) { if (onSuccess === undefined || onFailure === undefined) { var errorMsg = 'allocate callback handlers are undefined' this._log.error(errorMsg) throw new Error(errorMsg) } this.allocateP() .then(function (result) { onSuccess(result) }) .catch(function (error) { onFailure(error) }) } // Create permission to send data to a peer address createPermissionP(address) { if (address === undefined) { var errorMsg = 'create permission requires specified peer address' this._log.error(errorMsg) throw new Error(errorMsg) } // send a create permission request var args = {} args.nonce = this.nonce args.realm = this.realm args.user = this.username args.pwd = this.password args.address = address return this.sendCreatePermissionP(args) .then(function (createPermissionReply) { var errorCode = createPermissionReply.getAttribute(Attributes.ERROR_CODE) // check if the reply includes an error code attr if (errorCode) { throw new Error('create permission error ' + errorCode.reason) } // done }) } createPermission = function (address, onSuccess, onFailure) { if (onSuccess === undefined || onFailure === undefined) { var undefinedCbError = 'create permission callback handlers are undefined' this._log.error(undefinedCbError) throw new Error(undefinedCbError) } if (address === undefined) { var undefinedAddressError = 'create permission requires specified peer address' this._log.error(undefinedAddressError) throw new Error(undefinedAddressError) } this.createPermissionP(address) .then(function () { onSuccess() }) .catch(function (error) { onFailure(error) }) } // Create channel bindChannelP(address, port, channel) { if (address === undefined || port === undefined) { var undefinedAddressError = 'channel bind requires specified peer address and port' this._log.error(undefinedAddressError) throw new Error(undefinedAddressError) } // create channel id var min = 0x4000 var max = 0x7FFF if (channel !== undefined) { if (channel < min || channel > max) { var incorrectChannelError = 'channel id must be >= 0x4000 and =< 0x7FFF' this._log.error(incorrectChannelError) throw new Error(incorrectChannelError) } } else { channel = Math.floor(Math.random() * (max - min + 1)) + min } // send a channel bind request var args = {} args.nonce = this.nonce args.realm = this.realm args.user = this.username args.pwd = this.password args.address = address args.channel = channel args.port = port return this.sendChannelBindP(args) .then(function (channelBindReply) { var errorCode = channelBindReply.getAttribute(Attributes.ERROR_CODE) // check if the reply includes an error code attr if (errorCode) { throw new Error('bind error: ' + errorCode.reason) } return Q.fcall(function () { return channel }) }) } bindChannel = function (address, port, channel, onSuccess, onFailure) { if (onSuccess === undefined || onFailure === undefined) { var undefinedCbError = 'bind callback handlers are undefined' this._log.error(undefinedCbError) throw new Error(undefinedCbError) } if (address === undefined || port === undefined) { var undefinedAddressError = 'channel bind requires specified peer address and port' this._log.error(undefinedAddressError) throw new Error(undefinedAddressError) } this.bindChannelP(address, port, channel) .then(function (duration) { onSuccess(duration) }) .catch(function (error) { onFailure(error) }) } // Execute refresh refreshP = function (lifetime) { if (lifetime === undefined) { var undefinedLifetimeError = 'lifetime is undefined' this._log.error(undefinedLifetimeError) throw new Error(undefinedLifetimeError) } var self = this // send refresh request var args = {} args.nonce = this.nonce args.realm = this.realm args.user = this.username args.pwd = this.password args.lifetime = lifetime return this.sendRefreshP(args) .then(function (refreshReply) { var errorCode = refreshReply.getAttribute(Attributes.ERROR_CODE) // check if the reply includes an error code attr if (errorCode) { // check of this is a 438 Stale nonce error if (errorCode.code === 438) { // create a new refresh request var args = {} self.nonce = args.nonce = refreshReply.getAttribute(Attributes.NONCE).value self.realm = args.realm = refreshReply.getAttribute(Attributes.REALM).value args.user = self.username args.pwd = self.password return self.sendRefreshP(args) } else { // throw an error if error code !== 438 throw new Error('refresh error: ' + refreshReply.getAttribute(Attributes.ERROR_CODE).reason) } } else { // process refresh reply in next call return Q.fcall(function () { return refreshReply }) } }) .then(function (refreshReply) { var errorCode = refreshReply.getAttribute(Attributes.ERROR_CODE) // check if the reply includes an error code attr if (errorCode) { throw new Error('refresh error: ' + errorCode.reason) } // otherwise retrieve and return lifetime var lifetime = refreshReply.getAttribute(Attributes.LIFETIME).duration return Q.fcall(function () { return lifetime }) }) } refresh(lifetime, onSuccess, onFailure) { if (lifetime === undefined) { var undefinedLifetimeError = 'lifetime is undefined' this._log.error(undefinedLifetimeError) throw new Error(undefinedLifetimeError) } if (onSuccess === undefined || onFailure === undefined) { var undefinedCbError = 'refresh callback handlers are undefined' this._log.error(undefinedCbError) throw new Error(undefinedCbError) } this.refreshP(lifetime) .then(function (duration) { onSuccess(duration) }) .catch(function (error) { onFailure(error) }) } // Close this socket closeP() { const superCloseP = super.closeP.bind(this); return this.refreshP(0) .then(function () { return superCloseP }) } close(onSuccess, onFailure) { if (onSuccess === undefined || onFailure === undefined) { var errorMsg = 'close callback handlers are undefined' this._log.error(errorMsg) throw new Error(errorMsg) } var self = this this.closeP() .then(function () { onSuccess() }) .catch(function (error) { self._log.error('closing socket failed: ' + error.message) onFailure(errorMsg) }) } /** Message transmission */ // Send TURN allocation sendAllocateP(args) { this._log.debug('send allocate (using promises)') var message = composeAllocateRequest(args) return this.sendStunRequestP(message) } sendAllocate(args, onSuccess, onFailure) { this._log.debug('send allocate') if (onSuccess === undefined || onFailure === undefined) { var errorMsg = 'send allocate callback handlers are undefined' this._log.error(errorMsg) throw new Error(errorMsg) } this.sendAllocateP(args) .then(function (reply) { onSuccess(reply) }) .catch(function (error) { onFailure(error) }) } // Send TURN create permission sendCreatePermissionP(args) { this._log.debug('send create permission (using promises)') var message = composeCreatePermissionRequest(args) return this.sendStunRequestP(message) } sendCreatePermission(args, onSuccess, onFailure) { this._log.debug('send create permission') if (onSuccess === undefined || onFailure === undefined) { var errorMsg = 'send create permission callback handlers are undefined' this._log.error(errorMsg) throw new Error(errorMsg) } this.sendCreatePermissionP(args) .then(function (reply) { onSuccess(reply) }) .catch(function (error) { onFailure(error) }) } // Send TURN channel bind sendChannelBindP(args) { this._log.debug('send channel bind (using promises)') var message = composeChannelBindRequest(args) return this.sendStunRequestP(message) } sendChannelBind(args, onSuccess, onFailure) { this._log.debug('send channel bind') if (onSuccess === undefined || onFailure === undefined) { var errorMsg = 'send channel bind callback handlers are undefined' this._log.error(errorMsg) throw new Error(errorMsg) } this.sendChannelBindP(args) .then(function (reply) { onSuccess(reply) }) .catch(function (error) { onFailure(error) }) } // Send TURN refresh sendRefreshP(args) { this._log.debug('send refresh (using promises)') var message = composeRefreshRequest(args) return this.sendStunRequestP(message) } sendRefresh(args, onSuccess, onFailure) { this._log.debug('send refresh') if (onSuccess === undefined || onFailure === undefined) { var errorMsg = 'send refresh callback handlers are undefined' this._log.error(errorMsg) throw new Error(errorMsg) } this.sendRefreshP(args) .then(function (reply) { onSuccess(reply) }) .catch(function (error) { onFailure(error) }) } // Send data via relay/turn server sendToRelayP(bytes, address, port) { var args = { address: address, port: port, bytes: bytes } var message = composeSendIndication(args) return this.sendStunIndicationP(message) } sendToRelay(bytes, address, port, onSuccess, onFailure) { this._log.debug('send data') if (onSuccess === undefined || onFailure === undefined) { var errorMsg = 'send data callback handlers are undefined' this._log.error(errorMsg) throw new Error(errorMsg) } this.sendToRelayP(bytes, address, port) .then(function () { onSuccess() }) .catch(function (error) { onFailure(error) }) } // Send channel data via relay/turn server sendToChannelP(bytes, channel) { var args = { channel: channel, bytes: bytes } var message = composeChannelDataMessage(args) return this.sendStunIndicationP(message) } sendToChannel(bytes, channel, onSuccess, onFailure) { this._log.debug('send channel data') if (onSuccess === undefined || onFailure === undefined) { var errorMsg = 'send channel data callback handlers are undefined' this._log.error(errorMsg) throw new Error(errorMsg) } this.sendToChannelP(bytes, channel) .then(function () { onSuccess() }) .catch(function (error) { onFailure(error) }) } /** Message arrival */ // Incoming STUN indication onIncomingStunIndication(stunPacket, rinfo) { if (stunPacket.method === Packet.METHOD.DATA) { var dataBytes = stunPacket.getAttribute(Attributes.DATA).bytes var xorPeerAddress = stunPacket.getAttribute(Attributes.XOR_PEER_ADDRESS) this.emit('relayed-message', dataBytes, { address: xorPeerAddress.address, port: xorPeerAddress.port }) } else { const superOnIncomingStunIndication = super.onIncomingStunIndication.bind(this); superOnIncomingStunIndication(stunPacket, rinfo) } } // Dispatch ChannelData packet dispatchChannelDataPacket(packet, rinfo) { this.emit('relayed-message', packet.bytes, rinfo, packet.channel) } } /** Message composition */ function composeAllocateRequest (args) { var margs = merge(Object.create(TurnClient.DEFAULTS), args) // create attrs var attrs = new Attributes() _addSecurityAttributes(attrs, margs) attrs.add(new Attributes.Software(margs.software)) attrs.add(new Attributes.RequestedTransport()) if (margs.dontFragment !== undefined) { attrs.add(new Attributes.DontFragment()) } if (margs.lifetime !== undefined) { attrs.add(new Attributes.Lifetime(margs.lifetime)) } // create allocate packet var packet = new Packet(Packet.METHOD.ALLOCATE, Packet.TYPE.REQUEST, attrs) // encode packet var message = packet.encode() return message } function composeCreatePermissionRequest (args) { // check args if (args === undefined) { var undefinedArgsError = 'invalid create-permission attributes: args = undefined' _log.error(undefinedArgsError) throw new Error(undefinedArgsError) } if (args.address === undefined) { var undefinedAddressError = 'invalid create-permission attributes: args.address = undefined' _log.error(undefinedAddressError) throw new Error(undefinedAddressError) } // create attrs var attrs = new Attributes() _addSecurityAttributes(attrs, args) attrs.add(new Attributes.XORPeerAddress(args.address)) // create createPermission packet var packet = new Packet(Packet.METHOD.CREATEPERMISSION, Packet.TYPE.REQUEST, attrs) // encode packet var message = packet.encode() return message } function composeSendIndication (args) { // check args if (args === undefined) { var undefinedArgsError = 'invalid send attributes: args = undefined' _log.error(undefinedArgsError) throw new Error(undefinedArgsError) } if (args.address === undefined) { var undefinedAddressError = 'invalid send attributes: args.address = undefined' _log.error(undefinedAddressError) throw new Error(undefinedAddressError) } if (args.port === undefined) { var undefinedPortError = 'invalid send attributes: args.port = undefined' _log.error(undefinedPortError) throw new Error(undefinedPortError) } if (args.bytes === undefined) { var undefinedBytesError = 'invalid send attributes: args.bytes = undefined' _log.error(undefinedBytesError) throw new Error(undefinedBytesError) } var margs = merge(Object.create(TurnClient.DEFAULTS), args) // create attrs var attrs = new Attributes() attrs.add(new Attributes.XORPeerAddress(margs.address, margs.port)) if (margs.dontFragment) { attrs.add(new Attributes.DontFragment()) } attrs.add(new Attributes.Data(margs.bytes)) // create send packet var packet = new Packet(Packet.METHOD.SEND, Packet.TYPE.INDICATION, attrs) // encode packet var message = packet.encode() return message } function composeChannelBindRequest (args) { // check args if (args === undefined) { var undefinedArgsError = 'invalid channel-bind attributes: args = undefined' _log.error(undefinedArgsError) throw new Error(undefinedArgsError) } if (args.channel === undefined) { var undefinedChannelError = 'invalid channel-bind attributes: args.channel = undefined' _log.error(undefinedChannelError) throw new Error(undefinedChannelError) } if (args.address === undefined) { var undefinedAddressError = 'invalid channel-bind attributes: args.address = undefined' _log.error(undefinedAddressError) throw new Error(undefinedAddressError) } if (args.port === undefined) { var undefinedPortError = 'invalid channel-bind attributes: args.port = undefined' _log.error(undefinedPortError) throw new Error(undefinedPortError) } // create attrs var attrs = new Attributes() _addSecurityAttributes(attrs, args) attrs.add(new Attributes.ChannelNumber(args.channel)) attrs.add(new Attributes.XORPeerAddress(args.address, args.port)) // create channelBind packet var packet = new Packet(Packet.METHOD.CHANNELBIND, Packet.TYPE.REQUEST, attrs) // create channelBind packet var message = packet.encode() return message } function composeChannelDataMessage (args) { // check args if (args === undefined) { var undefinedArgsError = 'invalid channel-bind attributes: args = undefined' _log.error(undefinedArgsError) throw new Error(undefinedArgsError) } if (args.bytes === undefined) { var undefinedDataError = 'invalid channel-data attribute: bytes = undefined' _log.error(undefinedDataError) throw new Error(undefinedDataError) } if (args.channel === undefined) { var undefinedChannelError = 'invalid channel-data attribute: channel = undefined' _log.error(undefinedChannelError) throw new Error(undefinedChannelError) } // create channel-data packet var channelData = new ChannelData(args.channel, args.bytes) // encode packet var message = channelData.encode() return message } function composeRefreshRequest (args) { var margs = merge(Object.create(TurnClient.DEFAULTS), args) // create attrs var attrs = new Attributes() _addSecurityAttributes(attrs, margs) attrs.add(new Attributes.Software(margs.software)) attrs.add(new Attributes.Lifetime(margs.lifetime)) // create refresh packet var packet = new Packet(Packet.METHOD.REFRESH, Packet.TYPE.REQUEST, attrs) // encode packet var message = packet.encode() return message } function _addSecurityAttributes (attrs, args) { if (args.user) { attrs.add(new Attributes.Username(args.user)) } if (args.nonce) { attrs.add(new Attributes.Nonce(args.nonce)) } if (args.realm) { attrs.add(new Attributes.Realm(args.realm)) } if (args.user && args.pwd) { attrs.add(new Attributes.MessageIntegrity({ username: args.user, password: args.pwd, realm: args.realm })) } } module.exports = TurnClient