You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

413 lines
9.6 KiB

7 years ago
'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;
};