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
			| 
											3 years ago
										 | // 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 () { | ||
|  | 
 | ||
|  | }; |