/// /// /// /// /// /// /// module Turn { var log :Freedom_UproxyLogging.Log = freedom['core.log']('net'); /** * Represents a client known to the server. One of these objects is created * in response to each ALLOCATE request. */ class Allocation { /** Socket on which we are relaying datagrams for the client. */ socket:freedom_UdpSocket.Socket; } /** * Freedom module which handles relay sockets for the TURN server. */ export class Net { /** * All clients currently known to the server, indexed by tag. * Note that this map is essentially the (extremely inaccurately) named * "5-tuple" introduced in section 2.2 of the TURN RFC: * http://tools.ietf.org/html/rfc5766#section-2.2 */ private allocations_:{[s:string]:Promise} = {}; // TODO: define a type for event dispatcher in freedom-typescript-api constructor (private dispatchEvent_ ?:(name:string, args:any) => void) { log.debug('NET module created'); } public handleIpc = (data :ArrayBuffer) : Promise => { var request :Turn.StunMessage; try { request = Turn.parseStunMessage(new Uint8Array(data)); } catch (e) { return Promise.reject(new Error( 'failed to parse STUN message from IPC channel')); } // With which client is this message associated? var clientEndpoint :Turn.Endpoint; try { var ipcAttribute = Turn.findFirstAttributeWithType( Turn.MessageAttribute.IPC_TAG, request.attributes); try { clientEndpoint = Turn.parseXorMappedAddressAttribute( ipcAttribute.value); } catch (e) { return Promise.reject(new Error( 'could not parse address in IPC_TAG attribute: ' + e.message)); } } catch (e) { return Promise.reject(new Error( 'message received on IPC channel without IPC_TAG attribute')); } var tag = clientEndpoint.address + ':' + clientEndpoint.port; if (request.method == Turn.MessageMethod.ALLOCATE) { this.makeAllocation_(clientEndpoint).then((allocation:Allocation) => { allocation.socket.getInfo().then((socketInfo:freedom_UdpSocket.SocketInfo) => { this.emitIpc_({ method: Turn.MessageMethod.ALLOCATE, clazz: Turn.MessageClass.SUCCESS_RESPONSE, transactionId: request.transactionId, attributes: [{ // Endpoint on which the new socket is listening. // This is really the whole point of the thing. type: Turn.MessageAttribute.XOR_RELAYED_ADDRESS, value: Turn.formatXorMappedAddressAttribute( socketInfo.localAddress, socketInfo.localPort) }, { // Endpoint from which the client appears to us. // This is essentially a STUN response and is generally // provided as a convenience to TURN clients. type: Turn.MessageAttribute.XOR_MAPPED_ADDRESS, value: Turn.formatXorMappedAddressAttribute( clientEndpoint.address, clientEndpoint.port) }, { // Lifetime. type: Turn.MessageAttribute.LIFETIME, value: new Uint8Array([0x00, 0x00, 600 >> 8, 600 & 0xff]) // 600 = ten mins }] }, clientEndpoint); }); }, (e) => { // Send error response (failed to make allocation). this.emitIpc_({ method: Turn.MessageMethod.ALLOCATE, clazz: Turn.MessageClass.FAILURE_RESPONSE, transactionId: request.transactionId, attributes: [] }, clientEndpoint); }); } else if (request.method == Turn.MessageMethod.SEND) { // Extract the destination address and payload. var destinationAttribute :Turn.StunAttribute; var dataAttribute :Turn.StunAttribute; try { destinationAttribute = Turn.findFirstAttributeWithType( Turn.MessageAttribute.XOR_PEER_ADDRESS, request.attributes); dataAttribute = Turn.findFirstAttributeWithType( Turn.MessageAttribute.DATA, request.attributes); } catch (e) { return Promise.reject(new Error( 'no address or data attribute in SEND indication')); } var remoteEndpoint = Turn.parseXorMappedAddressAttribute( destinationAttribute.value); var payload = Turn.Net.bytesToArrayBuffer_(dataAttribute.value); if (!(tag in this.allocations_)) { return Promise.reject(new Error( 'received SEND indication for client without allocation')); } this.allocations_[tag].then((allocation:Allocation) => { allocation.socket.sendTo( payload, remoteEndpoint.address, remoteEndpoint.port); }); } else { return Promise.reject(new Error( 'unsupported IPC method: ' + request.method)); } return Promise.resolve(); } /** * Emits a Freedom message which should be relayed to the remote side. * The message is a STUN message, as received from a TURN client but with * the addition of an IPC_TAG attribute identifying the TURN client. */ private emitIpc_ = ( stunMessage:Turn.StunMessage, clientEndpoint:Turn.Endpoint) : void => { // Add an IPC_TAG attribute. stunMessage.attributes.push({ type: Turn.MessageAttribute.IPC_TAG, value: Turn.formatXorMappedAddressAttribute( clientEndpoint.address, clientEndpoint.port) }); this.dispatchEvent_('ipc', { data: Turn.formatStunMessage(stunMessage).buffer }); } /** Promises to allocate a socket, wrapped in an Allocation. */ private makeAllocation_ = ( clientEndpoint:Turn.Endpoint) : Promise => { var tag = clientEndpoint.address + ':' + clientEndpoint.port; if (tag in this.allocations_) { return this.allocations_[tag]; } var socket = freedom['core.udpsocket'](); var promise = socket.bind('127.0.0.1', 0) .then((resultCode:number) => { if (resultCode != 0) { return Promise.reject(new Error( 'could not create socket -- error code ' + resultCode)); } socket.getInfo().then((socketInfo:freedom_UdpSocket.SocketInfo) => { log.info('allocated socket for ' + tag + ' on ' + socketInfo.localAddress + ':' + socketInfo.localPort); }); return Promise.resolve({ socket: socket }); }); socket.on('onData', (recvFromInfo:freedom_UdpSocket.RecvFromInfo) => { this.emitIpc_({ method: Turn.MessageMethod.DATA, clazz: Turn.MessageClass.INDICATION, transactionId: Turn.Net.getRandomTransactionId_(), attributes: [{ type: Turn.MessageAttribute.XOR_PEER_ADDRESS, value: Turn.formatXorMappedAddressAttribute( recvFromInfo.address, recvFromInfo.port) }, { type: Turn.MessageAttribute.DATA, value: new Uint8Array(recvFromInfo.data) }] }, clientEndpoint); }); this.allocations_[tag] = promise; return promise; } /** * Copies a Uint8Array into a new ArrayBuffer. Useful when the array * has been constructed from a subarray of the buffer, in which case * bytes.buffer is a much larger array than you are expecting. * TODO: be smarter about using slice in these instances */ private static bytesToArrayBuffer_ = (bytes:Uint8Array) : ArrayBuffer => { var buffer = new ArrayBuffer(bytes.length); var view = new Uint8Array(buffer); for (var i = 0; i < bytes.length; i++) { view[i] = bytes[i]; } return buffer; } private static getRandomTransactionId_ = () : Uint8Array => { var bytes = new Uint8Array(20); for (var i = 0; i < 20; i++) { bytes[i] = Math.random() * 255; } return bytes; } } if (typeof freedom !== 'undefined') { freedom.net().providePromises(Turn.Net); } }