/// /// /** * Utilities for decoding and encoding STUN messages. * TURN uses STUN messages, adding some methods and attributes. * * http://tools.ietf.org/html/rfc5389#section-6 */ module Turn { /** A STUN/TURN message, used for requests and responses. */ export interface StunMessage { method :MessageMethod; clazz :MessageClass; // Subarrays are much faster than encoding and decoding to a string. transactionId :Uint8Array; attributes :StunAttribute[]; } /** A STUN/TURN attribute, which carries data in a message. */ export interface StunAttribute { type :number; value ?:Uint8Array; } /** * Primary purpose of a request. * The values here are a subset of what's defined for STUN and TURN. * STUN only defines one method: BIND. * TURN adds several more methods: * http://tools.ietf.org/html/rfc5766#section-13 */ export enum MessageMethod { BIND = 1, // STUN's method; unsupported here, though Chrome tries it ALLOCATE = 3, REFRESH = 4, SEND = 6, DATA = 7, CREATE_PERMISSION = 8, CHANNEL_BIND = 9 } /** * This is orthogonal to method. Probably best to read: * http://tools.ietf.org/html/rfc5389#section-6 */ export enum MessageClass { REQUEST = 1, SUCCESS_RESPONSE = 2, FAILURE_RESPONSE = 3, INDICATION = 4 } /** * STUN/TURN attributes in which we are interested: * http://tools.ietf.org/html/rfc5389#section-15 * http://tools.ietf.org/html/rfc5766#section-14 */ export enum MessageAttribute { MAPPED_ADDRESS = 0x01, USERNAME = 0x06, MESSAGE_INTEGRITY = 0x08, ERROR_CODE = 0x09, XOR_PEER_ADDRESS = 0x12, DATA = 0x13, REALM = 0x14, NONCE = 0x15, XOR_RELAYED_ADDRESS = 0x16, REQUESTED_TRANSPORT = 0x19, XOR_MAPPED_ADDRESS = 0x20, LIFETIME = 0x0d, /** * This attribute is appended to messages sent from one side of the * TURN server to the other. For performance reasons we do not always * strip this attribute before a message is relayed to a client; the * value chosen lies within the "comprehension-optional" and undefined * ranges which means that TURN clients should feel free to ignore it * and the attribute should not interfere with ICE: * http://tools.ietf.org/html/rfc5389#section-18.2 * http://www.iana.org/assignments/stun-parameters/stun-parameters.xhtml */ IPC_TAG = 0xeeff } /** Represents a host:port combination. */ export interface Endpoint { address:string; port:number; } /** * Username with which our HMAC_KEY was generated. Clients will need to use * this to access the server. */ export var USERNAME = 'test'; /** * Password with which our HMAC_KEY was generated. Clients will need to use * this to access the server. */ export var PASSWORD = 'test'; /** * Realm for this server. Has no real meaning -- but this is used as part of * message signing (see HMAC_KEY). */ export var REALM = 'myrealm'; /** * Key for the HMAC algorithm with which we must sign STUN responses: * http://tools.ietf.org/html/rfc5389#section-15.4 * * The key is defined as: * md5(username:realm:SASLprep(password)) * * Currently, since the server has just one user (test), our key, pre-hashing, * is effectively fixed as: * test:myrealm:test * * The hash can be easily generated on a Unix command-line: * echo -n 'test:myrealm:test'|md5sum * bd8adb317d5d542e5e2aba5bdb8f5ca2 * * Wireshark-ing a TURN session with rfc5766-turn-server reveals that the key * must be input to the HMAC algorithm as 16 bytes of *binary* data. So, we * supply our crypto library with a UTF-8 string generated from the 16 bytes. */ // TODO: dynamic username/password would be pretty easy to implement // TODO: would be nice to run uint8array -> string on boot but the files // don't seem to load in the right order...might be a typescript bug var HMAC_KEY = new Uint8Array([ 0xbd, 0x8a, 0xdb, 0x31, 0x7d, 0x5d, 0x54, 0x2e, 0x5e, 0x2a, 0xba, 0x5b, 0xdb, 0x8f, 0x5c, 0xa2 ]); /** * Returns the "magic cookie" bytes: * http://tools.ietf.org/html/rfc5389#section-6 */ function getMagicCookieBytes() : Uint8Array { return new Uint8Array([0x21, 0x12, 0xa4, 0x42]); } /** * Parses a byte array, returning a StunMessage object. * Throws an error if this is not a STUN request. */ export function parseStunMessage(bytes:Uint8Array) : Turn.StunMessage { // Fail if the request is too short to be valid. if (bytes.length < 20) { throw new Error('request too short'); } // From: // http://tools.ietf.org/html/rfc5389#section-6 // The first two bytes of the header are pretty weird, as the bits for // class and method are interleaved. Here's a breakdown: // 0 1 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +-+-+--+--+-+-+-+-+-+-+-+-+-+-+-+-+ // | | |M |M |M|M|M|C|M|M|M|C|M|M|M|M| // | | |11|10|9|8|7|1|6|5|4|0|3|2|1|0| // +-+-+--+--+-+-+-+-+-+-+-+-+-+-+-+-+ // Fail if the first two bits of the most significant byte are not zero. if (bytes[0] & 0xc0) { throw new Error('first two bits must be zero'); } // Fail if the magic cookie is not present. if (bytes[4] != 0x21 || bytes[5] != 0x12 || bytes[6] != 0xa4 || bytes[7] != 0x42) { throw new Error('magic cookie not found'); } // The class is determined by bits C1 and C0. var c1 = bytes[0] & 0x01; var c0 = bytes[1] & 0x10; var clazz :Turn.MessageClass; if (c1) { if (c0) { clazz = MessageClass.FAILURE_RESPONSE; } else { clazz = MessageClass.SUCCESS_RESPONSE; } } else if (c0) { clazz = MessageClass.INDICATION; } else { clazz = MessageClass.REQUEST; } // The method is determined by bits M0 through M12. // Though TURN's highest-numbered method is only 9, let's do all 12 bits // for the sake of completeness. // M0-M3. var method:number = bytes[1] & 0x0f; // M4-M6. method |= (bytes[1] >> 1) & 0x70; // M7-M12. method |= (bytes[0] << 6) & 0x0f80; // Transaction ID. var transactionId = bytes.subarray(8, 20); // Attributes. var attributes :Turn.StunAttribute[] = []; var attributeOffset = 20; while (attributeOffset < bytes.length) { var attribute = parseStunAttribute(bytes.subarray(attributeOffset)); attributes.push(attribute); attributeOffset += 4 + calculatePadding((attribute.value ? attribute.value.length : 0), 4); } return { clazz : clazz, method : method, transactionId: transactionId, attributes: attributes } } /** * Constructs a byte array from a StunMessage object. */ export function formatStunMessage(message:Turn.StunMessage) : Uint8Array { // Figure out how many bytes we'll need. var length = 0; for (var i = 0; i < message.attributes.length; i++) { var declaredLength = message.attributes[i].value ? message.attributes[i].value.length : 0; var paddedLength = calculatePadding(declaredLength, 4); length += (4 + paddedLength); } var buff = new ArrayBuffer(length + 20); var bytes = new Uint8Array(buff); // The first two bytes of the header are pretty weird, as the bits for // class and method are interleaved. Here's a breakdown: // 0 1 // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +-+-+--+--+-+-+-+-+-+-+-+-+-+-+-+-+ // | | |M |M |M|M|M|C|M|M|M|C|M|M|M|M| // | | |11|10|9|8|7|1|6|5|4|0|3|2|1|0| // +-+-+--+--+-+-+-+-+-+-+-+-+-+-+-+-+ // Method (M0-M12). // M0-M3. bytes[1] = message.method & 0xff; // M4-M6. bytes[1] |= (message.method << 1) & 0xe0; // M7-M12. bytes[0] = (message.method << 2) & 0x3e00; // Class (C1 and C0). var c1 = bytes[0] & 0x01; var c0 = bytes[1] & 0x10; var clazz :Turn.MessageClass; // C1. if (message.clazz == MessageClass.SUCCESS_RESPONSE || message.clazz == MessageClass.FAILURE_RESPONSE) { bytes[0] |= 0x01; } // C0. if (message.clazz == MessageClass.INDICATION || message.clazz == MessageClass.FAILURE_RESPONSE) { bytes[1] |= 0x10; } // Length. bytes[2] = length >> 8; bytes[3] = length & 0xff; // Magic cookie. bytes.set(getMagicCookieBytes(), 4); // Transaction ID. bytes.set(message.transactionId, 8); // Attributes. var attributeOffset = 20; for (var i = 0; i < message.attributes.length; i++) { var attribute = message.attributes[i]; attributeOffset += formatStunAttribute(attribute, bytes.subarray(attributeOffset)); } return bytes; } /** * As formatStunMessage() but appends a MESSAGE-INTEGRITY attribute. * Normally, this is the function you should call; the exceptions are tests * and send/data indications (which do not require a checksum). */ export function formatStunMessageWithIntegrity(message:Turn.StunMessage) : Uint8Array { // Append the attribute and obtain the bytes... message.attributes.push({ type: Turn.MessageAttribute.MESSAGE_INTEGRITY, value: new Uint8Array(20) }); var bytes = formatStunMessage(message); // ...and compute the checksum, and copy it into the bytes. // MESSAGE-INTEGRITY hashes are always 20 bytes in length. var hashBytes = computeHash(bytes); bytes.set(hashBytes, bytes.length - 20); return bytes; } /** * Computes the hash for the MESSAGE-INTEGRITY attribute: * http://tools.ietf.org/html/rfc5389#section-15.4 * * From the RFC: * "The text used as input to HMAC is the STUN message, including * the header, up to and including the attribute preceding the * MESSAGE-INTEGRITY attribute." * * The supplied bytes should be a STUN message, including a MESSAGE-INTEGRITY * attribute (which must be the final attribute), with length including that * attribute. * * Callers of this method should copy the computed hash into the * supplied byte array. */ export function computeHash(bytes:Uint8Array) : Uint8Array { var keyAsString = ArrayBuffers.arrayBufferToString(HMAC_KEY.buffer); // MESSAGE-INTEGRITY attributes are always 24 bytes long: // 4 bytes header + 20 bytes hash var bytesToBeHashed = bytes.subarray(0, bytes.byteLength - 24); // Think of the next few lines as uint8ArrayToString(). // This is necessary because, depending on how b is constructed, // b.buffer is not guaranteed to equal a, where b is a Uint8Array // view on an ArrayBuffer a (in particular, views created with // subarray will share the same parent ArrayBuffer). // TODO: add uint8ArrayToString to uproxy-build-tools var a :string[] = []; for (var i = 0; i < bytesToBeHashed.length; ++i) { a.push(String.fromCharCode(bytes[i])); } var bytesToBeHashedAsString = a.join(''); var hashAsString = sha1.str_hmac_sha1(keyAsString, bytesToBeHashedAsString); return new Uint8Array(ArrayBuffers.stringToArrayBuffer(hashAsString)); } /** * Converts the supplied attribute to bytes, placing the result in the * supplied byte array. Throws an error if the byte array is too small * to contain the attribute but otherwise ignores any trailing bytes. */ export function formatStunAttribute( attr:Turn.StunAttribute, bytes:Uint8Array) : number { var paddedLength = calculatePadding(attr.value ? attr.value.length : 0, 4); if (bytes.length < 4 + paddedLength) { throw new Error('too few bytes'); } // Type. bytes[0] = attr.type >> 8; bytes[1] = attr.type & 0xff; // Length. var length = attr.value ? attr.value.length : 0; bytes[2] = length >> 8; bytes[3] = length & 0xff; // Value. if (attr.value) { bytes.set(attr.value, 4); // Padding. for (var i = attr.value.length; i < paddedLength; i++) { bytes[4 + i] = 0; } } return 4 + paddedLength; } /** * Parses a STUN attribute: * http://tools.ietf.org/html/rfc5389#section-15 */ export function parseStunAttribute(bytes:Uint8Array) : Turn.StunAttribute { // Fail if the number of bytes is too small. if (bytes.length < 4) { throw new Error('too few bytes'); } var type = bytes[0] << 8 | bytes[1]; var length = bytes[2] << 8 | bytes[3]; var value :Uint8Array; if (length > 0) { value = bytes.subarray(4, 4 + length); } return { type : type, value : value }; } /** * Returns bytes suitable for use in a ERROR_CODE-typed StunAttribute: * http://tools.ietf.org/html/rfc5389#section-15.6 */ export function formatErrorCodeAttribute( code:number, reason:string) : Uint8Array { // TODO: check reason length is <128 characters var length = 4 + reason.length; var buffer = new ArrayBuffer(length); var bytes = new Uint8Array(buffer); // Reserved bits. bytes[0] = bytes[1] = bytes[2] = 0; // Class (hundreds digit of code). var clazz = code / 100; if (clazz < 3 || clazz > 6) { throw new Error('class must be between 3 and 6'); } bytes[2] = clazz; // Number (code modulo 100). bytes[3] = code % 100; // Reason. var reasonBuffer = ArrayBuffers.stringToArrayBuffer(reason); bytes.set(new Uint8Array(reasonBuffer), 4); return bytes; } /** * Returns bytes suitable for use in a MAPPED-ADDRESS attribute: * http://tools.ietf.org/html/rfc5389#section-15.1 * Although we never send MAPPED-ADDRESS attributes, this function is * useful for testing and formatting XOR-MAPPED-ADDRESS attributes. * TODO: support IPv6 (assumes IPv4) */ export function formatMappedAddressAttribute( address:string, port:number) : Uint8Array { var buffer = new ArrayBuffer(8); var bytes = new Uint8Array(buffer); bytes[0] = 0; // reserved bytes[1] = 0x01; // IPv4 // Port. bytes[2] = port >> 8; bytes[3] = port & 0xff; // Address. var s = address.split('.'); if (s.length != 4) { throw new Error('cannot parse address ' + address); } for (var i = 0; i < 4; i++) { bytes[4 + i] = parseInt(s[i]); } return bytes; } /** * Parses a MAPPED-ADDRESS attribute: * http://tools.ietf.org/html/rfc5389#section-15.1 * Again, although we never parse MAPPED-ADDRESS attributes, this function is * useful for testing and for parsing XOR-MAPPED-ADDRESS attributes. * TODO: support IPv6 (assumes IPv4) */ export function parseMappedAddressAttribute(bytes:Uint8Array) : Turn.Endpoint { if (bytes.length < 8) { throw new Error('attribute too short'); } if (bytes[0]) { throw new Error('first byte must be zero'); } if (bytes[1] != 0x01) { throw new Error('only ipv4 supported'); } // Port. var port = (bytes[2] << 8) | bytes[3]; // Address. var quadrants = [bytes[4], bytes[5], bytes[6], bytes[7]]; var address = quadrants.join('.'); return { address: address, port: port }; } /** * Parses an XOR-MAPPED-ADDRESS attribute: * http://tools.ietf.org/html/rfc5389#section-15.2 * TODO: support IPv6 (assumes IPv4) */ export function parseXorMappedAddressAttribute(bytes:Uint8Array) : Turn.Endpoint { if (bytes.length < 8) { throw new Error('attribute too short'); } // Port. var magicCookieBytes = getMagicCookieBytes(); bytes[2] ^= magicCookieBytes[0]; // most significant byte bytes[3] ^= magicCookieBytes[1]; // least significant byte // Address. for (var i = 0; i < 4; i++) { bytes[4 + i] ^= magicCookieBytes[i]; } return parseMappedAddressAttribute(bytes); } /** * Returns bytes suitable for use in XOR-MAPPED-ADDRESS and * XOR-RELAYED-ADDRESS attributes: * http://tools.ietf.org/html/rfc5389#section-15.2 * TODO: support IPv6 (assumes IPv4) */ export function formatXorMappedAddressAttribute( address:string, port:number) : Uint8Array { var bytes = formatMappedAddressAttribute(address, port); // From the RFC: // "X-Port is computed by taking the mapped port in host byte order, // XOR'ing it with the most significant 16 bits of the magic cookie, // and then the converting the result to network byte order." // It's not clear why you would XOR the host byte ordered-representation // so we just XOR the network byte representation. Examining the network // traffic with Wireshark indicates that this is correct. var magicCookie = getMagicCookieBytes(); bytes[2] ^= magicCookie[0]; bytes[3] ^= magicCookie[1]; // Address. for (var i = 0; i < 4; i++) { bytes[4 + i] ^= magicCookie[i]; } return bytes; } /** * Returns the first attribute in the supplied array having the * specified type. Raises an error if the attribute is not found. */ export function findFirstAttributeWithType( type:Turn.MessageAttribute, attributes:Turn.StunAttribute[]) : Turn.StunAttribute { for (var i = 0; i < attributes.length; i++) { var attribute = attributes[i]; if (attribute.type === type) { return attribute; } } throw new Error('attribute not found'); } /** Rounds x up to the nearest b, e.g. calculatePadding(5, 4) == 8. */ export function calculatePadding(x:number, b:number) : number { var t = Math.floor(x / b); if ((x % b) > 0) { t++; } return t * b; } }