/// /// /// /// /// /// /// module Turn { var log :Freedom_UproxyLogging.Log = freedom['core.log']('turn'); /** * A TURN server which delegates the creation and operation of relay sockets * to a separate "net" Freedom module. The separation is intended to * facilitate transformation of intra-process traffic, viz. obfuscation. The * intended use of this server is as a proxy for WebRTC traffic to provide, * when paired with a NAT-punching and obfuscated network transport, for * a hard-to-detect and hard-to-block peer-to-peer connection. * * Based on: * http://www.ietf.org/rfc/rfc5766.txt * * While this server should behave as a regular TURN server, its normal * (and most tested!) configuration is as a relay for a single WebRTC data * channel, servicing just one client which is attempting to communicate * with a single remote host. * * As such, please note: * - no attempt is made to model permissions (permission requests always * succeed) * - no attempt is made to model lifetime (allocations live for the * lifetime of the server) * - there's no support for channels (just send and data indications) * - while the server does sign its responses with a MESSAGE-INTEGRITY * attribute, it does not verify the client's signature * - only the long-term credential mechanism is supported */ export class Server { /** Socket on which the server is listening. */ private socket_ :freedom_UdpSocket.Socket; // TODO: the following two maps are a code smell...needs a re-think /** * These are invoked when the remote side sends us a response * to a relay socket creation request. */ private callbacks_:{[tag:string]:(response:Turn.StunMessage) => void} = {}; /** * These are fulfilled when the callback is invoked. */ private promises_:{[s:string]:Promise} = {}; // TODO: define a type for event dispatcher in freedom-typescript-api constructor (private dispatchEvent_ ?:(name:string, args:any) => void) { this.socket_ = freedom['core.udpsocket'](); } /** * Returns a promise to create a socket, bind to the specified address, and * start listening for datagrams. Specify port zero to have the system * choose a free port. */ public bind(address:string, port:number) : Promise { return this.socket_.bind(address, port) .then((resultCode:number) => { if (resultCode != 0) { throw new Error('listen failed with result code ' + resultCode); } return resultCode; }) .then(this.socket_.getInfo) .then((socketInfo:freedom_UdpSocket.SocketInfo) => { log.info('listening on ' + socketInfo.localAddress + ':' + socketInfo.localPort); this.socket_.on('onData', this.onData_); return { address: socketInfo.localAddress, port: socketInfo.localPort }; }); } /** * Called when data is received from a TURN client on our UDP socket. * Sends a response to the client, if one is required (send and data * indications are the exception). Note that the RFC states that any * message which cannot be handled or understood by the server should be * ignored. */ private onData_ = (recvFromInfo:freedom_UdpSocket.RecvFromInfo) => { try { var stunMessage = Turn.parseStunMessage(new Uint8Array(recvFromInfo.data)); var clientEndpoint = { address: recvFromInfo.address, port: recvFromInfo.port }; this.handleStunMessage(stunMessage, clientEndpoint) .then((response ?:Turn.StunMessage) => { if (response) { var responseBytes = Turn.formatStunMessageWithIntegrity(response); this.socket_.sendTo( responseBytes.buffer, recvFromInfo.address, recvFromInfo.port); } }, (e) => { log.error('error handling STUN message: ' + e.message); }); } catch (e) { log.warn('failed to parse STUN message from ' + recvFromInfo.address + ':' + recvFromInfo.port); } } /** * Resolves to the response which should be sent to the client, or undefined * if none is required, e.g. for send indications. Rejects if the STUN * method is unsupported or there is an error handling the message. * Public for testing. */ public handleStunMessage = ( stunMessage:Turn.StunMessage, clientEndpoint:Endpoint) : Promise => { if (stunMessage.method == Turn.MessageMethod.ALLOCATE) { return this.handleAllocateRequest_(stunMessage, clientEndpoint); } else if (stunMessage.method == Turn.MessageMethod.CREATE_PERMISSION) { return this.handleCreatePermissionRequest_(stunMessage); } else if (stunMessage.method == Turn.MessageMethod.REFRESH) { return this.handleRefreshRequest_(stunMessage); } else if (stunMessage.method == Turn.MessageMethod.SEND) { return this.handleSendIndication_(stunMessage, clientEndpoint); } return Promise.reject(new Error('unsupported STUN method ' + (Turn.MessageMethod[stunMessage.method] || stunMessage.method))); } /** * Resolves to a success response. Since we don't actually track * permissions, this is pretty straightforward. */ private handleCreatePermissionRequest_ = ( request:Turn.StunMessage) : Promise => { return Promise.resolve({ method: Turn.MessageMethod.CREATE_PERMISSION, clazz: Turn.MessageClass.SUCCESS_RESPONSE, transactionId: request.transactionId, attributes: [] }); } /** * Resolves to a success response. REFRESH messages don't seem to be * required by Chrome (at least for establishing data channels) but are * required by turnutils_uclient. */ private handleRefreshRequest_ = ( request:Turn.StunMessage) : Promise => { return Promise.resolve({ method: Turn.MessageMethod.REFRESH, clazz: Turn.MessageClass.SUCCESS_RESPONSE, transactionId: request.transactionId, attributes: [{ type: Turn.MessageAttribute.LIFETIME, value: new Uint8Array([0x00, 0x00, 600 >> 8, 600 & 0xff]) // 600 = ten mins }] }); } /** * Resolves to an ALLOCATE response, which will be a FAILURE_RESPONSE or * SUCCESS_RESPONSE depending on whether the request includes a username * attribute and whether a relay socket can be created on the remote side. * * Note that there are two classes of ALLOCATE requests: * 1. The first is the very first request sent by the client to a TURN * server to which the server should always respond with a *failure* * response which *also* contains attributes (notably realm) which * the client can include in subsequent ALLOCATE requests. * 2. In the second case, the client includes REALM, USERNAME, and * MESSAGE-INTEGRITY attributes and the server creates a relay socket * before responding to the client. * * Right now, the server has no real notion of usernames and realms so we * are just performing the dance that TURN clients expect, using the * presence of a USERNAME attribute to distinguish the first case from the * second. * * Section 10.2 outlines the precise behaviour required: * http://tools.ietf.org/html/rfc5389#section-10.2 */ private handleAllocateRequest_ = ( request:Turn.StunMessage, clientEndpoint:Endpoint) : Promise => { // If no USERNAME attribute is present then assume this is the client's // first interaction with the server and respond immediately with a // failure message, including REALM information for subsequent requests. try { Turn.findFirstAttributeWithType( Turn.MessageAttribute.USERNAME, request.attributes); } catch (e) { return Promise.resolve({ method: Turn.MessageMethod.ALLOCATE, clazz: Turn.MessageClass.FAILURE_RESPONSE, transactionId: request.transactionId, attributes: [{ type: Turn.MessageAttribute.ERROR_CODE, value: Turn.formatErrorCodeAttribute(401, 'not authorised') }, { type: Turn.MessageAttribute.NONCE, value: new Uint8Array(ArrayBuffers.stringToArrayBuffer('nonce')) }, { type: Turn.MessageAttribute.REALM, value: new Uint8Array(ArrayBuffers.stringToArrayBuffer(Turn.REALM)) }] }); } // If we haven't already done so, create a callback which will be invoked // when the remote side sends us a response to our relay socket request. var tag = clientEndpoint.address + ':' + clientEndpoint.port; var promise :Promise; if (tag in this.promises_) { promise = this.promises_[tag]; } else { promise = new Promise((F,R) => { this.callbacks_[tag] = (response:Turn.StunMessage) => { if (response.clazz === Turn.MessageClass.SUCCESS_RESPONSE) { log.debug('relay socket allocated for TURN client ' + clientEndpoint.address + ':' + clientEndpoint.port); F(response); } else { R(new Error('could not allocate relay socket for TURN client ' + clientEndpoint.address + ':' + clientEndpoint.port)); } }; }); this.promises_[tag] = promise; } // Request a new relay socket. // TODO: minimise the number of attributes sent this.emitIpc_(request, clientEndpoint); // Fulfill, once our relay socket callback has been invoked. return promise; } /** * Makes a request to the remote side to send a datagram on the client's * relay socket. */ private handleSendIndication_ = ( request:Turn.StunMessage, clientEndpoint:Endpoint) : Promise => { this.emitIpc_(request, clientEndpoint); return Promise.resolve(undefined); } /** * 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:Endpoint) : void => { stunMessage.attributes.push({ type: Turn.MessageAttribute.IPC_TAG, value: Turn.formatXorMappedAddressAttribute( clientEndpoint.address, clientEndpoint.port) }); this.dispatchEvent_('ipc', { data: Turn.formatStunMessage(stunMessage).buffer }); } /** * Handles a Freedom message from the remote side. */ public handleIpc = (data :ArrayBuffer) : Promise => { var stunMessage :Turn.StunMessage; try { stunMessage = 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, stunMessage.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 (stunMessage.method == Turn.MessageMethod.ALLOCATE) { // A response from one of our relay socket creation requests. // Invoke the relevant callback. // TODO: check callback exists var callback = this.callbacks_[tag]; callback(stunMessage); } else if (stunMessage.method == Turn.MessageMethod.DATA) { // The remote side received data on a relay socket. // Forward it to the relevant client. // TODO: consider removing the IPC_TAG attribute this.socket_.sendTo( data, clientEndpoint.address, clientEndpoint.port); } else { return Promise.reject(new Error( 'unsupported IPC method: ' + stunMessage.method)); } return Promise.resolve(); } } if (typeof freedom !== 'undefined') { freedom.turn().providePromises(Turn.Server); } }