├── LICENSE.md ├── README.md ├── ice_connection_state.dot ├── ice_connection_state.svg ├── lib ├── IceCandidatePair.js ├── IceChecklist.js ├── RTCIceCandidate.js ├── component.js ├── debug.js ├── discovery.js ├── ice-ufrag-pwd.js ├── ice.js ├── ice2.js ├── localAddresses.js ├── parseIceServerList.js └── stream.js ├── package.json └── test ├── ice.js └── ice2.js /LICENSE.md: -------------------------------------------------------------------------------- 1 | #THE BEER-WARE LICENSE (Revision 42): 2 | wrote this file. As long as you retain this notice you 3 | can do whatever you want with this stuff. If we meet some day, and you think 4 | this stuff is worth it, you can buy me a beer in return. 5 | 6 | Nick Desaulniers 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node-ICE 2 | An implementation of Interactive Connectivity Establishment (ICE, RFC 5245) for 3 | Node.js. 4 | 5 | ## Why is ICE useful? 6 | ICE solves the problem of establishing a UDP based connection between two peers 7 | potentially behind NATs without them having to muck around with their router's 8 | port forwarding settings. Without ICE, it's *highly likely* for UDP 9 | connections given between two random peers to fail. 10 | 11 | ## Implementation Details 12 | For those of you who have read the spec(s). This is a full implementation (not 13 | a lite implementation), with full trickle (as opposed to half trickle), and 14 | aggressive nomination (as opposed to regular nomination). 15 | 16 | This module was seperated from 17 | [my Node.js RTCPeerConnection](https://github.com/nickdesaulniers/node-rtc-peer-connection) 18 | implementation for reusability. As such, that library drives the interface to 19 | this library. 20 | 21 | ## What's Missing 22 | * TURN candidate support 23 | * Peer Reflexive candidate support 24 | 25 | ## API 26 | ... 27 | 28 | ## Relevant Specs 29 | * [RFC 5245 - ICE (Interactive Connectivity Establishment)](https://tools.ietf.org/html/rfc5245) 30 | * [RFC 5389 - STUN (Session Traversal Utilities for NAT)](https://tools.ietf.org/html/rfc5389) 31 | * [RFC 5766 - TURN (Traversal Using Relays around NAT)](https://tools.ietf.org/html/rfc5766) 32 | * [RFC 7064 - STUN URI Scheme](https://tools.ietf.org/html/rfc7064) 33 | * [DRAFT ICE-BIS - ICE (Interactive Connectivity Establishment)](https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00) 34 | * [DRAFT ICE-Trickle - Incremental Provisioning for ICE](https://tools.ietf.org/html/draft-ietf-ice-trickle-01) 35 | 36 | -------------------------------------------------------------------------------- /ice_connection_state.dot: -------------------------------------------------------------------------------- 1 | // dot -Tsvg ice_connection_state.dot > ice_connection_state.svg 2 | digraph ice_connection_state { 3 | // https://w3c.github.io/webrtc-pc/#rtciceconnectionstate-enum 4 | 5 | // The list of states (nodes) 6 | node [shape = doublecircle]; 7 | NEW; 8 | node [shape = circle]; 9 | CHECKING; 10 | CONNECTED; 11 | COMPLETED; 12 | FAILED; 13 | DISCONNECTED; 14 | CLOSED; 15 | 16 | // The list of transistions (edges) 17 | NEW -> CHECKING [label = "set remote candidates"]; 18 | // All states can transition to NEW or CLOSED. 19 | //NEW -> NEW [label = "ICE restart"]; 20 | //NEW -> CLOSED [label = "close"]; 21 | 22 | CHECKING -> CONNECTED [label = "one check completes"]; 23 | CHECKING -> FAILED [label = "all checks have failed"]; 24 | //CHECKING -> NEW [label = "ICE restart"]; 25 | //CHECKING -> CLOSED [label = "close"]; 26 | 27 | CONNECTED -> COMPLETED [label = "finished all checks"]; 28 | CONNECTED -> DISCONNECTED [label = "lost connectivity"]; 29 | //CONNECTED -> NEW [label = "ICE restart"]; 30 | //CONNECTED -> CLOSED [label = "close"]; 31 | 32 | COMPLETED -> DISCONNECTED [label = "lost connectivity"]; 33 | //COMPLETED -> NEW [label = "ICE restart"]; 34 | //COMPLETED -> CLOSED [label = "close"]; 35 | 36 | FAILED -> DISCONNECTED [label = "wtf how does this work?"]; // doesn't seem right 37 | //FAILED -> NEW [label = "ICE restart"]; 38 | //FAILED -> CLOSED [label = "close"]; 39 | 40 | DISCONNECTED -> CONNECTED [label = "was able to reconnect"]; 41 | DISCONNECTED -> COMPLETED [label = "finished all checks"]; 42 | DISCONNECTED -> FAILED [label = "all checks have failed"] 43 | //DISCONNECTED -> NEW [label = "ICE restart"]; 44 | //DISCONNECTED -> CLOSED [label = "close"]; 45 | } 46 | 47 | -------------------------------------------------------------------------------- /ice_connection_state.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | ice_connection_state 11 | 12 | 13 | NEW 14 | 15 | 16 | NEW 17 | 18 | 19 | CHECKING 20 | 21 | CHECKING 22 | 23 | 24 | NEW->CHECKING 25 | 26 | 27 | set remote candidates 28 | 29 | 30 | CONNECTED 31 | 32 | CONNECTED 33 | 34 | 35 | CHECKING->CONNECTED 36 | 37 | 38 | one check completes 39 | 40 | 41 | FAILED 42 | 43 | FAILED 44 | 45 | 46 | CHECKING->FAILED 47 | 48 | 49 | all checks have failed 50 | 51 | 52 | COMPLETED 53 | 54 | COMPLETED 55 | 56 | 57 | CONNECTED->COMPLETED 58 | 59 | 60 | finished all checks 61 | 62 | 63 | DISCONNECTED 64 | 65 | DISCONNECTED 66 | 67 | 68 | CONNECTED->DISCONNECTED 69 | 70 | 71 | lost connectivity 72 | 73 | 74 | COMPLETED->DISCONNECTED 75 | 76 | 77 | lost connectivity 78 | 79 | 80 | FAILED->DISCONNECTED 81 | 82 | 83 | wtf how does this work? 84 | 85 | 86 | DISCONNECTED->CONNECTED 87 | 88 | 89 | was able to reconnect 90 | 91 | 92 | DISCONNECTED->COMPLETED 93 | 94 | 95 | finished all checks 96 | 97 | 98 | DISCONNECTED->FAILED 99 | 100 | 101 | all checks have failed 102 | 103 | 104 | CLOSED 105 | 106 | CLOSED 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /lib/IceCandidatePair.js: -------------------------------------------------------------------------------- 1 | var Enum = require('enum'); 2 | var RTCIceCandidate = require('./RTCIceCandidate'); 3 | 4 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-5.1.3.4 5 | var iceStates = new Enum([ 6 | 'Waiting', 7 | 'InProgress', 8 | 'Succeeded', 9 | 'Failed', 10 | 'Frozen' // let it gooooo..... 11 | ]); 12 | 13 | function IceCandidatePair (localCandidate, remoteCandidate, iceControlling) { 14 | // As described in Figure 7 of 15 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-5.1.3.1 16 | if (!(localCandidate instanceof RTCIceCandidate)) { 17 | throw new TypeError('localCandidate is not an instance of RTCIceCandidate'); 18 | } 19 | if (!(remoteCandidate instanceof RTCIceCandidate)) { 20 | throw new TypeError('remoteCandidate is not an instance of RTCIceCandidate'); 21 | } 22 | 23 | this.local = localCandidate; 24 | this.remote = remoteCandidate; 25 | this.default = false; 26 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2.2 27 | this.valid = false; 28 | this.nominated = false; 29 | // this.state.key will give you the corresponding string 30 | this.state = iceStates.Frozen; 31 | this.priority = this.computePriority(iceControlling); 32 | }; 33 | 34 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-5.1.3.2 35 | function prioritize (g, d) { 36 | return Math.pow(2, 32) * Math.min(g, d) + 2 * Math.max(g, d) + 37 | (g > d ? 1 : 0); 38 | }; 39 | 40 | IceCandidatePair.prototype.computePriority = function (iceControlling) { 41 | if (iceControlling) { 42 | return prioritize(this.local.priority, this.remote.priority); 43 | } else { 44 | return prioritize(this.remote.priority, this.local.priority); 45 | } 46 | }; 47 | 48 | function toState (state) { 49 | return function () { 50 | this.state = state; 51 | }; 52 | }; 53 | 54 | IceCandidatePair.prototype.freeze = toState(iceStates.Frozen); 55 | IceCandidatePair.prototype.unfreeze = toState(iceStates.Waiting); 56 | IceCandidatePair.prototype.progress = toState(iceStates.InProgress); 57 | IceCandidatePair.prototype.succeed = toState(iceStates.Succeeded); 58 | 59 | function inState (state) { 60 | return function () { 61 | return this.state === state; 62 | }; 63 | }; 64 | 65 | IceCandidatePair.prototype.isWaiting = inState(iceStates.Waiting); 66 | IceCandidatePair.prototype.isFrozen = inState(iceStates.Frozen); 67 | IceCandidatePair.prototype.isFailed = inState(iceStates.Failed); 68 | IceCandidatePair.prototype.isSucceeded = inState(iceStates.Succeeded); 69 | 70 | module.exports = IceCandidatePair; 71 | 72 | -------------------------------------------------------------------------------- /lib/IceChecklist.js: -------------------------------------------------------------------------------- 1 | var Enum = require('enum'); 2 | var IceCandidatePair = require('./IceCandidatePair'); 3 | 4 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-5.1.3.4 5 | var checklistStates = new Enum([ 6 | // Would be nice to have a NOTSTARTED state. 7 | 'Running', 8 | 'Completed', 9 | 'Failed' 10 | ]); 11 | 12 | function IceChecklist () { 13 | this.candidatePairs = []; 14 | this.state = checklistStates.Running; 15 | this.pendingRequestTransactions = {}; 16 | this.validList = []; 17 | }; 18 | 19 | IceChecklist.prototype.add = function (pair) { 20 | if (!(pair instanceof IceCandidatePair)) { 21 | throw new TypeError('unable to add non-IceCandidatePair to ICE checklist'); 22 | } 23 | // https://tools.ietf.org/html/draft-ietf-ice-trickle-01#section-8.1 24 | if (this.candidatePairs.length === 100) { 25 | console.warn('dropping candidate pair, reached 100 pairs'); 26 | return; 27 | } 28 | this.candidatePairs.push(pair); 29 | }; 30 | 31 | function descendingPriority (pairA, pairB) { 32 | return pairA.priority < pairB.priority; 33 | }; 34 | 35 | IceChecklist.prototype.prioritize = function () { 36 | this.candidatePairs.sort(descendingPriority); 37 | }; 38 | 39 | function freeze (pair) { pair.freeze(); }; 40 | IceChecklist.prototype.freezeAll = function () { 41 | this.candidatePairs.forEach(freeze); 42 | }; 43 | 44 | IceChecklist.prototype.unfreezeFirst = function () { 45 | if (this.candidatePairs.length > 0) { 46 | this.candidatePairs[0].unfreeze(); 47 | } 48 | }; 49 | 50 | function thaw (pair) { 51 | if (pair.isFrozen()) { 52 | pair.unfreeze(); 53 | } 54 | }; 55 | 56 | IceChecklist.prototype.unfreezeAll = function () { 57 | this.candidatePairs.forEach(thaw); 58 | }; 59 | 60 | function firstWaiting (pair) { 61 | return pair.isWaiting(); 62 | }; 63 | 64 | IceChecklist.prototype.highestPriorityWaiting = function () { 65 | // this.prioritize should have been called, so candidatePairs should already 66 | // be sorted. 67 | return this.candidatePairs.find(firstWaiting); 68 | }; 69 | 70 | function firstFrozen (pair) { 71 | return pair.isFrozen(); 72 | }; 73 | 74 | IceChecklist.prototype.highestPriorityFrozen = function () { 75 | return this.candidatePairs.find(firstFrozen); 76 | }; 77 | 78 | function failedOrSucceeded (pair) { 79 | return pair.isFailed() || pair.isSucceeded(); 80 | }; 81 | 82 | IceChecklist.prototype.checkForFailure = function () { 83 | if (!this.candidatePairs.some(failedOrSucceeded) && 84 | this.validList.length === 0) { 85 | this.state = checklistStates.Failed; 86 | // TODO: maybe bubble this up? 87 | console.error('Ice Check list failure'); 88 | } 89 | }; 90 | 91 | IceChecklist.prototype.removeWaitingAndFrozenCandidates = function () { 92 | this.candidatePairs = this.candidatePairs.filter(failedOrSucceeded); 93 | }; 94 | 95 | function inState (state) { 96 | return function () { 97 | return this.state === state; 98 | }; 99 | }; 100 | 101 | IceChecklist.prototype.isRunning = inState(checklistStates.Running); 102 | IceChecklist.prototype.isCompleted = inState(checklistStates.Completed); 103 | IceChecklist.prototype.isFailed = inState(checklistStates.Failed); 104 | 105 | IceChecklist.prototype.complete = function () { 106 | this.state = checklistStates.Completed; 107 | }; 108 | 109 | module.exports = IceChecklist; 110 | 111 | -------------------------------------------------------------------------------- /lib/RTCIceCandidate.js: -------------------------------------------------------------------------------- 1 | var iceParse = require('wrtc-ice-cand-parse').parse; 2 | var net = require('net'); 3 | var normalizeIPv6 = require('ipv6-normalize'); 4 | 5 | // https://tools.ietf.org/html/rfc5245#section-4.1.2.2 6 | // https://w3c.github.io/webrtc-pc/#idl-def-RTCIceCandidateType 7 | var typePrefs = { 8 | host: 126, 9 | srflx: 100, 10 | relay: 0, 11 | }; 12 | 13 | var foundationCounter = 0; 14 | 15 | function prioritize (typePref, localPref, componentId) { 16 | // https://tools.ietf.org/html/rfc5245#section-4.1.2.1 17 | return (Math.pow(2, 24) * typePref + 18 | Math.pow(2, 8) * localPref + 19 | 256 - componentId) | 0; 20 | }; 21 | 22 | function computePriority (type, addr, componentId) { 23 | // https://tools.ietf.org/html/rfc5245#section-2.3 24 | // https://tools.ietf.org/html/rfc5245#section-4.1.2 25 | // https://tools.ietf.org/html/rfc5245#section-4.1.2.2 26 | var typePref = typePrefs[type]; 27 | var localPref = net.isIPv6(addr) ? 65535 : 0; 28 | return prioritize(typePref, localPref, componentId); 29 | }; 30 | 31 | function RTCIceCandidate (dict) { 32 | // https://w3c.github.io/webrtc-pc/#rtcicecandidate-dictionary 33 | // members: 34 | // candidate 35 | // sdpMid 36 | // sdpMLineIndex 37 | // foundation 38 | // priority 39 | // ip 40 | // protocol 41 | // port 42 | // type 43 | // tcpType 44 | // relatedAddress 45 | // relatedPort 46 | 47 | Object.assign(this, dict); 48 | 49 | if (this.candidate) { 50 | var p = iceParse(this.candidate); 51 | this.foundation = +p.foundation; 52 | this.priority = +p.priority; 53 | this.ip = p.localIP; 54 | this.protocol = p.transport.toLowerCase(); 55 | this.port = p.localPort; 56 | this.type = p.type; 57 | if (p.remoteIP && p.remotePort) { 58 | this.relatedAddress = p.remoteIP; 59 | this.relatedPort = p.remotePort; 60 | } 61 | } 62 | // TODO: 63 | // else generate candidate string 64 | 65 | if (net.isIPv6(this.ip)) { 66 | this.ip = normalizeIPv6(this.ip); 67 | } 68 | 69 | if (typeof this.port !== 'number') { 70 | this.port = +this.port; 71 | } 72 | 73 | if (!('componentId' in this)) { 74 | this.componentId = 0; 75 | } 76 | 77 | if (!('priority' in this)) { 78 | this.priority = computePriority(this.type, this.ip, this.componentId); 79 | } 80 | 81 | if (!('foundation' in this)) { 82 | // https://tools.ietf.org/html/rfc5245#section-4.2 83 | this.foundation = foundationCounter++; 84 | } 85 | 86 | this.protocol = this.protocol || 'udp'; 87 | 88 | if (!('_socket' in this)) { 89 | this._socket = null; 90 | } 91 | }; 92 | 93 | module.exports = RTCIceCandidate; 94 | 95 | -------------------------------------------------------------------------------- /lib/component.js: -------------------------------------------------------------------------------- 1 | function Component (componentId, agent, stream) { 2 | this.componentId = componentId; 3 | this.agent = agent; 4 | this.stream = stream; 5 | }; 6 | 7 | module.exports = Component; 8 | 9 | -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var Packet = require('vs-stun/lib/Packet'); 3 | 4 | //var leftArrow = '\u2190'; 5 | //var rightArrow = '\u2192'; 6 | 7 | function printDebugPacket (p, rinfo) { 8 | var type = Packet.getType(p); 9 | var str = ''; 10 | if (type === Packet.BINDING_SUCCESS) { 11 | str += chalk.green('[STUN] BINDING SUCCESS '); 12 | } else if (type === Packet.BINDING_REQUEST) { 13 | //console.log(); 14 | str += chalk.yellow('[STUN] BINDING REQUEST '); 15 | //p.doc.attributes.forEach(function (attr) { 16 | //console.log(attr.name, attr.value.obj); 17 | //}); 18 | } else { 19 | console.log(); 20 | console.log(Packet.typeToString(p)); 21 | p.doc.attributes.forEach(function (attr) { 22 | console.log(attr.name, attr.value.obj); 23 | }); 24 | } 25 | str += chalk.blue(rinfo.address) + ':' + chalk.magenta(rinfo.port); 26 | console.log(str); 27 | }; 28 | 29 | function printMatches (a, b) { 30 | console.log(chalk[a === b ? 'green' : 'red'](a + ' === ' + b)); 31 | //console.log(typeof a, typeof b) 32 | }; 33 | 34 | function printPeerReflexive (source, dest, candidatePair) { 35 | console.log('---'); 36 | console.warn('Found a peer reflexive candidate') 37 | printMatches(source.address, candidatePair.remote.ip); 38 | printMatches(source.port, candidatePair.remote.port); 39 | printMatches(dest.host, candidatePair.local.ip); 40 | printMatches(dest.port, candidatePair.local.port); 41 | console.log('---'); 42 | }; 43 | 44 | function formatAddrPort (obj) { 45 | return chalk.blue(obj.ip) + ':' + chalk.magenta(obj.port); 46 | }; 47 | 48 | function printPairs (pairList) { 49 | console.warn('valid list:') 50 | console.warn('local -> remote'); 51 | pairList.forEach(function (pair) { 52 | console.warn(formatAddrPort(pair.local) + ' -> ' + formatAddrPort(pair.remote)); 53 | }); 54 | console.warn('---'); 55 | }; 56 | 57 | function warnNonStunPacket (info, rinfo) { 58 | console.warn('not a stun packet'); 59 | console.warn(info.address + ':' + info.port + ' -> ' + rinfo.address + ':' + 60 | rinfo.port); 61 | }; 62 | 63 | function iceLocalHostCandidate (candidate) { 64 | console.warn(chalk.cyan('[ICE] LOCAL HOST CANDIDATE ') + 65 | formatAddrPort(candidate)); 66 | }; 67 | 68 | function iceLocalSrflxCandidate (candidate) { 69 | console.warn(chalk.cyan('[ICE] LOCAL SRFLX CANDIDATE ') + 70 | formatAddrPort(candidate)); 71 | }; 72 | 73 | function iceRemoteCandidate (candidate) { 74 | console.warn(chalk.cyan('[ICE] REMOTE ' + candidate.type.toUpperCase() + 75 | ' CANDIDATE ') + formatAddrPort(candidate)); 76 | }; 77 | 78 | module.exports = { 79 | iceLocalHostCandidate: iceLocalHostCandidate, 80 | iceLocalSrflxCandidate: iceLocalSrflxCandidate, 81 | iceRemoteCandidate: iceRemoteCandidate, 82 | printDebugPacket: printDebugPacket, 83 | printPeerReflexive: printPeerReflexive, 84 | printPairs: printPairs, 85 | warnNonStunPacket: warnNonStunPacket, 86 | }; 87 | 88 | -------------------------------------------------------------------------------- /lib/discovery.js: -------------------------------------------------------------------------------- 1 | var dgram = require('dgram'); 2 | var RTCIceCandidate = require('./RTCIceCandidate'); 3 | var vsStun = require('vs-stun'); 4 | 5 | function addLocalHostCandidateP (streamId, componentId, inet) { 6 | return function (resolve, reject) { 7 | var candidate = new RTCIceCandidate({ 8 | ip: inet.address, 9 | type: 'host', 10 | streamId: streamId, 11 | componentId: componentId, 12 | }); 13 | var socket = createSocket(inet.family).bind(null, inet.address, function () { 14 | candidate.port = socket.address().port; 15 | candidate._socket = socket; 16 | resolve(candidate); 17 | }); 18 | }; 19 | }; 20 | 21 | // this nonsense has to be async due to socket.bind 22 | function addLocalHostCandidate (streamId, componentId, inet) { 23 | return new Promise(addLocalHostCandidateP(streamId, componentId, inet)); 24 | }; 25 | 26 | function addLocalSrflxCandidate (streamId, componentId, socket, iceServer) { 27 | return new Promise(function (resolve, reject) { 28 | iceServer.host = iceServer.ip; 29 | vsStun.resolve(socket, iceServer, function (error, value) { 30 | if (error) { 31 | return reject(error); 32 | } 33 | if (!('public' in value)) { 34 | // this case happens when a stun server is of the wrong family 35 | return resolve(); 36 | } 37 | var candidate = new RTCIceCandidate({ 38 | ip: value.public.host, 39 | port: value.public.port, 40 | type: 'srflx', 41 | relatedAddress: value.local.host, 42 | relatedPort: value.local.port, 43 | streamId: streamId, 44 | componentId: componentId, 45 | _socket: socket, 46 | }); 47 | resolve(candidate); 48 | }); 49 | }); 50 | }; 51 | 52 | function udpFamily (family) { 53 | if (family === 'IPv4') { 54 | return 'udp4'; 55 | } else if (family === 'IPv6'){ 56 | return 'udp6'; 57 | } else { 58 | return ''; 59 | } 60 | }; 61 | 62 | function createSocket (inetFamily) { 63 | return dgram.createSocket(udpFamily(inetFamily)); 64 | }; 65 | 66 | module.exports = { 67 | addLocalHostCandidate: addLocalHostCandidate, 68 | addLocalSrflxCandidate: addLocalSrflxCandidate, 69 | }; 70 | 71 | -------------------------------------------------------------------------------- /lib/ice-ufrag-pwd.js: -------------------------------------------------------------------------------- 1 | // http://tools.ietf.org/html/rfc5245#section-15.1 2 | // https://tools.ietf.org/html/draft-ietf-mmusic-ice-sip-sdp-07#section-9.4 3 | var iceChar = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/'; 4 | 5 | function getRandomStr (len) { 6 | var str = ''; 7 | for (var i = 0; i < len; ++i) { 8 | str += iceChar[Math.random() * iceChar.length | 0]; 9 | } 10 | return str; 11 | }; 12 | 13 | // http://tools.ietf.org/html/rfc5245#section-15.4 14 | 15 | module.exports = { 16 | ufrag: getRandomStr.bind(null, 4), 17 | password: getRandomStr.bind(null, 22), 18 | }; 19 | 20 | -------------------------------------------------------------------------------- /lib/ice.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk'); 2 | var debug = require('./debug'); 3 | var udp = require('dgram'); 4 | var EventEmitter = require('events'); 5 | var forEachInet = require('foreachinet'); 6 | var IceCandidatePair = require('./IceCandidatePair'); 7 | var IceChecklist = require('./IceChecklist'); 8 | var RTCIceCandidate = require('./RTCIceCandidate'); 9 | var net = require('net'); 10 | var normalizeIPv6 = require('ipv6-normalize'); 11 | var Packet = require('vs-stun/lib/Packet'); 12 | var util = require('util'); 13 | var url = require('url'); 14 | var vsStun = require('vs-stun'); 15 | 16 | function IceAgent (config) { 17 | EventEmitter.call(this); 18 | 19 | // https://tools.ietf.org/html/rfc5245#section-2.2 20 | this.candidates = []; 21 | this.remoteCandidates = []; 22 | // If there were more media streams, there'd be a checklist per. 23 | this.checklist = new IceChecklist; 24 | 25 | // If not "controlling", then "controlled." The controlling peer's agent 26 | // nominates the candidate pair that will be used for the rest of 27 | // communication. Public and set by RTCPeerConnections's createOffer(). 28 | this.iceControlling = false; 29 | 30 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-12 31 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-12.1 32 | this.Ta = 1000;//20; // ms 33 | this.checkTimeout = null; 34 | 35 | // used for short term credentialing 36 | this.localUsername = null; 37 | this.localPassword = null; 38 | this.remoteUsername = null; 39 | this.remotePassword = null; 40 | 41 | if (!('iceServers' in config)) { 42 | throw new Error('missing iceServers list in config argument'); 43 | } 44 | this.config = config; 45 | }; 46 | 47 | util.inherits(IceAgent, EventEmitter); 48 | 49 | function onPacket (agent, socket, msg, rinfo) { 50 | //console.log('Received %d bytes from %s:%d', 51 | //msg.length, rinfo.address, rinfo.port); 52 | if (!Packet.parse.check(msg)) { 53 | debug.warnNonStunPacket(socket.address(), rinfo); 54 | debug.printPairs(agent.checklist.validList); 55 | return; 56 | } 57 | var p = Packet.parse(msg); 58 | debug.printDebugPacket(p, rinfo); 59 | 60 | // TODO: should use the states specified, and bubble these up so the 61 | // RTCPeerConnection can emit them. 62 | // https://w3c.github.io/webrtc-pc/#rtciceconnectionstate-enum 63 | var type = Packet.getType(p); 64 | if (type === Packet.BINDING_REQUEST) { 65 | agent.respondToBindingRequest(socket, p, rinfo); 66 | } else if (type === Packet.BINDING_SUCCESS) { 67 | agent.bindingSuccess(socket, p, rinfo); 68 | } 69 | }; 70 | 71 | function getFirstStunServer (servers) { 72 | var urlp = url.parse(servers[0].urls); 73 | return { 74 | host: urlp.hostname, 75 | port: urlp.port, 76 | }; 77 | }; 78 | 79 | function isLinkLocalAddress (inet) { 80 | // https://en.wikipedia.org/wiki/Link-local_address 81 | // TODO: handle ipv4 link local addresses 82 | return inet.family === 'IPv6' && inet.address.startsWith('fe80::'); 83 | }; 84 | 85 | function createSocket (agent, inetFamily) { 86 | var socketType = inetFamily === 'IPv4' ? 'udp4' : 'udp6'; 87 | var socket = udp.createSocket(socketType); 88 | socket.once('listening', agent.gatherStunCandidate.bind(agent, socket)); 89 | socket.on('message', onPacket.bind(null, agent, socket)); 90 | socket.on('error', function (e) { 91 | // we seem to keep getting ENOTFOUND stun.l.google.com. 92 | // https://github.com/nodejs/node-v0.x-archive/issues/5488#issuecomment-167597819 93 | // TODO: should check that e.hostname is in this.config.iceServers 94 | // Issue #47. 95 | if (e.code === 'ENOTFOUND') { 96 | //console.error('no record for domain used for stun: ' + e.hostname); 97 | return; 98 | } 99 | throw e; 100 | }); 101 | return socket; 102 | } 103 | 104 | IceAgent.prototype.gatherHostCandidates = function () { 105 | var promises = []; 106 | var agent = this; 107 | // TODO: set state to gathering 108 | // https://tools.ietf.org/html/rfc5245#section-4.1.1.1 109 | forEachInet(function (inet) { 110 | // Do not include link local addresses 111 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-4.1.1.1 112 | if (isLinkLocalAddress(inet) || inet.internal) { 113 | return; 114 | } 115 | var socket = createSocket(agent, inet.family); 116 | 117 | promises.push(new Promise(function (resolve, reject) { 118 | socket.bind(null, inet.address, function () { 119 | var info = socket.address(); 120 | var iceCandidate = { 121 | type: 'host', 122 | ip: info.address, 123 | port: info.port, 124 | _socket: socket, 125 | }; 126 | var candidate = new RTCIceCandidate(iceCandidate); 127 | debug.iceLocalHostCandidate(candidate); 128 | agent.candidates.push(candidate); 129 | resolve(candidate); 130 | }); 131 | })); 132 | }); 133 | return Promise.all(promises); 134 | }; 135 | 136 | IceAgent.prototype.gatherStunCandidate = function (socket) { 137 | var servers = this.config.iceServers; 138 | var stunServer = getFirstStunServer(servers); 139 | var agent = this; 140 | vsStun.resolve(socket, stunServer, function (error, value) { 141 | if (error) { 142 | // TODO: emit an icecandidateerror event 143 | throw error; 144 | } else if (!(('public' in value) && ('host' in value.public) && ('port' in value.public))) { 145 | // this case will happen if the STUN server is only ipv4 but we tried 146 | // to connect to it via an ipv6 socket or vice versa. 147 | // TODO: resolve the stun server hostname ourselves and don't even try 148 | // if they're different families. Issue #47. 149 | //console.error('got a response from an non-family stun server', value); 150 | return; 151 | // } else { // now getting ipv6 public values that are the same as the host? 152 | } else { 153 | var info = socket.address(); 154 | var iceCandidate = { 155 | type: 'srflx', 156 | ip: value.public.host, 157 | port: value.public.port, 158 | relatedAddress: info.address, 159 | relatedPort: info.port, 160 | _socket: socket, 161 | }; 162 | var candidate = new RTCIceCandidate(iceCandidate); 163 | debug.iceLocalSrflxCandidate(candidate); 164 | agent.candidates.push(candidate); 165 | // TODO: I think we should defer emitting this until we're in 166 | // have-local-offer state. Issue #50. 167 | agent.emit('icecandidate', candidate); 168 | } 169 | }); 170 | }; 171 | 172 | // TODO: gatherTurnCandidates 173 | 174 | // From an offer/answer sdp. candidates may be null if the remote is trickling. 175 | IceAgent.prototype.setRemoteCandidates = function (candidates) { 176 | if (candidates) { 177 | candidates.forEach(function (candidate) { 178 | this.addCandidate(candidate); 179 | }.bind(this)); 180 | } 181 | }; 182 | 183 | // These candidates should only be remote, not local. 184 | // https://tools.ietf.org/html/draft-ietf-ice-trickle-01#section-8.1 185 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-5.1.3 186 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-5.1.3.1 187 | IceAgent.prototype.addCandidate = function (candidate) { 188 | debug.iceRemoteCandidate(candidate); 189 | 190 | // Prevent duplicates. 191 | // https://tools.ietf.org/html/draft-ietf-ice-trickle-01#section-5.2 192 | var find = function (remoteCandidate) { 193 | return remoteCandidate.ip === candidate.ip && 194 | remoteCandidate.port === candidate.port; 195 | }; 196 | if (!this.remoteCandidates.some(find)) { 197 | this.remoteCandidates.push(candidate); 198 | } else { 199 | console.warn('duplicate candidate'); 200 | } 201 | 202 | this.candidates.forEach(function (localCandidate) { 203 | if (localCandidate.type === 'srflx') { 204 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-5.1.3.3 205 | return; 206 | } 207 | var localIpFam = net.isIP(localCandidate.ip); 208 | var remoteIpFam = net.isIP(candidate.ip); 209 | if (localIpFam > 0 && remoteIpFam > 0 && localIpFam === remoteIpFam) { 210 | this.checklist.add(new IceCandidatePair(localCandidate, candidate, 211 | this.iceControlling)); 212 | } 213 | }.bind(this)); 214 | 215 | //console.log('candidate pairs:') 216 | //debug.printPairs(this.checklist.candidatePairs); 217 | 218 | this.checklist.prioritize(); 219 | 220 | // Do not freeze all, then unfreeze the first as this conflicts with trickle. 221 | 222 | if (this.checklist.isCompleted()) { 223 | console.warn('not rerunning checks'); 224 | return; 225 | } 226 | 227 | if (!this.checkTimeout) { 228 | this.checkTimeout = setTimeout(this.check.bind(this), this.Ta); 229 | } 230 | }; 231 | 232 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-5.1.4 233 | IceAgent.prototype.check = function () { 234 | //console.log('Ta'); 235 | var pair = this.checklist.highestPriorityWaiting(); 236 | if (pair) { 237 | this.performCheck(pair); 238 | pair.progress(); 239 | } else { 240 | pair = this.checklist.highestPriorityFrozen(); 241 | if (pair) { 242 | pair.unfreeze(); 243 | this.performCheck(pair); 244 | pair.progress(); 245 | } else { 246 | console.log('done with checks'); 247 | if (this.checkTimeout) { 248 | clearTimeout(this.checkTimeout); 249 | this.checkTimeout = null; 250 | } 251 | this.updateStates(); 252 | return; 253 | } 254 | } 255 | this.checkTimeout = setTimeout(this.check.bind(this), this.Ta); 256 | }; 257 | 258 | IceAgent.prototype.performCheck = function (candidatePair) { 259 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.2 260 | 261 | if (!this.remoteUsername) { 262 | throw new Error('missing remote short term credentials'); 263 | } 264 | var username = this.remoteUsername + ':' + this.localUsername; 265 | var config = { 266 | username: username, 267 | password: this.remotePassword, 268 | }; 269 | var requestPacket = vsStun.create.bindingRequest(config); 270 | 271 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.2.1 272 | requestPacket.append.priority(candidatePair.local.priority); 273 | 274 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-7.1.1.2 275 | // Aggressive nomination. 276 | requestPacket.append.useCandidate(); 277 | 278 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2.4 279 | if (this.iceControlling) { 280 | candidatePair.nominated = true; 281 | } 282 | 283 | // save the transactionID, these are used to map the responses back to the 284 | // pair. 285 | this.checklist.pendingRequestTransactions[requestPacket.transactionID] = 286 | candidatePair; 287 | 288 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.2.2 289 | // TODO: need a tie breaker 290 | if (this.iceControlling) { 291 | requestPacket.append.iceControlling(); 292 | } else { 293 | requestPacket.append.iceControlled(); 294 | } 295 | 296 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.2.3 297 | requestPacket.append.username(username); 298 | 299 | requestPacket.append.messageIntegrity(); 300 | 301 | var socket = candidatePair.local._socket; 302 | socket.send(requestPacket.raw, 0, requestPacket.raw.length, 303 | candidatePair.remote.port, candidatePair.remote.ip); 304 | }; 305 | 306 | // https://tools.ietf.org/html/rfc5389#section-15.4 307 | // TODO: 308 | // https://github.com/d-vova/vs-stun/issues/2 309 | function validIntegrity () { 310 | return true; 311 | }; 312 | 313 | function validUsername (packet, username) { 314 | var usernameProvided = Packet.getAttribute(packet).username.split(':')[0]; 315 | return usernameProvided === username; 316 | }; 317 | 318 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.2 319 | IceAgent.prototype.respondToBindingRequest = function (socket, packet, rinfo) { 320 | // perform message integrity check 321 | if (!validIntegrity()) { 322 | throw new Error('bad integrity'); 323 | } 324 | 325 | // check username, first part 326 | if (!validUsername(packet, this.localUsername)) { 327 | throw new Error('unrecognized username'); 328 | } 329 | 330 | var config = { 331 | username: this.localUsername, 332 | password: this.localPassword, 333 | }; 334 | 335 | var responsePacket = vsStun.create.bindingSuccess(config); 336 | var transactionID = Packet.getTransactionID(packet); 337 | Packet.setTransactionID(responsePacket, transactionID); 338 | 339 | // MUST use fingerprint 340 | 341 | // repair role conflicts 342 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.2.1.1 343 | 344 | // compute mapped address 345 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.2.1.2 346 | // https://tools.ietf.org/html/rfc5389#section-15.1 347 | // https://tools.ietf.org/html/rfc5389#section-15.2 348 | // TODO: vs-stun should use the address property, not host. 349 | // https://github.com/d-vova/vs-stun/issues/7 350 | rinfo.host = rinfo.address; 351 | responsePacket.append.mappedAddress(rinfo); 352 | responsePacket.append.xorMappedAddress(rinfo); 353 | 354 | // learn of peer reflexive candidates 355 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.2.1.3 356 | 357 | // triggered checks 358 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.2.1.4 359 | 360 | // update the nominated flag 361 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.2.1.5 362 | 363 | responsePacket.append.messageIntegrity(); 364 | 365 | socket.send(responsePacket.raw, 0, responsePacket.raw.length, rinfo.port, 366 | rinfo.address); 367 | }; 368 | 369 | // validates that we're aware of this candidate pair, and that we haven't 370 | // discovered any peer reflexive candidates. 371 | function matchingSourceDest (source, dest, candidatePair) { 372 | return source.address === candidatePair.remote.ip && 373 | source.port === candidatePair.remote.port && 374 | dest.host === candidatePair.local.ip && 375 | dest.port === candidatePair.local.port; 376 | }; 377 | 378 | IceAgent.prototype.bindingSuccess = function (socket, packet, rinfo) { 379 | var candidatePair = 380 | this.checklist.pendingRequestTransactions[packet.transactionID]; 381 | delete this.checklist.pendingRequestTransactions[packet.transactionID]; 382 | 383 | if (!candidatePair) { 384 | // This usually occurs once for the external STUN server we first spoke to, 385 | // ie. stun.l.google.com 386 | console.warn('binding success for unknown transactionID'); 387 | return; 388 | } 389 | 390 | var dest = Packet.getAttribute(packet).xorMappedAddress; 391 | if (net.isIPv6(dest.host)) { 392 | dest.host = normalizeIPv6(dest.host); 393 | } 394 | 395 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2 396 | if (matchingSourceDest(rinfo, dest, candidatePair)) { 397 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2.3 398 | candidatePair.succeed(); 399 | this.checklist.unfreezeAll(); 400 | } else { 401 | // Found a peer reflexive candidate 402 | // TODO: Issue #52 403 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2.1 404 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.2.1.3 405 | debug.printPeerReflexive(rinfo, dest, candidatePair); 406 | } 407 | 408 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.2.2 409 | // TODO: there's more to do here 410 | candidatePair.valid = true; 411 | this.checklist.validList.push(candidatePair); 412 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-6.1.3.3 413 | this.checklist.checkForFailure(); 414 | }; 415 | 416 | IceAgent.prototype.updateStates = function () { 417 | // https://tools.ietf.org/html/draft-ietf-ice-rfc5245bis-00#section-7.1.2 418 | var anyValidPairs = this.checklist.validList.length > 0; 419 | if (!anyValidPairs && this.checklist.isRunning()) { 420 | console.warn('checks still running'); 421 | return; 422 | } 423 | 424 | if (anyValidPairs && this.checklist.isRunning()) { 425 | this.checklist.removeWaitingAndFrozenCandidates(); 426 | this.checklist.complete(); 427 | } 428 | 429 | if (this.checklist.isCompleted()) { 430 | this.emit('iceconnectionstatechange', 'completed'); 431 | } 432 | 433 | if (this.checklist.isFailed()) { 434 | console.warn('check list is failed'); 435 | this.emit('iceconnectionstatechange', 'failed'); 436 | } 437 | }; 438 | 439 | IceAgent.prototype.getSocketsForValidPairs = function () { 440 | var socketSet = new Set(this.checklist.validList.map(function (pair) { 441 | return pair.local._socket; 442 | })); 443 | // Would love to have the ... spread operator here. 444 | var list = []; 445 | for (var socket of socketSet) { 446 | list.push(socket); 447 | } 448 | return list; 449 | }; 450 | 451 | IceAgent.prototype.shutdown = function () { 452 | this.candidates.forEach(function (candidate) { 453 | candidate._socket.close(); 454 | }); 455 | }; 456 | 457 | module.exports = IceAgent; 458 | 459 | -------------------------------------------------------------------------------- /lib/ice2.js: -------------------------------------------------------------------------------- 1 | var discovery = require('./discovery'); 2 | var Enum = require('enum'); 3 | var EventEmitter = require('events'); 4 | var findOrCreateLocalAddresses = require('./localAddresses'); 5 | var parseIceServers = require('./parseIceServerList'); 6 | var Stream = require('./stream'); // TODO: maybe rename this to IceStream? 7 | var util = require('util'); 8 | 9 | // https://w3c.github.io/webrtc-pc/#rtcicegatheringstate-enum 10 | var gatheringStates = new Enum([ 11 | 'New', 12 | 'Gathering', 13 | 'Complete' 14 | ]); 15 | 16 | // https://w3c.github.io/webrtc-pc/#rtciceconnectionstate-enum 17 | var connectionStates = new Enum([ 18 | 'New', 19 | 'Checking', 20 | 'Connected', 21 | 'Completed', 22 | 'Failed', 23 | 'Disconnected', 24 | 'Closed' 25 | ]); 26 | 27 | function IceAgent (config) { 28 | EventEmitter.call(this); 29 | 30 | // streams are objects that have their own list of components 31 | this.streams = []; 32 | this.nextStreamId = 0; 33 | 34 | this.localAddresses = []; 35 | 36 | this.iceControlling = false; 37 | this.Ta = 20; // ms 38 | 39 | this.gatheringState = gatheringStates.New; 40 | this.connectionState = connectionStates.New; 41 | 42 | this.config = config; 43 | 44 | this.stunServers = []; 45 | this.turnServers = []; 46 | 47 | // TODO: this should be per stream 48 | this.pendingGathers = 0; 49 | 50 | parseIceServers(this, config.iceServers); 51 | }; 52 | util.inherits(IceAgent, EventEmitter); 53 | 54 | // APIs analagous to libnice 55 | //IceAgent.prototype.addLocalAddress = function (addr) {}; 56 | IceAgent.prototype.addStream = function (numComponents) { 57 | var stream = new Stream(this.nextStreamId++, numComponents, this); 58 | this.streams.push(stream); 59 | return stream.id; 60 | }; 61 | 62 | //IceAgent.prototype.removeStream = function (streamId) {}; 63 | //IceAgent.prototype.setPortRange = function () {}; 64 | IceAgent.prototype.gatherCandidates = function (streamId) { 65 | transitionGatherState(this, gatheringStates.Gathering); 66 | var stream = findStream(this, streamId); 67 | var addresses = findOrCreateLocalAddresses(this); 68 | var onError = this.emit.bind(this, 'error'); 69 | calculatePendingGathers(this, addresses.length, stream.components.length, 70 | this.stunServers.length, this.turnServers.length); 71 | addresses.forEach((address) => { 72 | stream.components.forEach((component) => { 73 | var p = discovery.addLocalHostCandidate(streamId, component.componentId, 74 | address); 75 | p.then((candidate) => { 76 | this.emit('icecandidate', candidate); 77 | checkDoneGathering(this); 78 | return this.gatherSrflxCandidate(candidate); 79 | }); 80 | p.catch(onError); 81 | }); 82 | }); 83 | }; 84 | 85 | IceAgent.prototype.setRemoteCredentials = function (ufrag, pwd) {}; 86 | IceAgent.prototype.setLocalCredentials = function (ufrag, pwd) {}; 87 | IceAgent.prototype.getLocalCredentials = function () {}; 88 | IceAgent.prototype.setRemoteCandidates = function (streamId, componentId, candidates) {}; 89 | IceAgent.prototype.getLocalCandidates = function (streamId, componentId) {}; 90 | IceAgent.prototype.getRemoteCandidates = function (streamId, componentId) {}; 91 | //IceAgent.prototype.restart = function () {}; 92 | //IceAgent.prototype.restartStream = function (streamId) {}; 93 | //IceAgent.prototype.setSelectedPair = function (streamId, componentId, lFoundation, rFoundation) {}; 94 | //IceAgent.prototype.getSelectedPair = function (streamId, componentId) {}; 95 | IceAgent.prototype.getSelectedSocket = function (streamId, componentId) {}; 96 | //IceAgent.prototype.setSelectedRemoteCandidate = function (streamId, componentId, candidate) {}; 97 | 98 | // APIs added by me. 99 | IceAgent.prototype.getGatheringState = function () { 100 | return this.gatheringState.key; 101 | }; 102 | IceAgent.prototype.getConnectionState = function () { 103 | return this.connectionState.key; 104 | }; 105 | 106 | IceAgent.prototype.gatherSrflxCandidate = function (hostCandidate) { 107 | var onError = this.emit.bind(this, 'error'); 108 | this.stunServers.forEach((server) => { 109 | var p = discovery.addLocalSrflxCandidate(hostCandidate.streamId, 110 | hostCandidate.componentId, hostCandidate._socket, server); 111 | p.then((candidate) => { 112 | if (candidate) { 113 | this.emit('icecandidate', candidate); 114 | } 115 | checkDoneGathering(this); 116 | }); 117 | p.catch(onError); 118 | }); 119 | }; 120 | 121 | IceAgent.prototype.gatherRelayCandidates = function () {}; 122 | 123 | // private helper functions 124 | function findStream (agent, streamId) { 125 | for (var i = 0, len = agent.streams.length; i < len; ++i) { 126 | var stream = agent.streams[i]; 127 | if (stream.id === streamId) { 128 | return stream; 129 | } 130 | } 131 | // This will happen if you call findStream without calling agent.addStream 132 | // first. 133 | agent.emit('error', 'unable for ICE agent to find a stream with id: ' + 134 | streamId); 135 | }; 136 | 137 | function transitionGatherState (agent, nextState) { 138 | agent.gatheringState = nextState; 139 | agent.emit('icegatheringstatechange'); 140 | }; 141 | 142 | function calculatePendingGathers (agent, numAddresses, numComponents, numStunServers, numTurnServers) { 143 | var numHostCandidates = numAddresses * numComponents; 144 | var numStunCandidates = numHostCandidates * numStunServers; 145 | var numTurnCandidates = numHostCandidates * numTurnServers; 146 | agent.pendingGathers = numHostCandidates + numStunCandidates + numTurnCandidates; 147 | }; 148 | 149 | function checkDoneGathering (agent) { 150 | if (--agent.pendingGathers === 0) { 151 | transitionGatherState(agent, gatheringStates.Complete); 152 | } 153 | }; 154 | 155 | module.exports = IceAgent; 156 | 157 | -------------------------------------------------------------------------------- /lib/localAddresses.js: -------------------------------------------------------------------------------- 1 | var forEachInet = require('forEachInet'); 2 | 3 | function findOrCreateLocalAddresses (agent) { 4 | if (agent.localAddresses.length < 1) { 5 | forEachInet(addNonLinkLocalOrInternalAddresses(agent)); 6 | } 7 | return agent.localAddresses; 8 | }; 9 | 10 | function addNonLinkLocalOrInternalAddresses (agent) { 11 | return function (inet) { 12 | if (!inet.internal && !isLinkLocalAddress(inet)){ 13 | agent.localAddresses.push(inet); 14 | } 15 | }; 16 | }; 17 | 18 | function isLinkLocalAddress (inet) { 19 | // TODO: handle ipv4 link local addresses 20 | return inet.family === 'IPv6' && inet.address.startsWith('fe80::'); 21 | }; 22 | 23 | module.exports = findOrCreateLocalAddresses; 24 | 25 | -------------------------------------------------------------------------------- /lib/parseIceServerList.js: -------------------------------------------------------------------------------- 1 | // This format is so stupid, we need to have a bunch of code to handle parsing. 2 | // https://w3c.github.io/webrtc-pc/#dictionary-rtciceserver-members 3 | 4 | function parseIceServers (agent, iceServers) { 5 | iceServers.forEach(function (iceServer) { 6 | var urls = typeof iceServer.urls === 'string' ? [iceServer.urls] : iceServer.urls; 7 | urls.forEach(function (url) { 8 | var split = url.split(':'); 9 | if (split.length !== 3) { 10 | return; 11 | } 12 | var dict = { 13 | ip: split[1], 14 | port: split[2], 15 | }; 16 | if (split[0] === 'stun' || split[0] === 'stuns') { 17 | agent.stunServers.push(dict); 18 | } else if (split[0] === 'turn' || split[0] === 'turns') { 19 | agent.turnServers.push(dict); 20 | } 21 | }); 22 | }); 23 | }; 24 | 25 | module.exports = parseIceServers; 26 | 27 | -------------------------------------------------------------------------------- /lib/stream.js: -------------------------------------------------------------------------------- 1 | var Component = require('./component'); 2 | var iceCredentials = require('./ice-ufrag-pwd'); 3 | 4 | function Stream (streamId, numComponents, agent) { 5 | this.components = []; 6 | this.agent = agent; 7 | this.id = streamId; 8 | this.localUsername = ''; 9 | this.localPassword = ''; 10 | this.remoteUsername = ''; 11 | this.remotePassword = ''; 12 | 13 | for (var i = 0; i < numComponents; ++i) { 14 | this.components.push(new Component(i + 1, agent, this)); 15 | } 16 | 17 | this.generateCredentials(); 18 | }; 19 | 20 | Stream.prototype.generateCredentials = function () { 21 | this.localUsername = iceCredentials.ufrag(); 22 | this.localPassword = iceCredentials.password(); 23 | }; 24 | 25 | Stream.prototype.getNumComponents = function () { 26 | return this.components.length(); 27 | }; 28 | 29 | module.exports = Stream; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "test": "find test/*.js | xargs -n 1 node | node_modules/tap-nyan/bin/cmd.js" 4 | }, 5 | "dependencies": { 6 | "chalk": "^1.1.1", 7 | "enum": "^2.3.0", 8 | "foreachinet": "^1.0.0", 9 | "ipv6-normalize": "^1.0.1", 10 | "vs-stun": "nickdesaulniers/vs-stun#nick", 11 | "wrtc-ice-cand-parse": "0.0.11" 12 | }, 13 | "devDependencies": { 14 | "tap-nyan": "0.0.2", 15 | "tape": "^4.4.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/ice.js: -------------------------------------------------------------------------------- 1 | var IceAgent = require('../lib/ice'); 2 | var RTCIceCandidate = require('../lib/RTCIceCandidate'); 3 | var test = require('tape'); 4 | 5 | var iceServers = [ 6 | { 7 | urls: 'stun:stun.l.google.com:19302', 8 | } 9 | ]; 10 | 11 | test('constructor should throw for missing ICE server list', function (t) { 12 | t.plan(1); 13 | 14 | t.throws(function () { 15 | var agent = new IceAgent; 16 | }); 17 | }); 18 | 19 | test('gatherHostCandidates', function (t) { 20 | t.plan(1); 21 | 22 | var agent = new IceAgent({ 23 | iceServers: iceServers, 24 | }); 25 | agent.gatherHostCandidates().then(function (candidates) { 26 | candidates.filter(function (candidate) { 27 | return candidate instanceof RTCIceCandidate; 28 | }); 29 | agent.shutdown(); 30 | t.ok(candidates.length > 0, 'had some host candidates'); 31 | }).catch(function (e) { 32 | agent.shutdown(); 33 | t.fail(e); 34 | }); 35 | }); 36 | 37 | -------------------------------------------------------------------------------- /test/ice2.js: -------------------------------------------------------------------------------- 1 | var IceAgent = require('../lib/ice2.js'); 2 | 3 | var agent = new IceAgent({ 4 | iceServers: [ 5 | { 6 | urls: 'stun:stun.l.google.com:19302', 7 | } 8 | ], 9 | }); 10 | 11 | agent.on('icecandidate', function (candidate) { 12 | console.log('ice candidate', candidate); 13 | }); 14 | 15 | agent.on('icecandidateerror', function (e) { 16 | console.error('ice candidate error', e); 17 | }); 18 | 19 | agent.on('iceconnectionstatechange', function (e) { 20 | console.log('connection state change', agent.getConnectionState()); 21 | }); 22 | 23 | agent.on('icegatheringstatechange', function (e) { 24 | console.log('gathering state change', agent.getGatheringState()); 25 | // if state === done 26 | // get local candidates 27 | // get local credentials 28 | // send to peer 29 | }); 30 | 31 | agent.on('error', function (e) { 32 | console.error('error', e); 33 | }); 34 | 35 | var streamId = agent.addStream(1); 36 | agent.gatherCandidates(streamId); 37 | console.log(agent); 38 | 39 | --------------------------------------------------------------------------------