├── .babelrc ├── .editorconfig ├── .eslintignore ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── package-lock.json ├── package.json ├── readme.md └── src ├── candidate.js ├── constants.js ├── index.js └── utils ├── compute-foundation.js ├── compute-priority.js └── internal-address.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": [ 5 | ["istanbul", { 6 | "exclude": [ 7 | "!**/node_modules/**", 8 | "**/test/**" 9 | ] 10 | }] 11 | ] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /fixtures 3 | /.jest-cache 4 | /coverage 5 | 6 | # IDE 7 | .idea 8 | /.vscode 9 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: ['https://www.buymeacoffee.com/reklatsmasters'] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | .idea 18 | *.txt 19 | /.jest-cache 20 | .vscode 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "10" 5 | - "8" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Dmitriy Tsvettsikh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nodertc/ice", 3 | "version": "0.0.0", 4 | "description": "Interactive Connectivity Establishment Protocol", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "npx eslint src" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/nodertc/ice.git" 12 | }, 13 | "keywords": [ 14 | "nodertc", 15 | "ice", 16 | "webrtc", 17 | "nat", 18 | "rfc8445", 19 | "stun" 20 | ], 21 | "author": "Dmitriy Tsvettsikh ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/nodertc/ice/issues" 25 | }, 26 | "engines": { 27 | "node": ">=8.3" 28 | }, 29 | "files": [ 30 | "src" 31 | ], 32 | "homepage": "https://github.com/nodertc/ice#readme", 33 | "devDependencies": { 34 | "@nodertc/eslint-config": "^0.3.0", 35 | "@types/node": "^12.6.3", 36 | "eslint": "^5.16.0", 37 | "jest": "^23.6.0", 38 | "prettier": "^1.18.2" 39 | }, 40 | "dependencies": { 41 | "array-flatten": "^2.1.2", 42 | "is-stun": "^2.0.0", 43 | "readable-stream": "^3.4.0", 44 | "stun": "^2.0.0", 45 | "timeout-refresh": "^1.0.0", 46 | "turbo-crc32": "^1.0.1" 47 | }, 48 | "eslintConfig": { 49 | "extends": "@nodertc" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # @nodertc/ice 2 | 3 | [![Build Status](https://travis-ci.com/nodertc/ice.svg?branch=master)](https://travis-ci.com/nodertc/ice) 4 | [![npm](https://img.shields.io/npm/v/@nodertc/ice.svg)](https://www.npmjs.com/package/@nodertc/ice) 5 | [![node](https://img.shields.io/node/v/@nodertc/ice.svg)](https://www.npmjs.com/package/@nodertc/ice) 6 | [![license](https://img.shields.io/npm/l/@nodertc/ice.svg)](https://www.npmjs.com/package/@nodertc/ice) 7 | [![downloads](https://img.shields.io/npm/dm/@nodertc/ice.svg)](https://www.npmjs.com/package/@nodertc/ice) 8 | [![Coverage Status](https://coveralls.io/repos/github/nodertc/ice/badge.svg?branch=master)](https://coveralls.io/github/nodertc/ice?branch=master) 9 | [![Gitter chat](https://badges.gitter.im/nodertc.png)](https://gitter.im/nodertc/community) 10 | 11 | Interactive Connectivity Establishment (ICE): A Protocol for Network Address Translator (NAT) Traversal. Implements [RFC8445](https://tools.ietf.org/html/rfc8445). 12 | 13 | ### Support 14 | 15 | [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/purple_img.png)](https://www.buymeacoffee.com/reklatsmasters) 16 | 17 | ## License 18 | 19 | MIT, 2019 © Dmitriy Tsvettsikh 20 | -------------------------------------------------------------------------------- /src/candidate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { isIP } = require('net'); 4 | const { 5 | ICE_ROLE_CONTROLLED, 6 | ICE_ROLE_CONTROLLING, 7 | ICE_PROTO_UDP, 8 | ICE_PROTO_TCP, 9 | ICE_TYPE_HOST, 10 | ICE_TYPE_SERVER, 11 | ICE_TYPE_PEER, 12 | ICE_TYPE_RELAYED, 13 | } = require('./constants'); 14 | 15 | const _address = Symbol('address'); 16 | const _port = Symbol('port'); 17 | const _protocol = Symbol('protocol'); 18 | const _type = Symbol('type'); 19 | const _foundation = Symbol('foundation'); 20 | const _role = Symbol('role'); 21 | const _priority = Symbol('priority'); 22 | 23 | /** 24 | * Base class to represent ICE candidate. 25 | */ 26 | class ICECandidate { 27 | /** 28 | * @class ICECandidate 29 | */ 30 | constructor() { 31 | this[_address] = undefined; 32 | this[_port] = undefined; 33 | this[_role] = undefined; 34 | this[_protocol] = undefined; 35 | this[_type] = undefined; 36 | this[_foundation] = undefined; 37 | this[_priority] = undefined; 38 | } 39 | 40 | /** 41 | * Get role. 42 | * @returns {string} 43 | */ 44 | get role() { 45 | return this[_role]; 46 | } 47 | 48 | /** 49 | * @returns {string} 50 | */ 51 | get address() { 52 | return this[_address]; 53 | } 54 | 55 | /** 56 | * @returns {number} 57 | */ 58 | get port() { 59 | return this[_port]; 60 | } 61 | 62 | /** 63 | * @returns {string} 64 | */ 65 | get protocol() { 66 | return this[_protocol]; 67 | } 68 | 69 | /** 70 | * IP address type. 71 | * @returns {string} 72 | */ 73 | get type() { 74 | return this[_type]; 75 | } 76 | 77 | /** 78 | * @returns {string} 79 | */ 80 | get foundation() { 81 | return this[_foundation]; 82 | } 83 | 84 | /** 85 | * @returns {number} 86 | */ 87 | get priority() { 88 | return this[_priority]; 89 | } 90 | } 91 | 92 | /** 93 | * @typedef {Object} ICECandidateOptions 94 | * @property {string} address 95 | * @property {number} port 96 | * @property {string} role 97 | * @property {string} type The type of the candidate. 98 | * @property {string} protocol 99 | * @property {string} foundation A sequence of up to 32 characters. 100 | * @property {number} priority The 32-bit priority of the candidate. 101 | */ 102 | 103 | /** 104 | * Check if argument is valid port. 105 | * @param {number} port 106 | * @returns {boolean} 107 | */ 108 | const isLegalPort = port => Number.isInteger(port) && port > 0 && port <= 0xffff; 109 | 110 | /** 111 | * Create ICE candidate. 112 | * @param {ICECandidateOptions} options 113 | * @returns {ICECandidate} 114 | */ 115 | function createCandidate(options) { 116 | const candidate = new ICECandidate(); 117 | 118 | if (isIP(options.address)) { 119 | candidate[_address] = options.address; 120 | } else { 121 | throw new Error('Invalid ICE candidate address'); 122 | } 123 | 124 | if (isLegalPort(options.port)) { 125 | candidate[_port] = options.port; 126 | } else { 127 | throw new Error('Invalid ICE candidate port'); 128 | } 129 | 130 | switch (options.role) { 131 | case ICE_ROLE_CONTROLLED: 132 | case ICE_ROLE_CONTROLLING: 133 | candidate[_role] = options.role; 134 | break; 135 | default: 136 | throw new Error('Invalid ICE candidate role'); 137 | } 138 | 139 | switch (options.protocol) { 140 | case ICE_PROTO_UDP: 141 | case ICE_PROTO_TCP: 142 | candidate[_protocol] = options.protocol; 143 | break; 144 | default: 145 | throw new Error('Invalid ICE candidate protocol'); 146 | } 147 | 148 | switch (options.type) { 149 | case ICE_TYPE_HOST: 150 | case ICE_TYPE_SERVER: 151 | case ICE_TYPE_PEER: 152 | case ICE_TYPE_RELAYED: 153 | candidate[_type] = options.type; 154 | break; 155 | default: 156 | throw new Error('Invalid ICE candidate type'); 157 | } 158 | 159 | if ( 160 | typeof options.foundation === 'string' && 161 | options.foundation.length > 0 && 162 | options.foundation.length <= 32 163 | ) { 164 | candidate[_foundation] = options.foundation; 165 | } else { 166 | throw new Error('Invalid ICE candidate foundation'); 167 | } 168 | 169 | if (Number.isInteger(options.priority)) { 170 | candidate[_priority] = options.priority; 171 | } else { 172 | throw new TypeError('Invalid ICE candidate priority'); 173 | } 174 | 175 | return candidate; 176 | } 177 | 178 | module.exports = { 179 | ICECandidate, 180 | createCandidate, 181 | }; 182 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ICE_ROLE_CONTROLLING = 'controlling'; 4 | const ICE_ROLE_CONTROLLED = 'controlled'; 5 | 6 | const ICE_PROTO_UDP = 'udp'; 7 | const ICE_PROTO_TCP = 'tcp'; 8 | 9 | // A candidate obtained by binding to a specific port 10 | // from an IP address on the host. This includes IP addresses on 11 | // physical interfaces and logical ones, such as ones obtained 12 | // through VPNs. 13 | const ICE_TYPE_HOST = 'host'; 14 | 15 | // A candidate whose IP address and port 16 | // are a binding allocated by a NAT for an ICE agent after it sends a 17 | // packet through the NAT to a server, such as a STUN server. 18 | const ICE_TYPE_SERVER = 'server-reflexive'; 19 | 20 | // A candidate whose IP address and port are 21 | // a binding allocated by a NAT for an ICE agent after it sends a 22 | // packet through the NAT to its peer. 23 | const ICE_TYPE_PEER = 'peer-reflexive'; 24 | 25 | // A candidate obtained from a relay server, such as a TURN server. 26 | const ICE_TYPE_RELAYED = 'relayed'; 27 | 28 | module.exports = { 29 | ICE_ROLE_CONTROLLING, 30 | ICE_ROLE_CONTROLLED, 31 | ICE_PROTO_UDP, 32 | ICE_PROTO_TCP, 33 | ICE_TYPE_HOST, 34 | ICE_TYPE_SERVER, 35 | ICE_TYPE_PEER, 36 | ICE_TYPE_RELAYED, 37 | }; 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { createCandidate } = require('./candidate'); 4 | const constants = require('./constants'); 5 | 6 | module.exports = { 7 | createCandidate, 8 | ...constants, 9 | }; 10 | -------------------------------------------------------------------------------- /src/utils/compute-foundation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crc32 = require('turbo-crc32/crc32'); 4 | 5 | const isString = s => typeof s === 'string'; 6 | 7 | const checkArgument = (s, name) => { 8 | if (!isString(s)) { 9 | throw new TypeError(`Argument ${name} should be a string`); 10 | } 11 | }; 12 | 13 | /** 14 | * Function to compute foundation string - 15 | * an arbitrary string that is the same for two candidates 16 | * that have the same type, base IP address, protocol (UDP, TCP, 17 | * etc.), and STUN or TURN server. 18 | * @param {string} type Type of ICE candidate. 19 | * @param {string} protocol UDP or TCP. 20 | * @param {string} address Candidate IP address. 21 | * @param {string} [relay] Address of STUN or TURN servers for reflexive and relayed candidates. 22 | * @returns {string} 23 | */ 24 | function computeFoundation(type, protocol, address, relay = '') { 25 | checkArgument(type, 'type'); 26 | checkArgument(protocol, 'protocol'); 27 | checkArgument(address, 'address'); 28 | checkArgument(relay, 'relay'); 29 | 30 | return String(crc32(type + protocol + address + relay)); 31 | } 32 | 33 | module.exports = computeFoundation; 34 | -------------------------------------------------------------------------------- /src/utils/compute-priority.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const X = 2 ** 24; 4 | const Y = 2 ** 8; 5 | const Z = 2 ** 0; 6 | 7 | const checkNumber = (n, name) => { 8 | if (!Number.isInteger(n)) { 9 | throw new TypeError(`Invalid type of an argument ${name}`); 10 | } 11 | }; 12 | 13 | /** 14 | * Function to compute candidate priority. 15 | * See https://tools.ietf.org/html/rfc8445#section-5.1.2.1. 16 | * @param {number} type Type preference. 17 | * @param {number} local Local preference. 18 | * @param {number} component Component ID. 19 | * @returns {number} 20 | */ 21 | function computePriority(type, local, component) { 22 | checkNumber(type, 'type'); 23 | checkNumber(local, 'local'); 24 | checkNumber(component, 'component'); 25 | 26 | if (type < 0 || type > 126) { 27 | throw new Error('Invalid type preference'); 28 | } 29 | 30 | if (local < 0 || local > 65535) { 31 | throw new Error('Invalid local preference'); 32 | } 33 | 34 | if (component < 1 || component > 256) { 35 | throw new Error('Invalid component ID'); 36 | } 37 | 38 | return X * type + Y * local + Z * (256 - component); 39 | } 40 | 41 | module.exports = computePriority; 42 | -------------------------------------------------------------------------------- /src/utils/internal-address.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { networkInterfaces } = require('os'); 4 | const flatten = require('array-flatten'); 5 | 6 | /** 7 | * Get the list of an internal IP addresses. 8 | * @returns {string[]} 9 | */ 10 | function internalAddress() { 11 | const interfaces = flatten(Object.values(networkInterfaces())); 12 | 13 | return interfaces 14 | .filter(iface => iface.family === 'IPv4' && iface.internal === false) 15 | .map(iface => iface.address); 16 | } 17 | 18 | module.exports = internalAddress; 19 | --------------------------------------------------------------------------------