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.5 KiB
						
					
					
				
			
		
		
	
	
							413 lines
						
					
					
						
							9.5 KiB
						
					
					
				| // Load modules
 | |
| 
 | |
| var Dgram = require('dgram');
 | |
| var Dns = require('dns');
 | |
| var Hoek = require('hoek');
 | |
| 
 | |
| 
 | |
| // Declare internals
 | |
| 
 | |
| var internals = {};
 | |
| 
 | |
| 
 | |
| exports.time = function (options, callback) {
 | |
| 
 | |
|     if (arguments.length !== 2) {
 | |
|         callback = arguments[0];
 | |
|         options = {};
 | |
|     }
 | |
| 
 | |
|     var settings = Hoek.clone(options);
 | |
|     settings.host = settings.host || 'pool.ntp.org';
 | |
|     settings.port = settings.port || 123;
 | |
|     settings.resolveReference = settings.resolveReference || false;
 | |
| 
 | |
|     // Declare variables used by callback
 | |
| 
 | |
|     var timeoutId = 0;
 | |
|     var sent = 0;
 | |
| 
 | |
|     // Ensure callback is only called once
 | |
| 
 | |
|     var finish = function (err, result) {
 | |
| 
 | |
|         if (timeoutId) {
 | |
|             clearTimeout(timeoutId);
 | |
|             timeoutId = 0;
 | |
|         }
 | |
| 
 | |
|         socket.removeAllListeners();
 | |
|         socket.once('error', internals.ignore);
 | |
|         socket.close();
 | |
|         return callback(err, result);
 | |
|     };
 | |
| 
 | |
|     finish = Hoek.once(finish);
 | |
| 
 | |
|     // Create UDP socket
 | |
| 
 | |
|     var socket = Dgram.createSocket('udp4');
 | |
| 
 | |
|     socket.once('error', function (err) {
 | |
| 
 | |
|         return finish(err);
 | |
|     });
 | |
| 
 | |
|     // Listen to incoming messages
 | |
| 
 | |
|     socket.on('message', function (buffer, rinfo) {
 | |
| 
 | |
|         var received = Date.now();
 | |
| 
 | |
|         var 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
 | |
| 
 | |
|         var T1 = message.originateTimestamp;
 | |
|         var T2 = message.receiveTimestamp;
 | |
|         var T3 = message.transmitTimestamp;
 | |
|         var 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, function (err, domains) {
 | |
| 
 | |
|             if (/* $lab:coverage:off$ */ !err /* $lab:coverage:on$ */) {
 | |
|                 message.referenceHost = domains[0];
 | |
|             }
 | |
| 
 | |
|             return finish(null, message);
 | |
|         });
 | |
|     });
 | |
| 
 | |
|     // Set timeout
 | |
| 
 | |
|     if (settings.timeout) {
 | |
|         timeoutId = setTimeout(function () {
 | |
| 
 | |
|             timeoutId = 0;
 | |
|             return finish(new Error('Timeout'));
 | |
|         }, settings.timeout);
 | |
|     }
 | |
| 
 | |
|     // Construct NTP message
 | |
| 
 | |
|     var message = new Buffer(48);
 | |
|     for (var 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, function (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
 | |
| 
 | |
|     var 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
 | |
| 
 | |
|     var vn = ((buffer[0] & 0x38) >> 3);
 | |
|     this.version = vn;
 | |
| 
 | |
|     // Mode
 | |
| 
 | |
|     var 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
 | |
| 
 | |
|     var 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)
 | |
| 
 | |
|     var 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) {
 | |
| 
 | |
|     var seconds = 0;
 | |
|     var fraction = 0;
 | |
| 
 | |
|     for (var i = 0; i < 4; ++i) {
 | |
|         seconds = (seconds * 256) + buffer[offset + i];
 | |
|     }
 | |
| 
 | |
|     for (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) {
 | |
| 
 | |
|     var seconds = Math.floor(ts / 1000) + 2208988800;
 | |
|     var 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 = {};
 | |
|     }
 | |
| 
 | |
|     var now = Date.now();
 | |
|     var 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(function () {
 | |
| 
 | |
|             callback(null, internals.last.offset);
 | |
|         });
 | |
| 
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     exports.time(options, function (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 = {
 | |
|     intervalId: 0
 | |
| };
 | |
| 
 | |
| 
 | |
| exports.start = function (options, callback) {
 | |
| 
 | |
|     if (arguments.length !== 2) {
 | |
|         callback = arguments[0];
 | |
|         options = {};
 | |
|     }
 | |
| 
 | |
|     if (internals.now.intervalId) {
 | |
|         process.nextTick(function () {
 | |
| 
 | |
|             callback();
 | |
|         });
 | |
| 
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     exports.offset(options, function (err, offset) {
 | |
| 
 | |
|         internals.now.intervalId = setInterval(function () {
 | |
| 
 | |
|             exports.offset(options, function () { });
 | |
|         }, options.clockSyncRefresh || 24 * 60 * 60 * 1000);                                // Daily
 | |
| 
 | |
|         return callback();
 | |
|     });
 | |
| };
 | |
| 
 | |
| 
 | |
| exports.stop = function () {
 | |
| 
 | |
|     if (!internals.now.intervalId) {
 | |
|         return;
 | |
|     }
 | |
| 
 | |
|     clearInterval(internals.now.intervalId);
 | |
|     internals.now.intervalId = 0;
 | |
| };
 | |
| 
 | |
| 
 | |
| exports.isLive = function () {
 | |
| 
 | |
|     return !!internals.now.intervalId;
 | |
| };
 | |
| 
 | |
| 
 | |
| exports.now = function () {
 | |
| 
 | |
|     var now = Date.now();
 | |
|     if (!exports.isLive() ||
 | |
|         now >= internals.last.expires) {
 | |
| 
 | |
|         return now;
 | |
|     }
 | |
| 
 | |
|     return now + internals.last.offset;
 | |
| };
 | |
| 
 | |
| 
 | |
| internals.ignore = function () {
 | |
| 
 | |
| };
 |