├── bin └── .gitkeep ├── test ├── func │ └── .gitkeep ├── perf │ └── .gitkeep ├── unit │ ├── .gitkeep │ └── Message.test.js └── fixtures │ ├── common.js │ ├── messages │ ├── sample-response-ipv4.js │ ├── sample-response-ipv6.js │ ├── sample-request.js │ └── sample-request-ltc.js │ └── parse-test-data.js ├── .editorconfig ├── .travis.yml ├── .gitignore ├── lib ├── storage │ └── MemoryStorage.js ├── Server.js └── Message.js ├── README.md ├── package.json ├── Gruntfile.js └── .jshintrc /bin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/func/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/perf/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Unix-style newlines with a newline ending every file 2 | [*] 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | indent_style = space 7 | indent_size = 4 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | install: 10 | - npm install grunt-cli 11 | - npm install 12 | 13 | cache: 14 | directories: 15 | - node_modules 16 | 17 | script: 18 | - ./node_modules/.bin/grunt travis 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # git keep files 2 | !.gitkeep 3 | 4 | # sublime text files 5 | *.sublime-* 6 | *.*~*.TMP 7 | 8 | # temp files 9 | .DS_Store 10 | Thumbs.db 11 | Desktop.ini 12 | 13 | # vim swap files 14 | *.sw* 15 | 16 | # emacs temp files 17 | *~ 18 | \#*# 19 | 20 | # node.js common ignores 21 | node_modules/ 22 | npm-debug.log 23 | -------------------------------------------------------------------------------- /lib/storage/MemoryStorage.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | 3 | function MemoryStorage() { 4 | this.transactions = {}; 5 | } 6 | module.exports = MemoryStorage; 7 | 8 | MemoryStorage.prototype.pushMessage = function(message) { 9 | if(!this.transactions[message.tid]) { 10 | this.transactions[message.tid] = [message]; 11 | } else { 12 | this.transactions[message.tid].push(message); 13 | } 14 | }; 15 | 16 | MemoryStorage.prototype.isTransactionActive = function(message) { 17 | if(this.transactions[message.tid]) { 18 | return true; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stun.js 2 | A STUN server written in node.js, compatible with [RFC-5389][2]. 3 | 4 | ## TODO 5 | 6 | - Complete the server implementation 7 | - Implement transaction tracking 8 | - Implement attribute reading 9 | 10 | ## Resources 11 | 12 | RFCs: 13 | - [RFC-5389 (STUN)][2] 14 | - [RFC-5769 (STUN tests)][3] 15 | - [RFC-5245 (ICE)][4] 16 | 17 | Implementations: 18 | - [stunserver][15] 19 | - [stutter.js][17] 20 | 21 | Misc Docs: 22 | - [WebRTC html5rocks][30] 23 | 24 | 25 | [2]: https://tools.ietf.org/html/rfc5389 26 | [3]: https://tools.ietf.org/html/rfc5769 27 | [4]: https://tools.ietf.org/html/rfc5245 28 | 29 | [15]: https://github.com/jselbie/stunserver 30 | [17]: https://github.com/davidrivera/stutter.js 31 | 32 | [30]: http://www.html5rocks.com/en/tutorials/webrtc/infrastructure/ 33 | -------------------------------------------------------------------------------- /test/fixtures/common.js: -------------------------------------------------------------------------------- 1 | //export some chai stuff 2 | global.chai = require('chai'); 3 | global.expect = global.chai.expect; 4 | 5 | //export some sinon 6 | global.sinon = require('sinon'); 7 | global.chai.use(require('sinon-chai')); 8 | 9 | var Buffer = require('buffer').Buffer; 10 | global.assertBufferEqual = function(a, b) { 11 | expect(Buffer.isBuffer(a)).to.be.ok; 12 | expect(Buffer.isBuffer(b)).to.be.ok; 13 | expect(a.length).to.equal(b.length, 'Buffer lengths do not match.'); 14 | 15 | for(var i = 0; i < a.length; ++i) { 16 | if(a[i] !== b[i]) { 17 | var sa = buff[i].toString(16).toUpperCase(), 18 | sb = msg.full[i].toString(16).toUpperCase(); 19 | 20 | while(sa.length < 2) { sa = '0' + sa; } 21 | while(sb.length < 2) { sb = '0' + sb; } 22 | 23 | throw new Error('Expected 0x' + sa + ' to equal 0x' + sb + ' at buffer index ' + i); 24 | } 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stun.js", 3 | "version": "0.0.1", 4 | "longName": "STUN Server Implementation", 5 | "description": "An RFC5389 compliant STUN/TURN server written in Node.js", 6 | 7 | "author": "Chad Engler ", 8 | "contributors": [], 9 | 10 | "homepage": "https://github.com/englercj/stun.js", 11 | "bugs": "https://github.com/englercj/stun.js/issues", 12 | 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/englercj/stun.js.git" 16 | }, 17 | 18 | "main": "lib/index", 19 | 20 | "dependencies": { 21 | "sprintf": "^0.1.3" 22 | }, 23 | 24 | "devDependencies": { 25 | "grunt": "^0.4.4", 26 | "grunt-contrib-jshint": "^0.10.0", 27 | "grunt-mocha-cov": "^0.2.1", 28 | 29 | "mocha-term-cov-reporter": "^0.2.0", 30 | 31 | "chai": "^1.9.1", 32 | "sinon": "^1.9.1", 33 | "sinon-chai": "^2.5.0" 34 | }, 35 | 36 | "scripts": { 37 | "test": "grunt test" 38 | }, 39 | 40 | "engines": { 41 | "node": "~0.10.20" 42 | }, 43 | 44 | "config": { 45 | "blanket": { 46 | "pattern": [ 47 | "stun.js/lib/", 48 | "stun.js/server/" 49 | ] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/unit/Message.test.js: -------------------------------------------------------------------------------- 1 | var Message = require('../../lib/Message'), 2 | buffer = require('buffer').Buffer, 3 | testData = require('../fixtures/parse-test-data'); 4 | 5 | describe('stun/Message', function() { 6 | it('should have the proper exports', function() { 7 | expect(Message).to.be.a('function'); 8 | 9 | expect(Message.fromBuffer).to.be.a('function'); 10 | }); 11 | 12 | describe('.fromBuffer', function() { 13 | it('should properly create a message', function() { 14 | testData.messages.forEach(function(msg) { 15 | var obj = Message.fromBuffer(msg.full); 16 | 17 | expect(obj.type).to.equal(msg.type); 18 | expect(obj.body.length).to.equal(msg.length); 19 | 20 | assertBufferEqual(obj.tid, msg.tid); 21 | 22 | expect(obj.method).to.equal(msg.method); 23 | expect(obj.class).to.equal(msg.class); 24 | }); 25 | }); 26 | 27 | it('should throw an error when the message is invalid', function() { 28 | testData.invalidMessages.forEach(function(msg) { 29 | expect(Message.fromBuffer.bind(null, msg.full)).to.throw(Error); 30 | }); 31 | }); 32 | }); 33 | 34 | describe('#toBuffer', function() { 35 | it('should properly serialize a message', function() { 36 | testData.messages.forEach(function(msg) { 37 | var obj = Message.fromBuffer(msg.full); 38 | 39 | assertBufferEqual(obj.toBuffer(), msg.full); 40 | }); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | //load npm tasks 3 | grunt.loadNpmTasks('grunt-contrib-jshint'); 4 | grunt.loadNpmTasks('grunt-mocha-cov'); 5 | 6 | //default 7 | grunt.registerTask('default', ['test']); 8 | 9 | //test tasks 10 | grunt.registerTask('test', ['jshint', 'mochacov:unit', 'mochacov:coverage']); 11 | grunt.registerTask('travis', ['jshint', 'mochacov:unit', 'mochacov:coverage'/*, 'mochacov:coveralls'*/]); 12 | 13 | //Project Configuration 14 | grunt.initConfig({ 15 | //settings used throughout the configuration 16 | pkg: grunt.file.readJSON('package.json'), 17 | dirs: { 18 | lib: 'lib', 19 | test: 'test' 20 | }, 21 | //jshint obviously lints our code 22 | jshint: { 23 | options: { 24 | jshintrc: '.jshintrc' 25 | }, 26 | lib: { 27 | src: '<%= dirs.lib %>/**/*.js' 28 | } 29 | }, 30 | //runs our tests 31 | mochacov: { 32 | options: { 33 | files: '<%= dirs.test %>/**/*.test.js', 34 | ui: 'bdd', 35 | colors: true, 36 | require: ['./test/fixtures/common'] 37 | }, 38 | unit: { 39 | options: { 40 | reporter: 'spec' 41 | } 42 | }, 43 | coverage: { 44 | options: { 45 | reporter: 'mocha-term-cov-reporter', 46 | coverage: true 47 | } 48 | }/*, 49 | coveralls: { 50 | options: { 51 | coveralls: { 52 | serviceName: 'travis-ci' 53 | } 54 | } 55 | }*/ 56 | } 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /test/fixtures/messages/sample-response-ipv4.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a test message from RFC5769 - Test Vectors for Session Traversal Utilities for NAT (STUN) 3 | * https://tools.ietf.org/html/rfc5769 4 | * 5 | * 6 | * 7 | * 2.2. Sample IPv4 Response 8 | * 9 | * This response uses the following parameter: 10 | * 11 | * Password: "VOkJxbRl1RmTxUk/WvJxBt" (without quotes) 12 | * 13 | * Software name: "test vector" (without quotes) 14 | * 15 | * Mapped address: 192.0.2.1 port 32853 16 | * 17 | * 01 01 00 3c Response type and message length 18 | * 21 12 a4 42 Magic cookie 19 | * b7 e7 a7 01 } 20 | * bc 34 d6 86 } Transaction ID 21 | * fa 87 df ae } 22 | * 80 22 00 0b SOFTWARE attribute header 23 | * 74 65 73 74 } 24 | * 20 76 65 63 } UTF-8 server name 25 | * 74 6f 72 20 } 26 | * 00 20 00 08 XOR-MAPPED-ADDRESS attribute header 27 | * 00 01 a1 47 Address family (IPv4) and xor'd mapped port number 28 | * e1 12 a6 43 Xor'd mapped IPv4 address 29 | * 00 08 00 14 MESSAGE-INTEGRITY attribute header 30 | * 2b 91 f5 99 } 31 | * fd 9e 90 c3 } 32 | * 8c 74 89 f9 } HMAC-SHA1 fingerprint 33 | * 2a f9 ba 53 } 34 | * f0 6b e7 d7 } 35 | * 80 28 00 04 FINGERPRINT attribute header 36 | * c0 7d 4c 96 CRC32 fingerprint 37 | * 38 | */ 39 | 40 | var Buffer = require('buffer').Buffer; 41 | module.exports = new Buffer([ 42 | 0x01, 0x01, 0x00, 0x3c, // Response type and message length 43 | 0x21, 0x12, 0xa4, 0x42, // Magic cookie 44 | 0xb7, 0xe7, 0xa7, 0x01, // } 45 | 0xbc, 0x34, 0xd6, 0x86, // } Transaction ID 46 | 0xfa, 0x87, 0xdf, 0xae, // } 47 | 0x80, 0x22, 0x00, 0x0b, // SOFTWARE attribute header 48 | 0x74, 0x65, 0x73, 0x74, // } 49 | 0x20, 0x76, 0x65, 0x63, // } UTF-8 server name 50 | 0x74, 0x6f, 0x72, 0x20, // } 51 | 0x00, 0x20, 0x00, 0x08, // XOR-MAPPED-ADDRESS attribute header 52 | 0x00, 0x01, 0xa1, 0x47, // Address family (IPv4) and xor'd mapped port number 53 | 0xe1, 0x12, 0xa6, 0x43, // Xor'd mapped IPv4 address 54 | 0x00, 0x08, 0x00, 0x14, // MESSAGE-INTEGRITY attribute header 55 | 0x2b, 0x91, 0xf5, 0x99, // } 56 | 0xfd, 0x9e, 0x90, 0xc3, // } 57 | 0x8c, 0x74, 0x89, 0xf9, // } HMAC-SHA1 fingerprint 58 | 0x2a, 0xf9, 0xba, 0x53, // } 59 | 0xf0, 0x6b, 0xe7, 0xd7, // } 60 | 0x80, 0x28, 0x00, 0x04, // FINGERPRINT attribute header 61 | 0xc0, 0x7d, 0x4c, 0x96 // CRC32 fingerprint 62 | ]); 63 | -------------------------------------------------------------------------------- /test/fixtures/messages/sample-response-ipv6.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a test message from RFC5769 - Test Vectors for Session Traversal Utilities for NAT (STUN) 3 | * https://tools.ietf.org/html/rfc5769 4 | * 5 | * 6 | * 7 | * 2.3. Sample IPv6 Response 8 | * 9 | * This response uses the following parameter: 10 | * 11 | * Password: "VOkJxbRl1RmTxUk/WvJxBt" (without quotes) 12 | * 13 | * Software name: "test vector" (without quotes) 14 | * 15 | * Mapped address: 2001:db8:1234:5678:11:2233:4455:6677 port 32853 16 | * 17 | * 01 01 00 48 Response type and message length 18 | * 21 12 a4 42 Magic cookie 19 | * b7 e7 a7 01 } 20 | * bc 34 d6 86 } Transaction ID 21 | * fa 87 df ae } 22 | * 80 22 00 0b SOFTWARE attribute header 23 | * 74 65 73 74 } 24 | * 20 76 65 63 } UTF-8 server name 25 | * 74 6f 72 20 } 26 | * 00 20 00 14 XOR-MAPPED-ADDRESS attribute header 27 | * 00 02 a1 47 Address family (IPv6) and xor'd mapped port number 28 | * 01 13 a9 fa } 29 | * a5 d3 f1 79 } Xor'd mapped IPv6 address 30 | * bc 25 f4 b5 } 31 | * be d2 b9 d9 } 32 | * 00 08 00 14 MESSAGE-INTEGRITY attribute header 33 | * a3 82 95 4e } 34 | * 4b e6 7b f1 } 35 | * 17 84 c9 7c } HMAC-SHA1 fingerprint 36 | * 82 92 c2 75 } 37 | * bf e3 ed 41 } 38 | * 80 28 00 04 FINGERPRINT attribute header 39 | * c8 fb 0b 4c CRC32 fingerprint 40 | * 41 | */ 42 | 43 | var Buffer = require('buffer').Buffer; 44 | module.exports = new Buffer([ 45 | 0x01, 0x01, 0x00, 0x48, // Response type and message length 46 | 0x21, 0x12, 0xa4, 0x42, // Magic cookie 47 | 0xb7, 0xe7, 0xa7, 0x01, // } 48 | 0xbc, 0x34, 0xd6, 0x86, // } Transaction ID 49 | 0xfa, 0x87, 0xdf, 0xae, // } 50 | 0x80, 0x22, 0x00, 0x0b, // SOFTWARE attribute header 51 | 0x74, 0x65, 0x73, 0x74, // } 52 | 0x20, 0x76, 0x65, 0x63, // } UTF-8 server name 53 | 0x74, 0x6f, 0x72, 0x20, // } 54 | 0x00, 0x20, 0x00, 0x14, // XOR-MAPPED-ADDRESS attribute header 55 | 0x00, 0x02, 0xa1, 0x47, // Address family (IPv6) and xor'd mapped port number 56 | 0x01, 0x13, 0xa9, 0xfa, // } 57 | 0xa5, 0xd3, 0xf1, 0x79, // } Xor'd mapped IPv6 address 58 | 0xbc, 0x25, 0xf4, 0xb5, // } 59 | 0xbe, 0xd2, 0xb9, 0xd9, // } 60 | 0x00, 0x08, 0x00, 0x14, // MESSAGE-INTEGRITY attribute header 61 | 0xa3, 0x82, 0x95, 0x4e, // } 62 | 0x4b, 0xe6, 0x7b, 0xf1, // } 63 | 0x17, 0x84, 0xc9, 0x7c, // } HMAC-SHA1 fingerprint 64 | 0x82, 0x92, 0xc2, 0x75, // } 65 | 0xbf, 0xe3, 0xed, 0x41, // } 66 | 0x80, 0x28, 0x00, 0x04, // FINGERPRINT attribute header 67 | 0xc8, 0xfb, 0x0b, 0x4c // CRC32 fingerprint 68 | ]); 69 | -------------------------------------------------------------------------------- /test/fixtures/messages/sample-request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a test message from RFC5769 - Test Vectors for Session Traversal Utilities for NAT (STUN) 3 | * https://tools.ietf.org/html/rfc5769 4 | * 5 | * 6 | * 7 | * 2.1. Sample Request 8 | * 9 | * This request uses the following parameters: 10 | * 11 | * Software name: "STUN test client" (without quotes) 12 | * 13 | * Username: "evtj:h6vY" (without quotes) 14 | * 15 | * Password: "VOkJxbRl1RmTxUk/WvJxBt" (without quotes) 16 | * 17 | * 00 01 00 58 Request type and message length 18 | * 21 12 a4 42 Magic cookie 19 | * b7 e7 a7 01 } 20 | * bc 34 d6 86 } Transaction ID 21 | * fa 87 df ae } 22 | * 80 22 00 10 SOFTWARE attribute header 23 | * 53 54 55 4e } 24 | * 20 74 65 73 } User-agent... 25 | * 74 20 63 6c } ...name 26 | * 69 65 6e 74 } 27 | * 00 24 00 04 PRIORITY attribute header 28 | * 6e 00 01 ff ICE priority value 29 | * 80 29 00 08 ICE-CONTROLLED attribute header 30 | * 93 2f f9 b1 } Pseudo-random tie breaker... 31 | * 51 26 3b 36 } ...for ICE control 32 | * 00 06 00 09 USERNAME attribute header 33 | * 65 76 74 6a } 34 | * 3a 68 36 76 } Username (9 bytes) and padding (3 bytes) 35 | * 59 20 20 20 } 36 | * 00 08 00 14 MESSAGE-INTEGRITY attribute header 37 | * 9a ea a7 0c } 38 | * bf d8 cb 56 } 39 | * 78 1e f2 b5 } HMAC-SHA1 fingerprint 40 | * b2 d3 f2 49 } 41 | * c1 b5 71 a2 } 42 | * 80 28 00 04 FINGERPRINT attribute header 43 | * e5 7a 3b cf CRC32 fingerprint 44 | * 45 | */ 46 | 47 | var Buffer = require('buffer').Buffer; 48 | module.exports = new Buffer([ 49 | 0x00, 0x01, 0x00, 0x58, // Request type and message length 50 | 0x21, 0x12, 0xa4, 0x42, // Magic cookie 51 | 0xb7, 0xe7, 0xa7, 0x01, // } 52 | 0xbc, 0x34, 0xd6, 0x86, // } Transaction ID 53 | 0xfa, 0x87, 0xdf, 0xae, // } 54 | 0x80, 0x22, 0x00, 0x10, // SOFTWARE attribute header 55 | 0x53, 0x54, 0x55, 0x4e, // } 56 | 0x20, 0x74, 0x65, 0x73, // } User-agent... 57 | 0x74, 0x20, 0x63, 0x6c, // } ...name 58 | 0x69, 0x65, 0x6e, 0x74, // } 59 | 0x00, 0x24, 0x00, 0x04, // PRIORITY attribute header 60 | 0x6e, 0x00, 0x01, 0xff, // ICE priority value 61 | 0x80, 0x29, 0x00, 0x08, // ICE-CONTROLLED attribute header 62 | 0x93, 0x2f, 0xf9, 0xb1, // } Pseudo-random tie breaker... 63 | 0x51, 0x26, 0x3b, 0x36, // } ...for ICE control 64 | 0x00, 0x06, 0x00, 0x09, // USERNAME attribute header 65 | 0x65, 0x76, 0x74, 0x6a, // } 66 | 0x3a, 0x68, 0x36, 0x76, // } Username (9 bytes) and padding (3 bytes) 67 | 0x59, 0x20, 0x20, 0x20, // } 68 | 0x00, 0x08, 0x00, 0x14, // MESSAGE-INTEGRITY attribute header 69 | 0x9a, 0xea, 0xa7, 0x0c, // } 70 | 0xbf, 0xd8, 0xcb, 0x56, // } 71 | 0x78, 0x1e, 0xf2, 0xb5, // } HMAC-SHA1 fingerprint 72 | 0xb2, 0xd3, 0xf2, 0x49, // } 73 | 0xc1, 0xb5, 0x71, 0xa2, // } 74 | 0x80, 0x28, 0x00, 0x04, // FINGERPRINT attribute header 75 | 0xe5, 0x7a, 0x3b, 0xcf // CRC32 fingerprint 76 | ]); 77 | -------------------------------------------------------------------------------- /test/fixtures/messages/sample-request-ltc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a test message from RFC5769 - Test Vectors for Session Traversal Utilities for NAT (STUN) 3 | * https://tools.ietf.org/html/rfc5769 4 | * 5 | * 6 | * 7 | * 2.4. Sample Request with Long-Term Authentication 8 | * 9 | * This request uses the following parameters: 10 | * 11 | * Username: "" 12 | * (without quotes) unaffected by SASLprep [RFC4013] processing 13 | * 14 | * Password: "TheMtr" and "TheMatrIX" (without 15 | * quotes) respectively before and after SASLprep processing 16 | * 17 | * Nonce: "f//499k954d6OL34oL9FSTvy64sA" (without quotes) 18 | * 19 | * Realm: "example.org" (without quotes) 20 | * 21 | * 00 01 00 60 Request type and message length 22 | * 21 12 a4 42 Magic cookie 23 | * 78 ad 34 33 } 24 | * c6 ad 72 c0 } Transaction ID 25 | * 29 da 41 2e } 26 | * 00 06 00 12 USERNAME attribute header 27 | * e3 83 9e e3 } 28 | * 83 88 e3 83 } 29 | * aa e3 83 83 } Username value (18 bytes) and padding (2 bytes) 30 | * e3 82 af e3 } 31 | * 82 b9 00 00 } 32 | * 00 15 00 1c NONCE attribute header 33 | * 66 2f 2f 34 } 34 | * 39 39 6b 39 } 35 | * 35 34 64 36 } 36 | * 4f 4c 33 34 } Nonce value 37 | * 6f 4c 39 46 } 38 | * 53 54 76 79 } 39 | * 36 34 73 41 } 40 | * 00 14 00 0b REALM attribute header 41 | * 65 78 61 6d } 42 | * 70 6c 65 2e } Realm value (11 bytes) and padding (1 byte) 43 | * 6f 72 67 00 } 44 | * 00 08 00 14 MESSAGE-INTEGRITY attribute header 45 | * f6 70 24 65 } 46 | * 6d d6 4a 3e } 47 | * 02 b8 e0 71 } HMAC-SHA1 fingerprint 48 | * 2e 85 c9 a2 } 49 | * 8c a8 96 66 } 50 | * 51 | */ 52 | 53 | var Buffer = require('buffer').Buffer; 54 | module.exports = new Buffer([ 55 | 0x00, 0x01, 0x00, 0x60, // Request type and message length 56 | 0x21, 0x12, 0xa4, 0x42, // Magic cookie 57 | 0x78, 0xad, 0x34, 0x33, // } 58 | 0xc6, 0xad, 0x72, 0xc0, // } Transaction ID 59 | 0x29, 0xda, 0x41, 0x2e, // } 60 | 0x00, 0x06, 0x00, 0x12, // USERNAME attribute header 61 | 0xe3, 0x83, 0x9e, 0xe3, // } 62 | 0x83, 0x88, 0xe3, 0x83, // } 63 | 0xaa, 0xe3, 0x83, 0x83, // } Username value (18 bytes) and padding (2 bytes) 64 | 0xe3, 0x82, 0xaf, 0xe3, // } 65 | 0x82, 0xb9, 0x00, 0x00, // } 66 | 0x00, 0x15, 0x00, 0x1c, // NONCE attribute header 67 | 0x66, 0x2f, 0x2f, 0x34, // } 68 | 0x39, 0x39, 0x6b, 0x39, // } 69 | 0x35, 0x34, 0x64, 0x36, // } 70 | 0x4f, 0x4c, 0x33, 0x34, // } Nonce value 71 | 0x6f, 0x4c, 0x39, 0x46, // } 72 | 0x53, 0x54, 0x76, 0x79, // } 73 | 0x36, 0x34, 0x73, 0x41, // } 74 | 0x00, 0x14, 0x00, 0x0b, // REALM attribute header 75 | 0x65, 0x78, 0x61, 0x6d, // } 76 | 0x70, 0x6c, 0x65, 0x2e, // } Realm value (11 bytes) and padding (1 byte) 77 | 0x6f, 0x72, 0x67, 0x00, // } 78 | 0x00, 0x08, 0x00, 0x14, // MESSAGE-INTEGRITY attribute header 79 | 0xf6, 0x70, 0x24, 0x65, // } 80 | 0x6d, 0xd6, 0x4a, 0x3e, // } 81 | 0x02, 0xb8, 0xe0, 0x71, // } HMAC-SHA1 fingerprint 82 | 0x2e, 0x85, 0xc9, 0xa2, // } 83 | 0x8c, 0xa8, 0x96, 0x66 // } 84 | ]); 85 | -------------------------------------------------------------------------------- /lib/Server.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'), 2 | util = require('util'), 3 | EventEmitter = require('events').EventEmitter, 4 | Message = require('./Message'); 5 | 6 | function Server(options, storageService) { 7 | EventEmitter.call(this); 8 | 9 | options = options || {}; 10 | 11 | this.type = options.type || Server.TYPE.TLS; 12 | 13 | this.storage = storageService || new require('./storage/MemoryStorage')(); 14 | 15 | //if type is TLS and tls options aren't passed, error out 16 | if(this.type === Server.TYPE.TLS && !options.tls) { 17 | throw new Error('Server is of type TLS, but no tls options were passed.'); 18 | } 19 | 20 | if(options.server) { 21 | var adr = options.server.address(); 22 | this.port = adr.port; 23 | this.host = adr.host || adr.address; 24 | 25 | this.server = options.server; 26 | } else { 27 | this.port = options.port || (this.type === Server.TYPE.TLS ? Server.DEFAULT_TLS_PORT : Server.DEFAULT_PORT); 28 | this.host = options.host || '0.0.0.0'; 29 | 30 | this.server = this._createServer(options.tls); 31 | } 32 | } 33 | module.exports = Server; 34 | 35 | util.inherit(Server, EventEmitter); 36 | 37 | Server.prototype.start = function(cb) { 38 | if(this.type === Server.TYPE.UDP) { 39 | this.server.bind(this.port, this.host, cb); 40 | } else { 41 | this.server.listen(this.port, this.host, cb); 42 | } 43 | }; 44 | 45 | Server.prototype.close = function() { 46 | this.server.close(); 47 | }; 48 | 49 | Server.prototype._onMessage = function(buffer, rinfo) { 50 | var message; 51 | try { 52 | message = Message.fromBuffer(buffer); 53 | } catch(e) { 54 | return; 55 | } 56 | 57 | //for Response classes ensure there is an active transaction for this ID 58 | if(message.class === Message.CLASSES.SUCCESS_RESP || message.class === Message.CLASSES.ERROR_RESP) { 59 | if(!this.storage.isTransactionActive(message)) { 60 | return; //invalid message, silently discard 61 | } 62 | } 63 | 64 | /*If the FINGERPRINT extension 65 | is being used, the agent checks that the FINGERPRINT attribute is 66 | present and contains the correct value. If any errors are detected, 67 | the message is silently discarded.*/ 68 | 69 | //TODO: Authentication processing 70 | 71 | /*Once the authentication checks are done, the STUN agent checks for 72 | unknown attributes and known-but-unexpected attributes in the 73 | message. Unknown comprehension-optional attributes MUST be ignored 74 | by the agent. Known-but-unexpected attributes SHOULD be ignored by 75 | the agent. Unknown comprehension-required attributes cause 76 | processing that depends on the message class and is described below.*/ 77 | }; 78 | 79 | Server.prototype._createServer = function(tlsOptions) { 80 | switch(this.type) { 81 | case Server.TYPE.UDP: 82 | this.net = require('dgram'); 83 | this.server = this.net.createSocket('udp4'); 84 | this.server.bind(this.port, this.host); 85 | this.server.on('message', this._onMessage.bind(this)); 86 | break; 87 | 88 | case Server.TYPE.TCP: 89 | this.net = require('net'); 90 | this.server = this.net.createServer(); 91 | this.server.listen(this.port, this.host); 92 | this.server.on('connection', this._onTcpConnection.bind(this)); 93 | break; 94 | 95 | case Server.TYPE.TLS: 96 | /* falls through */ 97 | default: 98 | this.net = require('tls'); 99 | this.server = this.net.createServer(tlsOptions); 100 | this.server.listen(this.port, this.host); 101 | this.server.on('secureConnection', this._onTcpConnection.bind(this)); 102 | } 103 | }; 104 | 105 | Server.prototype._onTcpConnection = function(stream) { 106 | stream.on('data', this._onMessage.bind(this)); 107 | }; 108 | 109 | Server.TYPE = { 110 | TLS: 0, 111 | TCP: 1, 112 | UDP: 2 113 | }; 114 | 115 | Server.DEFAULT_PORT = 3478; 116 | Server.DEFAULT_TLS_PORT = 5349; 117 | -------------------------------------------------------------------------------- /test/fixtures/parse-test-data.js: -------------------------------------------------------------------------------- 1 | var Buffer = require('buffer').Buffer; 2 | 3 | var data = { 4 | messages: [ 5 | //valid messages 6 | { 7 | type: fbin('00000100000001'), 8 | method: fbin('000000000001'), //binding request 9 | class: fbin('10'), //success message 10 | 11 | cookie: 0x2112A442, 12 | length: fbin('0000000000000111'), 13 | tid: new Buffer([ 14 | 0x01, 0x1A, 0xFD, 0xDF, 15 | 0x45, 0x2F, 0xFA, 0xDD, 16 | 0x51, 0x9B, 0xCA, 0x56 17 | ]) 18 | }, 19 | { 20 | type: fbin('00000100010010'), 21 | method: fbin('000000000010'), 22 | class: fbin('11'), 23 | 24 | cookie: 0x2112A442, 25 | length: fbin('0000000000000111'), 26 | tid: new Buffer([ 27 | 0x01, 0x1A, 0xFD, 0xDF, 28 | 0x45, 0x2F, 0xFA, 0xDD, 29 | 0x51, 0x9B, 0xCA, 0x56 30 | ]) 31 | }, 32 | { 33 | type: fbin('00000100000010'), 34 | method: fbin('000000000010'), 35 | class: fbin('10'), 36 | 37 | cookie: 0x2112A442, 38 | length: fbin('0000000000000111'), 39 | tid: new Buffer([ 40 | 0x01, 0x1A, 0xFD, 0xDF, 41 | 0x45, 0x2F, 0xFA, 0xDD, 42 | 0x51, 0x9B, 0xCA, 0x56 43 | ]) 44 | }, 45 | { 46 | type: fbin('00000000000001'), 47 | method: fbin('000000000001'), 48 | class: fbin('00'), 49 | 50 | cookie: 0x2112A442, 51 | length: fbin('0000000000000111'), 52 | tid: new Buffer([ 53 | 0x01, 0x1A, 0xFD, 0xDF, 54 | 0x45, 0x2F, 0xFA, 0xDD, 55 | 0x51, 0x9B, 0xCA, 0x56 56 | ]) 57 | } 58 | ], 59 | invalidMessages: [ 60 | //invalid messages 61 | { 62 | type: fbin('1100000100000001'), //invalid type header (first two bits are not 0) 63 | method: fbin('010010000001'), 64 | class: fbin('10'), 65 | 66 | cookie: 0x2112A442, 67 | length: fbin('0000000000000111'), 68 | tid: new Buffer([ 69 | 0x01, 0x1A, 0xFD, 0xDF, 70 | 0x45, 0x2F, 0xFA, 0xDD, 71 | 0x51, 0x9B, 0xCA, 0x56 72 | ]) 73 | }, 74 | { 75 | type: fbin('00000100000001'), 76 | method: fbin('100001001001'), 77 | class: fbin('10'), 78 | 79 | cookie: 0x0102A242, //invalid magic cookie 80 | length: fbin('0000000000000111'), 81 | tid: new Buffer([ 82 | 0x01, 0x1A, 0xFD, 0xDF, 83 | 0x45, 0x2F, 0xFA, 0xDD, 84 | 0x51, 0x9B, 0xCA, 0x56 85 | ]) 86 | }, 87 | { 88 | type: fbin('10100010110001'), 89 | method: fbin('101001010001'), 90 | class: fbin('01'), 91 | 92 | cookie: 0x2112A442, 93 | length: fbin('0000000000000111'), 94 | tid: new Buffer([ 95 | 0x01, 0x1A, 0xFD, 0xDF, 96 | 0x45, 0x2F, 0xFA, 0xDD, 97 | 0x51, 0x9B, 0xCA, 0x56 98 | ]) 99 | }, 100 | { 101 | type: fbin('00000000001011'), 102 | method: fbin('000000001011'), 103 | class: fbin('00'), 104 | 105 | cookie: 0x2112A442, 106 | length: fbin('0000011111111111'), 107 | tid: new Buffer([ 108 | 0x01, 0x1A, 0xFD, 0xDF, 109 | 0x45, 0x2F, 0xFA, 0xDD, 110 | 0x51, 0x9B, 0xCA, 0x56 111 | ]) 112 | }, 113 | { 114 | type: fbin('00000000010010'), 115 | method: fbin('000000000010'), 116 | class: fbin('01'), 117 | 118 | cookie: 0x2112A442, 119 | length: fbin('0000000000000111'), 120 | tid: new Buffer([ 121 | 0x01, 0x1A, 0xFD, 0xDF, 122 | 0x45, 0x2F, 0xFA, 0xDD, 123 | 0x51, 0x9B, 0xCA, 0x56 124 | ]) 125 | } 126 | ] 127 | }; 128 | 129 | data.messages.concat(data.invalidMessages).forEach(function(msg) { 130 | msg.full = Buffer.concat([ 131 | new Buffer([ 132 | msg.type >> 8, msg.type, msg.length >> 8, msg.length, 133 | msg.cookie >> 24, msg.cookie >> 16, msg.cookie >> 8, msg.cookie 134 | ]), 135 | msg.tid 136 | ]); 137 | }); 138 | 139 | function fbin(s) { 140 | return parseInt(s, 2); 141 | } 142 | 143 | module.exports = data; 144 | -------------------------------------------------------------------------------- /lib/Message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Message header format: 3 | * 4 | * All STUN messages MUST start with a 20-byte header followed by zero 5 | * or more Attributes. The STUN header contains a STUN message type, 6 | * magic cookie, transaction ID, and message length. 7 | * 8 | * 0 1 2 3 9 | * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 10 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 11 | * |0 0| STUN Message Type | Message Length | 12 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 13 | * | Magic Cookie | 14 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 15 | * | | 16 | * | Transaction ID (96 bits) | 17 | * | | 18 | * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 19 | * 20 | * The most significant 2 bits of every STUN message MUST be zeroes. 21 | * The magic cookie field of every STUN message MUST be 0x2112A442. 22 | * 23 | * The message type field is decomposed further into the following 24 | * structure: 25 | * 26 | * 0 1 27 | * 2 3 4 5 6 7 8 9 0 1 2 3 4 5 28 | * 29 | * +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ 30 | * |M |M |M|M|M|C|M|M|M|C|M|M|M|M| 31 | * |11|10|9|8|7|1|6|5|4|0|3|2|1|0| 32 | * +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ 33 | * 34 | */ 35 | 36 | function Message(type, tid, body) { 37 | this.type = type; 38 | this.body = body; 39 | this.tid = tid; 40 | 41 | // (type & 0b11111000000000) >> 2 | (type & 0b00000011100000) >> 1 | (type & 0b00000000001111) 42 | this.method = ((type & 0x3E00) >> 2) | ((type & 0xE0) >> 1) | (type & 0x0F); 43 | 44 | // (type & 0b00000100000000) >> 7 | (type & 0b00000000010000) >> 4 45 | this.class = ((type & 0x0100) >> 7) | ((type & 0x10) >> 4); 46 | } 47 | module.exports = Message; 48 | 49 | /////////////////// 50 | // Instance Methods & Properties 51 | /////////////////// 52 | 53 | /*Message.prototype.getMethodName = function() { 54 | return Message.METHODNAMES[this.method]; 55 | }; 56 | 57 | Message.prototype.getClassName = function() { 58 | return Message.CLASSNAMES[this.class]; 59 | };*/ 60 | 61 | Message.prototype.toBuffer = function() { 62 | var buff = new Buffer(8); 63 | 64 | buff.writeUInt16BE(this.type, 0); 65 | buff.writeUInt16BE(this.body.length, 2); 66 | buff.writeUInt32BE(Message.MAGIC_COOKIE, 4); 67 | 68 | return Buffer.concat([buff, this.tid.slice(0, 12)]); 69 | }; 70 | 71 | /////////////////// 72 | // Static Methods & Properties 73 | /////////////////// 74 | 75 | Message.fromBuffer = function(data, offset) { 76 | offset = offset || 0; 77 | 78 | //ANDs the first byte with 0b11000000 to ensure the first two bits are zero 79 | if((data[offset] & 192) !== 0) { 80 | Message.parseError('First two bits are not zero. First byte: 0x%02X', data[offset]); 81 | } 82 | 83 | //ensure the magic cookie value is set, which indicates this is an RFC5389 STUN Message 84 | if(data.readUInt32BE(offset + 4) !== Message.MAGIC_COOKIE) { 85 | Message.parseError('The magic cookie field is: 0x%08X', data.readUInt32BE(offset + 4)); 86 | } 87 | 88 | //read data from message header 89 | var type = data.readUInt16BE(offset), 90 | length = data.readUInt16BE(offset + 2), 91 | tid = data.slice(offset + 8, offset + 20); 92 | 93 | //ensure length is proper 94 | if(length + Message.HEAD_SIZE > Message.MAX_SIZE) { 95 | Message.parseError( 96 | 'The message length is too long. Passed length: %d bytes; maximum is: %d bytes', 97 | length, 98 | Message.MAX_SIZE - Message.HEAD_SIZE 99 | ); 100 | } 101 | 102 | var message = new Message(type, tid, (new Array(length + 1)).join('A')); 103 | 104 | //check if method is supported 105 | if(!Message.METHOD_NAMES[message.method]) { 106 | Message.parseError('The message method is not supported. Passed value: 0x%03X', message.method); 107 | } 108 | 109 | //check is class is supported 110 | if(Message.METHOD_CLASSES[message.method].indexOf(message.class) === -1) { 111 | Message.parseError('The message class is not supported for this method. Method: 0x%03X, class: 0x%02X', message.method, message.class); 112 | } 113 | 114 | //pass validation, return message 115 | return message; 116 | }; 117 | 118 | Message.CLASSES = { 119 | REQUEST: 0x00, 120 | INDICATION: 0x01, 121 | SUCCESS_RESP: 0x02, 122 | ERROR_RESP: 0x03 123 | }; 124 | 125 | Message.CLASS_NAMES = {}; 126 | for(var c in Message.CLASSES) { 127 | Message.CLASS_NAMES[Message.CLASSES[c]] = c.replace(/_/g, '-'); 128 | } 129 | 130 | Message.METHODS = { 131 | RESERVED: 0x000, 132 | BINDING: 0x001, 133 | SHARED_SECRET: 0x002, 134 | }; 135 | 136 | Message.METHOD_NAMES = {}; 137 | for(var m in Message.METHODS) { 138 | Message.METHOD_NAMES[Message.METHODS[m]] = m.replace(/_/g, '-'); 139 | } 140 | 141 | Message.METHOD_CLASSES = {}; 142 | Message.METHOD_CLASSES[Message.METHODS.RESERVED] = []; 143 | Message.METHOD_CLASSES[Message.METHODS.BINDING] = [ 144 | Message.CLASSES.REQUEST, 145 | Message.CLASSES.SUCCESS_RESP, 146 | Message.CLASSES.ERROR_RESP 147 | ]; 148 | Message.METHOD_CLASSES[Message.METHODS.SHARED_SECRET] = [ 149 | Message.CLASSES.REQUEST, 150 | Message.CLASSES.SUCCESS_RESP, 151 | Message.CLASSES.ERROR_RESP 152 | ]; 153 | 154 | Message.ATTRIBUTES = { 155 | //Comprehension-required range (0x0000-0x7FFF): 156 | RESERVED: 0x0000, //0x0000: (Reserved) 157 | MAPPED_ADDRESS: 0x0001, //0x0001: MAPPED-ADDRESS 158 | RESPONSE_ADDRESS: 0x0002, //0x0002: (Reserved; was RESPONSE-ADDRESS) 159 | CHANGE_ADDRESS: 0x0003, //0x0003: (Reserved; was CHANGE-ADDRESS) 160 | SOURCE_ADDRESS: 0x0004, //0x0004: (Reserved; was SOURCE-ADDRESS) 161 | CHANGED_ADDRESS: 0x0005, //0x0005: (Reserved; was CHANGED-ADDRESS) 162 | USERNAME: 0x0006, //0x0006: USERNAME 163 | PASSWORD: 0x0007, //0x0007: (Reserved; was PASSWORD) 164 | MESSAGE_INTEGRITY: 0x0008, //0x0008: MESSAGE-INTEGRITY 165 | ERROR_CODE: 0x0009, //0x0009: ERROR-CODE 166 | UNKNOWN_ATTRIBUTES: 0x000A, //0x000A: UNKNOWN-ATTRIBUTES 167 | REFLECTED_FROM: 0x000B, //0x000B: (Reserved; was REFLECTED-FROM) 168 | REALM: 0x0014, //0x0014: REALM 169 | NONCE: 0x0015, //0x0015: NONCE 170 | XOR_MAPPED_ADDRESS: 0x0020, //0x0020: XOR-MAPPED-ADDRESS 171 | 172 | //Comprehension-optional range (0x8000-0xFFFF) 173 | SOFTWARE: 0x8022, //0x8022: SOFTWARE 174 | ALTERNATE_SERVER: 0x8023, //0x8023: ALTERNATE-SERVER 175 | FINGERPRINT: 0x8028 //0x8028: FINGERPRINT 176 | }; 177 | 178 | Message.ATTRIBUTENAMES = {}; 179 | for(var a in Message.ATTRIBUTES) { 180 | Message.ATTRIBUTENAMES[Message.ATTRIBUTES[a]] = a.replace(/_/g, '-'); 181 | } 182 | 183 | Message.MAGIC_COOKIE = 0x2112A442; 184 | Message.HEAD_SIZE = 20; 185 | Message.MAX_SIZE = 576; 186 | 187 | var sprintf = require('sprintf').sprintf; 188 | 189 | Message.parseError = function(msg) { 190 | msg = sprintf.apply(null, arguments); 191 | 192 | throw new Error('Invalid data/offset. ' + msg); 193 | }; 194 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // -------------------------------------------------------------------- 3 | // JSHint Configuration 4 | // -------------------------------------------------------------------- 5 | // 6 | // @author Chad Engler 7 | 8 | // == Enforcing Options =============================================== 9 | // 10 | // These options tell JSHint to be more strict towards your code. Use 11 | // them if you want to allow only a safe subset of JavaScript, very 12 | // useful when your codebase is shared with a big number of developers 13 | // with different skill levels. 14 | 15 | "bitwise" : false, // Disallow bitwise operators (&, |, ^, etc.). 16 | "camelcase" : true, // Force all variable names to use either camelCase or UPPER_CASE. 17 | "curly" : false, // Require {} for every new block or scope. 18 | "eqeqeq" : true, // Require triple equals i.e. `===`. 19 | "es3" : false, // Enforce conforming to ECMAScript 3. 20 | "forin" : false, // Disallow `for in` loops without `hasOwnPrototype`. 21 | "immed" : true, // Require immediate invocations to be wrapped in parens e.g. `( function(){}() );` 22 | "indent" : 4, // Require that 4 spaces are used for indentation. 23 | "latedef" : true, // Prohibit variable use before definition. 24 | "newcap" : true, // Require capitalization of all constructor functions e.g. `new F()`. 25 | "noarg" : true, // Prohibit use of `arguments.caller` and `arguments.callee`. 26 | "noempty" : true, // Prohibit use of empty blocks. 27 | "nonew" : true, // Prohibit use of constructors for side-effects. 28 | "plusplus" : false, // Disallow use of `++` & `--`. 29 | "quotmark" : true, // Force consistency when using quote marks. 30 | "undef" : true, // Require all non-global variables be declared before they are used. 31 | "unused" : true, // Warn when varaibles are created by not used. 32 | "strict" : false, // Require `use strict` pragma in every file. 33 | "trailing" : true, // Prohibit trailing whitespaces. 34 | "maxparams" : 6, // Prohibit having more than X number of params in a function. 35 | "maxdepth" : 6, // Prohibit nested blocks from going more than X levels deep. 36 | "maxstatements" : false, // Restrict the number of statements in a function. 37 | "maxcomplexity" : false, // Restrict the cyclomatic complexity of the code. 38 | "maxlen" : 125, // Require that all lines are 100 characters or less. 39 | "globals" : { // Register globals that are used in the code. 40 | // Mocha BDD globals 41 | "describe" : false, 42 | "it" : false, 43 | 44 | // Chai globals that we export in ./test/fixtures/common.js 45 | "chai" : false, 46 | "expect" : false, 47 | 48 | // Sinon globals that we export in ./test/fixtures/common.js 49 | "sinon" : false 50 | }, 51 | 52 | // == Relaxing Options ================================================ 53 | // 54 | // These options allow you to suppress certain types of warnings. Use 55 | // them only if you are absolutely positive that you know what you are 56 | // doing. 57 | 58 | "asi" : false, // Tolerate Automatic Semicolon Insertion (no semicolons). 59 | "boss" : false, // Tolerate assignments inside if, for & while. Usually conditions & loops are for comparison, not assignments. 60 | "debug" : false, // Allow debugger statements e.g. browser breakpoints. 61 | "eqnull" : false, // Tolerate use of `== null`. 62 | "esnext" : false, // Allow ES.next specific features such as `const` and `let`. 63 | "evil" : false, // Tolerate use of `eval`. 64 | "expr" : false, // Tolerate `ExpressionStatement` as Programs. 65 | "funcscope" : false, // Tolerate declarations of variables inside of control structures while accessing them later from the outside. 66 | "globalstrict" : false, // Allow global "use strict" (also enables 'strict'). 67 | "iterator" : false, // Allow usage of __iterator__ property. 68 | "lastsemic" : false, // Tolerate missing semicolons when the it is omitted for the last statement in a one-line block. 69 | "laxbreak" : false, // Tolerate unsafe line breaks e.g. `return [\n] x` without semicolons. 70 | "laxcomma" : false, // Suppress warnings about comma-first coding style. 71 | "loopfunc" : false, // Allow functions to be defined within loops. 72 | "moz" : false, // Code that uses Mozilla JS extensions will set this to true 73 | "multistr" : false, // Tolerate multi-line strings. 74 | "proto" : false, // Tolerate __proto__ property. This property is deprecated. 75 | "scripturl" : false, // Tolerate script-targeted URLs. 76 | "smarttabs" : false, // Tolerate mixed tabs and spaces when the latter are used for alignmnent only. 77 | "shadow" : false, // Allows re-define variables later in code e.g. `var x=1; x=2;`. 78 | "sub" : false, // Tolerate all forms of subscript notation besides dot notation e.g. `dict['key']` instead of `dict.key`. 79 | "supernew" : false, // Tolerate `new function () { ... };` and `new Object;`. 80 | "validthis" : false, // Tolerate strict violations when the code is running in strict mode and you use this in a non-constructor function. 81 | 82 | // == Environments ==================================================== 83 | // 84 | // These options pre-define global variables that are exposed by 85 | // popular JavaScript libraries and runtime environments—such as 86 | // browser or node.js. 87 | 88 | "browser" : false, // Standard browser globals e.g. `window`, `document`. 89 | "couch" : false, // Enable globals exposed by CouchDB. 90 | "devel" : false, // Allow development statements e.g. `console.log();`. 91 | "dojo" : false, // Enable globals exposed by Dojo Toolkit. 92 | "jquery" : false, // Enable globals exposed by jQuery JavaScript library. 93 | "mootools" : false, // Enable globals exposed by MooTools JavaScript framework. 94 | "node" : true, // Enable globals available when code is running inside of the NodeJS runtime environment. 95 | "nonstandard" : false, // Define non-standard but widely adopted globals such as escape and unescape. 96 | "prototypejs" : false, // Enable globals exposed by Prototype JavaScript framework. 97 | "rhino" : false, // Enable globals available when your code is running inside of the Rhino runtime environment. 98 | "worker" : false, // Enable globals available when your code is running as a WebWorker. 99 | "wsh" : false, // Enable globals available when your code is running as a script for the Windows Script Host. 100 | "yui" : false, // Enable globals exposed by YUI library. 101 | 102 | // == JSLint Legacy =================================================== 103 | // 104 | // These options are legacy from JSLint. Aside from bug fixes they will 105 | // not be improved in any way and might be removed at any point. 106 | 107 | "nomen" : false, // Prohibit use of initial or trailing underbars in names. 108 | "onevar" : false, // Allow only one `var` statement per function. 109 | "passfail" : false, // Stop on first error. 110 | "white" : false, // Check against strict whitespace and indentation rules. 111 | 112 | // == Undocumented Options ============================================ 113 | // 114 | // While I've found these options in [example1][2] and [example2][3] 115 | // they are not described in the [JSHint Options documentation][4]. 116 | // 117 | // [4]: http://www.jshint.com/options/ 118 | 119 | "maxerr" : 100 // Maximum errors before stopping. 120 | } 121 | --------------------------------------------------------------------------------