Initial commit
[yaffs-website] / node_modules / sntp / lib / index.js
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 };