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.
412 lines
9.6 KiB
412 lines
9.6 KiB
'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; |
|
};
|
|
|