3 var Dgram = require('dgram');
4 var Dns = require('dns');
5 var Hoek = require('hoek');
13 exports.time = function (options, callback) {
15 if (arguments.length !== 2) {
16 callback = arguments[0];
20 var settings = Hoek.clone(options);
21 settings.host = settings.host || 'pool.ntp.org';
22 settings.port = settings.port || 123;
23 settings.resolveReference = settings.resolveReference || false;
25 // Declare variables used by callback
30 // Ensure callback is only called once
32 var finish = function (err, result) {
35 clearTimeout(timeoutId);
39 socket.removeAllListeners();
40 socket.once('error', internals.ignore);
42 return callback(err, result);
45 finish = Hoek.once(finish);
49 var socket = Dgram.createSocket('udp4');
51 socket.once('error', function (err) {
56 // Listen to incoming messages
58 socket.on('message', function (buffer, rinfo) {
60 var received = Date.now();
62 var message = new internals.NtpMessage(buffer);
63 if (!message.isValid) {
64 return finish(new Error('Invalid server response'), message);
67 if (message.originateTimestamp !== sent) {
68 return finish(new Error('Wrong originate timestamp'), message);
71 // Timestamp Name ID When Generated
72 // ------------------------------------------------------------
73 // Originate Timestamp T1 time request sent by client
74 // Receive Timestamp T2 time request received by server
75 // Transmit Timestamp T3 time reply sent by server
76 // Destination Timestamp T4 time reply received by client
78 // The roundtrip delay d and system clock offset t are defined as:
80 // d = (T4 - T1) - (T3 - T2) t = ((T2 - T1) + (T3 - T4)) / 2
82 var T1 = message.originateTimestamp;
83 var T2 = message.receiveTimestamp;
84 var T3 = message.transmitTimestamp;
87 message.d = (T4 - T1) - (T3 - T2);
88 message.t = ((T2 - T1) + (T3 - T4)) / 2;
89 message.receivedLocally = received;
91 if (!settings.resolveReference ||
92 message.stratum !== 'secondary') {
94 return finish(null, message);
97 // Resolve reference IP address
99 Dns.reverse(message.referenceId, function (err, domains) {
101 if (/* $lab:coverage:off$ */ !err /* $lab:coverage:on$ */) {
102 message.referenceHost = domains[0];
105 return finish(null, message);
111 if (settings.timeout) {
112 timeoutId = setTimeout(function () {
115 return finish(new Error('Timeout'));
116 }, settings.timeout);
119 // Construct NTP message
121 var message = new Buffer(48);
122 for (var i = 0; i < 48; i++) { // Zero message
126 message[0] = (0 << 6) + (4 << 3) + (3 << 0) // Set version number to 4 and Mode to 3 (client)
128 internals.fromMsecs(sent, message, 40); // Set transmit timestamp (returns as originate)
132 socket.send(message, 0, message.length, settings.port, settings.host, function (err, bytes) {
137 return finish(err || new Error('Could not send entire message'));
143 internals.NtpMessage = function (buffer) {
145 this.isValid = false;
149 if (buffer.length !== 48) {
155 var li = (buffer[0] >> 6);
157 case 0: this.leapIndicator = 'no-warning'; break;
158 case 1: this.leapIndicator = 'last-minute-61'; break;
159 case 2: this.leapIndicator = 'last-minute-59'; break;
160 case 3: this.leapIndicator = 'alarm'; break;
165 var vn = ((buffer[0] & 0x38) >> 3);
170 var mode = (buffer[0] & 0x7);
172 case 1: this.mode = 'symmetric-active'; break;
173 case 2: this.mode = 'symmetric-passive'; break;
174 case 3: this.mode = 'client'; break;
175 case 4: this.mode = 'server'; break;
176 case 5: this.mode = 'broadcast'; break;
179 case 7: this.mode = 'reserved'; break;
184 var stratum = buffer[1];
186 this.stratum = 'death';
188 else if (stratum === 1) {
189 this.stratum = 'primary';
191 else if (stratum <= 15) {
192 this.stratum = 'secondary';
195 this.stratum = 'reserved';
198 // Poll interval (msec)
200 this.pollInterval = Math.round(Math.pow(2, buffer[2])) * 1000;
204 this.precision = Math.pow(2, buffer[3]) * 1000;
206 // Root delay (msecs)
208 var rootDelay = 256 * (256 * (256 * buffer[4] + buffer[5]) + buffer[6]) + buffer[7];
209 this.rootDelay = 1000 * (rootDelay / 0x10000);
211 // Root dispersion (msecs)
213 this.rootDispersion = ((buffer[8] << 8) + buffer[9] + ((buffer[10] << 8) + buffer[11]) / Math.pow(2, 16)) * 1000;
215 // Reference identifier
217 this.referenceId = '';
218 switch (this.stratum) {
221 this.referenceId = String.fromCharCode(buffer[12]) + String.fromCharCode(buffer[13]) + String.fromCharCode(buffer[14]) + String.fromCharCode(buffer[15]);
224 this.referenceId = '' + buffer[12] + '.' + buffer[13] + '.' + buffer[14] + '.' + buffer[15];
228 // Reference timestamp
230 this.referenceTimestamp = internals.toMsecs(buffer, 16);
232 // Originate timestamp
234 this.originateTimestamp = internals.toMsecs(buffer, 24);
238 this.receiveTimestamp = internals.toMsecs(buffer, 32);
240 // Transmit timestamp
242 this.transmitTimestamp = internals.toMsecs(buffer, 40);
246 if (this.version === 4 &&
247 this.stratum !== 'reserved' &&
248 this.mode === 'server' &&
249 this.originateTimestamp &&
250 this.receiveTimestamp &&
251 this.transmitTimestamp) {
260 internals.toMsecs = function (buffer, offset) {
265 for (var i = 0; i < 4; ++i) {
266 seconds = (seconds * 256) + buffer[offset + i];
269 for (i = 4; i < 8; ++i) {
270 fraction = (fraction * 256) + buffer[offset + i];
273 return ((seconds - 2208988800 + (fraction / Math.pow(2, 32))) * 1000);
277 internals.fromMsecs = function (ts, buffer, offset) {
279 var seconds = Math.floor(ts / 1000) + 2208988800;
280 var fraction = Math.round((ts % 1000) / 1000 * Math.pow(2, 32));
282 buffer[offset + 0] = (seconds & 0xFF000000) >> 24;
283 buffer[offset + 1] = (seconds & 0x00FF0000) >> 16;
284 buffer[offset + 2] = (seconds & 0x0000FF00) >> 8;
285 buffer[offset + 3] = (seconds & 0x000000FF);
287 buffer[offset + 4] = (fraction & 0xFF000000) >> 24;
288 buffer[offset + 5] = (fraction & 0x00FF0000) >> 16;
289 buffer[offset + 6] = (fraction & 0x0000FF00) >> 8;
290 buffer[offset + 7] = (fraction & 0x000000FF);
304 exports.offset = function (options, callback) {
306 if (arguments.length !== 2) {
307 callback = arguments[0];
311 var now = Date.now();
312 var clockSyncRefresh = options.clockSyncRefresh || 24 * 60 * 60 * 1000; // Daily
314 if (internals.last.offset &&
315 internals.last.host === options.host &&
316 internals.last.port === options.port &&
317 now < internals.last.expires) {
319 process.nextTick(function () {
321 callback(null, internals.last.offset);
327 exports.time(options, function (err, time) {
330 return callback(err, 0);
334 offset: Math.round(time.t),
335 expires: now + clockSyncRefresh,
340 return callback(null, internals.last.offset);
352 exports.start = function (options, callback) {
354 if (arguments.length !== 2) {
355 callback = arguments[0];
359 if (internals.now.intervalId) {
360 process.nextTick(function () {
368 exports.offset(options, function (err, offset) {
370 internals.now.intervalId = setInterval(function () {
372 exports.offset(options, function () { });
373 }, options.clockSyncRefresh || 24 * 60 * 60 * 1000); // Daily
380 exports.stop = function () {
382 if (!internals.now.intervalId) {
386 clearInterval(internals.now.intervalId);
387 internals.now.intervalId = 0;
391 exports.isLive = function () {
393 return !!internals.now.intervalId;
397 exports.now = function () {
399 var now = Date.now();
400 if (!exports.isLive() ||
401 now >= internals.last.expires) {
406 return now + internals.last.offset;
410 internals.ignore = function () {