blob: 16d6ebcf307ee401b69a6de6906bcb8e09bdff65 [file] [log] [blame]
var events = require ("events");
var net = require ("net");
var raw = require ("raw-socket");
var util = require ("util");
function _expandConstantObject (object) {
var keys = [];
for (key in object)
keys.push (key);
for (var i = 0; i < keys.length; i++)
object[object[keys[i]]] = parseInt (keys[i]);
}
var NetworkProtocol = {
1: "IPv4",
2: "IPv6"
};
_expandConstantObject (NetworkProtocol);
function DestinationUnreachableError (source) {
this.name = "DestinationUnreachableError";
this.message = "Destination unreachable (source=" + source + ")";
this.source = source;
}
util.inherits (DestinationUnreachableError, Error);
function PacketTooBigError (source) {
this.name = "PacketTooBigError";
this.message = "Packet too big (source=" + source + ")";
this.source = source;
}
util.inherits (PacketTooBigError, Error);
function ParameterProblemError (source) {
this.name = "ParameterProblemError";
this.message = "Parameter problem (source=" + source + ")";
this.source = source;
}
util.inherits (ParameterProblemError, Error);
function RedirectReceivedError (source) {
this.name = "RedirectReceivedError";
this.message = "Redireect received (source=" + source + ")";
this.source = source;
}
util.inherits (RedirectReceivedError, Error);
function RequestTimedOutError () {
this.name = "RequestTimedOutError";
this.message = "Request timed out";
}
util.inherits (RequestTimedOutError, Error);
function SourceQuenchError (source) {
this.name = "SourceQuenchError";
this.message = "Source quench (source=" + source + ")";
this.source = source;
}
util.inherits (SourceQuenchError, Error);
function TimeExceededError (source) {
this.name = "TimeExceededError";
this.message = "Time exceeded (source=" + source + ")";
this.source = source;
}
util.inherits (TimeExceededError, Error);
function Session (options) {
this.retries = (options && options.retries) ? options.retries : 1;
this.timeout = (options && options.timeout) ? options.timeout : 2000;
this.packetSize = (options && options.packetSize) ? options.packetSize : 16;
if (this.packetSize < 12)
this.packetSize = 12;
this.addressFamily = (options && options.networkProtocol
&& options.networkProtocol == NetworkProtocol.IPv6)
? raw.AddressFamily.IPv6
: raw.AddressFamily.IPv4;
this._debug = (options && options._debug) ? true : false;
this.defaultTTL = (options && options.ttl) ? options.ttl : 128;
this.sessionId = (options && options.sessionId)
? options.sessionId
: process.pid;
this.sessionId = this.sessionId % 65535;
this.nextId = 1;
this.socket = null;
this.reqs = {};
this.reqsPending = 0;
this.getSocket ();
};
util.inherits (Session, events.EventEmitter);
Session.prototype.close = function () {
if (this.socket)
this.socket.close ();
this.flush (new Error ("Socket forcibly closed"));
delete this.socket;
return this;
};
Session.prototype._debugRequest = function (target, req) {
console.log ("request: addressFamily=" + this.addressFamily + " target="
+ req.target + " id=" + req.id + " buffer="
+ req.buffer.toString ("hex"));
}
Session.prototype._debugResponse = function (source, buffer) {
console.log ("response: addressFamily=" + this.addressFamily + " source="
+ source + " buffer=" + buffer.toString ("hex"));
}
Session.prototype.flush = function (error) {
for (id in this.reqs) {
var req = this.reqRemove (id);
var sent = req.sent ? req.sent : new Date ();
req.callback (error, req.target, sent, new Date ());
}
};
Session.prototype.getSocket = function () {
if (this.socket)
return this.socket;
var protocol = this.addressFamily == raw.AddressFamily.IPv6
? raw.Protocol.ICMPv6
: raw.Protocol.ICMP;
var me = this;
var options = {
addressFamily: this.addressFamily,
protocol: protocol
};
this.socket = raw.createSocket (options);
this.socket.on ("error", this.onSocketError.bind (me));
this.socket.on ("close", this.onSocketClose.bind (me));
this.socket.on ("message", this.onSocketMessage.bind (me));
this.ttl = null;
this.setTTL (this.defaultTTL);
return this.socket;
};
Session.prototype.fromBuffer = function (buffer) {
var offset, type, code;
if (this.addressFamily == raw.AddressFamily.IPv6) {
// IPv6 raw sockets don't pass the IPv6 header back to us
offset = 0;
if (buffer.length - offset < 8)
return;
// We don't believe any IPv6 options will be passed back to us so we
// don't attempt to pass them here.
type = buffer.readUInt8 (offset);
code = buffer.readUInt8 (offset + 1);
} else {
// Need at least 20 bytes for an IP header, and it should be IPv4
if (buffer.length < 20 || (buffer[0] & 0xf0) != 0x40)
return;
// The length of the IPv4 header is in mulitples of double words
var ip_length = (buffer[0] & 0x0f) * 4;
// ICMP header is 8 bytes, we don't care about the data for now
if (buffer.length - ip_length < 8)
return;
var ip_icmp_offset = ip_length;
// ICMP message too short
if (buffer.length - ip_icmp_offset < 8)
return;
type = buffer.readUInt8 (ip_icmp_offset);
code = buffer.readUInt8 (ip_icmp_offset + 1);
// For error type responses the sequence and identifier cannot be
// extracted in the same way as echo responses, the data part contains
// the IP header from our request, followed with at least 8 bytes from
// the echo request that generated the error, so we first go to the IP
// header, then skip that to get to the ICMP packet which contains the
// sequence and identifier.
if (type == 3 || type == 4 || type == 5 || type == 11) {
var ip_icmp_ip_offset = ip_icmp_offset + 8;
// Need at least 20 bytes for an IP header, and it should be IPv4
if (buffer.length - ip_icmp_ip_offset < 20
|| (buffer[ip_icmp_ip_offset] & 0xf0) != 0x40)
return;
// The length of the IPv4 header is in mulitples of double words
var ip_icmp_ip_length = (buffer[ip_icmp_ip_offset] & 0x0f) * 4;
// ICMP message too short
if (buffer.length - ip_icmp_ip_offset - ip_icmp_ip_length < 8)
return;
offset = ip_icmp_ip_offset + ip_icmp_ip_length;
} else {
offset = ip_icmp_offset
}
}
// Response is not for a request we generated
if (buffer.readUInt16BE (offset + 4) != this.sessionId)
return;
buffer[offset + 4] = 0;
var id = buffer.readUInt16BE (offset + 6);
var req = this.reqs[id];
if (req) {
req.type = type;
req.code = code;
return req;
} else {
return null;
}
};
Session.prototype.onBeforeSocketSend = function (req) {
this.setTTL (req.ttl ? req.ttl : this.defaultTTL);
}
Session.prototype.onSocketClose = function () {
this.emit ("close");
this.flush (new Error ("Socket closed"));
};
Session.prototype.onSocketError = function (error) {
this.emit ("error", error);
};
Session.prototype.onSocketMessage = function (buffer, source) {
if (this._debug)
this._debugResponse (source, buffer);
var req = this.fromBuffer (buffer);
if (req) {
this.reqRemove (req.id);
if (this.addressFamily == raw.AddressFamily.IPv6) {
if (req.type == 1) {
req.callback (new DestinationUnreachableError (source), req.target,
req.sent, new Date ());
} else if (req.type == 2) {
req.callback (new PacketTooBigError (source), req.target,
req.sent, new Date ());
} else if (req.type == 3) {
req.callback (new TimeExceededError (source), req.target,
req.sent, new Date ());
} else if (req.type == 4) {
req.callback (new ParameterProblemError (source), req.target,
req.sent, new Date ());
} else if (req.type == 129) {
req.callback (null, req.target,
req.sent, new Date ());
} else {
req.callback (new Error ("Unknown response type '" + req.type
+ "' (source=" + source + ")"), req.target,
req.sent, new Date ());
}
} else {
if (req.type == 0) {
req.callback (null, req.target,
req.sent, new Date ());
} else if (req.type == 3) {
req.callback (new DestinationUnreachableError (source), req.target,
req.sent, new Date ());
} else if (req.type == 4) {
req.callback (new SourceQuenchError (source), req.target,
req.sent, new Date ());
} else if (req.type == 5) {
req.callback (new RedirectReceivedError (source), req.target,
req.sent, new Date ());
} else if (req.type == 11) {
req.callback (new TimeExceededError (source), req.target,
req.sent, new Date ());
} else {
req.callback (new Error ("Unknown response type '" + req.type
+ "' (source=" + source + ")"), req.target,
req.sent, new Date ());
}
}
}
};
Session.prototype.onSocketSend = function (req, error, bytes) {
if (! req.sent)
req.sent = new Date ();
if (error) {
this.reqRemove (req.id);
req.callback (error, req.target, req.sent, req.sent);
} else {
var me = this;
req.timer = setTimeout (this.onTimeout.bind (me, req), req.timeout);
}
};
Session.prototype.onTimeout = function (req) {
if (req.retries > 0) {
req.retries--;
this.send (req);
} else {
this.reqRemove (req.id);
req.callback (new RequestTimedOutError ("Request timed out"),
req.target, req.sent, new Date ());
}
};
// Keep searching for an ID which is not in use
Session.prototype._generateId = function () {
var startId = this.nextId++;
while (1) {
if (this.nextId > 65535)
this.nextId = 1;
if (this.reqs[this.nextId]) {
this.nextId++;
} else {
return this.nextId;
}
// No free request IDs
if (this.nextId == startId)
return;
}
}
Session.prototype.pingHost = function (target, callback) {
var id = this._generateId ();
if (! id) {
callback (new Error ("Too many requests outstanding"), target);
return this;
}
var req = {
id: id,
retries: this.retries,
timeout: this.timeout,
callback: callback,
target: target
};
this.reqQueue (req);
return this;
};
Session.prototype.reqQueue = function (req) {
req.buffer = this.toBuffer (req);
if (this._debug)
this._debugRequest (req.target, req);
this.reqs[req.id] = req;
this.reqsPending++;
this.send (req);
return this;
}
Session.prototype.reqRemove = function (id) {
var req = this.reqs[id];
if (req) {
clearTimeout (req.timer);
delete req.timer;
delete this.reqs[req.id];
this.reqsPending--;
}
// If we have no more outstanding requests pause readable events
if (this.reqsPending <= 0)
if (! this.getSocket ().recvPaused)
this.getSocket ().pauseRecv ();
return req;
};
Session.prototype.send = function (req) {
var buffer = req.buffer;
var me = this;
// Resume readable events if the raw socket is paused
if (this.getSocket ().recvPaused)
this.getSocket ().resumeRecv ();
this.getSocket ().send (buffer, 0, buffer.length, req.target,
this.onBeforeSocketSend.bind (me, req),
this.onSocketSend.bind (me, req));
};
Session.prototype.setTTL = function (ttl) {
if (this.ttl && this.ttl == ttl)
return;
var level = this.addressFamily == raw.AddressFamily.IPv6
? raw.SocketLevel.IPPROTO_IPV6
: raw.SocketLevel.IPPROTO_IP;
this.getSocket ().setOption (level, raw.SocketOption.IP_TTL, ttl);
this.ttl = ttl;
}
Session.prototype.toBuffer = function (req) {
var buffer = new Buffer (this.packetSize);
// Since our buffer represents real memory we should initialise it to
// prevent its previous contents from leaking to the network.
for (var i = 8; i < this.packetSize; i++)
buffer[i] = 0;
var type = this.addressFamily == raw.AddressFamily.IPv6 ? 128 : 8;
buffer.writeUInt8 (type, 0);
buffer.writeUInt8 (0, 1);
buffer.writeUInt16BE (0, 2);
buffer.writeUInt16BE (this.sessionId, 4);
buffer.writeUInt16BE (req.id, 6);
raw.writeChecksum (buffer, 2, raw.createChecksum (buffer));
return buffer;
};
Session.prototype.traceRouteCallback = function (trace, req, error, target,
sent, rcvd) {
if (trace.feedCallback (error, target, req.ttl, sent, rcvd)) {
trace.doneCallback (new Error ("Trace forcibly stopped"), target);
return;
}
if (error) {
if (req.ttl >= trace.ttl) {
trace.doneCallback (error, target);
return;
}
if ((error instanceof RequestTimedOutError) && ++trace.timeouts >= 3) {
trace.doneCallback (new Error ("Too many timeouts"), target);
return;
}
var id = this._generateId ();
if (! id) {
trace.doneCallback (new Error ("Too many requests outstanding"),
target);
return;
}
req.ttl++;
req.id = id;
var me = this;
req.retries = this.retries;
req.sent = null;
this.reqQueue (req);
} else {
trace.doneCallback (null, target);
}
}
Session.prototype.traceRoute = function (target, ttl, feedCallback,
doneCallback) {
if (! doneCallback) {
doneCallback = feedCallback;
feedCallback = ttl;
ttl = this.ttl;
}
var id = this._generateId ();
if (! id) {
var sent = new Date ();
callback (new Error ("Too many requests outstanding"), target,
sent, sent);
return this;
}
var trace = {
feedCallback: feedCallback,
doneCallback: doneCallback,
ttl: ttl,
timeouts: 0
};
var me = this;
var req = {
id: id,
retries: this.retries,
timeout: this.timeout,
ttl: 1,
target: target
};
req.callback = me.traceRouteCallback.bind (me, trace, req);
this.reqQueue (req);
return this;
};
exports.createSession = function (options) {
return new Session (options || {});
};
exports.NetworkProtocol = NetworkProtocol;
exports.Session = Session;
exports.DestinationUnreachableError = DestinationUnreachableError;
exports.PacketTooBigError = PacketTooBigError;
exports.ParameterProblemError = ParameterProblemError;
exports.RedirectReceivedError = RedirectReceivedError;
exports.RequestTimedOutError = RequestTimedOutError;
exports.SourceQuenchError = SourceQuenchError;
exports.TimeExceededError = TimeExceededError;