UNPKG

node-stun

Version:

STUN (Session Traversal Unitilities for NAT) for Node.js

218 lines (184 loc) 6.26 kB
'use strict'; var dgram = require('dgram'); var util = require('util'); var EventEmitter = require('events').EventEmitter; var Message = require('./message'); /** * Server class. * @class * @param {object} config Config object. * @param {object} config.primary Primary address * @param {string} config.primary.host Primary host name * @param {string|number} config.primary.port Primary port number * @param {object} config.secondary Secondary address * @param {string} config.secondary.host Secondary host name * @param {string|number} config.secondary.port Secondary port number */ function Server(config) { this._addr0 = config.primary.host; this._addr1 = config.secondary.host; this._port0 = parseInt(config.primary.port); this._port1 = parseInt(config.secondary.port); this._sockets = []; this._stats = { numRcvd: 0, numSent: 0, numMalformed: 0, numUnsupported: 0 }; this._logger = require('./logger').create(this); } util.inherits(Server, EventEmitter); /** @private */ Server.prototype._onListening = function (sid) { var sin = this._sockets[sid].address(); this._logger.info("soc[" + sid + "] listening on " + sin.address + ":" + sin.port); }; Server.prototype._onReceived = function (sid, msg, rinfo) { this._logger.debug("soc[" + sid + "] received from " + rinfo.address + ":" + rinfo.port); var stunmsg = new Message(); var fid = sid; // source socket ID for response this._stats.numRcvd++; try { stunmsg.deserialize(msg); } catch (e) { this._stats.numMalformed++; this._logger.warn("Error: " + e.message); return; } // We are only interested in binding request. if (stunmsg.getType() != 'breq') { this._stats.numUnsupported++; return; } var val; // Modify source socket ID (fid) based on // CHANGE-REQUEST attribute. val = stunmsg.getAttribute('changeReq'); if (val != undefined) { if (val.changeIp) { fid ^= 0x2; } if (val.changePort) { fid ^= 0x1; } } // Check if it has timestamp attribute. var txTs; var rcvdAt = Date.now(); val = stunmsg.getAttribute('timestamp'); if (val != undefined) { txTs = val.timestamp; } //this._logger.debug("sid=" + sid + " fid=" + fid); try { // Initialize the message object to reuse. // The init() does not reset transaction ID. stunmsg.init(); stunmsg.setType('bres'); // Add mapped address. stunmsg.addAttribute('mappedAddr', { 'family': 'ipv4', 'port': rinfo.port, 'addr': rinfo.address }); // Offer CHANGED-ADDRESS only when this._addr1 is defined. if (this._addr1 != undefined) { var chAddr = (sid & 0x2)? this._addr0:this._addr1; var chPort = (sid & 0x1)?this._port0:this._port1; stunmsg.addAttribute('changedAddr', { 'family': 'ipv4', 'port': chPort, 'addr': chAddr }); } var soc = this._sockets[fid]; // Add source address. stunmsg.addAttribute('sourceAddr', { 'family': 'ipv4', 'port': soc.address().port, 'addr': soc.address().address }); // Add timestamp if existed in the request. if (txTs) { stunmsg.addAttribute('timestamp', { 'respDelay': ((Date.now() - rcvdAt) & 0xffff), 'timestamp': txTs }); } var resp = stunmsg.serialize(); if (!soc) { throw new Error("Invalid from ID: " + fid); } this._logger.debug('soc[' + fid + '] sending ' + resp.length + ' bytes to ' + rinfo.address + ':' + rinfo.port); soc.send( resp, 0, resp.length, rinfo.port, rinfo.address); } catch (e) { this._stats.numMalformed++; this._logger.debug("Error: " + e.message); } this._stats.numSent++; }; Server.prototype._getPort = function (sid) { return (sid & 1)? this._port1:this._port0; }; Server.prototype._getAddr = function (sid) { return (sid & 2)? this._addr1:this._addr0; }; /** * Starts listening to STUN requests from clients. * @throws {Error} Server address undefined. */ Server.prototype.listen = function () { var self = this; // Sanity check if (!this._addr0) { throw new Error("Address undefined"); } if (!this._addr1) { throw new Error("Address undefined"); } for (var i = 0; i < 4; ++i) { // Create socket and add it to socket array. var soc = dgram.createSocket("udp4"); this._sockets.push(soc); switch (i) { case 0: soc.on("listening", function () { self._onListening(0); }); soc.on("message", function (msg, rinfo) { self._onReceived(0, msg, rinfo); }); break; case 1: soc.on("listening", function () { self._onListening(1); }); soc.on("message", function (msg, rinfo) { self._onReceived(1, msg, rinfo); }); break; case 2: soc.on("listening", function () { self._onListening(2); }); soc.on("message", function (msg, rinfo) { self._onReceived(2, msg, rinfo); }); break; case 3: soc.on("listening", function () { self._onListening(3); }); soc.on("message", function (msg, rinfo) { self._onReceived(3, msg, rinfo); }); break; default: throw new RangeError("Out of socket array"); } // Start listening. soc.bind(self._getPort(i), self._getAddr(i)); } }; /** * Closes the STUN server. */ Server.prototype.close = function () { while (this._sockets.length > 0) { var soc = this._sockets.shift(); var sin = soc.address(); this._logger.info("Closing socket on " + sin.address + ":" + sin.port); soc.close(); } }; module.exports = Server;