├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── circle.yml ├── examples ├── channel_bind.js ├── channel_data.js └── file_transfer.js ├── index.js ├── lib ├── attributes.js ├── attributes │ ├── channel-number.js │ ├── data.js │ ├── dont-fragment.js │ ├── even-port.js │ ├── lifetime.js │ ├── requested-transport.js │ ├── reservation-token.js │ ├── xor-peer-address.js │ └── xor-relayed-address.js ├── channel_data.js ├── packet.js └── turn_client.js ├── package.json └── test ├── attributes.unit.js ├── packet.unit.js └── turn_client.unit.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | test/all*.sh 3 | examples/all.sh 4 | examples/test-mpeg_512kb.mp4 5 | examples/test.txt 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Configuration File 3 | "maxerr" : 10, // {int} Maximum error before stopping 4 | 5 | // Enforcing 6 | "bitwise" : false, // true: Prohibit bitwise operators (&, |, ^, etc.) 7 | "camelcase" : false, // true: Identifiers must be in camelCase 8 | "curly" : false, // true: Require {} for every new block or scope 9 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 10 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 11 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 12 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 13 | "indent" : 4, // {int} Number of spaces to use for indentation 14 | "latedef" : false, // true: Require variables/functions to be defined before being used 15 | "newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()` 16 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 17 | "noempty" : true, // true: Prohibit use of empty blocks 18 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 19 | "nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment) 20 | "plusplus" : false, // true: Prohibit use of `++` & `--` 21 | "quotmark" : false, // Quotation mark consistency: 22 | // false : do nothing (default) 23 | // true : ensure whatever is used is consistent 24 | // "single" : require single quotes 25 | // "double" : require double quotes 26 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 27 | "unused" : true, // true: Require all defined variables be used 28 | "strict" : true, // true: Requires all functions run in ES5 Strict Mode 29 | "maxparams" : 10, // {int} Max number of formal params allowed per function 30 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 31 | "maxstatements" : false, // {int} Max number statements per function 32 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 33 | "maxlen" : 100, // {int} Max number of characters per line 34 | 35 | // Relaxing 36 | "asi" : true, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 37 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 38 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 39 | "eqnull" : false, // true: Tolerate use of `== null` 40 | //"es5" : true, // true: Allow ES5 syntax (ex: getters and setters) (on by default) 41 | "esnext" : true, // true: Allow ES.next (ES6) syntax (ex: `const`) 42 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 43 | // (ex: `for each`, multiple try/catch, function expression…) 44 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 45 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 46 | "funcscope" : false, // true: Tolerate defining variables inside control statements 47 | "globalstrict" : true, // true: Allow global "use strict" (also enables 'strict') 48 | "iterator" : false, // true: Tolerate using the `__iterator__` property 49 | "lastsemic" : true, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 50 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 51 | "laxcomma" : false, // true: Tolerate comma-first style coding 52 | "loopfunc" : true, // true: Tolerate functions being defined in loops 53 | "multistr" : false, // true: Tolerate multi-line strings 54 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 55 | "notypeof" : false, // true: Tolerate invalid typeof operator values 56 | "proto" : false, // true: Tolerate using the `__proto__` property 57 | "scripturl" : false, // true: Tolerate script-targeted URLs 58 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 59 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 60 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 61 | "validthis" : false, // true: Tolerate using this in a non-constructor function 62 | 63 | // Environments 64 | "browser" : true, // Web Browser (window, document, etc) 65 | "browserify" : false, // Browserify (node.js code in the browser) 66 | "couch" : false, // CouchDB 67 | "devel" : true, // Development/debugging (alert, confirm, etc) 68 | "dojo" : false, // Dojo Toolkit 69 | "jasmine" : false, // Jasmine 70 | "jquery" : false, // jQuery 71 | "mocha" : true, // Mocha 72 | "mootools" : false, // MooTools 73 | "node" : true, // Node.js 74 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 75 | "prototypejs" : false, // Prototype and Scriptaculous 76 | "qunit" : false, // QUnit 77 | "rhino" : false, // Rhino 78 | "shelljs" : false, // ShellJS 79 | "worker" : false, // Web Workers 80 | "wsh" : false, // Windows Scripting Host 81 | "yui" : false, // Yahoo User Interface 82 | 83 | // Custom Globals 84 | "globals" : {} // additional predefined global variables 85 | } 86 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Nico Janssens 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/MicroMinion/turn-js.svg?style=shield)](https://circleci.com/gh/MicroMinion/turn-js) 2 | [![npm](https://img.shields.io/npm/v/turn-js.svg)](https://npmjs.org/package/turn-js) 3 | 4 | # Turn-JS 5 | #### TURN (Traversal Using Relay NAT) library written entirely in JavaScript. 6 | 7 | ## Features 8 | 9 | - implements (most of) the features specified in [RFC 5766](https://tools.ietf.org/html/rfc5766) 10 | - supports TCP and UDP communication 11 | - offers callback and promise based API 12 | - can be browserified (to be used in chrome apps) 13 | 14 | ## Install 15 | 16 | ``` 17 | npm install turn-js 18 | ``` 19 | 20 | ## Usage 21 | 22 | ### Callbacks 23 | 24 | ### Promises 25 | 26 | ## API 27 | 28 | ### `myClient = turn(serverAddr, serverPort, user, pwd, transport)` 29 | 30 | ### `myClient.allocate(function(address) {}, function(error) {})` 31 | 32 | ### `myClient.allocateP()` 33 | 34 | ### `myClient.createPermission(address, function() {}, function(error) {})` 35 | 36 | ### `myClient.createPermission(address)` 37 | 38 | ### `myClient.bindChannel(address, port, channel, lifetime, function() {}, function(error) {})` 39 | 40 | ### `myClient.bindChannelP(address, port, channel)` 41 | 42 | ### `myClient.refresh(lifetime, function() {}, function(error) {})` 43 | 44 | ### `myClient.refreshP(lifetime)` 45 | 46 | ### `myClient.close(function() {}, function(error) {})` 47 | 48 | ### `myClient.sendToRelay(bytes, address, port, function() {}, function(error))` 49 | 50 | ### `myClient.sendToRelayP(bytes, address, port)` 51 | 52 | ### `myClient.sendToChannel(bytes, channel, function() {}, function(error) {})` 53 | 54 | ### `myClient.sendToChannelP(bytes, channel)` 55 | 56 | 57 | ## Events 58 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.9.0 4 | services: 5 | - docker 6 | 7 | dependencies: 8 | post: 9 | - docker info 10 | 11 | test: 12 | pre: 13 | - docker run -d -e EXTERNAL_IP=10.0.4.1 --name=turnserver --restart="on-failure:10" --net=host -p 3478:3478 -p 3478:3478/udp bprodoehl/turnserver 14 | - sudo lxc-attach -n "$(docker inspect --format "{{.Id}}" turnserver)" -- bash -c "echo user=test:test >> /etc/turnserver.conf" 15 | - sudo lxc-attach -n "$(docker inspect --format "{{.Id}}" turnserver)" -- bash -c "echo realm=microminion.io >> /etc/turnserver.conf" 16 | - sudo lxc-attach -n "$(docker inspect --format "{{.Id}}" turnserver)" -- bash -c "echo verbose >> /etc/turnserver.conf" 17 | - docker restart turnserver 18 | - sleep 10 19 | override: 20 | - npm run test-node 21 | - npm run build 22 | 23 | deployment: 24 | npm: 25 | branch: master 26 | commands: 27 | - echo -e "$NPM_USER\n$NPM_PASS\n$NPM_EMAIL" | npm login 28 | - npm run 2npm 29 | -------------------------------------------------------------------------------- /examples/channel_bind.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transports = require('stun-js').transports 4 | var turn = require('../index') 5 | 6 | var argv = require('yargs') 7 | .usage('Usage: $0 [params]') 8 | .demand('a') 9 | .alias('a', 'addr') 10 | .nargs('a', 1) 11 | .describe('a', 'TURN server address') 12 | .demand('p') 13 | .alias('p', 'port') 14 | .nargs('p', 1) 15 | .describe('p', 'TURN server port') 16 | .alias('u', 'user') 17 | .nargs('u', 1) 18 | .describe('u', 'TURN server user account') 19 | .alias('w', 'pwd') 20 | .nargs('w', 1) 21 | .describe('w', 'TURN server user password') 22 | .alias('t', 'transport') 23 | .choices('t', ['tcp', 'udp']) 24 | .default('t', 'udp') 25 | .nargs('t', 1) 26 | .describe('t', 'Transport protocol') 27 | .help('h') 28 | .alias('h', 'help') 29 | .argv 30 | 31 | var clientAlice, clientBob 32 | if (argv.transport === 'udp') { 33 | clientAlice = turn(argv.addr, argv.port, argv.user, argv.pwd) 34 | clientBob = turn(argv.addr, argv.port, argv.user, argv.pwd) 35 | } else { 36 | var transportAlice = new transports.TCP() 37 | clientAlice = turn(argv.addr, argv.port, argv.user, argv.pwd, transportAlice) 38 | var transportBob = new transports.TCP() 39 | clientBob = turn(argv.addr, argv.port, argv.user, argv.pwd, transportBob) 40 | } 41 | var srflxAddressAlice, srflxAddressBob, relayAddressAlice, relayAddressBob 42 | var channelAlice, channelBob 43 | 44 | var testQuestion = 'What is the meaning of life?' 45 | var testAnswer = 'A movie.' 46 | var testRuns = 10 47 | var messagesSent = 0 48 | 49 | var sendRequest = function (onSuccess) { 50 | var bytes = Buffer.from(testQuestion) 51 | clientAlice.sendToChannel( 52 | bytes, 53 | channelBob, 54 | function () { // on success 55 | console.log('question sent from alice to bob') 56 | if (onSuccess) { 57 | onSuccess() 58 | } 59 | }, 60 | function (error) { // on error 61 | console.error(error) 62 | } 63 | ) 64 | } 65 | 66 | var sendReply = function () { 67 | var bytes = Buffer.from(testAnswer) 68 | clientBob.sendToChannel( 69 | bytes, 70 | channelAlice, 71 | function () { // on success 72 | console.log('response sent from bob to alice') 73 | }, 74 | function (error) { // on failure 75 | console.error(error) 76 | } 77 | ) 78 | } 79 | 80 | clientAlice.on('relayed-message', function (bytes, peerAddress) { 81 | var message = bytes.toString() 82 | console.log('alice received response ' + message + ' from ' + JSON.stringify(peerAddress)) 83 | if (messagesSent === testRuns) { 84 | clientAlice.closeP() 85 | .then(function () { 86 | return clientBob.closeP() 87 | }) 88 | .then(function () { 89 | console.log("that's all folks") 90 | process.exit(0) 91 | }) 92 | .catch(function (error) { 93 | console.log('ERROR: ' + error) 94 | }) 95 | } else { 96 | sendRequest(function () { 97 | messagesSent++ 98 | }) 99 | } 100 | }) 101 | 102 | clientBob.on('relayed-message', function (bytes, peerAddress) { 103 | var message = bytes.toString() 104 | console.log('bob received question ' + message + ' from ' + JSON.stringify(peerAddress)) 105 | sendReply() 106 | }) 107 | 108 | // init alice and bob's client + allocate session alice 109 | clientBob.initP() 110 | .then(function () { 111 | return clientAlice.initP() 112 | }) 113 | .then(function () { 114 | return clientAlice.allocateP() 115 | }) 116 | .then(function (allocateAddress) { 117 | srflxAddressAlice = allocateAddress.mappedAddress 118 | relayAddressAlice = allocateAddress.relayedAddress 119 | console.log("alice's srflx address = " + srflxAddressAlice.address + ':' + srflxAddressAlice.port) 120 | console.log("alice's relay address = " + relayAddressAlice.address + ':' + relayAddressAlice.port) 121 | // allocate session bob 122 | return clientBob.allocateP() 123 | }) 124 | .then(function (allocateAddress) { 125 | srflxAddressBob = allocateAddress.mappedAddress 126 | relayAddressBob = allocateAddress.relayedAddress 127 | console.log("bob's address = " + srflxAddressBob.address + ':' + srflxAddressBob.port) 128 | console.log("bob's relay address = " + relayAddressBob.address + ':' + relayAddressBob.port) 129 | // create permission for alice to send messages to bob 130 | return clientBob.createPermissionP(relayAddressAlice.address) 131 | }) 132 | .then(function () { 133 | // create permission for bob to send messages to alice 134 | return clientAlice.createPermissionP(relayAddressBob.address) 135 | }) 136 | .then(function () { 137 | // create channel from alice to bob 138 | return clientAlice.bindChannelP(relayAddressBob.address, relayAddressBob.port) 139 | }) 140 | .then(function (channel) { 141 | channelBob = channel 142 | console.log("alice's channel to bob = " + channelBob) 143 | // create channel from bob to alice 144 | return clientBob.bindChannelP(relayAddressAlice.address, relayAddressAlice.port) 145 | }) 146 | .then(function (channel) { 147 | channelAlice = channel 148 | console.log("bob's channel to alice = " + channelAlice) 149 | // send test message 150 | sendRequest(function () { 151 | messagesSent++ 152 | }) 153 | }) 154 | -------------------------------------------------------------------------------- /examples/channel_data.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var transports = require('stun-js').transports 4 | var turn = require('../index') 5 | 6 | var argv = require('yargs') 7 | .usage('Usage: $0 [params]') 8 | .demand('a') 9 | .alias('a', 'addr') 10 | .nargs('a', 1) 11 | .describe('a', 'TURN server address') 12 | .demand('p') 13 | .alias('p', 'port') 14 | .nargs('p', 1) 15 | .describe('p', 'TURN server port') 16 | .alias('u', 'user') 17 | .nargs('u', 1) 18 | .describe('u', 'TURN server user account') 19 | .alias('w', 'pwd') 20 | .nargs('w', 1) 21 | .describe('w', 'TURN server user password') 22 | .alias('t', 'transport') 23 | .choices('t', ['tcp', 'udp']) 24 | .default('t', 'udp') 25 | .nargs('t', 1) 26 | .describe('t', 'Transport protocol') 27 | .help('h') 28 | .alias('h', 'help') 29 | .argv 30 | 31 | var clientAlice, clientBob 32 | if (argv.transport === 'udp') { 33 | clientAlice = turn(argv.addr, argv.port, argv.user, argv.pwd) 34 | clientBob = turn(argv.addr, argv.port, argv.user, argv.pwd) 35 | } else { 36 | var transportAlice = new transports.TCP() 37 | clientAlice = turn(argv.addr, argv.port, argv.user, argv.pwd, transportAlice) 38 | var transportBob = new transports.TCP() 39 | clientBob = turn(argv.addr, argv.port, argv.user, argv.pwd, transportBob) 40 | } 41 | var srflxAddressAlice, srflxAddressBob, relayAddressAlice, relayAddressBob 42 | 43 | var testQuestion = 'What is the meaning of life?' 44 | var testAnswer = 'A movie.' 45 | var testRuns = 10 46 | var messagesSent = 0 47 | 48 | var sendRequest = function (onSuccess) { 49 | var bytes = Buffer.from(testQuestion) 50 | clientAlice.sendToRelay( 51 | bytes, 52 | relayAddressBob.address, 53 | relayAddressBob.port, 54 | function () { // on success 55 | console.log('question sent from alice to bob') 56 | if (onSuccess) { 57 | onSuccess() 58 | } 59 | }, 60 | function (error) { // on failure 61 | console.error(error) 62 | } 63 | ) 64 | } 65 | 66 | var sendReply = function () { 67 | var bytes = Buffer.from(testAnswer) 68 | clientBob.sendToRelay( 69 | bytes, 70 | relayAddressAlice.address, 71 | relayAddressAlice.port, 72 | function () { 73 | console.log('response sent from bob to alice') 74 | }, 75 | function (error) { 76 | console.error(error) 77 | } 78 | ) 79 | } 80 | 81 | clientAlice.on('relayed-message', function (bytes, peerAddress) { 82 | var message = bytes.toString() 83 | console.log('alice received response: ' + message) 84 | if (messagesSent === testRuns) { 85 | clientAlice.closeP() 86 | .then(function () { 87 | return clientBob.closeP() 88 | }) 89 | .then(function () { 90 | console.log("that's all folks") 91 | process.exit(0) 92 | }) 93 | .catch(function (error) { 94 | console.log('ERROR: ' + error) 95 | }) 96 | } else { 97 | sendRequest(function () { 98 | messagesSent++ 99 | }) 100 | } 101 | }) 102 | 103 | clientBob.on('relayed-message', function (bytes, peerAddress) { 104 | var message = bytes.toString() 105 | console.log('bob received question: ' + message) 106 | sendReply() 107 | }) 108 | 109 | // init alice and bob's client + allocate session alice 110 | clientBob.initP() 111 | .then(function () { 112 | return clientAlice.initP() 113 | }) 114 | .then(function () { 115 | return clientAlice.allocateP() 116 | }) 117 | .then(function (allocateAddress) { 118 | srflxAddressAlice = allocateAddress.mappedAddress 119 | relayAddressAlice = allocateAddress.relayedAddress 120 | console.log("alice's srflx address = " + srflxAddressAlice.address + ':' + srflxAddressAlice.port) 121 | console.log("alice's relay address = " + relayAddressAlice.address + ':' + relayAddressAlice.port) 122 | // allocate session bob 123 | return clientBob.allocateP() 124 | }) 125 | .then(function (allocateAddress) { 126 | srflxAddressBob = allocateAddress.mappedAddress 127 | relayAddressBob = allocateAddress.relayedAddress 128 | console.log("bob's address = " + srflxAddressBob.address + ':' + srflxAddressBob.port) 129 | console.log("bob's relay address = " + relayAddressBob.address + ':' + relayAddressBob.port) 130 | // create permission for alice to send messages to bob 131 | return clientBob.createPermissionP(relayAddressAlice.address) 132 | }) 133 | .then(function () { 134 | // create permission for bob to send messages to alice 135 | return clientAlice.createPermissionP(relayAddressBob.address) 136 | }) 137 | .then(function () { 138 | // send request 139 | sendRequest(function () { 140 | messagesSent++ 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /examples/file_transfer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var fs = require('fs') 4 | var path = require('path') 5 | var transports = require('stun-js').transports 6 | var turn = require('../index') 7 | 8 | var argv = require('yargs') 9 | .usage('Usage: $0 [params]') 10 | .demand('f') 11 | .alias('f', 'file') 12 | .nargs('f', 1) 13 | .describe('f', 'file to transmit via TURN server') 14 | .demand('a') 15 | .alias('a', 'addr') 16 | .nargs('a', 1) 17 | .describe('a', 'TURN server address') 18 | .demand('p') 19 | .alias('p', 'port') 20 | .nargs('p', 1) 21 | .describe('p', 'TURN server port') 22 | .alias('u', 'user') 23 | .nargs('u', 1) 24 | .describe('u', 'TURN server user account') 25 | .alias('w', 'pwd') 26 | .nargs('w', 1) 27 | .describe('w', 'TURN server user password') 28 | .boolean('l') 29 | .describe('l', 'verbose logging') 30 | .alias('l', 'log') 31 | .help('h') 32 | .alias('h', 'help') 33 | .argv 34 | 35 | var clientAlice = turn(argv.addr, argv.port, argv.user, argv.pwd, new transports.TCP()) 36 | var clientBob = turn(argv.addr, argv.port, argv.user, argv.pwd, new transports.TCP()) 37 | var clientAliceClosed = false 38 | var clientBobClosed = false 39 | 40 | var srflxAddressAlice, srflxAddressBob, relayAddressAlice, relayAddressBob 41 | var channelAlice, channelBob 42 | 43 | var startMessageType = 0x00 44 | var dataMessageType = 0x01 45 | var endMessageType = 0x10 46 | 47 | var readStream, writeStream 48 | 49 | var chunkNb = 0 50 | var expSeqNb = 0 51 | 52 | // incoming messages 53 | clientBob.on('relayed-message', function (bytes, peerAddress) { 54 | if (argv.log) { 55 | console.log('bob received ' + bytes.length + ' byte(s) from alice') 56 | } 57 | var type = bytes.slice(0, 1).readUInt8(0) 58 | switch (type) { 59 | case startMessageType: 60 | var filenameBytes = bytes.slice(1, bytes.length) 61 | var filename = 'copy.' + filenameBytes.toString() 62 | writeStream = fs.createWriteStream(filename) 63 | break 64 | case dataMessageType: 65 | var seqNbBytes = bytes.slice(1, 3) 66 | var seqNb = seqNbBytes.readUInt16BE(0) 67 | if (seqNb === expSeqNb) { 68 | expSeqNb++ 69 | } else { 70 | console.error('Woops, expected chunk ' + expSeqNb + ', instead received ' + seqNb) 71 | expSeqNb = seqNb + 1 72 | process.exit(0) 73 | } 74 | readStream.resume() 75 | var data = bytes.slice(3, bytes.length) 76 | writeStream.write(data, 'binary') 77 | break 78 | case endMessageType: 79 | writeStream.end() 80 | clientBob.closeP() 81 | .then(function () { 82 | console.log("bob's client is closed") 83 | clientBobClosed = true 84 | if (clientAliceClosed && clientBobClosed) { 85 | process.exit(0) 86 | } 87 | }) 88 | break 89 | default: 90 | console.error("Add dazed and confused, don't know how to process message type " + type) 91 | } 92 | }) 93 | 94 | // init alice and bob's client + allocate session alice 95 | clientBob.initP() 96 | .then(function () { 97 | return clientAlice.initP() 98 | }) 99 | .then(function () { 100 | return clientAlice.allocateP() 101 | }) 102 | .then(function (allocateAddress) { 103 | srflxAddressAlice = allocateAddress.mappedAddress 104 | relayAddressAlice = allocateAddress.relayedAddress 105 | console.log("alice's srflx address = " + srflxAddressAlice.address + ':' + srflxAddressAlice.port) 106 | console.log("alice's relay address = " + relayAddressAlice.address + ':' + relayAddressAlice.port) 107 | // allocate session bob 108 | return clientBob.allocateP() 109 | }) 110 | .then(function (allocateAddress) { 111 | srflxAddressBob = allocateAddress.mappedAddress 112 | relayAddressBob = allocateAddress.relayedAddress 113 | console.log("bob's address = " + srflxAddressBob.address + ':' + srflxAddressBob.port) 114 | console.log("bob's relay address = " + relayAddressBob.address + ':' + relayAddressBob.port) 115 | // create permission for alice to send messages to bob 116 | return clientBob.createPermissionP(relayAddressAlice.address) 117 | }) 118 | .then(function () { 119 | // create permission for bob to send messages to alice 120 | return clientAlice.createPermissionP(relayAddressBob.address) 121 | }) 122 | .then(function () { 123 | // create channel from alice to bob 124 | return clientAlice.bindChannelP(relayAddressBob.address, relayAddressBob.port) 125 | }) 126 | .then(function (channel) { 127 | channelBob = channel 128 | console.log("alice's channel to bob = " + channelBob) 129 | // create channel from bob to alice 130 | return clientBob.bindChannelP(relayAddressAlice.address, relayAddressAlice.port) 131 | }) 132 | .then(function (channel) { 133 | channelAlice = channel 134 | console.log("bob's channel to alice = " + channelAlice) 135 | // send filename from alice to bob 136 | var filename = path.basename(argv.file) 137 | if (argv.log) { 138 | console.log('alice sends filename ' + filename + ' to bob') 139 | } 140 | var typeByte = Buffer.alloc(1) 141 | typeByte.writeUInt8(startMessageType) 142 | var filenameBytes = Buffer.from(filename) 143 | var bytes = Buffer.concat([typeByte, filenameBytes]) 144 | return clientAlice.sendToChannelP(bytes, channelBob) 145 | }) 146 | .then(function () { 147 | var bufferSize = 16384 - 1 - 2 - 4 // buffer size - type byte - seqnb bytes - channeldata header 148 | // create file readstream and send chunks 149 | readStream = fs.createReadStream(argv.file, { highWaterMark: bufferSize }) 150 | readStream.on('data', function (chunk) { 151 | var typeByte = Buffer.alloc(1) 152 | typeByte.writeUInt8(dataMessageType) 153 | var seqNbBytes = Buffer.alloc(2) 154 | seqNbBytes.writeUInt16BE(chunkNb) 155 | var bytes = Buffer.concat([typeByte, seqNbBytes, chunk]) 156 | readStream.pause() 157 | clientAlice.sendToChannel( 158 | bytes, 159 | channelBob, 160 | function () { // on success 161 | if (argv.log) { 162 | console.log('alice sent chunk of ' + bytes.length + ' bytes to bob') 163 | } 164 | chunkNb++ 165 | // readStream.resume() 166 | }, 167 | function (error) { // on failure 168 | console.error(error) 169 | } 170 | ) 171 | }) 172 | readStream.on('end', function () { 173 | // send end message 174 | var typeByte = Buffer.alloc(1) 175 | typeByte.writeUInt8(endMessageType) 176 | clientAlice.sendToChannelP(typeByte, channelBob) 177 | .then(function () { 178 | if (argv.log) { 179 | console.log('alice sent end message to bob') 180 | } 181 | return clientAlice.closeP() 182 | }) 183 | .then(function () { 184 | console.log("alice's client is closed") 185 | clientAliceClosed = true 186 | if (clientAliceClosed && clientBobClosed) { 187 | process.exit(0) 188 | } 189 | }) 190 | .catch(function (error) { 191 | console.error(error) 192 | }) 193 | }) 194 | }) 195 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Attributes = require('./lib/attributes') 4 | var ChannelData = require('./lib/channel_data') 5 | var Packet = require('./lib/packet') 6 | var transports = require('stun-js').transports 7 | var TurnClient = require('./lib/turn_client') 8 | 9 | module.exports = function createClient (address, port, user, pwd, transport) { 10 | return new TurnClient(address, port, user, pwd, transport) 11 | } 12 | 13 | // TURN components 14 | module.exports.Attributes = Attributes 15 | module.exports.ChannelData = ChannelData 16 | module.exports.Packet = Packet 17 | module.exports.TurnClient = TurnClient 18 | // STUN transports 19 | module.exports.transports = transports 20 | -------------------------------------------------------------------------------- /lib/attributes.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var stun = require('stun-js') 4 | 5 | // STUN attributes 6 | var Attributes = stun.Attributes 7 | 8 | // RFC 5766 (TURN) attributes 9 | Attributes.ChannelNumber = require('./attributes/channel-number') 10 | Attributes.Data = require('./attributes/data') 11 | Attributes.DontFragment = require('./attributes/dont-fragment') 12 | Attributes.EvenPort = require('./attributes/even-port') 13 | Attributes.Lifetime = require('./attributes/lifetime') 14 | Attributes.RequestedTransport = require('./attributes/requested-transport') 15 | Attributes.ReservationToken = require('./attributes/reservation-token') 16 | Attributes.XORPeerAddress = require('./attributes/xor-peer-address') 17 | Attributes.XORRelayedAddress = require('./attributes/xor-relayed-address') 18 | 19 | // RFC 5766 (TURN) 20 | Attributes.CHANNEL_NUMBER = 0x000C 21 | Attributes.DATA = 0x0013 22 | Attributes.DONT_FRAGMENT = 0x001A 23 | Attributes.EVEN_PORT = 0x0018 24 | Attributes.LIFETIME = 0x000D 25 | Attributes.REQUESTED_TRANSPORT = 0x0019 26 | Attributes.RESERVATION_TOKEN = 0x0022 27 | Attributes.XOR_PEER_ADDRESS = 0x0012 28 | Attributes.XOR_RELAYED_ADDRESS = 0x0016 29 | 30 | // RFC 5766 (TURN) 31 | Attributes.TYPES[Attributes.CHANNEL_NUMBER] = Attributes.ChannelNumber 32 | Attributes.TYPES[Attributes.LIFETIME] = Attributes.Lifetime 33 | Attributes.TYPES[Attributes.XOR_PEER_ADDRESS] = Attributes.XORPeerAddress 34 | Attributes.TYPES[Attributes.DATA] = Attributes.Data 35 | Attributes.TYPES[Attributes.XOR_RELAYED_ADDRESS] = Attributes.XORRelayedAddress 36 | Attributes.TYPES[Attributes.EVEN_PORT] = Attributes.EvenPort 37 | Attributes.TYPES[Attributes.REQUESTED_TRANSPORT] = Attributes.RequestedTransport 38 | Attributes.TYPES[Attributes.DONT_FRAGMENT] = Attributes.DontFragment 39 | Attributes.TYPES[Attributes.RESERVATION_TOKEN] = Attributes.ReservationToken 40 | 41 | module.exports = Attributes 42 | -------------------------------------------------------------------------------- /lib/attributes/channel-number.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var winston = require('winston-debug') 4 | var winstonWrapper = require('winston-meta-wrapper') 5 | 6 | var ChannelNumberAttr = function (channel) { 7 | // logging 8 | this._log = winstonWrapper(winston) 9 | this._log.addMeta({ 10 | module: 'turn:attributes' 11 | }) 12 | // verifying channel 13 | if (typeof channel === 'undefined') { 14 | var channelUndefinedError = 'channel-number attribute undefined' 15 | this._log.error(channelUndefinedError) 16 | throw new Error(channelUndefinedError) 17 | } 18 | if (Number(channel) === 'NaN') { 19 | var channelNaNError = 'invalid channel-number attribute' 20 | this._log.error(channelNaNError) 21 | throw new Error(channelNaNError) 22 | } 23 | // init 24 | this.channel = channel 25 | this.type = 0x000C 26 | // done 27 | this._log.debug('channel-number = ' + this.channel) 28 | } 29 | 30 | ChannelNumberAttr.prototype.encode = function () { 31 | // type 32 | var typeBytes = Buffer.alloc(2) 33 | typeBytes.writeUInt16BE(this.type, 0) 34 | // value 35 | var valueBytes = Buffer.alloc(4) 36 | valueBytes.writeUInt16BE(this.channel) 37 | valueBytes.writeUInt16BE(0, 2) // reserved for future use 38 | // length 39 | var lengthBytes = Buffer.alloc(2) 40 | lengthBytes.writeUInt16BE(valueBytes.length, 0) 41 | // combination 42 | var result = Buffer.concat([typeBytes, lengthBytes, valueBytes]) 43 | // done 44 | return result 45 | } 46 | 47 | ChannelNumberAttr.decode = function (attrBytes) { 48 | if (attrBytes.length !== 4) { 49 | throw new Error('invalid channel-number attribute') 50 | } 51 | var channel = attrBytes.readUInt16BE(0) // only two bytes are used 52 | return new ChannelNumberAttr(channel) 53 | } 54 | 55 | module.exports = ChannelNumberAttr 56 | -------------------------------------------------------------------------------- /lib/attributes/data.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var padding = require('stun-js').padding 4 | var winston = require('winston-debug') 5 | var winstonWrapper = require('winston-meta-wrapper') 6 | 7 | var DataAttr = function (bytes) { 8 | // logging 9 | this._log = winstonWrapper(winston) 10 | this._log.addMeta({ 11 | module: 'turn:attributes' 12 | }) 13 | // verifying bytes 14 | if (bytes === undefined) { 15 | var errorMsg = 'invalid bytes attribute' 16 | this._log.error(errorMsg) 17 | throw new Error(errorMsg) 18 | } 19 | // init 20 | this.bytes = bytes 21 | this.type = 0x0013 22 | // done 23 | this._log.debug('data attr: ' + this.bytes) 24 | } 25 | 26 | DataAttr.prototype.encode = function () { 27 | // type 28 | var typeBytes = Buffer.alloc(2) 29 | typeBytes.writeUInt16BE(this.type, 0) 30 | // value 31 | var valueBytes = this.bytes 32 | // length 33 | var lengthBytes = Buffer.alloc(2) 34 | lengthBytes.writeUInt16BE(valueBytes.length, 0) 35 | // padding 36 | var paddingBytes = padding.getBytes(valueBytes.length) 37 | // combination 38 | var result = Buffer.concat([typeBytes, lengthBytes, valueBytes, paddingBytes]) 39 | // done 40 | return result 41 | } 42 | 43 | DataAttr.decode = function (attrBytes) { 44 | return new DataAttr(attrBytes) 45 | } 46 | 47 | module.exports = DataAttr 48 | -------------------------------------------------------------------------------- /lib/attributes/dont-fragment.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var winston = require('winston-debug') 4 | var winstonWrapper = require('winston-meta-wrapper') 5 | 6 | var DontFragmentAttr = function () { 7 | // logging 8 | this._log = winstonWrapper(winston) 9 | this._log.addMeta({ 10 | module: 'turn:attributes' 11 | }) 12 | // init 13 | this.type = 0x001A 14 | // done 15 | this._log.debug("don't fragment attr") 16 | } 17 | 18 | DontFragmentAttr.prototype.encode = function () { 19 | // type 20 | var typeBytes = Buffer.alloc(2) 21 | typeBytes.writeUInt16BE(this.type, 0) 22 | // length 23 | var lengthBytes = Buffer.alloc(2) 24 | lengthBytes.writeUInt16BE(0, 0) 25 | // combination 26 | var result = Buffer.concat([typeBytes, lengthBytes]) 27 | // done 28 | return result 29 | } 30 | 31 | DontFragmentAttr.decode = function (attrBytes) { 32 | return new DontFragmentAttr() 33 | } 34 | 35 | module.exports = DontFragmentAttr 36 | -------------------------------------------------------------------------------- /lib/attributes/even-port.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var padding = require('stun-js').padding 4 | var winston = require('winston-debug') 5 | var winstonWrapper = require('winston-meta-wrapper') 6 | 7 | var EvenPortAttr = function (reserveNextHigherPortNumber) { 8 | // logging 9 | this._log = winstonWrapper(winston) 10 | this._log.addMeta({ 11 | module: 'turn:attributes' 12 | }) 13 | // verify reserveNextHigherPortNumber 14 | if (typeof reserveNextHigherPortNumber !== 'boolean') { 15 | var errorMsg = 'invalid even port attribute' 16 | this._log.error(errorMsg) 17 | throw new Error(errorMsg) 18 | } 19 | // init 20 | this.reserveNextHigherPortNumber = reserveNextHigherPortNumber 21 | this.type = 0x0018 22 | // done 23 | this._log.debug('even port attr w reserve-next-higher-port-number bit set to ' + this.reserveNextHigherPortNumber) 24 | } 25 | 26 | EvenPortAttr.prototype.encode = function () { 27 | // type 28 | var typeBytes = Buffer.alloc(2) 29 | typeBytes.writeUInt16BE(this.type, 0) 30 | // value 31 | var valueBytes = Buffer.alloc(1) 32 | this.reserveNextHigherPortNumber ? valueBytes.writeUInt8(0x80) : valueBytes.writeUInt8(0x00) 33 | // length 34 | var lengthBytes = Buffer.alloc(2) 35 | lengthBytes.writeUInt16BE(valueBytes.length, 0) 36 | // padding 37 | var paddingBytes = padding.getBytes(valueBytes.length) 38 | // combination 39 | var result = Buffer.concat([typeBytes, lengthBytes, valueBytes, paddingBytes]) 40 | // done 41 | return result 42 | } 43 | 44 | EvenPortAttr.decode = function (attrBytes) { 45 | var reserveNextHigherPortNumber = (attrBytes.readUInt8(0) === 0x80) // other bytes are 0 46 | return new EvenPortAttr(reserveNextHigherPortNumber) 47 | } 48 | 49 | module.exports = EvenPortAttr 50 | -------------------------------------------------------------------------------- /lib/attributes/lifetime.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var winston = require('winston-debug') 4 | var winstonWrapper = require('winston-meta-wrapper') 5 | 6 | var LifetimeAttr = function (duration) { 7 | // logging 8 | this._log = winstonWrapper(winston) 9 | this._log.addMeta({ 10 | module: 'turn:attributes' 11 | }) 12 | // verify duration 13 | if (typeof duration !== 'number') { 14 | var errorMsg = 'invalid lifetime attribute' 15 | this._log.error(errorMsg) 16 | throw new Error(errorMsg) 17 | } 18 | // init 19 | this.duration = duration 20 | this.type = 0x000D 21 | // done 22 | this._log.debug('lifetime attr: ' + this.duration) 23 | } 24 | 25 | LifetimeAttr.prototype.encode = function () { 26 | // type 27 | var typeBytes = Buffer.alloc(2) 28 | typeBytes.writeUInt16BE(this.type, 0) 29 | // value 30 | var valueBytes = Buffer.alloc(4) 31 | valueBytes.writeUInt32BE(this.duration, 0) 32 | // length 33 | var lengthBytes = Buffer.alloc(2) 34 | lengthBytes.writeUInt16BE(valueBytes.length, 0) 35 | // combination 36 | var result = Buffer.concat([typeBytes, lengthBytes, valueBytes]) 37 | // done 38 | return result 39 | } 40 | 41 | LifetimeAttr.decode = function (attrBytes) { 42 | if (attrBytes.length !== 4) { 43 | throw new Error('invalid lifetime attribute') 44 | } 45 | var duration = attrBytes.readUInt32BE(0) 46 | return new LifetimeAttr(duration) 47 | } 48 | 49 | module.exports = LifetimeAttr 50 | -------------------------------------------------------------------------------- /lib/attributes/requested-transport.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var winston = require('winston-debug') 4 | var winstonWrapper = require('winston-meta-wrapper') 5 | 6 | var RequestedTransportAttr = function () { 7 | // logging 8 | this._log = winstonWrapper(winston) 9 | this._log.addMeta({ 10 | module: 'turn:attributes' 11 | }) 12 | // init 13 | this.value = 17 // UDP only 14 | this.type = 0x0019 15 | // done 16 | this._log.debug('requested transport attr: ' + this.value) 17 | } 18 | 19 | RequestedTransportAttr.prototype.encode = function () { 20 | // type 21 | var typeBytes = Buffer.alloc(2) 22 | typeBytes.writeUInt16BE(this.type, 0) 23 | // value 24 | var valueBytes = Buffer.alloc(4) 25 | valueBytes.writeUIntBE(this.value, 0, 1) 26 | for (var i = 1; i <= 3; i++) { // RFFU bytes 27 | valueBytes.writeUIntBE(0, i, 1) 28 | } 29 | // length 30 | var lengthBytes = Buffer.alloc(2) 31 | lengthBytes.writeUInt16BE(valueBytes.length, 0) 32 | // combination 33 | var result = Buffer.concat([typeBytes, lengthBytes, valueBytes]) 34 | // done 35 | return result 36 | } 37 | 38 | RequestedTransportAttr.decode = function (attrBytes) { 39 | if (attrBytes.length !== 4) { 40 | throw new Error('invalid requested transport attribute') 41 | } 42 | var value = attrBytes.readUInt32BE(0) 43 | return new RequestedTransportAttr(value) 44 | } 45 | 46 | module.exports = RequestedTransportAttr 47 | -------------------------------------------------------------------------------- /lib/attributes/reservation-token.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var winston = require('winston-debug') 4 | var winstonWrapper = require('winston-meta-wrapper') 5 | 6 | var ReservationTokenAttr = function (token) { 7 | // logging 8 | this._log = winstonWrapper(winston) 9 | this._log.addMeta({ 10 | module: 'turn:attributes' 11 | }) 12 | // verify token 13 | if (token === undefined || Buffer.byteLength(token) !== 8) { 14 | var errorMsg = 'invalid reservation token attribute' 15 | this._log.error(errorMsg) 16 | throw new Error('error') 17 | } 18 | // init 19 | this.token = token 20 | this.type = 0x0022 21 | // done 22 | this._log.debug('reservation token attr: ' + this.token) 23 | } 24 | 25 | ReservationTokenAttr.prototype.encode = function () { 26 | // type 27 | var typeBytes = Buffer.alloc(2) 28 | typeBytes.writeUInt16BE(this.type, 0) 29 | // value 30 | var valueBytes = Buffer.from(this.token) 31 | if (valueBytes.length !== 8) { 32 | throw new Error('invalid reservation token attribute') 33 | } 34 | // length 35 | var lengthBytes = Buffer.alloc(2) 36 | lengthBytes.writeUInt16BE(valueBytes.length, 0) 37 | // combination 38 | var result = Buffer.concat([typeBytes, lengthBytes, valueBytes]) 39 | // done 40 | return result 41 | } 42 | 43 | ReservationTokenAttr.decode = function (attrBytes) { 44 | if (attrBytes.length !== 8) { 45 | throw new Error('invalid reservation-token attribute') 46 | } 47 | var token = attrBytes.toString() 48 | return new ReservationTokenAttr(token) 49 | } 50 | 51 | module.exports = ReservationTokenAttr 52 | -------------------------------------------------------------------------------- /lib/attributes/xor-peer-address.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var addressAttr = require('stun-js').address 4 | var winston = require('winston-debug') 5 | var winstonWrapper = require('winston-meta-wrapper') 6 | 7 | var XORPeerAddressAttr = function (address, port) { 8 | // logging 9 | this._log = winstonWrapper(winston) 10 | this._log.addMeta({ 11 | module: 'turn:attributes' 12 | }) 13 | // verify address 14 | if (address === undefined) { 15 | var errorMsg = 'invalid xor peer address attribute' 16 | this._log.error(errorMsg) 17 | throw new Error(errorMsg) 18 | } 19 | // init 20 | this.address = address 21 | this.port = port || 0 22 | this.type = 0x0012 23 | // done 24 | this._log.debug('xor peer address attr: ' + this.address + ':' + this.port) 25 | } 26 | 27 | XORPeerAddressAttr.prototype.encode = function (magic, tid) { 28 | if (magic === undefined || tid === undefined) { 29 | var errorMsg = 'invalid xorPeerAddressAttr.encode params' 30 | this._log.error(errorMsg) 31 | throw new Error(errorMsg) 32 | } 33 | // type 34 | var typeBytes = Buffer.alloc(2) 35 | typeBytes.writeUInt16BE(this.type, 0) 36 | // value 37 | var valueBytes = addressAttr.encodeXor(this.address, this.port, magic, tid) 38 | // length 39 | var lengthBytes = Buffer.alloc(2) 40 | lengthBytes.writeUInt16BE(valueBytes.length, 0) 41 | // combination 42 | var result = Buffer.concat([typeBytes, lengthBytes, valueBytes]) 43 | // done 44 | return result 45 | } 46 | 47 | XORPeerAddressAttr.decode = function (attrBytes, headerBytes) { 48 | var magicBytes = headerBytes.slice(4, 8) // BE 49 | var tidBytes = headerBytes.slice(8, 20) // BE 50 | 51 | var result = addressAttr.decodeXor(attrBytes, magicBytes, tidBytes) 52 | return new XORPeerAddressAttr(result.address, result.port) 53 | } 54 | 55 | module.exports = XORPeerAddressAttr 56 | -------------------------------------------------------------------------------- /lib/attributes/xor-relayed-address.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var addressAttr = require('stun-js').address 4 | var winston = require('winston-debug') 5 | var winstonWrapper = require('winston-meta-wrapper') 6 | 7 | var XORRelayedAddressAttr = function (address, port) { 8 | // logging 9 | this._log = winstonWrapper(winston) 10 | this._log.addMeta({ 11 | module: 'turn:attributes' 12 | }) 13 | // verify address and port values 14 | if (address === undefined || port === undefined) { 15 | var errorMsg = 'invalid xor relayed address attribute' 16 | this._log.error(errorMsg) 17 | throw new Error(errorMsg) 18 | } 19 | // init 20 | this.address = address 21 | this.port = port 22 | this.type = 0x0016 23 | // done 24 | this._log.debug('xor relayed address attr: ' + this.address + ':' + this.port) 25 | } 26 | 27 | XORRelayedAddressAttr.prototype.encode = function (magic, tid) { 28 | if (magic === undefined || tid === undefined) { 29 | var errorMsg = 'invalid xorRelayedAddressAttr.encode params' 30 | this._log.error(errorMsg) 31 | throw new Error(errorMsg) 32 | } 33 | // type 34 | var typeBytes = Buffer.alloc(2) 35 | typeBytes.writeUInt16BE(this.type, 0) 36 | // value 37 | var valueBytes = addressAttr.encodeXor(this.address, this.port, magic, tid) 38 | // length 39 | var lengthBytes = Buffer.alloc(2) 40 | lengthBytes.writeUInt16BE(valueBytes.length, 0) 41 | // combination 42 | var result = Buffer.concat([typeBytes, lengthBytes, valueBytes]) 43 | // done 44 | return result 45 | } 46 | 47 | XORRelayedAddressAttr.decode = function (attrBytes, headerBytes) { 48 | var magicBytes = headerBytes.slice(4, 8) // BE 49 | var tidBytes = headerBytes.slice(8, 20) // BE 50 | 51 | var result = addressAttr.decodeXor(attrBytes, magicBytes, tidBytes) 52 | return new XORRelayedAddressAttr(result.address, result.port) 53 | } 54 | 55 | module.exports = XORRelayedAddressAttr 56 | -------------------------------------------------------------------------------- /lib/channel_data.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var padding = require('stun-js').padding 4 | var winston = require('winston-debug') 5 | var winstonWrapper = require('winston-meta-wrapper') 6 | 7 | var _log = winstonWrapper(winston) 8 | _log.addMeta({ 9 | module: 'turn:channel-data' 10 | }) 11 | 12 | // channel-data class 13 | var ChannelData = function (channel, bytes) { 14 | // logging 15 | this._log = winstonWrapper(winston) 16 | this._log.addMeta({ 17 | module: 'turn:channel-data' 18 | }) 19 | // verify channel and bytes 20 | if (bytes === undefined) { 21 | var undefinedBytesError = 'invalid channel-data attribute: bytes = undefined' 22 | this._log.error(undefinedBytesError) 23 | throw new Error(undefinedBytesError) 24 | } 25 | if (channel === undefined) { 26 | var undefinedChannelError = 'invalid channel-data attribute: channel = undefined' 27 | this._log.error(undefinedChannelError) 28 | throw new Error(undefinedChannelError) 29 | } 30 | // init 31 | this.channel = channel 32 | this.bytes = bytes 33 | // done 34 | this._log.debug('channel-data attrs: channel = ' + this.channel + ', length = ' + bytes.length + ' bytes') 35 | } 36 | 37 | // packet header length 38 | ChannelData.HEADER_LENGTH = 4 39 | 40 | // see RFC 5766, sct 11.5 41 | ChannelData.prototype.encode = function () { 42 | // create channel bytes 43 | var channelBytes = Buffer.alloc(2) 44 | channelBytes.writeUInt16BE(this.channel, 0) 45 | // create data bytes 46 | var dataBytes = this.bytes 47 | // create length bytes 48 | var lengthBytes = Buffer.alloc(2) 49 | lengthBytes.writeUInt16BE(dataBytes.length) 50 | // padding 51 | var paddingBytes = padding.getBytes(dataBytes.length) 52 | // glue everything together 53 | var message = Buffer.concat([channelBytes, lengthBytes, dataBytes, paddingBytes]) 54 | return message 55 | } 56 | 57 | ChannelData.decode = function (bytes, isFrame) { 58 | // check if packet starts with 0b01 59 | if (!ChannelData._isChannelDataPacket(bytes)) { 60 | _log.debug('this is not a ChannelData packet') 61 | return 62 | } 63 | // check if buffer contains enough bytes to parse header 64 | if (bytes.length < ChannelData.HEADER_LENGTH) { 65 | _log.debug('not enough bytes to parse ChannelData header, giving up') 66 | return 67 | } 68 | // decode channel 69 | var channelBytes = bytes.slice(0, 2) 70 | var channel = channelBytes.readUInt16BE(0) 71 | // check channel number 72 | if (channel < 0x4000) { 73 | _log.debug('channel numbers < 0x4000 are reserved and not available for use, since they conflict with the STUN header') 74 | return 75 | } 76 | if (channel > 0x7FFF) { 77 | _log.debug('channel numbers > 0x7FFF are unassigned') 78 | return 79 | } 80 | // decode data length 81 | var lengthBytes = bytes.slice(2, ChannelData.HEADER_LENGTH) 82 | var dataLength = lengthBytes.readUInt16BE(0) 83 | // check if buffer contains enough bytes to parse channel data 84 | if (bytes.length < dataLength) { 85 | _log.debug('not enough bytes to parse channel data, giving up') 86 | return 87 | } 88 | // get data bytes 89 | var dataBytes = bytes.slice(ChannelData.HEADER_LENGTH, ChannelData.HEADER_LENGTH + dataLength) 90 | // get padding bytes if this is not a frame (i.e. bytes originate from TCP connection) -- and if present, then silently discard them 91 | var packetLength = ChannelData.HEADER_LENGTH + dataLength + ((4 - dataLength % 4) % 4) // header + data + padding to the nearest multiple of 4 92 | if (!isFrame && bytes.length < packetLength) { 93 | _log.debug('not enough bytes to parse channel data padding bytes, giving up') 94 | return 95 | } 96 | var paddingBytes = bytes.slice(ChannelData.HEADER_LENGTH + dataLength, packetLength) // padding bytes, if any, are silently discarded 97 | // generate result 98 | var result = {} 99 | result.packet = new ChannelData(channel, dataBytes) 100 | result.remainingBytes = bytes.slice(packetLength, bytes.length) 101 | // do we expect remaining bytes? 102 | if (isFrame && result.remainingBytes.length !== 0) { 103 | var errorMsg = 'not expecting remaining bytes after processing full frame packet' 104 | _log.error(errorMsg) 105 | throw new Error(errorMsg) 106 | } 107 | // done 108 | return result 109 | } 110 | 111 | // check if this is a channel data packet (starts with 0b01) 112 | ChannelData._isChannelDataPacket = function (bytes) { 113 | var block = bytes.readUInt8(0) 114 | var bit1 = containsFlag(block, 0x80) 115 | var bit2 = containsFlag(block, 0x40) 116 | return (!bit1 && bit2) 117 | } 118 | 119 | function containsFlag (number, flag) { 120 | return (number & flag) === flag 121 | } 122 | 123 | module.exports = ChannelData 124 | -------------------------------------------------------------------------------- /lib/packet.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var stun = require('stun-js') 4 | 5 | // STUN packet 6 | var Packet = stun.Packet 7 | 8 | // RFC 5766 (TURN) 9 | Packet.METHOD.ALLOCATE = 0x003 10 | Packet.METHOD.REFRESH = 0x004 11 | Packet.METHOD.SEND = 0x006 12 | Packet.METHOD.DATA = 0x007 13 | Packet.METHOD.CREATEPERMISSION = 0x008 14 | Packet.METHOD.CHANNELBIND = 0x009 15 | 16 | module.exports = Packet 17 | -------------------------------------------------------------------------------- /lib/turn_client.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var merge = require('merge') 4 | var Q = require('q') 5 | var winston = require('winston-debug') 6 | var winstonWrapper = require('winston-meta-wrapper') 7 | 8 | var Attributes = require('./attributes') 9 | var ChannelData = require('./channel_data') 10 | var Packet = require('./packet') 11 | var StunClient = require('stun-js').StunClient 12 | 13 | var pjson = require('../package.json') 14 | var defaultSoftwareTag = pjson.name + ' v' + pjson.version 15 | 16 | var _log = winstonWrapper(winston) 17 | _log.addMeta({ 18 | module: 'turn:client' 19 | }) 20 | 21 | // Constructor 22 | class TurnClient extends StunClient { 23 | 24 | static CHANNEL_BINDING_LIFETIME = 600 25 | static DEFAULT_ALLOCATION_LIFETIME = 600 26 | static CREATE_PERMISSION_LIFETIME = 300 27 | static DEFAULTS = { 28 | software: defaultSoftwareTag, 29 | lifetime: TurnClient.DEFAULT_ALLOCATION_LIFETIME, 30 | dontFragment: false 31 | } 32 | 33 | constructor(host, port, username, password, transport) { 34 | super(host, port, transport); 35 | // init 36 | this.username = username 37 | this.password = password 38 | // logging 39 | this._log = winstonWrapper(winston) 40 | this._log.addMeta({ 41 | module: 'turn:client' 42 | }) 43 | // register channel_data decoder 44 | this.decoders.push({ 45 | decoder: ChannelData.decode, 46 | listener: this.dispatchChannelDataPacket.bind(this) 47 | }) 48 | } 49 | 50 | /** TurnClient opertions */ 51 | 52 | // Execute allocation 53 | allocateP() { 54 | var self = this 55 | // send an allocate request without credentials 56 | return this.sendAllocateP() 57 | .then(function (allocateReply) { 58 | var errorCode = allocateReply.getAttribute(Attributes.ERROR_CODE) 59 | // check if the reply includes an error code attr 60 | if (errorCode) { 61 | // check of this is a 401 Unauthorized or a 438 Stale Nonce error 62 | if ([401, 438].indexOf(errorCode.code) !== -1) { 63 | // throw error if username and password are undefined 64 | if (self.username === undefined || self.password === undefined) { 65 | throw new Error('allocate error: unauthorized access, while username and/or password are undefined') 66 | } 67 | // create a new allocate request 68 | var args = {} 69 | self.nonce = args.nonce = allocateReply.getAttribute(Attributes.NONCE).value 70 | self.realm = args.realm = allocateReply.getAttribute(Attributes.REALM).value 71 | args.user = self.username 72 | args.pwd = self.password 73 | return self.sendAllocateP(args) 74 | } else { 75 | // throw an error if error code !== 401 76 | self._log.error('allocate error: ' + errorCode.reason) 77 | self._log.error('allocate response: ' + JSON.stringify(allocateReply)) 78 | throw new Error('allocate error: ' + errorCode.reason) 79 | } 80 | } else { 81 | // process allocate reply in next call 82 | return Q.fcall(function () { 83 | return allocateReply 84 | }) 85 | } 86 | }) 87 | // let's process that allocate reply 88 | .then(function (allocateReply) { 89 | var errorCode = allocateReply.getAttribute(Attributes.ERROR_CODE) 90 | // check if the reply includes an error code attr 91 | if (errorCode) { 92 | throw new Error('allocate error: ' + errorCode.reason) 93 | } 94 | // store mapped address 95 | var mappedAddressAttr = allocateReply.getAttribute(Attributes.XOR_MAPPED_ADDRESS) 96 | if (!mappedAddressAttr) { 97 | mappedAddressAttr = allocateReply.getAttribute(Attributes.MAPPED_ADDRESS) 98 | } 99 | self.mappedAddress = { 100 | address: mappedAddressAttr.address, 101 | port: mappedAddressAttr.port 102 | } 103 | // store relayed address 104 | var relayedAddressAttr = allocateReply.getAttribute(Attributes.XOR_RELAYED_ADDRESS) 105 | self.relayedAddress = { 106 | address: relayedAddressAttr.address, 107 | port: relayedAddressAttr.port 108 | } 109 | // retrieve lifetime attr, if present 110 | var lifetimeAttr = allocateReply.getAttribute(Attributes.LIFETIME) 111 | // create and return result 112 | var result = { 113 | mappedAddress: self.mappedAddress, 114 | relayedAddress: self.relayedAddress 115 | } 116 | if (lifetimeAttr) { 117 | result.lifetime = lifetimeAttr.duration 118 | } 119 | 120 | return Q.fcall(function () { 121 | return result 122 | }) 123 | }) 124 | } 125 | 126 | allocate(onSuccess, onFailure) { 127 | if (onSuccess === undefined || onFailure === undefined) { 128 | var errorMsg = 'allocate callback handlers are undefined' 129 | this._log.error(errorMsg) 130 | throw new Error(errorMsg) 131 | } 132 | this.allocateP() 133 | .then(function (result) { 134 | onSuccess(result) 135 | }) 136 | .catch(function (error) { 137 | onFailure(error) 138 | }) 139 | } 140 | 141 | // Create permission to send data to a peer address 142 | createPermissionP(address) { 143 | if (address === undefined) { 144 | var errorMsg = 'create permission requires specified peer address' 145 | this._log.error(errorMsg) 146 | throw new Error(errorMsg) 147 | } 148 | // send a create permission request 149 | var args = {} 150 | args.nonce = this.nonce 151 | args.realm = this.realm 152 | args.user = this.username 153 | args.pwd = this.password 154 | args.address = address 155 | return this.sendCreatePermissionP(args) 156 | .then(function (createPermissionReply) { 157 | var errorCode = createPermissionReply.getAttribute(Attributes.ERROR_CODE) 158 | // check if the reply includes an error code attr 159 | if (errorCode) { 160 | throw new Error('create permission error ' + errorCode.reason) 161 | } 162 | // done 163 | }) 164 | } 165 | 166 | createPermission = function (address, onSuccess, onFailure) { 167 | if (onSuccess === undefined || onFailure === undefined) { 168 | var undefinedCbError = 'create permission callback handlers are undefined' 169 | this._log.error(undefinedCbError) 170 | throw new Error(undefinedCbError) 171 | } 172 | if (address === undefined) { 173 | var undefinedAddressError = 'create permission requires specified peer address' 174 | this._log.error(undefinedAddressError) 175 | throw new Error(undefinedAddressError) 176 | } 177 | this.createPermissionP(address) 178 | .then(function () { 179 | onSuccess() 180 | }) 181 | .catch(function (error) { 182 | onFailure(error) 183 | }) 184 | } 185 | 186 | // Create channel 187 | bindChannelP(address, port, channel) { 188 | if (address === undefined || port === undefined) { 189 | var undefinedAddressError = 'channel bind requires specified peer address and port' 190 | this._log.error(undefinedAddressError) 191 | throw new Error(undefinedAddressError) 192 | } 193 | // create channel id 194 | var min = 0x4000 195 | var max = 0x7FFF 196 | if (channel !== undefined) { 197 | if (channel < min || channel > max) { 198 | var incorrectChannelError = 'channel id must be >= 0x4000 and =< 0x7FFF' 199 | this._log.error(incorrectChannelError) 200 | throw new Error(incorrectChannelError) 201 | } 202 | } else { 203 | channel = Math.floor(Math.random() * (max - min + 1)) + min 204 | } 205 | // send a channel bind request 206 | var args = {} 207 | args.nonce = this.nonce 208 | args.realm = this.realm 209 | args.user = this.username 210 | args.pwd = this.password 211 | args.address = address 212 | args.channel = channel 213 | args.port = port 214 | return this.sendChannelBindP(args) 215 | .then(function (channelBindReply) { 216 | var errorCode = channelBindReply.getAttribute(Attributes.ERROR_CODE) 217 | // check if the reply includes an error code attr 218 | if (errorCode) { 219 | throw new Error('bind error: ' + errorCode.reason) 220 | } 221 | return Q.fcall(function () { 222 | return channel 223 | }) 224 | }) 225 | } 226 | 227 | bindChannel = function (address, port, channel, onSuccess, onFailure) { 228 | if (onSuccess === undefined || onFailure === undefined) { 229 | var undefinedCbError = 'bind callback handlers are undefined' 230 | this._log.error(undefinedCbError) 231 | throw new Error(undefinedCbError) 232 | } 233 | if (address === undefined || port === undefined) { 234 | var undefinedAddressError = 'channel bind requires specified peer address and port' 235 | this._log.error(undefinedAddressError) 236 | throw new Error(undefinedAddressError) 237 | } 238 | this.bindChannelP(address, port, channel) 239 | .then(function (duration) { 240 | onSuccess(duration) 241 | }) 242 | .catch(function (error) { 243 | onFailure(error) 244 | }) 245 | } 246 | 247 | // Execute refresh 248 | refreshP = function (lifetime) { 249 | if (lifetime === undefined) { 250 | var undefinedLifetimeError = 'lifetime is undefined' 251 | this._log.error(undefinedLifetimeError) 252 | throw new Error(undefinedLifetimeError) 253 | } 254 | var self = this 255 | // send refresh request 256 | var args = {} 257 | args.nonce = this.nonce 258 | args.realm = this.realm 259 | args.user = this.username 260 | args.pwd = this.password 261 | args.lifetime = lifetime 262 | return this.sendRefreshP(args) 263 | .then(function (refreshReply) { 264 | var errorCode = refreshReply.getAttribute(Attributes.ERROR_CODE) 265 | // check if the reply includes an error code attr 266 | if (errorCode) { 267 | // check of this is a 438 Stale nonce error 268 | if (errorCode.code === 438) { 269 | // create a new refresh request 270 | var args = {} 271 | self.nonce = args.nonce = refreshReply.getAttribute(Attributes.NONCE).value 272 | self.realm = args.realm = refreshReply.getAttribute(Attributes.REALM).value 273 | args.user = self.username 274 | args.pwd = self.password 275 | return self.sendRefreshP(args) 276 | } else { 277 | // throw an error if error code !== 438 278 | throw new Error('refresh error: ' + refreshReply.getAttribute(Attributes.ERROR_CODE).reason) 279 | } 280 | } else { 281 | // process refresh reply in next call 282 | return Q.fcall(function () { 283 | return refreshReply 284 | }) 285 | } 286 | }) 287 | .then(function (refreshReply) { 288 | var errorCode = refreshReply.getAttribute(Attributes.ERROR_CODE) 289 | // check if the reply includes an error code attr 290 | if (errorCode) { 291 | throw new Error('refresh error: ' + errorCode.reason) 292 | } 293 | // otherwise retrieve and return lifetime 294 | var lifetime = refreshReply.getAttribute(Attributes.LIFETIME).duration 295 | return Q.fcall(function () { 296 | return lifetime 297 | }) 298 | }) 299 | } 300 | 301 | refresh(lifetime, onSuccess, onFailure) { 302 | if (lifetime === undefined) { 303 | var undefinedLifetimeError = 'lifetime is undefined' 304 | this._log.error(undefinedLifetimeError) 305 | throw new Error(undefinedLifetimeError) 306 | } 307 | if (onSuccess === undefined || onFailure === undefined) { 308 | var undefinedCbError = 'refresh callback handlers are undefined' 309 | this._log.error(undefinedCbError) 310 | throw new Error(undefinedCbError) 311 | } 312 | this.refreshP(lifetime) 313 | .then(function (duration) { 314 | onSuccess(duration) 315 | }) 316 | .catch(function (error) { 317 | onFailure(error) 318 | }) 319 | } 320 | 321 | // Close this socket 322 | closeP() { 323 | const superCloseP = super.closeP.bind(this); 324 | return this.refreshP(0) 325 | .then(function () { 326 | return superCloseP 327 | }) 328 | } 329 | 330 | close(onSuccess, onFailure) { 331 | if (onSuccess === undefined || onFailure === undefined) { 332 | var errorMsg = 'close callback handlers are undefined' 333 | this._log.error(errorMsg) 334 | throw new Error(errorMsg) 335 | } 336 | var self = this 337 | this.closeP() 338 | .then(function () { 339 | onSuccess() 340 | }) 341 | .catch(function (error) { 342 | self._log.error('closing socket failed: ' + error.message) 343 | onFailure(errorMsg) 344 | }) 345 | } 346 | 347 | /** Message transmission */ 348 | 349 | // Send TURN allocation 350 | sendAllocateP(args) { 351 | this._log.debug('send allocate (using promises)') 352 | var message = composeAllocateRequest(args) 353 | return this.sendStunRequestP(message) 354 | } 355 | 356 | sendAllocate(args, onSuccess, onFailure) { 357 | this._log.debug('send allocate') 358 | if (onSuccess === undefined || onFailure === undefined) { 359 | var errorMsg = 'send allocate callback handlers are undefined' 360 | this._log.error(errorMsg) 361 | throw new Error(errorMsg) 362 | } 363 | this.sendAllocateP(args) 364 | .then(function (reply) { 365 | onSuccess(reply) 366 | }) 367 | .catch(function (error) { 368 | onFailure(error) 369 | }) 370 | } 371 | 372 | // Send TURN create permission 373 | sendCreatePermissionP(args) { 374 | this._log.debug('send create permission (using promises)') 375 | var message = composeCreatePermissionRequest(args) 376 | return this.sendStunRequestP(message) 377 | } 378 | 379 | sendCreatePermission(args, onSuccess, onFailure) { 380 | this._log.debug('send create permission') 381 | if (onSuccess === undefined || onFailure === undefined) { 382 | var errorMsg = 'send create permission callback handlers are undefined' 383 | this._log.error(errorMsg) 384 | throw new Error(errorMsg) 385 | } 386 | this.sendCreatePermissionP(args) 387 | .then(function (reply) { 388 | onSuccess(reply) 389 | }) 390 | .catch(function (error) { 391 | onFailure(error) 392 | }) 393 | } 394 | 395 | // Send TURN channel bind 396 | sendChannelBindP(args) { 397 | this._log.debug('send channel bind (using promises)') 398 | var message = composeChannelBindRequest(args) 399 | return this.sendStunRequestP(message) 400 | } 401 | 402 | sendChannelBind(args, onSuccess, onFailure) { 403 | this._log.debug('send channel bind') 404 | if (onSuccess === undefined || onFailure === undefined) { 405 | var errorMsg = 'send channel bind callback handlers are undefined' 406 | this._log.error(errorMsg) 407 | throw new Error(errorMsg) 408 | } 409 | this.sendChannelBindP(args) 410 | .then(function (reply) { 411 | onSuccess(reply) 412 | }) 413 | .catch(function (error) { 414 | onFailure(error) 415 | }) 416 | } 417 | 418 | // Send TURN refresh 419 | sendRefreshP(args) { 420 | this._log.debug('send refresh (using promises)') 421 | var message = composeRefreshRequest(args) 422 | return this.sendStunRequestP(message) 423 | } 424 | 425 | sendRefresh(args, onSuccess, onFailure) { 426 | this._log.debug('send refresh') 427 | if (onSuccess === undefined || onFailure === undefined) { 428 | var errorMsg = 'send refresh callback handlers are undefined' 429 | this._log.error(errorMsg) 430 | throw new Error(errorMsg) 431 | } 432 | this.sendRefreshP(args) 433 | .then(function (reply) { 434 | onSuccess(reply) 435 | }) 436 | .catch(function (error) { 437 | onFailure(error) 438 | }) 439 | } 440 | 441 | // Send data via relay/turn server 442 | sendToRelayP(bytes, address, port) { 443 | var args = { 444 | address: address, 445 | port: port, 446 | bytes: bytes 447 | } 448 | var message = composeSendIndication(args) 449 | return this.sendStunIndicationP(message) 450 | } 451 | 452 | sendToRelay(bytes, address, port, onSuccess, onFailure) { 453 | this._log.debug('send data') 454 | if (onSuccess === undefined || onFailure === undefined) { 455 | var errorMsg = 'send data callback handlers are undefined' 456 | this._log.error(errorMsg) 457 | throw new Error(errorMsg) 458 | } 459 | this.sendToRelayP(bytes, address, port) 460 | .then(function () { 461 | onSuccess() 462 | }) 463 | .catch(function (error) { 464 | onFailure(error) 465 | }) 466 | } 467 | 468 | // Send channel data via relay/turn server 469 | sendToChannelP(bytes, channel) { 470 | var args = { 471 | channel: channel, 472 | bytes: bytes 473 | } 474 | var message = composeChannelDataMessage(args) 475 | return this.sendStunIndicationP(message) 476 | } 477 | 478 | sendToChannel(bytes, channel, onSuccess, onFailure) { 479 | this._log.debug('send channel data') 480 | if (onSuccess === undefined || onFailure === undefined) { 481 | var errorMsg = 'send channel data callback handlers are undefined' 482 | this._log.error(errorMsg) 483 | throw new Error(errorMsg) 484 | } 485 | this.sendToChannelP(bytes, channel) 486 | .then(function () { 487 | onSuccess() 488 | }) 489 | .catch(function (error) { 490 | onFailure(error) 491 | }) 492 | } 493 | 494 | /** Message arrival */ 495 | 496 | // Incoming STUN indication 497 | onIncomingStunIndication(stunPacket, rinfo) { 498 | if (stunPacket.method === Packet.METHOD.DATA) { 499 | var dataBytes = stunPacket.getAttribute(Attributes.DATA).bytes 500 | var xorPeerAddress = stunPacket.getAttribute(Attributes.XOR_PEER_ADDRESS) 501 | this.emit('relayed-message', dataBytes, { 502 | address: xorPeerAddress.address, 503 | port: xorPeerAddress.port 504 | }) 505 | } else { 506 | const superOnIncomingStunIndication = super.onIncomingStunIndication.bind(this); 507 | superOnIncomingStunIndication(stunPacket, rinfo) 508 | } 509 | } 510 | 511 | // Dispatch ChannelData packet 512 | dispatchChannelDataPacket(packet, rinfo) { 513 | this.emit('relayed-message', packet.bytes, rinfo, packet.channel) 514 | } 515 | } 516 | 517 | /** Message composition */ 518 | 519 | function composeAllocateRequest (args) { 520 | var margs = merge(Object.create(TurnClient.DEFAULTS), args) 521 | // create attrs 522 | var attrs = new Attributes() 523 | _addSecurityAttributes(attrs, margs) 524 | attrs.add(new Attributes.Software(margs.software)) 525 | attrs.add(new Attributes.RequestedTransport()) 526 | if (margs.dontFragment !== undefined) { 527 | attrs.add(new Attributes.DontFragment()) 528 | } 529 | if (margs.lifetime !== undefined) { 530 | attrs.add(new Attributes.Lifetime(margs.lifetime)) 531 | } 532 | // create allocate packet 533 | var packet = new Packet(Packet.METHOD.ALLOCATE, Packet.TYPE.REQUEST, attrs) 534 | // encode packet 535 | var message = packet.encode() 536 | return message 537 | } 538 | 539 | function composeCreatePermissionRequest (args) { 540 | // check args 541 | if (args === undefined) { 542 | var undefinedArgsError = 'invalid create-permission attributes: args = undefined' 543 | _log.error(undefinedArgsError) 544 | throw new Error(undefinedArgsError) 545 | } 546 | if (args.address === undefined) { 547 | var undefinedAddressError = 'invalid create-permission attributes: args.address = undefined' 548 | _log.error(undefinedAddressError) 549 | throw new Error(undefinedAddressError) 550 | } 551 | // create attrs 552 | var attrs = new Attributes() 553 | _addSecurityAttributes(attrs, args) 554 | attrs.add(new Attributes.XORPeerAddress(args.address)) 555 | // create createPermission packet 556 | var packet = new Packet(Packet.METHOD.CREATEPERMISSION, Packet.TYPE.REQUEST, attrs) 557 | // encode packet 558 | var message = packet.encode() 559 | return message 560 | } 561 | 562 | function composeSendIndication (args) { 563 | // check args 564 | if (args === undefined) { 565 | var undefinedArgsError = 'invalid send attributes: args = undefined' 566 | _log.error(undefinedArgsError) 567 | throw new Error(undefinedArgsError) 568 | } 569 | if (args.address === undefined) { 570 | var undefinedAddressError = 'invalid send attributes: args.address = undefined' 571 | _log.error(undefinedAddressError) 572 | throw new Error(undefinedAddressError) 573 | } 574 | if (args.port === undefined) { 575 | var undefinedPortError = 'invalid send attributes: args.port = undefined' 576 | _log.error(undefinedPortError) 577 | throw new Error(undefinedPortError) 578 | } 579 | if (args.bytes === undefined) { 580 | var undefinedBytesError = 'invalid send attributes: args.bytes = undefined' 581 | _log.error(undefinedBytesError) 582 | throw new Error(undefinedBytesError) 583 | } 584 | var margs = merge(Object.create(TurnClient.DEFAULTS), args) 585 | // create attrs 586 | var attrs = new Attributes() 587 | attrs.add(new Attributes.XORPeerAddress(margs.address, margs.port)) 588 | if (margs.dontFragment) { 589 | attrs.add(new Attributes.DontFragment()) 590 | } 591 | attrs.add(new Attributes.Data(margs.bytes)) 592 | // create send packet 593 | var packet = new Packet(Packet.METHOD.SEND, Packet.TYPE.INDICATION, attrs) 594 | // encode packet 595 | var message = packet.encode() 596 | return message 597 | } 598 | 599 | function composeChannelBindRequest (args) { 600 | // check args 601 | if (args === undefined) { 602 | var undefinedArgsError = 'invalid channel-bind attributes: args = undefined' 603 | _log.error(undefinedArgsError) 604 | throw new Error(undefinedArgsError) 605 | } 606 | if (args.channel === undefined) { 607 | var undefinedChannelError = 'invalid channel-bind attributes: args.channel = undefined' 608 | _log.error(undefinedChannelError) 609 | throw new Error(undefinedChannelError) 610 | } 611 | if (args.address === undefined) { 612 | var undefinedAddressError = 'invalid channel-bind attributes: args.address = undefined' 613 | _log.error(undefinedAddressError) 614 | throw new Error(undefinedAddressError) 615 | } 616 | if (args.port === undefined) { 617 | var undefinedPortError = 'invalid channel-bind attributes: args.port = undefined' 618 | _log.error(undefinedPortError) 619 | throw new Error(undefinedPortError) 620 | } 621 | // create attrs 622 | var attrs = new Attributes() 623 | _addSecurityAttributes(attrs, args) 624 | attrs.add(new Attributes.ChannelNumber(args.channel)) 625 | attrs.add(new Attributes.XORPeerAddress(args.address, args.port)) 626 | // create channelBind packet 627 | var packet = new Packet(Packet.METHOD.CHANNELBIND, Packet.TYPE.REQUEST, attrs) 628 | // create channelBind packet 629 | var message = packet.encode() 630 | return message 631 | } 632 | 633 | function composeChannelDataMessage (args) { 634 | // check args 635 | if (args === undefined) { 636 | var undefinedArgsError = 'invalid channel-bind attributes: args = undefined' 637 | _log.error(undefinedArgsError) 638 | throw new Error(undefinedArgsError) 639 | } 640 | if (args.bytes === undefined) { 641 | var undefinedDataError = 'invalid channel-data attribute: bytes = undefined' 642 | _log.error(undefinedDataError) 643 | throw new Error(undefinedDataError) 644 | } 645 | if (args.channel === undefined) { 646 | var undefinedChannelError = 'invalid channel-data attribute: channel = undefined' 647 | _log.error(undefinedChannelError) 648 | throw new Error(undefinedChannelError) 649 | } 650 | // create channel-data packet 651 | var channelData = new ChannelData(args.channel, args.bytes) 652 | // encode packet 653 | var message = channelData.encode() 654 | return message 655 | } 656 | 657 | function composeRefreshRequest (args) { 658 | var margs = merge(Object.create(TurnClient.DEFAULTS), args) 659 | // create attrs 660 | var attrs = new Attributes() 661 | _addSecurityAttributes(attrs, margs) 662 | attrs.add(new Attributes.Software(margs.software)) 663 | attrs.add(new Attributes.Lifetime(margs.lifetime)) 664 | // create refresh packet 665 | var packet = new Packet(Packet.METHOD.REFRESH, Packet.TYPE.REQUEST, attrs) 666 | // encode packet 667 | var message = packet.encode() 668 | return message 669 | } 670 | 671 | function _addSecurityAttributes (attrs, args) { 672 | if (args.user) { 673 | attrs.add(new Attributes.Username(args.user)) 674 | } 675 | if (args.nonce) { 676 | attrs.add(new Attributes.Nonce(args.nonce)) 677 | } 678 | if (args.realm) { 679 | attrs.add(new Attributes.Realm(args.realm)) 680 | } 681 | if (args.user && args.pwd) { 682 | attrs.add(new Attributes.MessageIntegrity({ 683 | username: args.user, 684 | password: args.pwd, 685 | realm: args.realm 686 | })) 687 | } 688 | } 689 | 690 | module.exports = TurnClient 691 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "turn-js", 3 | "version": "0.7.0", 4 | "description": "TURN (Traversal Using Relay NAT) library written entirely in JavaScript", 5 | "main": "index.js", 6 | "keywords": [ 7 | "nat", 8 | "turn", 9 | "udp" 10 | ], 11 | "author": { 12 | "name": "Nico Janssens", 13 | "email": "nico.b.janssens@gmail.com" 14 | }, 15 | "license": "MIT", 16 | "engines": { 17 | "node": ">=6.9.0" 18 | }, 19 | "browser": { 20 | "winston": "winston-browser" 21 | }, 22 | "dependencies": { 23 | "args-js": "^0.10.12", 24 | "merge": "^1.2.0", 25 | "q": "^1.4.1", 26 | "stun-js": "^0.7.0", 27 | "winston": "^2.3.1", 28 | "winston-browser": "^1.0.0", 29 | "winston-debug": "^1.1.0", 30 | "winston-meta-wrapper": "^1.0.0" 31 | }, 32 | "devDependencies": { 33 | "babel-preset-es2015": "^6.22.0", 34 | "babelify": "^7.3.0", 35 | "browserify": "^14.1.0", 36 | "chai": "^3.5.0", 37 | "chai-as-promised": "^6.0.0", 38 | "chrome-dgram": "^3.0.1", 39 | "mocha": "^3.2.0", 40 | "publish": "^0.6.0", 41 | "uglify-js": "^2.7.5", 42 | "yargs": "^6.5.0" 43 | }, 44 | "repository": { 45 | "type": "git", 46 | "url": "git://github.com/microminion/turn-js.git" 47 | }, 48 | "scripts": { 49 | "build": "browserify -s TurnClient -e -t [ babelify --global --presets [ es2015 ] ] ./ | uglifyjs -c warnings=false -m > turn.min.js", 50 | "build-debug": "browserify -s TurnClient -e ./ > turn.debug.js", 51 | "size": "npm run build && cat turn.min.js | gzip | wc -c", 52 | "test-node": "./node_modules/.bin/mocha test/*.unit.js", 53 | "clean": "rm -f turn.*.js && rm -rf node_modules", 54 | "2npm": "publish" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/attributes.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Attributes = require('../lib/attributes') 4 | var Packet = require('../lib/packet') 5 | 6 | var chai = require('chai') 7 | var expect = chai.expect 8 | 9 | describe('#TURN attributes', function () { 10 | it('should encode and decode a channel-number attribute', function (done) { 11 | var testChannel = 0x4001 12 | var ChannelNumber = Attributes.ChannelNumber 13 | var channelNumber = new ChannelNumber(testChannel) 14 | var bytes = channelNumber.encode() 15 | var decodedChannelNumber = ChannelNumber.decode(bytes.slice(4, bytes.lenght)) 16 | expect(decodedChannelNumber.channel).to.exist 17 | expect(decodedChannelNumber.channel).to.equal(testChannel) 18 | done() 19 | }) 20 | 21 | it('should encode and decode a lifetime attribute', function (done) { 22 | var testDuration = 3600 23 | var Lifetime = Attributes.Lifetime 24 | var lifetime = new Lifetime(testDuration) 25 | var bytes = lifetime.encode() 26 | var decodedLifetime = Lifetime.decode(bytes.slice(4, bytes.lenght)) 27 | expect(decodedLifetime.duration).to.exist 28 | expect(decodedLifetime.duration).to.equal(testDuration) 29 | done() 30 | }) 31 | 32 | it('should encode and decode an xor-peer-address attribute', function (done) { 33 | var testAddress = '127.0.0.1' 34 | var testPort = 2345 35 | var tid = Math.random() * Packet.TID_MAX 36 | var magic = Packet.MAGIC_COOKIE 37 | var testHeaderBytes = createTestHeaderBytes(magic, tid) 38 | var XORPeerAddress = Attributes.XORPeerAddress 39 | var xorPeerAddress = new XORPeerAddress(testAddress, testPort) 40 | var bytes = xorPeerAddress.encode(magic, tid) 41 | var decodedXORPeerAddress = XORPeerAddress.decode(bytes.slice(4, bytes.lenght), testHeaderBytes) 42 | expect(decodedXORPeerAddress.address).to.exist 43 | expect(decodedXORPeerAddress.address).to.equal(testAddress) 44 | expect(decodedXORPeerAddress.port).to.exist 45 | expect(decodedXORPeerAddress.port).to.equal(testPort) 46 | done() 47 | }) 48 | 49 | it('should encode and decode a data attribute', function (done) { 50 | var testData = 'this is such an awesome library' 51 | var testBytes = Buffer.from(testData) 52 | var Data = Attributes.Data 53 | var data = new Data(testBytes) 54 | var bytes = data.encode() 55 | var length = bytes.readUInt16BE(2) 56 | var decodedData = Data.decode(bytes.slice(4, 4 + length)) 57 | expect(decodedData.bytes).to.exist 58 | expect(decodedData.bytes.toString()).to.equal(testData) 59 | done() 60 | }) 61 | 62 | it('should encode and decode an xor-relayed-address attribute', function (done) { 63 | var testAddress = '127.0.0.1' 64 | var testPort = 2345 65 | var tid = Math.random() * Packet.TID_MAX 66 | var magic = Packet.MAGIC_COOKIE 67 | var testHeaderBytes = createTestHeaderBytes(magic, tid) 68 | var XORRelayedAddress = Attributes.XORRelayedAddress 69 | var xorRelayedAddress = new XORRelayedAddress(testAddress, testPort) 70 | var bytes = xorRelayedAddress.encode(magic, tid) 71 | var decodedXORRelayedAddress = XORRelayedAddress.decode(bytes.slice(4, bytes.lenght), testHeaderBytes) 72 | expect(decodedXORRelayedAddress.address).to.exist 73 | expect(decodedXORRelayedAddress.address).to.equal(testAddress) 74 | expect(decodedXORRelayedAddress.port).to.exist 75 | expect(decodedXORRelayedAddress.port).to.equal(testPort) 76 | done() 77 | }) 78 | 79 | it('should encode and decode an even port attribute', function (done) { 80 | var reserveNextHigherPortNumber = true 81 | var EvenPort = Attributes.EvenPort 82 | var evenPort = new EvenPort(reserveNextHigherPortNumber) 83 | var bytes = evenPort.encode() 84 | var decodedEvenPort = EvenPort.decode(bytes.slice(4, bytes.lenght)) 85 | expect(decodedEvenPort.reserveNextHigherPortNumber).to.exist 86 | expect(decodedEvenPort.reserveNextHigherPortNumber).to.equal(reserveNextHigherPortNumber) 87 | done() 88 | }) 89 | 90 | it('should encode and decode a requested-transport attribute', function (done) { 91 | var RequestedTransport = Attributes.RequestedTransport 92 | var requestedTransport = new RequestedTransport() 93 | var bytes = requestedTransport.encode() 94 | var decodedRequestedTransport = RequestedTransport.decode(bytes.slice(4, bytes.lenght)) 95 | expect(decodedRequestedTransport.value).to.exist 96 | expect(decodedRequestedTransport.value).to.equal(17) 97 | done() 98 | }) 99 | 100 | it("should encode and decode a don't fragment attribute", function (done) { 101 | var DontFragment = Attributes.DontFragment 102 | var dontFragment = new DontFragment() 103 | var bytes = dontFragment.encode() 104 | var decodedDontFragment = DontFragment.decode(bytes.slice(4, bytes.lenght)) 105 | expect(decodedDontFragment).to.exist 106 | done() 107 | }) 108 | 109 | it('should encode and decode a reservation-token attribute', function (done) { 110 | var testToken = 'abcdefgh' 111 | var ReservationToken = Attributes.ReservationToken 112 | var reservationToken = new ReservationToken(testToken) 113 | var bytes = reservationToken.encode() 114 | var decodedReservationToken = ReservationToken.decode(bytes.slice(4, bytes.lenght)) 115 | expect(decodedReservationToken.token).to.exist 116 | expect(decodedReservationToken.token).to.equal(testToken) 117 | done() 118 | }) 119 | }) 120 | 121 | function createTestHeaderBytes (magic, tid) { 122 | var encodedHeader = Buffer.alloc(Packet.HEADER_LENGTH) 123 | var type = Packet.METHOD.ALLOCATE | Packet.TYPE.REQUEST 124 | var length = 0 125 | encodedHeader.writeUInt16BE((type & 0x3fff), 0) 126 | encodedHeader.writeUInt16BE(length, 2) 127 | encodedHeader.writeUInt32BE(magic, 4) 128 | encodedHeader.writeUInt32BE(0, 8) 129 | encodedHeader.writeUInt32BE(0, 12) 130 | encodedHeader.writeUInt32BE(tid, 16) 131 | return encodedHeader 132 | } 133 | -------------------------------------------------------------------------------- /test/packet.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var Attributes = require('../lib/attributes') 4 | var Packet = require('../lib/packet') 5 | 6 | var chai = require('chai') 7 | var expect = chai.expect 8 | 9 | var username = 'foo' 10 | var software = 'turn-js-test' 11 | var lifetime = 3600 12 | 13 | describe('#TURN operations', function () { 14 | it('should encode and decode an unauthenticated allocation packet', function (done) { 15 | // create new packet 16 | var attrs = new Attributes() 17 | attrs.add(new Attributes.Username(username)) 18 | attrs.add(new Attributes.Software(software)) 19 | attrs.add(new Attributes.Lifetime(lifetime)) 20 | attrs.add(new Attributes.RequestedTransport()) 21 | attrs.add(new Attributes.DontFragment()) 22 | var packet = new Packet(Packet.METHOD.ALLOCATE, Packet.TYPE.REQUEST, attrs) 23 | // encode test packet 24 | var data = packet.encode() 25 | // decode test packet 26 | var turnDecoding = Packet.decode(data) 27 | var decodedPacket = turnDecoding.packet 28 | // verify method 29 | expect(decodedPacket.method).to.equal(Packet.METHOD.ALLOCATE) 30 | expect(decodedPacket.type).to.equal(Packet.TYPE.REQUEST) 31 | // verify attributes 32 | expect(decodedPacket.getAttribute(Attributes.USERNAME)).not.to.be.undefined 33 | expect(decodedPacket.getAttribute(Attributes.USERNAME).name).to.equal(username) 34 | expect(decodedPacket.getAttribute(Attributes.SOFTWARE)).not.to.be.undefined 35 | expect(decodedPacket.getAttribute(Attributes.SOFTWARE).description).to.equal(software) 36 | expect(decodedPacket.getAttribute(Attributes.LIFETIME)).not.to.be.undefined 37 | expect(decodedPacket.getAttribute(Attributes.LIFETIME).duration).to.equal(lifetime) 38 | expect(decodedPacket.getAttribute(Attributes.REQUESTED_TRANSPORT)).not.to.be.undefined 39 | expect(decodedPacket.getAttribute(Attributes.REQUESTED_TRANSPORT).value).to.equal(17) 40 | expect(decodedPacket.getAttribute(Attributes.DONT_FRAGMENT)).not.to.be.undefined 41 | // check remaining bytes 42 | var remainingBytes = turnDecoding.remainingBytes 43 | expect(remainingBytes.length).to.equal(0) 44 | // all good 45 | done() 46 | }) 47 | 48 | it('should encode and decode an authenticated allocation packet', function (done) { 49 | // TODO 50 | done() 51 | }) 52 | 53 | it('should encode and decode an unauthenticated createPermission packet', function (done) { 54 | var address = '192.168.99.1' 55 | // create new packet 56 | var attrs = new Attributes() 57 | attrs.add(new Attributes.Username(username)) 58 | attrs.add(new Attributes.XORPeerAddress(address)) 59 | var packet = new Packet(Packet.METHOD.CREATEPERMISSION, Packet.TYPE.INDICATION, attrs) 60 | // encode test packet 61 | var data = packet.encode() 62 | // decode test packet 63 | var turnDecoding = Packet.decode(data) 64 | var decodedPacket = turnDecoding.packet 65 | // verify method 66 | expect(decodedPacket.method).to.equal(Packet.METHOD.CREATEPERMISSION) 67 | expect(decodedPacket.type).to.equal(Packet.TYPE.INDICATION) 68 | // verify attributes 69 | expect(decodedPacket.getAttribute(Attributes.USERNAME)).not.to.be.undefined 70 | expect(decodedPacket.getAttribute(Attributes.USERNAME).name).to.equal(username) 71 | expect(decodedPacket.getAttribute(Attributes.XOR_PEER_ADDRESS)).not.to.be.undefined 72 | // check remaining bytes 73 | var remainingBytes = turnDecoding.remainingBytes 74 | expect(remainingBytes.length).to.equal(0) 75 | // all good 76 | done() 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /test/turn_client.unit.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var dgram = require('dgram') 4 | var transports = require('stun-js').transports 5 | var TurnClient = require('../lib/turn_client') 6 | 7 | var chai = require('chai') 8 | var chaiAsPromised = require('chai-as-promised') 9 | var expect = chai.expect 10 | chai.use(chaiAsPromised) 11 | chai.should() 12 | 13 | if (!process.env.TURN_ADDR) { 14 | throw new Error('TURN_ADDR undefined -- giving up') 15 | } 16 | if (!process.env.TURN_PORT) { 17 | throw new Error('TURN_PORT undefined -- giving up') 18 | } 19 | if (!process.env.TURN_USER) { 20 | throw new Error('TURN_USER undefined -- giving up') 21 | } 22 | if (!process.env.TURN_PASS) { 23 | throw new Error('TURN_PASS undefined -- giving up') 24 | } 25 | 26 | var turnAddr = process.env.TURN_ADDR 27 | var turnPort = parseInt(process.env.TURN_PORT) 28 | var turnUser = process.env.TURN_USER 29 | var turnPwd = process.env.TURN_PASS 30 | var socketPort = 33333 31 | 32 | var winston = require('winston-debug') 33 | winston.level = 'debug' 34 | 35 | describe('#TURN operations', function () { 36 | this.timeout(15000) 37 | 38 | it('should execute TURN allocate operation over UDP socket using promises', function (done) { 39 | var retransmissionTimer 40 | // send a TURN allocate request and verify the reply 41 | var sendAllocateRequest = function (client, socket) { 42 | client.allocateP() 43 | .then(function (result) { 44 | // end retransmissionTimer 45 | clearTimeout(retransmissionTimer) 46 | // test results 47 | expect(result).not.to.be.undefined 48 | expect(result).to.have.property('mappedAddress') 49 | expect(result.mappedAddress).to.have.property('address') 50 | expect(result.mappedAddress).to.have.property('port') 51 | // expect(result.mappedAddress.address).to.equal(testGW) 52 | expect(result).to.have.property('relayedAddress') 53 | expect(result.relayedAddress).to.have.property('address') 54 | expect(result.relayedAddress).to.have.property('port') 55 | // expect(result.relayedAddress.address).to.equal(turnAddr) 56 | return client.closeP() 57 | }) 58 | .then(function () { 59 | expect(socket.listeners('message').length).to.equal(1) 60 | expect(socket.listeners('error').length).to.equal(1) 61 | // close socket 62 | socket.close(function () { 63 | done() 64 | }) 65 | }) 66 | .catch(function (error) { 67 | done(error) 68 | }) 69 | } 70 | // create socket 71 | var socket = dgram.createSocket('udp4') 72 | socket.on('message', function (message, rinfo) { // 73 | done(new Error('message callback should not be fired')) 74 | }) 75 | socket.on('error', function (error) { 76 | done(error) 77 | }) 78 | socket.on('listening', function () { 79 | // create turn client and pass socket over 80 | var transport = new transports.UDP(socket) 81 | var client = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, transport) 82 | client.init(function () { 83 | // retransmission timer -- we're using UDP ... 84 | retransmissionTimer = setTimeout(function () { 85 | console.log('resending ALLOCATE request') 86 | sendAllocateRequest(client, socket) 87 | }, 5000) 88 | // allocate request 89 | sendAllocateRequest(client, socket) 90 | }) 91 | }) 92 | socket.bind(socketPort) 93 | }) 94 | 95 | it('should execute TURN allocate operation over TCP socket using callbacks', function (done) { 96 | var transport = new transports.TCP() 97 | var client = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, transport) 98 | 99 | var onError = function (error) { 100 | done(error) 101 | } 102 | var onReady = function (result) { 103 | expect(result).not.to.be.undefined 104 | expect(result).to.have.property('mappedAddress') 105 | expect(result.mappedAddress).to.have.property('address') 106 | expect(result.mappedAddress).to.have.property('port') 107 | // expect(result.mappedAddress.address).to.equal(testGW) 108 | expect(result).to.have.property('relayedAddress') 109 | expect(result.relayedAddress).to.have.property('address') 110 | expect(result.relayedAddress).to.have.property('port') 111 | // expect(result.relayedAddress.address).to.equal(turnAddr) 112 | client.close( 113 | function () { 114 | done() 115 | }, 116 | onError 117 | ) 118 | } 119 | 120 | client.init(function () { 121 | client.allocate(onReady, onError) 122 | }) 123 | }) 124 | 125 | it('should execute TURN allocate operation over unspecified UDP socket using promises', function (done) { 126 | var retransmissionTimer 127 | // send a TURN allocate request and verify the reply 128 | var sendAllocateRequest = function (client) { 129 | client.allocateP() 130 | .then(function (result) { 131 | // end retransmissionTimer 132 | clearTimeout(retransmissionTimer) 133 | // test results 134 | expect(result).not.to.be.undefined 135 | expect(result).to.have.property('mappedAddress') 136 | expect(result.mappedAddress).to.have.property('address') 137 | expect(result.mappedAddress).to.have.property('port') 138 | // expect(result.mappedAddress.address).to.equal(testGW) 139 | expect(result).to.have.property('relayedAddress') 140 | expect(result.relayedAddress).to.have.property('address') 141 | expect(result.relayedAddress).to.have.property('port') 142 | // expect(result.relayedAddress.address).to.equal(turnAddr) 143 | return client.closeP() 144 | }) 145 | .then(function () { 146 | done() 147 | }) 148 | .catch(function (error) { 149 | done(error) 150 | }) 151 | } 152 | // create turn client 153 | var client = new TurnClient(turnAddr, turnPort, turnUser, turnPwd) 154 | client.init(function () { 155 | // retransmission timer -- we're using UDP ... 156 | retransmissionTimer = setTimeout(function () { 157 | console.log('resending ALLOCATE request') 158 | sendAllocateRequest(client) 159 | }, 5000) 160 | // allocate request 161 | sendAllocateRequest(client) 162 | }) 163 | }) 164 | 165 | it('should execute TURN allocate followed by refresh over UDP socket using promises', function (done) { 166 | var lifetime = 3600 167 | var retransmissionTimer 168 | // send a TURN allocate request and verify the reply 169 | var sendAllocateAndRefreshRequest = function (client) { 170 | client.allocateP() 171 | .then(function (result) { 172 | return client.refreshP(lifetime) 173 | }) 174 | .then(function (duration) { 175 | // end retransmissionTimer 176 | clearTimeout(retransmissionTimer) 177 | // test results 178 | expect(duration).to.equal(lifetime) 179 | // close turn client 180 | return client.closeP() 181 | }) 182 | .then(function () { 183 | expect(socket.listeners('message').length).to.equal(1) 184 | expect(socket.listeners('error').length).to.equal(1) 185 | done() 186 | }) 187 | .catch(function (error) { 188 | done(error) 189 | }) 190 | } 191 | // create socket 192 | var socket = dgram.createSocket('udp4') 193 | socket.on('message', function (message, rinfo) { // 194 | done(new Error('message callback should not be fired')) 195 | }) 196 | socket.on('error', function (error) { 197 | done(error) 198 | }) 199 | socket.on('listening', function () { 200 | // create stun client and pass socket over 201 | var client = new TurnClient(turnAddr, turnPort, turnUser, turnPwd) 202 | client.init(function () { 203 | // retransmission timer -- we're using UDP ... 204 | retransmissionTimer = setTimeout(function () { 205 | console.log('resending ALLOCATE and REFRESH request') 206 | sendAllocateAndRefreshRequest(client) 207 | }, 5000) 208 | sendAllocateAndRefreshRequest(client) 209 | }) 210 | }) 211 | socket.bind(socketPort) 212 | }) 213 | 214 | it('should execute TURN allocate followed by create permission over TCP socket using promises', function () { 215 | var transport = new transports.TCP() 216 | var client = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, transport) 217 | var turnAddress = '1.2.3.4' 218 | return client.initP() 219 | .then(function () { 220 | return client.allocateP() 221 | }) 222 | .then(function (result) { 223 | return client.createPermissionP(turnAddress) 224 | }) 225 | .then(function () { 226 | return client.closeP() 227 | }) 228 | }) 229 | 230 | it('should execute TURN allocate followed by two consecutive create permissions (testing permission refresh) over TCP socket using promises', function () { 231 | var transport = new transports.TCP() 232 | var client = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, transport) 233 | var turnAddress = '1.2.3.4' 234 | return client.initP() 235 | .then(function () { 236 | return client.allocateP() 237 | }) 238 | .then(function (result) { 239 | return client.createPermissionP(turnAddress) 240 | }) 241 | .then(function () { 242 | return client.createPermissionP(turnAddress) 243 | }) 244 | .then(function () { 245 | return client.closeP() 246 | }) 247 | }) 248 | 249 | it('should receive messages that are sent via relay server over TCP sockets', function (done) { 250 | var testData = 'hello there' 251 | var testRuns = 10 252 | var messagesReceived = 0 253 | 254 | // alice's client 255 | var clientAlice = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, new transports.TCP()) 256 | // bob's client 257 | var clientBob = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, new transports.TCP()) 258 | var srflxAddressAlice, srflxAddressBob, relayAddressAlice, relayAddressBob 259 | 260 | var sendTestMessageFromAliceToBob = function () { 261 | var bytes = Buffer.from(testData) 262 | clientAlice.sendToRelay( 263 | bytes, 264 | relayAddressBob.address, 265 | relayAddressBob.port, 266 | function () { 267 | console.log('message sent to ' + relayAddressBob.address + ':' + relayAddressBob.port) 268 | }, // on success 269 | function (error) { 270 | done(error) 271 | } 272 | ) 273 | } 274 | 275 | // subscribe to incoming messages 276 | clientBob.on('relayed-message', function (bytes, peerAddress) { 277 | var message = bytes.toString() 278 | expect(message).to.equal(testData) 279 | console.log('receiving test message ' + message) 280 | messagesReceived++ 281 | if (messagesReceived === testRuns) { 282 | clientBob.closeP() 283 | .then(function () { 284 | return clientAlice.closeP() 285 | }) 286 | .then(function () { 287 | done() 288 | }) 289 | .catch(function (error) { 290 | done(error) 291 | }) 292 | } else { 293 | sendTestMessageFromAliceToBob() 294 | } 295 | }) 296 | 297 | // init alice and bob + allocate relaying session for alice 298 | clientBob.initP() 299 | .then(function () { 300 | return clientAlice.initP() 301 | }) 302 | .then(function () { 303 | return clientAlice.allocateP() 304 | }) 305 | .then(function (allocateAddress) { 306 | srflxAddressAlice = allocateAddress.mappedAddress 307 | relayAddressAlice = allocateAddress.relayedAddress 308 | console.log("alice's srflx address = " + srflxAddressAlice.address + ':' + srflxAddressAlice.port) 309 | console.log("alice's relay address = " + relayAddressAlice.address + ':' + relayAddressAlice.port) 310 | // allocate relaying session for bob 311 | return clientBob.allocateP() 312 | }) 313 | .then(function (allocateAddress) { 314 | srflxAddressBob = allocateAddress.mappedAddress 315 | relayAddressBob = allocateAddress.relayedAddress 316 | console.log("bob's address = " + srflxAddressBob.address + ':' + srflxAddressBob.port) 317 | console.log("bob's relay address = " + relayAddressBob.address + ':' + relayAddressBob.port) 318 | // create permission for alice to send messages to bob 319 | return clientBob.createPermissionP(relayAddressAlice.address) 320 | }) 321 | .then(function () { 322 | // create permission for bob to send messages to alice 323 | return clientAlice.createPermissionP(relayAddressBob.address) 324 | }) 325 | .then(function () { 326 | // send test message 327 | sendTestMessageFromAliceToBob() 328 | }) 329 | .catch(function (error) { 330 | done(error) 331 | }) 332 | }) 333 | 334 | it('should concurrently receive messages that are sent via relay server over TCP sockets, using multiple clients', function (done) { 335 | var nbSessions = 10 336 | var nbSessionEnded = 0 337 | 338 | var runTestSession = function (onDone) { 339 | var testData = 'hello there' 340 | var testRuns = 10 341 | var messagesReceived = 0 342 | 343 | var clientAlice = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, new transports.TCP()) 344 | var clientBob = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, new transports.TCP()) 345 | var srflxAddressAlice, srflxAddressBob, relayAddressAlice, relayAddressBob 346 | 347 | var sendTestMessageFromAliceToBob = function () { 348 | var bytes = Buffer.from(testData) 349 | clientAlice.sendToRelay( 350 | bytes, 351 | relayAddressBob.address, 352 | relayAddressBob.port, 353 | function () { 354 | console.log('message sent to ' + relayAddressBob.address + ':' + relayAddressBob.port) 355 | }, // on success 356 | function (error) { 357 | done(error) 358 | } 359 | ) 360 | } 361 | 362 | // subscribe to incoming messages 363 | clientBob.on('relayed-message', function (bytes, peerAddress) { 364 | var message = bytes.toString() 365 | expect(message).to.equal(testData) 366 | console.log('receiving test message ' + message) 367 | messagesReceived++ 368 | if (messagesReceived === testRuns) { 369 | clientBob.closeP() 370 | .then(function () { 371 | return clientAlice.closeP() 372 | }) 373 | .then(function () { 374 | onDone() 375 | }) 376 | .catch(function (error) { 377 | done(error) 378 | }) 379 | } else { 380 | sendTestMessageFromAliceToBob() 381 | } 382 | }) 383 | 384 | // init alice and bob + allocate relaying session for alice 385 | clientBob.initP() 386 | .then(function () { 387 | return clientAlice.initP() 388 | }) 389 | .then(function () { 390 | return clientAlice.allocateP() 391 | }) 392 | .then(function (allocateAddress) { 393 | srflxAddressAlice = allocateAddress.mappedAddress 394 | relayAddressAlice = allocateAddress.relayedAddress 395 | console.log("alice's srflx address = " + srflxAddressAlice.address + ':' + srflxAddressAlice.port) 396 | console.log("alice's relay address = " + relayAddressAlice.address + ':' + relayAddressAlice.port) 397 | // allocate relaying session for bob 398 | return clientBob.allocateP() 399 | }) 400 | .then(function (allocateAddress) { 401 | srflxAddressBob = allocateAddress.mappedAddress 402 | relayAddressBob = allocateAddress.relayedAddress 403 | console.log("bob's address = " + srflxAddressBob.address + ':' + srflxAddressBob.port) 404 | console.log("bob's relay address = " + relayAddressBob.address + ':' + relayAddressBob.port) 405 | // create permission for alice to send messages to bob 406 | return clientBob.createPermissionP(relayAddressAlice.address) 407 | }) 408 | .then(function () { 409 | // create permission for bob to send messages to alice 410 | return clientAlice.createPermissionP(relayAddressBob.address) 411 | }) 412 | .then(function () { 413 | // send test message 414 | sendTestMessageFromAliceToBob() 415 | }) 416 | .catch(function (error) { 417 | done(error) 418 | }) 419 | } 420 | 421 | for (var i = 0; i < nbSessions; i++) { 422 | runTestSession(function () { 423 | nbSessionEnded++ 424 | if (nbSessionEnded === nbSessions) { 425 | done() 426 | } 427 | }) 428 | } 429 | 430 | }) 431 | 432 | it('should execute TURN channel binding and receive messages sent via these channels over TCP sockets using promises', function (done) { 433 | var testData = 'hello there' 434 | var testRuns = 10 435 | var messagesReceived = 0 436 | 437 | // alice's client 438 | var clientAlice = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, new transports.TCP()) 439 | // bob's client 440 | var clientBob = new TurnClient(turnAddr, turnPort, turnUser, turnPwd, new transports.TCP()) 441 | var srflxAddressAlice, srflxAddressBob, relayAddressAlice, relayAddressBob 442 | var channelId 443 | 444 | var sendTestMessageFromAliceToBob = function () { 445 | var bytes = Buffer.from(testData) 446 | clientAlice.sendToChannel( 447 | bytes, 448 | channelId, 449 | function () { 450 | console.log('message sent to channel ' + channelId) 451 | }, 452 | function (error) { 453 | done(error) 454 | } 455 | ) 456 | } 457 | 458 | // subscribe to incoming messages 459 | clientBob.on('relayed-message', function (bytes, peerAddress) { 460 | var message = bytes.toString() 461 | expect(message).to.equal(testData) 462 | console.log('receiving test message ' + message) 463 | messagesReceived++ 464 | if (messagesReceived === testRuns) { 465 | clientBob.closeP() 466 | .then(function () { 467 | return clientAlice.closeP() 468 | }) 469 | .then(function () { 470 | done() 471 | }) 472 | .catch(function (error) { 473 | done(error) 474 | }) 475 | } else { 476 | sendTestMessageFromAliceToBob() 477 | } 478 | }) 479 | 480 | // init alice and bob + allocate relaying session for alice 481 | clientBob.initP() 482 | .then(function () { 483 | return clientAlice.initP() 484 | }) 485 | .then(function () { 486 | return clientAlice.allocateP() 487 | }) 488 | .then(function (allocateAddress) { 489 | srflxAddressAlice = allocateAddress.mappedAddress 490 | relayAddressAlice = allocateAddress.relayedAddress 491 | console.log("alice's srflx address = " + srflxAddressAlice.address + ':' + srflxAddressAlice.port) 492 | console.log("alice's relay address = " + relayAddressAlice.address + ':' + relayAddressAlice.port) 493 | // allocate relaying session for bob 494 | return clientBob.allocateP() 495 | }) 496 | .then(function (allocateAddress) { 497 | srflxAddressBob = allocateAddress.mappedAddress 498 | relayAddressBob = allocateAddress.relayedAddress 499 | console.log("bob's address = " + srflxAddressBob.address + ':' + srflxAddressBob.port) 500 | console.log("bob's relay address = " + relayAddressBob.address + ':' + relayAddressBob.port) 501 | // create permission for alice to send messages to bob 502 | return clientBob.createPermissionP(relayAddressAlice.address) 503 | }) 504 | .then(function () { 505 | // create channel from alice to bob 506 | return clientAlice.bindChannelP(relayAddressBob.address, relayAddressBob.port) 507 | }) 508 | .then(function (channel) { 509 | expect(channel).not.to.be.undefined 510 | channelId = channel 511 | // mimic refreshing of channel binding 512 | return clientAlice.bindChannelP(relayAddressBob.address, relayAddressBob.port, channel) 513 | }) 514 | .then(function (channel) { 515 | expect(channel).to.equal(channelId) 516 | // create permission for bob to send messages to alice 517 | return clientAlice.createPermissionP(relayAddressBob.address) 518 | }) 519 | .then(function () { 520 | // create channel from bob to alice 521 | return clientBob.bindChannelP(relayAddressAlice.address, relayAddressAlice.port) 522 | }) 523 | .then(function (anotherChannel) { 524 | // send test message 525 | sendTestMessageFromAliceToBob() 526 | }) 527 | .catch(function (error) { 528 | done(error) 529 | }) 530 | }) 531 | }) 532 | --------------------------------------------------------------------------------