Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
81154 views
1
// Load modules
2
3
var Dgram = require('dgram');
4
var Dns = require('dns');
5
var Hoek = require('hoek');
6
7
8
// Declare internals
9
10
var internals = {};
11
12
13
exports.time = function (options, callback) {
14
15
if (arguments.length !== 2) {
16
callback = arguments[0];
17
options = {};
18
}
19
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;
24
25
// Declare variables used by callback
26
27
var timeoutId = 0;
28
var sent = 0;
29
30
// Ensure callback is only called once
31
32
var finish = function (err, result) {
33
34
if (timeoutId) {
35
clearTimeout(timeoutId);
36
timeoutId = 0;
37
}
38
39
socket.removeAllListeners();
40
socket.once('error', internals.ignore);
41
socket.close();
42
return callback(err, result);
43
};
44
45
finish = Hoek.once(finish);
46
47
// Create UDP socket
48
49
var socket = Dgram.createSocket('udp4');
50
51
socket.once('error', function (err) {
52
53
return finish(err);
54
});
55
56
// Listen to incoming messages
57
58
socket.on('message', function (buffer, rinfo) {
59
60
var received = Date.now();
61
62
var message = new internals.NtpMessage(buffer);
63
if (!message.isValid) {
64
return finish(new Error('Invalid server response'), message);
65
}
66
67
if (message.originateTimestamp !== sent) {
68
return finish(new Error('Wrong originate timestamp'), message);
69
}
70
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
77
//
78
// The roundtrip delay d and system clock offset t are defined as:
79
//
80
// d = (T4 - T1) - (T3 - T2) t = ((T2 - T1) + (T3 - T4)) / 2
81
82
var T1 = message.originateTimestamp;
83
var T2 = message.receiveTimestamp;
84
var T3 = message.transmitTimestamp;
85
var T4 = received;
86
87
message.d = (T4 - T1) - (T3 - T2);
88
message.t = ((T2 - T1) + (T3 - T4)) / 2;
89
message.receivedLocally = received;
90
91
if (!settings.resolveReference ||
92
message.stratum !== 'secondary') {
93
94
return finish(null, message);
95
}
96
97
// Resolve reference IP address
98
99
Dns.reverse(message.referenceId, function (err, domains) {
100
101
if (/* $lab:coverage:off$ */ !err /* $lab:coverage:on$ */) {
102
message.referenceHost = domains[0];
103
}
104
105
return finish(null, message);
106
});
107
});
108
109
// Set timeout
110
111
if (settings.timeout) {
112
timeoutId = setTimeout(function () {
113
114
timeoutId = 0;
115
return finish(new Error('Timeout'));
116
}, settings.timeout);
117
}
118
119
// Construct NTP message
120
121
var message = new Buffer(48);
122
for (var i = 0; i < 48; i++) { // Zero message
123
message[i] = 0;
124
}
125
126
message[0] = (0 << 6) + (4 << 3) + (3 << 0) // Set version number to 4 and Mode to 3 (client)
127
sent = Date.now();
128
internals.fromMsecs(sent, message, 40); // Set transmit timestamp (returns as originate)
129
130
// Send NTP request
131
132
socket.send(message, 0, message.length, settings.port, settings.host, function (err, bytes) {
133
134
if (err ||
135
bytes !== 48) {
136
137
return finish(err || new Error('Could not send entire message'));
138
}
139
});
140
};
141
142
143
internals.NtpMessage = function (buffer) {
144
145
this.isValid = false;
146
147
// Validate
148
149
if (buffer.length !== 48) {
150
return;
151
}
152
153
// Leap indicator
154
155
var li = (buffer[0] >> 6);
156
switch (li) {
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;
161
}
162
163
// Version
164
165
var vn = ((buffer[0] & 0x38) >> 3);
166
this.version = vn;
167
168
// Mode
169
170
var mode = (buffer[0] & 0x7);
171
switch (mode) {
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;
177
case 0:
178
case 6:
179
case 7: this.mode = 'reserved'; break;
180
}
181
182
// Stratum
183
184
var stratum = buffer[1];
185
if (stratum === 0) {
186
this.stratum = 'death';
187
}
188
else if (stratum === 1) {
189
this.stratum = 'primary';
190
}
191
else if (stratum <= 15) {
192
this.stratum = 'secondary';
193
}
194
else {
195
this.stratum = 'reserved';
196
}
197
198
// Poll interval (msec)
199
200
this.pollInterval = Math.round(Math.pow(2, buffer[2])) * 1000;
201
202
// Precision (msecs)
203
204
this.precision = Math.pow(2, buffer[3]) * 1000;
205
206
// Root delay (msecs)
207
208
var rootDelay = 256 * (256 * (256 * buffer[4] + buffer[5]) + buffer[6]) + buffer[7];
209
this.rootDelay = 1000 * (rootDelay / 0x10000);
210
211
// Root dispersion (msecs)
212
213
this.rootDispersion = ((buffer[8] << 8) + buffer[9] + ((buffer[10] << 8) + buffer[11]) / Math.pow(2, 16)) * 1000;
214
215
// Reference identifier
216
217
this.referenceId = '';
218
switch (this.stratum) {
219
case 'death':
220
case 'primary':
221
this.referenceId = String.fromCharCode(buffer[12]) + String.fromCharCode(buffer[13]) + String.fromCharCode(buffer[14]) + String.fromCharCode(buffer[15]);
222
break;
223
case 'secondary':
224
this.referenceId = '' + buffer[12] + '.' + buffer[13] + '.' + buffer[14] + '.' + buffer[15];
225
break;
226
}
227
228
// Reference timestamp
229
230
this.referenceTimestamp = internals.toMsecs(buffer, 16);
231
232
// Originate timestamp
233
234
this.originateTimestamp = internals.toMsecs(buffer, 24);
235
236
// Receive timestamp
237
238
this.receiveTimestamp = internals.toMsecs(buffer, 32);
239
240
// Transmit timestamp
241
242
this.transmitTimestamp = internals.toMsecs(buffer, 40);
243
244
// Validate
245
246
if (this.version === 4 &&
247
this.stratum !== 'reserved' &&
248
this.mode === 'server' &&
249
this.originateTimestamp &&
250
this.receiveTimestamp &&
251
this.transmitTimestamp) {
252
253
this.isValid = true;
254
}
255
256
return this;
257
};
258
259
260
internals.toMsecs = function (buffer, offset) {
261
262
var seconds = 0;
263
var fraction = 0;
264
265
for (var i = 0; i < 4; ++i) {
266
seconds = (seconds * 256) + buffer[offset + i];
267
}
268
269
for (i = 4; i < 8; ++i) {
270
fraction = (fraction * 256) + buffer[offset + i];
271
}
272
273
return ((seconds - 2208988800 + (fraction / Math.pow(2, 32))) * 1000);
274
};
275
276
277
internals.fromMsecs = function (ts, buffer, offset) {
278
279
var seconds = Math.floor(ts / 1000) + 2208988800;
280
var fraction = Math.round((ts % 1000) / 1000 * Math.pow(2, 32));
281
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);
286
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);
291
};
292
293
294
// Offset singleton
295
296
internals.last = {
297
offset: 0,
298
expires: 0,
299
host: '',
300
port: 0
301
};
302
303
304
exports.offset = function (options, callback) {
305
306
if (arguments.length !== 2) {
307
callback = arguments[0];
308
options = {};
309
}
310
311
var now = Date.now();
312
var clockSyncRefresh = options.clockSyncRefresh || 24 * 60 * 60 * 1000; // Daily
313
314
if (internals.last.offset &&
315
internals.last.host === options.host &&
316
internals.last.port === options.port &&
317
now < internals.last.expires) {
318
319
process.nextTick(function () {
320
321
callback(null, internals.last.offset);
322
});
323
324
return;
325
}
326
327
exports.time(options, function (err, time) {
328
329
if (err) {
330
return callback(err, 0);
331
}
332
333
internals.last = {
334
offset: Math.round(time.t),
335
expires: now + clockSyncRefresh,
336
host: options.host,
337
port: options.port
338
};
339
340
return callback(null, internals.last.offset);
341
});
342
};
343
344
345
// Now singleton
346
347
internals.now = {
348
intervalId: 0
349
};
350
351
352
exports.start = function (options, callback) {
353
354
if (arguments.length !== 2) {
355
callback = arguments[0];
356
options = {};
357
}
358
359
if (internals.now.intervalId) {
360
process.nextTick(function () {
361
362
callback();
363
});
364
365
return;
366
}
367
368
exports.offset(options, function (err, offset) {
369
370
internals.now.intervalId = setInterval(function () {
371
372
exports.offset(options, function () { });
373
}, options.clockSyncRefresh || 24 * 60 * 60 * 1000); // Daily
374
375
return callback();
376
});
377
};
378
379
380
exports.stop = function () {
381
382
if (!internals.now.intervalId) {
383
return;
384
}
385
386
clearInterval(internals.now.intervalId);
387
internals.now.intervalId = 0;
388
};
389
390
391
exports.isLive = function () {
392
393
return !!internals.now.intervalId;
394
};
395
396
397
exports.now = function () {
398
399
var now = Date.now();
400
if (!exports.isLive() ||
401
now >= internals.last.expires) {
402
403
return now;
404
}
405
406
return now + internals.last.offset;
407
};
408
409
410
internals.ignore = function () {
411
412
};
413
414