'use strict'; // Load modules const Dgram = require('dgram'); const Dns = require('dns'); const Hoek = require('hoek'); // Declare internals const internals = {}; exports.time = function (options, callback) { if (arguments.length !== 2) { callback = arguments[0]; options = {}; } const settings = Hoek.clone(options); settings.host = settings.host || 'time.google.com'; settings.port = settings.port || 123; settings.resolveReference = settings.resolveReference || false; // Declare variables used by callback let timeoutId = null; let sent = 0; // Ensure callback is only called once const finish = Hoek.once((err, result) => { clearTimeout(timeoutId); socket.removeAllListeners(); socket.once('error', Hoek.ignore); try { socket.close(); } catch (ignoreErr) { } // Ignore errors if the socket is already closed return callback(err, result); }); // Set timeout if (settings.timeout) { timeoutId = setTimeout(() => { return finish(new Error('Timeout')); }, settings.timeout); } // Create UDP socket const socket = Dgram.createSocket('udp4'); socket.once('error', (err) => finish(err)); // Listen to incoming messages socket.on('message', (buffer, rinfo) => { const received = Date.now(); const 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 const T1 = message.originateTimestamp; const T2 = message.receiveTimestamp; const T3 = message.transmitTimestamp; const 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, (err, domains) => { if (/* $lab:coverage:off$ */ !err /* $lab:coverage:on$ */) { message.referenceHost = domains[0]; } return finish(null, message); }); }); // Construct NTP message const message = new Buffer(48); for (let 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, (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 const 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 const vn = ((buffer[0] & 0x38) >> 3); this.version = vn; // Mode const 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 const 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) const 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) { let seconds = 0; let fraction = 0; for (let i = 0; i < 4; ++i) { seconds = (seconds * 256) + buffer[offset + i]; } for (let 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) { const seconds = Math.floor(ts / 1000) + 2208988800; const 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 = {}; } const now = Date.now(); const 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(() => callback(null, internals.last.offset)); return; } exports.time(options, (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 = { started: false, intervalId: null }; exports.start = function (options, callback) { if (arguments.length !== 2) { callback = arguments[0]; options = {}; } if (internals.now.started) { process.nextTick(() => callback()); return; } const report = (err) => { if (err && options.onError) { options.onError(err); } }; internals.now.started = true; exports.offset(options, (err, offset) => { report(err); internals.now.intervalId = setInterval(() => { exports.offset(options, report); }, options.clockSyncRefresh || 24 * 60 * 60 * 1000); // Daily return callback(); }); }; exports.stop = function () { if (!internals.now.started) { return; } clearInterval(internals.now.intervalId); internals.now.started = false; internals.now.intervalId = null; }; exports.isLive = function () { return internals.now.started; }; exports.now = function () { const now = Date.now(); if (!exports.isLive() || now >= internals.last.expires) { return now; } return now + internals.last.offset; };