/// /// /// describe("stun messages", function() { function getTransactionIdBytes() : Uint8Array { return new Uint8Array([ 0x2f, 0x68, 0x65, 0x79, 0x6b, 0x6b, 0x31, 0x54, 0x46, 0x32, 0x36, 0x57]); } /** Returns the "magic cookie" bytes. */ function getMagicCookieBytes() : Uint8Array { return new Uint8Array([0x21, 0x12, 0xa4, 0x42]); } /** * Returns the bytes of a simple valid STUN message. * The message has the SUCCESS_RESPONSE (C1 bit) class and * has the CHANNEL_BIND method. */ function getSimpleStunMessageBytes() : Uint8Array { var bytes = new Uint8Array(20); bytes[0] = 0x01; // type (method + class) bytes[1] = 0x09; bytes[2] = 0; // length bytes[3] = 0; bytes.set(getMagicCookieBytes(), 4); bytes.set(getTransactionIdBytes(), 8); return bytes; }; /** As #getSimpleStunMessageBytes but the message has two attributes. */ function getStunMessageWithAttributesBytes() : Uint8Array { var attrBytes = new Uint8Array([ // First attribute. 1200 >> 8, 1200 & 0xFF, // type 1200 0, 2, // length 0x55, 0x56, 0, 0, // value (and 32-bit boundary padding) // Second attribute. 1000 >> 8, 1000 & 0xFF, // type 1000 0, 4, // length 0x55, 0x56, 0x57, 0x58]); // value var simpleBytes = getSimpleStunMessageBytes(); var bigBuff = new ArrayBuffer(simpleBytes.length + attrBytes.length); var bigBytes = new Uint8Array(bigBuff); bigBytes.set(simpleBytes); bigBytes.set(attrBytes, simpleBytes.length); bigBytes[3] = attrBytes.length; // message length. return bigBytes; } /** Returns the message encoded by #getSimpleStunMessageBytes. */ function getSimpleStunMessage() : Turn.StunMessage { return { method: Turn.MessageMethod.CHANNEL_BIND, clazz: Turn.MessageClass.SUCCESS_RESPONSE, transactionId: getTransactionIdBytes(), attributes: [] }; } /** Returns the message encoded by #getStunMessageWithAttributesBytes. */ function getStunMessageWithAttributes() : Turn.StunMessage { var message = getSimpleStunMessage(); message.attributes = [{ type: 1200, value: new Uint8Array([0x55, 0x56]) }, { type: 1000, value: new Uint8Array([0x55, 0x56, 0x57, 0x58]) }]; return message; } /** Returns the bytes of a MAPPED-ADDRESS attribute. */ function getMappedAddressAttributeBytes() : Uint8Array { return new Uint8Array([ 0x00, // reserved 0x01, // address family (IPv4) 32000 >> 8, // msb of port 32000 & 0xff, // lsb of port 192, 168, 1, 1]); // address } /** XOR-MAPPED-ADDRESS equivalent of #getMappedAddressAttributeBytes. */ function getXorMappedAddressAttributeBytes() : Uint8Array { var bytes = getMappedAddressAttributeBytes(); var magicCookieBytes = getMagicCookieBytes(); bytes[2] ^= magicCookieBytes[0]; // msb of port bytes[3] ^= magicCookieBytes[1]; // lsb of port for (var i = 0; i < 4; i++) { bytes[4 + i] ^= magicCookieBytes[i]; } return bytes; } /** * Returns the endpoint encoded by #getMappedAddressAttributeBytes * and #getXorMappedAddressAttributeBytes. */ function getEndpoint() : Turn.Endpoint { return { address: '192.168.1.1', port: 32000 }; } it('reject short messages', function() { expect(function() { Turn.parseStunMessage(new Uint8Array(new ArrayBuffer(5))); }).toThrow(); }); it('reject non-zero first two bits', function() { var bytes = getSimpleStunMessageBytes(); bytes[0] = 0xd0; expect(function() { Turn.parseStunMessage(bytes); }).toThrow(); }); it('parse simple message', function() { var bytes = getSimpleStunMessageBytes(); var req = Turn.parseStunMessage(bytes); expect(req.clazz).toEqual(Turn.MessageClass.SUCCESS_RESPONSE); expect(req.method).toEqual(Turn.MessageMethod.CHANNEL_BIND); expect(compareUint8Array(req.transactionId, getTransactionIdBytes())).toBe(true); expect(req.attributes.length).toEqual(0); }); it('format simple message', function() { var message = getSimpleStunMessage(); var bytes = Turn.formatStunMessage(message); var expectedBytes = getSimpleStunMessageBytes(); expect(compareUint8Array(expectedBytes, bytes)).toBe(true); }); it('method bit operations', function() { // Set the first two bytes to read: // 0011 0011 1101 1101 // Masking out the leading two bits and C1 and C0 leaves: // xx11 001x 110x 1101 -> 1100 1110 1101 (0xced) var bytes = getSimpleStunMessageBytes(); bytes[0] = 0x33; bytes[1] = 0xdd; var req = Turn.parseStunMessage(bytes); expect(req.clazz).toEqual(Turn.MessageClass.FAILURE_RESPONSE); expect(req.method).toEqual(0xced); }); /** * Sets one of the magic cookie bytes to the wrong value and verifies * that an exception is raised. */ it('parse simple attribute', function() { var bytes = getSimpleStunMessageBytes(); bytes[6] = 0xFF; expect(function() { Turn.parseStunMessage(bytes); }).toThrow(); }); it('reject short attributes', function() { expect(function() { Turn.parseStunAttribute(new Uint8Array(new ArrayBuffer(3))); }).toThrow(); }); it('parse simple attribute', function() { var bytes = new Uint8Array([ 1200 >> 8, 1200 & 0xFF, // type 0, 2, // length 0x55, 0x56]); // value var attr = Turn.parseStunAttribute(bytes); expect(attr.type).toEqual(1200); expect(attr.value.length).toEqual(2); expect(attr.value[0]).toEqual(0x55); expect(attr.value[1]).toEqual(0x56); }); it('format simple attribute', function() { var attr = { type: 1200, value: new Uint8Array([0x55, 0x56]) }; var bytes = new Uint8Array(8); Turn.formatStunAttribute(attr, bytes); var expectedBytes = new Uint8Array([ 1200 >> 8, 1200 & 0xFF, // type 0, 2, // length 0x55, 0x56, 0, 0]); // value and padding expect(compareUint8Array(expectedBytes, bytes)).toBe(true); }); it('parse zero-length attribute', function() { var bytes = new Uint8Array([ 0, 0, // type 0, 0]); // length var attr = Turn.parseStunAttribute(bytes); expect(attr.value).toBeUndefined(); }); it('format zero-length attribute', function() { var attr = { type: 1 }; var bytes = new Uint8Array(4); Turn.formatStunAttribute(attr, bytes); var expectedBytes = new Uint8Array([ 0, 1, // type 0, 0]); // length expect(compareUint8Array(expectedBytes, bytes)).toBe(true); }); it('format attribute with too few bytes', function() { var attr = { type: 1 }; var bytes = new Uint8Array(3); expect(function() { Turn.formatStunAttribute(attr, bytes); }).toThrow(); }); it('parse message with attributes', function() { var bytes = getStunMessageWithAttributesBytes(); var req = Turn.parseStunMessage(bytes); expect(req.attributes.length).toEqual(2); var attr1 = req.attributes[0]; expect(attr1.type).toEqual(1200); var expectedBytes = new Uint8Array([0x55, 0x56]); expect(compareUint8Array(expectedBytes, attr1.value)).toBe(true); var attr2 = req.attributes[1]; expect(attr2.type).toEqual(1000); var expectedBytes = new Uint8Array([0x55, 0x56, 0x57, 0x58]); expect(compareUint8Array(expectedBytes, attr2.value)).toBe(true); }); it('format message with attributes', function() { var message = getStunMessageWithAttributes(); var bytes = Turn.formatStunMessage(message); var expectedBytes = getStunMessageWithAttributesBytes(); expect(compareUint8Array(expectedBytes, bytes)).toBe(true); }); it('calculate padding', function() { expect(Turn.calculatePadding(0, 4)).toEqual(0); expect(Turn.calculatePadding(1, 4)).toEqual(4); expect(Turn.calculatePadding(4, 4)).toEqual(4); expect(Turn.calculatePadding(7, 4)).toEqual(8); }); it('format error-code attribute', function() { var bytes = Turn.formatErrorCodeAttribute(399, 'test'); var expectedBytes = new Uint8Array([ 0x00, // reserved 0x00, // reserved 0x03, // reserved + class 99, // number 't'.charCodeAt(0), 'e'.charCodeAt(0), 's'.charCodeAt(0), 't'.charCodeAt(0)]); expect(compareUint8Array(expectedBytes, bytes)).toBe(true); }); it('find stun attributes', function() { var message = getStunMessageWithAttributes(); // The first attribute is type 1200 and has two bytes worth of data. var attribute = Turn.findFirstAttributeWithType(1200, message.attributes); expect(attribute).toBeDefined(); expect(attribute.value.byteLength).toEqual(2); // The second attribute is type 1000 and has four bytes worth of data. attribute = Turn.findFirstAttributeWithType(1000, message.attributes); expect(attribute).toBeDefined(); expect(attribute.value.byteLength).toEqual(4); // This should not be found. expect(function() { Turn.findFirstAttributeWithType(500, message.attributes); }).toThrow(); }); it('format bad MAPPED-ADDRESS attribute', function() { expect(function() { Turn.formatMappedAddressAttribute('nonsense.xxx', 7); }).toThrow(); }); it('format MAPPED-ADDRESS attribute', function() { var bytes = Turn.formatMappedAddressAttribute('192.168.1.1', 32000); var expectedBytes = getMappedAddressAttributeBytes(); expect(compareUint8Array(expectedBytes, bytes)).toBe(true); }); it('format XOR-MAPPED-ADDRESS attribute', function() { var bytes = Turn.formatXorMappedAddressAttribute('192.168.1.1', 32000); var expectedBytes = getXorMappedAddressAttributeBytes(); expect(compareUint8Array(expectedBytes, bytes)).toBe(true); }); it('parse MAPPED-ADDRESS attribute', function() { var bytes = getMappedAddressAttributeBytes(); var attribute = Turn.parseMappedAddressAttribute(bytes); expect(attribute).toEqual(getEndpoint()); }); it('parse XOR-MAPPED-ADDRESS attribute', function() { var bytes = getXorMappedAddressAttributeBytes(); var attribute = Turn.parseXorMappedAddressAttribute(bytes); expect(attribute).toEqual(getEndpoint()); }); it('compute hash', function() { // This is the bytes comprising the following StunMessage, wireshark-ed // from a session between Chrome and rfc5766-turn-server: // { // method: ALLOCATE // clazz: SUCCESS_RESPONSE // transactionId: 4f:49:62:31:57:4c:58:67:39:74:4c:4a // attributes: { // XOR-RELAYED-ADDRESS: 127.0.0.1:64226 // XOR-MAPPED-ADDRESS: 172.26.76.61:48328 // LIFETIME: 600 // SOFTWARE: Citrix-3.2.3.5 'Marshal West' // MESSAGE-INTEGRITY: c7:28:13:13:33:68:fc:5b:8c:ae:fc:da:94:aa:99:45:6a:8f:2b:74 // } // } var dataBytes = new Uint8Array([ 0x01, 0x03, 0x00, 0x5c, 0x21, 0x12, 0xa4, 0x42, 0x4f, 0x49, 0x62, 0x31, 0x57, 0x4c, 0x58, 0x67, 0x39, 0x74, 0x4c, 0x4a, 0x00, 0x16, 0x00, 0x08, 0x00, 0x01, 0xdb, 0xf0, 0x5e, 0x12, 0xa4, 0x43, 0x00, 0x20, 0x00, 0x08, 0x00, 0x01, 0x9d, 0xda, 0x8d, 0x08, 0xe8, 0x7f, 0x00, 0x0d, 0x00, 0x04, 0x00, 0x00, 0x02, 0x58, 0x80, 0x22, 0x00, 0x1d, 0x43, 0x69, 0x74, 0x72, 0x69, 0x78, 0x2d, 0x33, 0x2e, 0x32, 0x2e, 0x33, 0x2e, 0x35, 0x20, 0x27, 0x4d, 0x61, 0x72, 0x73, 0x68, 0x61, 0x6c, 0x20, 0x57, 0x65, 0x73, 0x74, 0x27, 0x32, 0x2e, 0x33, // MESSAGE-INTEGRITY starts here. 0x00, 0x08, 0x00, 0x14, 0xc7, 0x28, 0x13, 0x13, 0x33, 0x68, 0xfc, 0x5b, 0x8c, 0xae, 0xfc, 0xda, 0x94, 0xaa, 0x99, 0x45, 0x6a, 0x8f, 0x2b, 0x74]); var expectedHashBytes = new Uint8Array([ 0xc7, 0x28, 0x13, 0x13, 0x33, 0x68, 0xfc, 0x5b, 0x8c, 0xae, 0xfc, 0xda, 0x94, 0xaa, 0x99, 0x45, 0x6a, 0x8f, 0x2b, 0x74]); var hashBytes = Turn.computeHash(dataBytes); expect(compareUint8Array(expectedHashBytes, hashBytes)).toBe(true); }); // Bah, toEqual() doesn't work for Uint8Array. function compareUint8Array(a1:Uint8Array, a2:Uint8Array) { if (a1.length != a2.length) { return false; } for (var i = 0; i < a1.length; i++) { if (a1[i] != a2[i]) { return false; } } return true; } });