stunsrv
Version:
A STUN client/server package.
1,605 lines (1,401 loc) • 49.5 kB
JavaScript
/*
* Copyright (c) 2011 Yutaka Takeda <yt0916 at gmail.com>
* MIT Lincesed
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
var dns = require('dns');
var dgram = require('dgram');
var crypto = require('crypto');
/**
* @namespace
* Recommended namespace for stun.js.
* @name stun
* @exports exports as stun
* @example
* var stun = require('stun');
*/
/**
* Transport address dependency types.
* <ul>
* <li>stun.Type.I: "I" (Independent)</li>
* <li>stun.Type.PD: "PD" (Port dependent)</li>
* <li>stun.Type.AD: "AD" (Address dependent)</li>
* <li>stun.Type.APD: "APD" (Address&Port Dependent)</li>
* <li>stun.Type.UNDEF: "UNDEF" (Undefined)</li>
* </ul>
*/
exports.Type = {
/**
* Independent. Returns a constant string value of "I".
*/
get I() { return "I"; },
/**
* Port dependent. Returns a constant string value of "PD".
*/
get PD() { return "PD"; },
/**
* Address dependent. Returns a constant string value of "AD".
*/
get AD() { return "AD"; },
/**
* Address and port dependent. Returns a constant string value of "APD".
*/
get APD() { return "APD" },
/**
* Type undefined/undetermined. Returns a constant string value of "UNDEF".
*/
get UNDEF() { return "UNDEF"; }
}
/**
* Discovery mode.
* <ul>
* <li>stun.Mode.FULL: 0</li>
* <li>stun.Mode.NB_ONLY: 1</li>
* </ul>
*/
exports.Mode = {
/** Performs full NAT type discovery. Returns 0.*/
get FULL() { return 0; },
/** NAT binding discovery only. Returns 1. */
get NB_ONLY() { return 1; }
}
/**
* Result code.
* <ul>
* <li>stun.Result.OK: 0</li>
* <li>stun.Result.HOST_NOT_FOUND: -1</li>
* <li>stun.Result.UDP_BLOCKED: -2</li>
* <li>stun.Result.NB_INCOMPLETE: -3</li>
* </ul>
*/
exports.Result = {
/** Successful. */
get OK() { return 0; },
/** Domain does not exit. (DNS name resolution failed.) */
get HOST_NOT_FOUND() { return -1; },
/** No reply from server. Server may be down. */
get UDP_BLOCKED() { return -2; },
/** Partial UDP blockage. NB type discovery was incomplete. */
get NB_INCOMPLETE() { return -3; }
}
/**
* StunMessage factory.
* @type StunMessage
*/
exports.createMessage = function() {
return new StunMessage();
};
/**
* StunClient factory.
* @type StunClient
*/
exports.createClient = function() {
return new StunClient();
};
/**
* StunServer factory.
* @type StunServer
*/
exports.createServer = function() {
return new StunServer();
};
// Tools.
function inet_aton(a) {
var d = a.split('.');
return ((((((+d[0])*256)+(+d[1]))*256)+(+d[2]))*256)+(+d[3]);
}
function inet_ntoa(n) {
var d = n%256;
for (var i = 3; i > 0; i--) {
n = Math.floor(n/256);
d = n%256 + '.' + d;
}
return d;
}
/**
* Constructor for StunMessage object.
* @class
* @see stun.createMessage()
*/
function StunMessage() {
// Message types.
var _mesgTypes = {
"breq" : 0x0001,
"bres" : 0x0101,
"berr" : 0x0111, // Not supported
"sreq" : 0x0002, // Not supported
"sres" : 0x0102, // Not supported
"serr" : 0x0112, // Not supported
};
// Attribute types.
var _attrTypes = {
// RFC 3489
"mappedAddr" : 0x0001,
"respAddr" : 0x0002, // Not supported
"changeReq" : 0x0003,
"sourceAddr" : 0x0004,
"changedAddr" : 0x0005, // Not supported
"username" : 0x0006, // Not supported
"password" : 0x0007, // Not supported
"msgIntegrity" : 0x0008, // Not supported
"errorCode" : 0x0009, // Not supported
"unknownAttr" : 0x000a, // Not supported
"reflectedFrom" : 0x000b, // Not supported
// RFC 3489bis
"xorMappedAddr" : 0x0020, // Not supported
// Proprietary.
"timestamp" : 0x0032, // <16:srv-delay><16:tx-timestamp>
};
var _families = { "ipv4" : 0x01 };
var _type = _mesgTypes.breq;
var _tid;
var _attrs = [];
var _checkAttrAddr = function(value) {
if(value["family"] == undefined) {
value["family"] = "ipv4"
}
if(value["port"] == undefined) {
throw new Error("Port undefined");
}
if(value["addr"] == undefined) {
throw new Error("Addr undefined");
}
};
var _getMesgTypeByVal = function(val) {
for(type in _mesgTypes) {
if(_mesgTypes[type] == val) {
return type;
}
}
throw new Error("Type undefined: " + val);
}
var _getAttrTypeByVal = function(val) {
for(type in _attrTypes) {
if(_attrTypes[type] == val) {
return type;
}
}
throw new Error("Unknown attr value: " + val);
}
var _readAddr = function(ctx) {
var family;
var port;
var addr;
ctx.pos++; // skip first byte
for (f in _families) {
if(_families[f] == ctx.buf[ctx.pos]) {
family = f;
break;
}
}
if(family == undefined) throw new Error("Unsupported family: " + ctx.buf[ctx.pos]);
ctx.pos++;
port = ctx.buf[ctx.pos++] << 8;
port |= ctx.buf[ctx.pos++];
// Bit operations can handle only 32-bit values.
// Here needs to use multiplication instead of
// shift/or operations to avoid inverting signedness.
addr = ctx.buf[ctx.pos++] * 0x1000000;
addr += ctx.buf[ctx.pos++] << 16;
addr += ctx.buf[ctx.pos++] << 8;
addr += ctx.buf[ctx.pos++];
return { 'family': family, 'port': port, 'addr': inet_ntoa(addr) };
};
var _writeAddr = function(ctx, code, attrVal) {
if(ctx.buf.length < ctx.pos + 12) throw new Error("Insufficient buffer");
// Append attribute header.
ctx.buf[ctx.pos++] = code >> 8;
ctx.buf[ctx.pos++] = code & 0xff;
ctx.buf[ctx.pos++] = 0x00;
ctx.buf[ctx.pos++] = 0x08;
// Append attribute value.
ctx.buf[ctx.pos++] = 0x00;
ctx.buf[ctx.pos++] = _families[attrVal.family];
ctx.buf[ctx.pos++] = attrVal.port >> 8;
ctx.buf[ctx.pos++] = attrVal.port & 0xff;
var addr = inet_aton(attrVal.addr);
ctx.buf[ctx.pos++] = addr >> 24;
ctx.buf[ctx.pos++] = (addr >> 16) & 0xff;
ctx.buf[ctx.pos++] = (addr >> 8) & 0xff;
ctx.buf[ctx.pos++] = addr & 0xff;
};
var _readChangeReq = function(ctx) {
ctx.pos += 3;
var chIp = false;
var chPort = false;
if(ctx.buf[ctx.pos] & 0x4) { chIp = true; };
if(ctx.buf[ctx.pos] & 0x2) { chPort = true; };
ctx.pos++;
return { 'changeIp': chIp, 'changePort': chPort };
};
var _writeChangeReq = function(ctx, attrVal) {
if(ctx.buf.length < ctx.pos + 8) throw new Error("Insufficient buffer");
// Append attribute header.
ctx.buf[ctx.pos++] = _attrTypes.changeReq >> 8;
ctx.buf[ctx.pos++] = _attrTypes.changeReq & 0xff;
ctx.buf[ctx.pos++] = 0x00;
ctx.buf[ctx.pos++] = 0x04;
// Append attribute value.
ctx.buf[ctx.pos++] = 0x00;
ctx.buf[ctx.pos++] = 0x00;
ctx.buf[ctx.pos++] = 0x00;
ctx.buf[ctx.pos++] = ((attrVal.changeIp)? 0x4:0x0) | ((attrVal.changePort)? 0x2:0x0)
};
var _readTimestamp = function(ctx) {
var respDelay;
var timestamp;
respDelay = ctx.buf[ctx.pos++] << 8
respDelay |= ctx.buf[ctx.pos++]
timestamp = ctx.buf[ctx.pos++] << 8
timestamp |= ctx.buf[ctx.pos++]
return { 'respDelay': respDelay, 'timestamp': timestamp };
};
var _writeTimestamp = function(ctx, attrVal) {
if(ctx.buf.length < ctx.pos + 8) throw new Error("Insufficient buffer");
// Append attribute header.
ctx.buf[ctx.pos++] = _attrTypes.timestamp >> 8;
ctx.buf[ctx.pos++] = _attrTypes.timestamp & 0xff;
ctx.buf[ctx.pos++] = 0x00;
ctx.buf[ctx.pos++] = 0x04;
// Append attribute value.
ctx.buf[ctx.pos++] = attrVal.respDelay >> 8;
ctx.buf[ctx.pos++] = attrVal.respDelay & 0xff;
ctx.buf[ctx.pos++] = attrVal.timestamp >> 8;
ctx.buf[ctx.pos++] = attrVal.timestamp & 0xff;
};
/**
* Initializes StunMessage object.
*/
this.init = function() {
_type = _mesgTypes.breq;
_attrs = [];
};
/**
* Sets STUN message type.
* @param {string} type Message type.
* @throws {RangeError} Unknown message type.
*/
this.setType = function(type) {
_type = _mesgTypes[type];
if(_type < 0) throw new RangeError("Unknown message type");
};
/**
* Gets STUN message type.
* @throws {Error} Type undefined.
* @type string
*/
this.getType = function() {
return _getMesgTypeByVal(_type);
}
/**
* Sets transaction ID.
* @param {string} tid 16-byte transaction ID.
*/
this.setTransactionId = function(tid) {
_tid = tid;
};
/**
* Gets transaction ID.
* @type string
*/
this.getTransactionId = function() {
return _tid;
};
/**
* Adds a STUN attribute.
* @param {string} attrType Attribute type.
* @param {object} attrVal Attribute value. Structure of this
* value varies depending on the type.
* @throws {RangeError} Unknown attribute type.
* @throws {Error} The 'changeIp' property is undefined.
* @throws {Error} The 'changePort' property is undefined.
*/
this.addAttribute = function(attrType, attrVal) {
var code = _attrTypes[attrType];
if(code < 0) throw new RangeError("Unknown attribute type");
// Validate attrVal
switch(code)
{
case 0x0001: // mappedAddr
case 0x0002: // respAddr
case 0x0004: // sourceAddr
case 0x0005: // changedAddr
case 0x0020: // xorMappedAddr
_checkAttrAddr(attrVal);
break;
case 0x0003: // change-req
if(attrVal["changeIp"] == undefined) {
throw new Error("change IP undefined");
}
if(attrVal["changePort"] == undefined) {
throw new Error("change Port undefined");
}
break;
case 0x0032: // timestamp
if(attrVal.respDelay > 0xffff) attrVal.respDealy = 0xffff;
if(attrVal.timestamp > 0xffff) attrVal.timestamp = 0xffff;
break;
case 0x0006: // username
case 0x0007: // password
case 0x0008: // msgIntegrity
case 0x0009: // errorCode
case 0x000a: // unknownAttr
case 0x000b: // reflectedFrom
default:
throw new Error("Unsupported attribute " + attrType);
}
// If the attribute type already exists, replace it with the new one.
for(var i = 0; i < _attrs.length; ++i) {
if(_attrs[i].type == attrType) {
_attrs[i].value = attrVal;
replaced = true;
return;
}
}
_attrs.push({type:attrType, value:attrVal});
};
/**
* Gets a list of STUN attributes.
* @type array
*/
this.getAttributes = function() {
return _attrs;
}
/**
* Gets a STUN attributes by its type.
* @param {string} attrType Attribute type.
* @type object
*/
this.getAttribute = function(attrType) {
for(var i = 0; i < _attrs.length; ++i) {
if(_attrs[i].type == attrType) {
return _attrs[i].value;
}
}
return null; // the attribute not found.
}
/**
* Gets byte length a serialized buffer would be.
* @throws {RangeError} Unknown attribute type.
* @type number
*/
this.getLength = function() {
var len = 20; // header size (fixed)
for(var i = 0; i < _attrs.length; ++i) {
var code = _attrTypes[_attrs[i].type];
if(code < 0) throw new RangeError("Unknown attribute type");
// Validate attrVal
switch(code)
{
case 0x0001: // mappedAddr
case 0x0002: // respAddr
case 0x0004: // sourceAddr
case 0x0005: // changedAddr
case 0x0020: // xorMappedAddr
len += 12;
break;
case 0x0003: // changeReq
len += 8;
break;
case 0x0032: // timestamp
len += 8;
break;
case 0x0006: // username
case 0x0007: // password
case 0x0008: // msgIntegrity
case 0x0009: // errorCode
case 0x000a: // unknownAttr
case 0x000b: // reflectedFrom
default:
throw new Error("Unsupported attribute: " + code);
}
}
return len;
};
/**
* Returns a serialized data of type Buffer.
* @throws {Error} Incorrect transaction ID.
* @throws {RangeError} Unknown attribute type.
* @type buffer
*/
this.serialize = function() {
var ctx = {
buf: new Buffer(this.getLength()),
pos: 0};
// Write 'Type'
ctx.buf[ctx.pos++] = _type >> 8;
ctx.buf[ctx.pos++] = _type & 0xff;
// Write 'Length'
ctx.buf[ctx.pos++] = (ctx.buf.length - 20) >> 8;
ctx.buf[ctx.pos++] = (ctx.buf.length - 20) & 0xff;
// Write 'Transaction ID'
if(_tid == undefined || _tid.length != 16) {
throw new Error("Incorrect transaction ID");
}
for(var i = 0; i < 16; ++i) {
ctx.buf[ctx.pos++] = _tid.charCodeAt(i);
}
for(var i = 0; i < _attrs.length; ++i) {
var code = _attrTypes[_attrs[i].type];
if(code < 0) throw new RangeError("Unknown attribute type");
// Append attribute value
switch(code) {
case 0x0001: // mappedAddr
case 0x0002: // respAddr
case 0x0004: // sourceAddr
case 0x0005: // changedAddr
_writeAddr(ctx, code, _attrs[i].value);
break;
case 0x0003: // changeReq
_writeChangeReq(ctx, _attrs[i].value);
break;
case 0x0032: // timestamp
_writeTimestamp(ctx, _attrs[i].value);
break;
case 0x0006: // username
case 0x0007: // password
case 0x0008: // msgIntegrity
case 0x0009: // errorCode
case 0x000a: // unknownAttr
case 0x000b: // reflectedFrom
default:
throw new Error("Unsupported attribute");
}
}
return ctx.buf;
};
/**
* Deserializes a serialized data into this object.
* @param {buffer} buffer Data to be deserialized.
* @throws {Error} Malformed data in the buffer.
*/
this.deserialize = function(buffer) {
var ctx = {
pos:0,
buf:buffer
};
// Initialize.
_type = 0;
_tid = undefined;
_attrs = [];
// buffer must be >= 20 bytes.
if(ctx.buf.length < 20)
throw new Error("Malformed data");
// Parse type.
_type = ctx.buf[ctx.pos++] << 8;
_type |= ctx.buf[ctx.pos++];
// Parse length
var len;
len = ctx.buf[ctx.pos++] << 8;
len |= ctx.buf[ctx.pos++];
// Parse tid.
_tid = ctx.buf.toString('binary', ctx.pos, ctx.pos + 16);
ctx.pos += 16;
// The remaining length should match the value in the length field.
if(ctx.buf.length - 20 != len)
throw new Error("Malformed data");
while(ctx.pos < ctx.buf.length) {
// Remaining size in the buffer must be >= 4.
if(ctx.buf.length - ctx.pos < 4)
throw new Error("Malformed data");
var attrLen;
var code;
code = ctx.buf[ctx.pos++] << 8;
code |= ctx.buf[ctx.pos++];
attrLen = ctx.buf[ctx.pos++] << 8;
attrLen |= ctx.buf[ctx.pos++];
// Remaining size must be >= attrLen.
if(ctx.buf.length - ctx.pos < attrLen)
throw new Error("Malformed data: code=" + code + " rem=" + (ctx.buf.length - ctx.pos) + " len=" + attrLen);
var attrVal;
switch(code) {
case 0x0001: // mappedAddAr
case 0x0002: // respAddr
case 0x0004: // sourceAddr
case 0x0005: // changedAddr
if(attrLen != 8) throw new Error("Malformed data");
attrVal = _readAddr(ctx);
break;
case 0x0003: // changeReq
if(attrLen != 4) throw new Error("Malformed data");
attrVal = _readChangeReq(ctx);
break;
case 0x0032: // xorMappedAddr
if(attrLen != 4) throw new Error("Malformed data");
attrVal = _readTimestamp(ctx);
break;
case 0x0006: // username
case 0x0007: // password
case 0x0008: // msgIntegrity
case 0x0009: // errorCode
case 0x000a: // unknownAttr
case 0x000b: // reflectedFrom
default:
// We do not know of this type.
// Skip this attribute.
ctx.pos += attrLen;
continue;
}
_attrs.push({type:_getAttrTypeByVal(code), value:attrVal});
}
};
}
/////////////////////////////////////////////////////////////////////
/**
* Constructor for StunClient object.
* @class
* @see stun.createClient()
*/
function StunClient() {
var _State = {
// src dst chIp chPort breq
IDLE : 0, // ----- ---- ---- ------ ------
RESOLV : 1, // - - - - -
NBDaDp : 2, // _soc0 DaDp 0 0 _breq0
NBDaCp : 3, // _soc0 DaCp 0 0 _breq0
NBCaDp : 4, // _soc0 CaDp 0 0 _breq0
NBCaCp : 5, // _soc0 CaCp 0 0 _breq0
EFDiscov: 6, // _soc0 DaDp 1 1 _breq0
// _soc0 DaDp 1 0 _breq1
COMPLETE: 7
};
// Private:
var _domain; // FQDN
var _serv0; // Dotted decimal.
var _serv1; // Dotted decimal.
var _port0 = 3478;
var _port1; // Obtained via CHANGE-ADDRESS
var _local = { addr:'0.0.0.0', port:0 };
var _soc0;
var _soc1;
var _breq0; // Binding request 0 of type StunMessage.
var _breq1; // Binding request 1 of type StunMessage.
var _state = _State.IDLE;
var _mapped = [
{ addr:0, port:0 }, // mapped addr from DaDp
{ addr:0, port:0 }, // mapped addr from DaCp
{ addr:0, port:0 }, // mapped addr from CaDp
{ addr:0, port:0 }]; // mapped addr from CaCp
// pd ad
// 0 0 : Independent
// 0 1 : Address dependent
// 1 0 : Port dependent (rare)
// 1 1 : Address & port dependent
// -1 * : pd check in progress
// * -1 : ad check in progress
var _ef = { ad: undefined, pd: undefined };
var _numSocs = 0;
var _cbOnComplete;
var _cbOnClosed;
var _intervalId;
var _retrans = 0; // num of retransmissions
var _elapsed = 0; // *100 msec
var _mode = exports.Mode.FULL;
var _now = function() { return (new Date()).getTime(); };
var _rtt = new function () {
var _sum = 0;
var _num = 0;
this.init = function() { _sum = 0; _num = 0; };
this.addSample = function(rtt) { _sum += rtt; _num++; };
this.get = function() { return _num?(_sum/_num):0; };
};
var _isLocalAddr = function(addr) {
var dummy = dgram.createSocket('udp4');
try {
dummy.bind(0, addr);
}
catch(e) {
if(e.code == 'EADDRNOTAVAIL') {
dummy.close();
return false;
}
throw e;
}
dummy.close();
return true;
}
var _discover = function() {
// Create socket 0.
_soc0 = dgram.createSocket("udp4");
_soc0.on("listening", function () {
_onListening();
});
_soc0.on("message", function (msg, rinfo) {
_onReceived(msg, rinfo);
});
_soc0.on("close", function () {
_onClosed();
});
// Start listening on the local port.
_soc0.bind(0, _local.addr);
// Get assigned port name for this socket.
_local.addr = _soc0.address().address;
_local.port = _soc0.address().port;
_breq0 = new StunMessage();
_breq0.init();
_breq0.setType('breq');
_breq0.setTransactionId(_randTransId());
_breq0.addAttribute(
'timestamp', {
'respDelay': 0,
'timestamp': (_now() & 0xffff)});
var msg = _breq0.serialize();
_soc0.send(msg, 0, msg.length, _port0, _serv0);
_retrans = 0;
_elapsed = 0;
_intervalId = setInterval(_onTick, 100);
_state = _State.NBDaDp;
}
var _onResolved = function(err, addresses) {
if(err) {
console.log(err);
if(_cbOnComplete != undefined) {
_cbOnComplete(exports.Result.HOST_NOT_FOUND);
}
return;
}
_serv0 = addresses[0];
_discover();
}
var _onListening = function() {
_numSocs++;
//console.log("_numSocs++: " + _numSocs);
};
var _onClosed = function() {
if(_numSocs > 0) {
_numSocs--;
//console.log("_numSocs--: " + _numSocs);
if(_cbOnClosed != undefined && !_numSocs) {
_cbOnClosed();
}
}
};
var _onTick = function() {
// _retrans _elapsed
// 0 1( 1) == Math.min((1 << _retrans), 16)
// 1 2( 3)
// 2 4( 7)
// 3 8(15)
// 4 16(31)
// 5 16(47)
// 6 16(63)
// 7 16(79)
// 8 16(95)
_elapsed++;
if(_elapsed >= Math.min((1 << _retrans), 16)) {
// Retransmission timeout.
_retrans++;
_elapsed = 0;
if( _state == _State.NBDaDp ||
_state == _State.NBDaCp ||
_state == _State.NBCaDp ||
_state == _State.NBCaCp) {
if(_retrans < 9) {
_breq0.addAttribute(
'timestamp', {
'respDelay': 0,
'timestamp': (_now() & 0xffff)});
var sbuf = _breq0.serialize();
var toAddr;
var toPort;
switch(_state) {
case _State.NBDaDp:
toAddr = _serv0; toPort = _port0;
break;
case _State.NBDaCp:
toAddr = _serv0; toPort = _port1;
break;
case _State.NBCaDp:
toAddr = _serv1; toPort = _port0;
break;
case _State.NBCaCp:
toAddr = _serv1; toPort = _port1;
break;
}
_soc0.send(sbuf, 0, sbuf.length, toPort, toAddr);
console.log("NB-Rtx0: len=" + sbuf.length + " retrans=" + _retrans + " elapsed=" + _elapsed + " to=" + toAddr + ":" + toPort);
}
else {
clearInterval(_intervalId);
var firstNB = (_state == _State.NBDaDp);
_state = _State.COMPLETE;
if(_cbOnComplete != undefined) {
if(firstNB) {
_cbOnComplete(exports.Result.UDP_BLOCKED);
}
else {
// First binding succeeded, then subsequent
// binding should work, but didn't.
_cbOnComplete(exports.Result.NB_INCOMPLETE);
}
}
}
}
else if(_state == _State.EFDiscov) {
if(_ef.ad == undefined) {
if(_retrans < 9) {
var sbuf = _breq0.serialize();
_soc1.send(sbuf, 0, sbuf.length, _port0, _serv0);
console.log("EF-Rtx0: retrans=" + _retrans + " elapsed=" + _elapsed);
}
else {
_ef.ad = 1;
}
}
if(_ef.pd == undefined) {
if(_retrans < 9) {
var sbuf = _breq1.serialize();
_soc1.send(sbuf, 0, sbuf.length, _port0, _serv0);
console.log("EF-Rtx1: retrans=" + _retrans + " elapsed=" + _elapsed);
}
else {
_ef.pd = 1;
}
}
if(_ef.ad != undefined && _ef.pd != undefined) {
clearInterval(_intervalId);
_state = _State.COMPLETE;
if(_cbOnComplete != undefined) {
_cbOnComplete(exports.Result.OK);
}
}
}
else {
console.log("Warning: unexpected timer event. Forgot to clear timer?");
clearInterval(_intervalId);
}
}
}
var _onReceived = function(msg, rinfo) {
var bres = new StunMessage();
var val;
try {
bres.deserialize(msg);
}
catch(e) {
_stats.numMalformed++;
console.log("Error: " + e.message);
return;
}
// We are only interested in binding response.
if(bres.getType() != 'bres') {
return;
}
if(_state == _State.NBDaDp) {
if(bres.getTransactionId() != _breq0.getTransactionId()) {
return; // discard
}
clearInterval(_intervalId);
// Get MAPPED-ADDRESS value.
val = bres.getAttribute('mappedAddr');
if(val == undefined) {
console.log("Error: MAPPED-ADDRESS not present");
return;
}
_mapped[0].addr = val.addr;
_mapped[0].port = val.port;
// Get CHANGED-ADDRESS value.
val = bres.getAttribute('changedAddr');
if(val == undefined) {
console.log("Error: MAPPED-ADDRESS not present");
return;
}
_serv1 = val.addr;
_port1 = val.port;
// Calculate RTT if timestamp is attached.
val = bres.getAttribute('timestamp');
if(val != undefined) {
_rtt.addSample(((_now() & 0xffff) - val.timestamp) - val.respDelay);
}
console.log("MAPPED0: addr=" + _mapped[0].addr + ":" + _mapped[0].port);
//console.log("CHANGED: addr=" + _serv1 + ":" + _port1);
// Start NBDaCp.
_breq0.init();
_breq0.setType('breq');
_breq0.setTransactionId(_randTransId());
_breq0.addAttribute(
'timestamp', {
'respDelay': 0,
'timestamp': (_now() & 0xffff)});
var sbuf = _breq0.serialize();
_soc0.send(sbuf, 0, sbuf.length, _port1, _serv0);
_retrans = 0;
_elapsed = 0;
_intervalId = setInterval(_onTick, 100);
_state = _State.NBDaCp;
}
else if(_state == _State.NBDaCp) {
if(bres.getTransactionId() != _breq0.getTransactionId()) {
return; // discard
}
clearInterval(_intervalId);
// Get MAPPED-ADDRESS value.
val = bres.getAttribute('mappedAddr');
if(val == undefined) {
console.log("Error: MAPPED-ADDRESS not present");
return;
}
_mapped[1].addr = val.addr;
_mapped[1].port = val.port;
// Calculate RTT if timestamp is attached.
val = bres.getAttribute('timestamp');
if(val != undefined) {
_rtt.addSample(((_now() & 0xffff) - val.timestamp) - val.respDelay);
}
console.log("MAPPED1: addr=" + _mapped[1].addr + ":" + _mapped[1].port);
// Start NBCaDp.
_breq0.init();
_breq0.setType('breq');
_breq0.setTransactionId(_randTransId());
_breq0.addAttribute(
'timestamp', {
'respDelay': 0,
'timestamp': (_now() & 0xffff)});
var sbuf = _breq0.serialize();
_soc0.send(sbuf, 0, sbuf.length, _port0, _serv1);
_retrans = 0;
_elapsed = 0;
_intervalId = setInterval(_onTick, 100);
_state = _State.NBCaDp;
}
else if(_state == _State.NBCaDp) {
if(bres.getTransactionId() != _breq0.getTransactionId()) {
return; // discard
}
clearInterval(_intervalId);
// Get MAPPED-ADDRESS value.
val = bres.getAttribute('mappedAddr');
if(val == undefined) {
console.log("Error: MAPPED-ADDRESS not present");
return;
}
_mapped[2].addr = val.addr;
_mapped[2].port = val.port;
// Calculate RTT if timestamp is attached.
val = bres.getAttribute('timestamp');
if(val != undefined) {
_rtt.addSample(((_now() & 0xffff) - val.timestamp) - val.respDelay);
}
console.log("MAPPED2: addr=" + _mapped[2].addr + ":" + _mapped[2].port);
// Start NBCaCp.
_breq0.init();
_breq0.setType('breq');
_breq0.setTransactionId(_randTransId());
_breq0.addAttribute(
'timestamp', {
'respDelay': 0,
'timestamp': (_now() & 0xffff)});
var sbuf = _breq0.serialize();
_soc0.send(sbuf, 0, sbuf.length, _port1, _serv1);
_retrans = 0;
_elapsed = 0;
_intervalId = setInterval(_onTick, 100);
_state = _State.NBCaCp;
}
else if(_state == _State.NBCaCp) {
if(bres.getTransactionId() != _breq0.getTransactionId()) {
return; // discard
}
clearInterval(_intervalId);
// Get MAPPED-ADDRESS value.
val = bres.getAttribute('mappedAddr');
if(val == undefined) {
console.log("Error: MAPPED-ADDRESS not present");
return;
}
_mapped[3].addr = val.addr;
_mapped[3].port = val.port;
// Calculate RTT if timestamp is attached.
val = bres.getAttribute('timestamp');
if(val != undefined) {
_rtt.addSample(((_now() & 0xffff) - val.timestamp) - val.respDelay);
}
console.log("MAPPED3: addr=" + _mapped[3].addr + ":" + _mapped[3].port);
// Start NBDiscov.
_ef.ad = undefined;
_ef.pd = undefined;
// Create another socket (_soc1) from which EFDiscov is performed).
_soc1 = dgram.createSocket("udp4");
_soc1.on("listening", function () {
_onListening();
});
_soc1.on("message", function (msg, rinfo) {
_onReceived(msg, rinfo);
});
_soc1.on("close", function () {
_onClosed();
});
// Start listening on the local port.
_soc1.bind(0, _local.addr);
// changeIp=true,changePort=true from _soc1
var sbuf
_breq0.init();
_breq0.setType('breq');
_breq0.setTransactionId(_randTransId());
_breq0.addAttribute(
'changeReq', {
'changeIp': true,
'changePort': true});
sbuf = _breq0.serialize();
_soc1.send(sbuf, 0, sbuf.length, _port0, _serv0);
// changeIp=false,changePort=true from _soc1
_breq1 = new StunMessage();
_breq1.setType('breq');
_breq1.setTransactionId(_randTransId());
_breq1.addAttribute(
'changeReq', {
'changeIp': false,
'changePort': true});
sbuf = _breq1.serialize();
_soc1.send(sbuf, 0, sbuf.length, _port0, _serv0);
_retrans = 0;
_elapsed = 0;
_intervalId = setInterval(_onTick, 100);
_state = _State.EFDiscov;
}
else if(_state == _State.EFDiscov) {
var res = -1;
if(_ef.ad == undefined) {
if(bres.getTransactionId() == _breq0.getTransactionId()) {
res = 0;
}
}
if(res < 0 && _ef.pd == undefined) {
if(bres.getTransactionId() == _breq1.getTransactionId()) {
res = 1;
}
}
if(res < 0) return; // discard
if(res == 0) { _ef.ad = 0; }
else { _ef.pd = 0; }
if(_ef.ad != undefined && _ef.pd != undefined) {
clearInterval(_intervalId);
_state = _State.COMPLETE;
if(_cbOnComplete != undefined) {
_cbOnComplete(exports.Result.OK);
}
}
}
else {
return; // discard
}
};
var _randTransId = function() {
var seed = process.pid.toString(16);
seed += Math.round(Math.random() * 0x100000000).toString(16);
seed += (new Date()).getTime().toString(16);
var md5 = crypto.createHash('md5');
md5.update(seed);
return md5.digest();
}
// Public:
/**
* Sets local address. Use of this method is optional. If your
* local device has more then one interfaces, you can specify
* one of these interfaces form which STUN is performed.
* @param {string} addr Local IP address.
* @throws {Error} The address not available.
*/
this.setLocalAddr = function(addr) {
if(!_isLocalAddr(addr)) {
throw new Error("Addr not available");
}
_local.addr = addr;
_local.port = 0;
};
/**
* Sets STUN server address.
* @param {string} addr Domain name of the STUN server. Dotted
* decimal IP address can be used.
* @param {number} port Port number of the STUN server. If not
* defined, default port number 3478 will be used.
*/
this.setServerAddr = function(addr, port) {
var d = addr.split('.');
if(d.length != 4 || (
parseInt(d[0]) == NaN ||
parseInt(d[1]) == NaN ||
parseInt(d[2]) == NaN ||
parseInt(d[3]) == NaN))
{
_domain = addr;
_serv0 = undefined;
}
else {
_domain = undefined;
_serv0 = addr;
}
if(port != undefined) { _port = port; }
};
/**
* Starts NAT discovery.
* @param {function} callback Callback made when NAT discovery is complete.
* The callback function takes an argument - a result code of type {number}
* defined as stun.Result.
* @see stun.Result
* @param {number} Mode. (Not implemented. May leave it undefined)
* @throws {Error} STUN is already in progress.
* @throws {Error} STUN server address is not defined yet.
*/
this.start = function(callback, mode) {
// Sanity check
if(_state !== _State.IDLE)
throw new Error("Not allowed in state " + _state);
if(_domain == undefined && _serv0 == undefined)
throw new Error("Address undefined");
_cbOnComplete = callback;
_mode = (mode == undefined)? exports.Mode.FULL:mode;
// Initialize.
_rtt.init();
if(_serv0 == undefined) {
dns.resolve4(_domain, _onResolved);
_state = _State.RESOLV;
}
else { _discover(); }
};
/**
* Closes STUN client.
* @param {function} callback Callback made when UDP sockets in use
* are all closed.
*/
this.close = function(callback) {
_cbOnClosed = callback;
if(_soc0 != undefined) {
var sin = _soc0.address();
_soc0.close();
}
if(_soc1 != undefined) {
var sin = _soc1.address();
_soc1.close();
}
};
/**
* Tells whether we are behind a NAT or not.
* @type boolean
*/
this.isNatted = function() {
if(_local.addr == '0.0.0.0') {
return !_isLocalAddr(_mapped[0].addr);
}
return (_mapped[0].addr)? (_mapped[0].addr != _local.addr):undefined;
}
/**
* Gets NAT binding type.
* @type string
* @see stun.Type
*/
this.getNB = function() {
if(!this.isNatted()) {
return exports.Type.I;
}
if(_mapped[1].addr && _mapped[2].addr && _mapped[3].addr) {
if(_mapped[0].port == _mapped[2].port) {
if(_mapped[0].port == _mapped[1].port) {
return exports.Type.I;
}
return exports.Type.PD;
}
if(_mapped[0].port == _mapped[1].port) {
return exports.Type.AD;
}
return exports.Type.APD;
}
return exports.Type.UNDEF;
};
/**
* Gets endpoint filter type.
* @type string
* @see stun.Type
*/
this.getEF = function() {
if(this.isNatted() == undefined) {
return exports.Type.UNDEF;
}
if(!this.isNatted()) {
return exports.Type.I;
}
if(_ef.ad == undefined) {
console.log("_ef.ad was undefined");
return exports.Type.UNDEF;
}
if(_ef.pd == undefined) {
console.log("_ef.pd was undefined");
return exports.Type.UNDEF;
}
if(_ef.ad == 0) {
if(_ef.pd == 0) {
return exports.Type.I;
}
return exports.Type.PD;
}
if(_ef.pd == 0) {
return exports.Type.AD;
}
return exports.Type.APD;
};
/**
* Gets name of NAT type.
* @type string
*/
this.getNatType = function() {
var natted = this.isNatted();
var nb = this.getNB();
var ef = this.getEF();
if(natted == undefined) return "UDP blocked";
if(!natted) return "Open to internet";
if(nb == exports.Type.UNDEF || ef == exports.Type.UNDEF)
return "Natted (details not available)";
if(nb == exports.Type.I) {
// Cone.
if(ef == exports.Type.I) return "Full cone";
if(ef == exports.Type.PD) return "Port-only-restricted cone";
if(ef == exports.Type.AD) return "Address-restricted cone";
return "Port-restricted cone";
}
return "Symmetric";
}
/**
* Gets mapped address (IP address & port) returned by STUN server.
* @type object
*/
this.getMappedAddr = function() {
return { address:_mapped[0].addr, port:_mapped[0].port };
};
/**
* Gets RTT (Round-Trip Time) in milliseconds measured during
* NAT binding discovery.
* @type number
*/
this.getRtt = function() { return _rtt.get(); };
}
/////////////////////////////////////////////////////////////////////
/**
* Constructor for StunServer object.
* To instantiate a StunServer object, use createServer() function.
* @class
* @see stun.createServer()
*/
function StunServer() {
// Private:
var _addr0;
var _addr1;
var _respAddr0;
var _respAddr1;
var _port0 = 3478;
var _port1 = 3479;
var _sockets = [];
var _stats = {
numRcvd: 0,
numSent: 0,
numMalformed: 0,
numUnsupported: 0,
};
var _now = function() { return (new Date()).getTime(); };
var _onListening = function(sid) {
var sin = _sockets[sid].address();
console.log("soc[" + sid + "] listening on " + sin.address + ":" + sin.port);
};
var _onReceived = function(sid, msg, rinfo) {
console.log("soc[" + sid + "] received from " + rinfo.address + ":" + rinfo.port);
var stunmsg = new StunMessage();
var fid = sid; // source socket ID for response
_stats.numRcvd++;
try {
stunmsg.deserialize(msg);
}
catch(e) {
_stats.numMalformed++;
console.log("Error: " + e.message);
return;
}
// We are only interested in binding request.
if(stunmsg.getType() != 'breq') {
_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 = _now();
val = stunmsg.getAttribute('timestamp');
if(val != undefined) {
txTs = val.timestamp;
}
//console.log("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 _addr1 is defined.
if(_addr1 != undefined) {
var respAddr0 = _respAddr0?_respAddr0:_addr0;
var respAddr1 = _respAddr1?_respAddr1:_addr1;
var chAddr = (sid & 0x2)?respAddr0:respAddr1;
var chPort = (sid & 0x1)?_port0:_port1;
stunmsg.addAttribute(
'changedAddr', {
'family': 'ipv4',
'port': chPort,
'addr': chAddr});
}
var soc = _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 != undefined) {
stunmsg.addAttribute(
'timestamp', {
'respDelay': ((_now() - rcvdAt) & 0xffff),
'timestamp': txTs});
}
var resp = stunmsg.serialize();
if(soc == undefined) throw new Error("Invalid from ID: " + fid);
console.log('soc[' + fid + '] sending ' + resp.length + ' bytes');
soc.send( resp,
0,
resp.length,
rinfo.port,
rinfo.address);
}
catch(e) {
_stats.numMalformed++;
console.log("Error: " + e.message);
}
_stats.numSent++;
};
var _getPort = function(sid) {
return (sid & 1)?_port1:_port0;
};
var _getAddr = function(sid) {
return (sid & 2)?_addr1:_addr0;
};
// Public:
/**
* Sets primary server address.
* @param {string} addr0 Dotted decimal IP address.
*/
this.setAddress0 = function(addr0) {
_addr0 = addr0;
};
/**
* Sets primary server address.
* @param {string} respAddr0 Dotted decimal IP address.
*/
this.setResponseAddress0 = function(respAddr0) {
_respAddr0 = respAddr0;
};
/**
* Sets primary server address.
* @param {string} respAddr1 Dotted decimal IP address.
*/
this.setResponseAddress1 = function(respAddr1) {
_respAddr1 = respAddr1;
};
/**
* Sets secondary server address.
* @param {string} addr1 Dotted decimal IP address.
*/
this.setAddress1 = function(addr1) {
_addr1 = addr1;
};
/**
* Sets first port number.
* @param {string} port0 Integer port number. Defaults to 3478
*/
this.setPort0 = function(port0) {
_port0 = port0;
};
/**
* Sets second port number.
* @param {string} port1 Integer port number. Defaults to 3479
*/
this.setPort1 = function(port1) {
_port1 = port1;
};
/**
* Starts listening to STUN requests from clients.
* @throws {Error} Server address undefined.
*/
this.listen = function() {
// Sanity check
if(_addr0 == undefined) throw new Error("Address undefined");
if(_addr1 == undefined) 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");
_sockets.push(soc);
switch(i) {
case 0:
soc.on("listening", function () { _onListening(0); });
soc.on("message", function (msg, rinfo) { _onReceived(0, msg, rinfo); });
break;
case 1:
soc.on("listening", function () { _onListening(1); });
soc.on("message", function (msg, rinfo) { _onReceived(1, msg, rinfo); });
break;
case 2:
soc.on("listening", function () { _onListening(2); });
soc.on("message", function (msg, rinfo) { _onReceived(2, msg, rinfo); });
break;
case 3:
soc.on("listening", function () { _onListening(3); });
soc.on("message", function (msg, rinfo) { _onReceived(3, msg, rinfo); });
break;
default:
throw new RangeError("Out of socket array");
}
// Start listening.
soc.bind(_getPort(i), _getAddr(i));
}
};
/**
* Closes the STUN server.
*/
this.close = function() {
while(_sockets.length > 0) {
var soc = _sockets.shift();
var sin = soc.address();
console.log("Closing socket on " + sin.address + ":" + sin.port);
soc.close();
}
};
}