| // Load modules |
| |
| var Dgram = require('dgram'); |
| var Dns = require('dns'); |
| var Hoek = require('hoek'); |
| |
| |
| // Declare internals |
| |
| var internals = {}; |
| |
| |
| exports.time = function (options, callback) { |
| |
| if (arguments.length !== 2) { |
| callback = arguments[0]; |
| options = {}; |
| } |
| |
| var settings = Hoek.clone(options); |
| settings.host = settings.host || 'pool.ntp.org'; |
| settings.port = settings.port || 123; |
| settings.resolveReference = settings.resolveReference || false; |
| |
| // Declare variables used by callback |
| |
| var timeoutId = 0; |
| var sent = 0; |
| |
| // Ensure callback is only called once |
| |
| var finish = function (err, result) { |
| |
| if (timeoutId) { |
| clearTimeout(timeoutId); |
| timeoutId = 0; |
| } |
| |
| socket.removeAllListeners(); |
| socket.once('error', internals.ignore); |
| socket.close(); |
| return callback(err, result); |
| }; |
| |
| finish = Hoek.once(finish); |
| |
| // Create UDP socket |
| |
| var socket = Dgram.createSocket('udp4'); |
| |
| socket.once('error', function (err) { |
| |
| return finish(err); |
| }); |
| |
| // Listen to incoming messages |
| |
| socket.on('message', function (buffer, rinfo) { |
| |
| var received = Date.now(); |
| |
| var message = new internals.NtpMessage(buffer); |
| if (!message.isValid) { |
| return finish(new Error('Invalid server response'), message); |
| } |
| |
| if (message.originateTimestamp !== sent) { |
| return finish(new Error('Wrong originate timestamp'), message); |
| } |
| |
| // Timestamp Name ID When Generated |
| // ------------------------------------------------------------ |
| // Originate Timestamp T1 time request sent by client |
| // Receive Timestamp T2 time request received by server |
| // Transmit Timestamp T3 time reply sent by server |
| // Destination Timestamp T4 time reply received by client |
| // |
| // The roundtrip delay d and system clock offset t are defined as: |
| // |
| // d = (T4 - T1) - (T3 - T2) t = ((T2 - T1) + (T3 - T4)) / 2 |
| |
| var T1 = message.originateTimestamp; |
| var T2 = message.receiveTimestamp; |
| var T3 = message.transmitTimestamp; |
| var T4 = received; |
| |
| message.d = (T4 - T1) - (T3 - T2); |
| message.t = ((T2 - T1) + (T3 - T4)) / 2; |
| message.receivedLocally = received; |
| |
| if (!settings.resolveReference || |
| message.stratum !== 'secondary') { |
| |
| return finish(null, message); |
| } |
| |
| // Resolve reference IP address |
| |
| Dns.reverse(message.referenceId, function (err, domains) { |
| |
| if (/* $lab:coverage:off$ */ !err /* $lab:coverage:on$ */) { |
| message.referenceHost = domains[0]; |
| } |
| |
| return finish(null, message); |
| }); |
| }); |
| |
| // Set timeout |
| |
| if (settings.timeout) { |
| timeoutId = setTimeout(function () { |
| |
| timeoutId = 0; |
| return finish(new Error('Timeout')); |
| }, settings.timeout); |
| } |
| |
| // Construct NTP message |
| |
| var message = new Buffer(48); |
| for (var i = 0; i < 48; i++) { // Zero message |
| message[i] = 0; |
| } |
| |
| message[0] = (0 << 6) + (4 << 3) + (3 << 0) // Set version number to 4 and Mode to 3 (client) |
| sent = Date.now(); |
| internals.fromMsecs(sent, message, 40); // Set transmit timestamp (returns as originate) |
| |
| // Send NTP request |
| |
| socket.send(message, 0, message.length, settings.port, settings.host, function (err, bytes) { |
| |
| if (err || |
| bytes !== 48) { |
| |
| return finish(err || new Error('Could not send entire message')); |
| } |
| }); |
| }; |
| |
| |
| internals.NtpMessage = function (buffer) { |
| |
| this.isValid = false; |
| |
| // Validate |
| |
| if (buffer.length !== 48) { |
| return; |
| } |
| |
| // Leap indicator |
| |
| var li = (buffer[0] >> 6); |
| switch (li) { |
| case 0: this.leapIndicator = 'no-warning'; break; |
| case 1: this.leapIndicator = 'last-minute-61'; break; |
| case 2: this.leapIndicator = 'last-minute-59'; break; |
| case 3: this.leapIndicator = 'alarm'; break; |
| } |
| |
| // Version |
| |
| var vn = ((buffer[0] & 0x38) >> 3); |
| this.version = vn; |
| |
| // Mode |
| |
| var mode = (buffer[0] & 0x7); |
| switch (mode) { |
| case 1: this.mode = 'symmetric-active'; break; |
| case 2: this.mode = 'symmetric-passive'; break; |
| case 3: this.mode = 'client'; break; |
| case 4: this.mode = 'server'; break; |
| case 5: this.mode = 'broadcast'; break; |
| case 0: |
| case 6: |
| case 7: this.mode = 'reserved'; break; |
| } |
| |
| // Stratum |
| |
| var stratum = buffer[1]; |
| if (stratum === 0) { |
| this.stratum = 'death'; |
| } |
| else if (stratum === 1) { |
| this.stratum = 'primary'; |
| } |
| else if (stratum <= 15) { |
| this.stratum = 'secondary'; |
| } |
| else { |
| this.stratum = 'reserved'; |
| } |
| |
| // Poll interval (msec) |
| |
| this.pollInterval = Math.round(Math.pow(2, buffer[2])) * 1000; |
| |
| // Precision (msecs) |
| |
| this.precision = Math.pow(2, buffer[3]) * 1000; |
| |
| // Root delay (msecs) |
| |
| var rootDelay = 256 * (256 * (256 * buffer[4] + buffer[5]) + buffer[6]) + buffer[7]; |
| this.rootDelay = 1000 * (rootDelay / 0x10000); |
| |
| // Root dispersion (msecs) |
| |
| this.rootDispersion = ((buffer[8] << 8) + buffer[9] + ((buffer[10] << 8) + buffer[11]) / Math.pow(2, 16)) * 1000; |
| |
| // Reference identifier |
| |
| this.referenceId = ''; |
| switch (this.stratum) { |
| case 'death': |
| case 'primary': |
| this.referenceId = String.fromCharCode(buffer[12]) + String.fromCharCode(buffer[13]) + String.fromCharCode(buffer[14]) + String.fromCharCode(buffer[15]); |
| break; |
| case 'secondary': |
| this.referenceId = '' + buffer[12] + '.' + buffer[13] + '.' + buffer[14] + '.' + buffer[15]; |
| break; |
| } |
| |
| // Reference timestamp |
| |
| this.referenceTimestamp = internals.toMsecs(buffer, 16); |
| |
| // Originate timestamp |
| |
| this.originateTimestamp = internals.toMsecs(buffer, 24); |
| |
| // Receive timestamp |
| |
| this.receiveTimestamp = internals.toMsecs(buffer, 32); |
| |
| // Transmit timestamp |
| |
| this.transmitTimestamp = internals.toMsecs(buffer, 40); |
| |
| // Validate |
| |
| if (this.version === 4 && |
| this.stratum !== 'reserved' && |
| this.mode === 'server' && |
| this.originateTimestamp && |
| this.receiveTimestamp && |
| this.transmitTimestamp) { |
| |
| this.isValid = true; |
| } |
| |
| return this; |
| }; |
| |
| |
| internals.toMsecs = function (buffer, offset) { |
| |
| var seconds = 0; |
| var fraction = 0; |
| |
| for (var i = 0; i < 4; ++i) { |
| seconds = (seconds * 256) + buffer[offset + i]; |
| } |
| |
| for (i = 4; i < 8; ++i) { |
| fraction = (fraction * 256) + buffer[offset + i]; |
| } |
| |
| return ((seconds - 2208988800 + (fraction / Math.pow(2, 32))) * 1000); |
| }; |
| |
| |
| internals.fromMsecs = function (ts, buffer, offset) { |
| |
| var seconds = Math.floor(ts / 1000) + 2208988800; |
| var fraction = Math.round((ts % 1000) / 1000 * Math.pow(2, 32)); |
| |
| buffer[offset + 0] = (seconds & 0xFF000000) >> 24; |
| buffer[offset + 1] = (seconds & 0x00FF0000) >> 16; |
| buffer[offset + 2] = (seconds & 0x0000FF00) >> 8; |
| buffer[offset + 3] = (seconds & 0x000000FF); |
| |
| buffer[offset + 4] = (fraction & 0xFF000000) >> 24; |
| buffer[offset + 5] = (fraction & 0x00FF0000) >> 16; |
| buffer[offset + 6] = (fraction & 0x0000FF00) >> 8; |
| buffer[offset + 7] = (fraction & 0x000000FF); |
| }; |
| |
| |
| // Offset singleton |
| |
| internals.last = { |
| offset: 0, |
| expires: 0, |
| host: '', |
| port: 0 |
| }; |
| |
| |
| exports.offset = function (options, callback) { |
| |
| if (arguments.length !== 2) { |
| callback = arguments[0]; |
| options = {}; |
| } |
| |
| var now = Date.now(); |
| var clockSyncRefresh = options.clockSyncRefresh || 24 * 60 * 60 * 1000; // Daily |
| |
| if (internals.last.offset && |
| internals.last.host === options.host && |
| internals.last.port === options.port && |
| now < internals.last.expires) { |
| |
| process.nextTick(function () { |
| |
| callback(null, internals.last.offset); |
| }); |
| |
| return; |
| } |
| |
| exports.time(options, function (err, time) { |
| |
| if (err) { |
| return callback(err, 0); |
| } |
| |
| internals.last = { |
| offset: Math.round(time.t), |
| expires: now + clockSyncRefresh, |
| host: options.host, |
| port: options.port |
| }; |
| |
| return callback(null, internals.last.offset); |
| }); |
| }; |
| |
| |
| // Now singleton |
| |
| internals.now = { |
| intervalId: 0 |
| }; |
| |
| |
| exports.start = function (options, callback) { |
| |
| if (arguments.length !== 2) { |
| callback = arguments[0]; |
| options = {}; |
| } |
| |
| if (internals.now.intervalId) { |
| process.nextTick(function () { |
| |
| callback(); |
| }); |
| |
| return; |
| } |
| |
| exports.offset(options, function (err, offset) { |
| |
| internals.now.intervalId = setInterval(function () { |
| |
| exports.offset(options, function () { }); |
| }, options.clockSyncRefresh || 24 * 60 * 60 * 1000); // Daily |
| |
| return callback(); |
| }); |
| }; |
| |
| |
| exports.stop = function () { |
| |
| if (!internals.now.intervalId) { |
| return; |
| } |
| |
| clearInterval(internals.now.intervalId); |
| internals.now.intervalId = 0; |
| }; |
| |
| |
| exports.isLive = function () { |
| |
| return !!internals.now.intervalId; |
| }; |
| |
| |
| exports.now = function () { |
| |
| var now = Date.now(); |
| if (!exports.isLive() || |
| now >= internals.last.expires) { |
| |
| return now; |
| } |
| |
| return now + internals.last.offset; |
| }; |
| |
| |
| internals.ignore = function () { |
| |
| }; |