| |
| 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; |