index.js 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. // Load modules
  2. var Dgram = require('dgram');
  3. var Dns = require('dns');
  4. var Hoek = require('hoek');
  5. // Declare internals
  6. var internals = {};
  7. exports.time = function (options, callback) {
  8. if (arguments.length !== 2) {
  9. callback = arguments[0];
  10. options = {};
  11. }
  12. var settings = Hoek.clone(options);
  13. settings.host = settings.host || 'pool.ntp.org';
  14. settings.port = settings.port || 123;
  15. settings.resolveReference = settings.resolveReference || false;
  16. // Declare variables used by callback
  17. var timeoutId = 0;
  18. var sent = 0;
  19. // Ensure callback is only called once
  20. var finish = function (err, result) {
  21. if (timeoutId) {
  22. clearTimeout(timeoutId);
  23. timeoutId = 0;
  24. }
  25. socket.removeAllListeners();
  26. socket.once('error', internals.ignore);
  27. socket.close();
  28. return callback(err, result);
  29. };
  30. finish = Hoek.once(finish);
  31. // Create UDP socket
  32. var socket = Dgram.createSocket('udp4');
  33. socket.once('error', function (err) {
  34. return finish(err);
  35. });
  36. // Listen to incoming messages
  37. socket.on('message', function (buffer, rinfo) {
  38. var received = Date.now();
  39. var message = new internals.NtpMessage(buffer);
  40. if (!message.isValid) {
  41. return finish(new Error('Invalid server response'), message);
  42. }
  43. if (message.originateTimestamp !== sent) {
  44. return finish(new Error('Wrong originate timestamp'), message);
  45. }
  46. // Timestamp Name ID When Generated
  47. // ------------------------------------------------------------
  48. // Originate Timestamp T1 time request sent by client
  49. // Receive Timestamp T2 time request received by server
  50. // Transmit Timestamp T3 time reply sent by server
  51. // Destination Timestamp T4 time reply received by client
  52. //
  53. // The roundtrip delay d and system clock offset t are defined as:
  54. //
  55. // d = (T4 - T1) - (T3 - T2) t = ((T2 - T1) + (T3 - T4)) / 2
  56. var T1 = message.originateTimestamp;
  57. var T2 = message.receiveTimestamp;
  58. var T3 = message.transmitTimestamp;
  59. var T4 = received;
  60. message.d = (T4 - T1) - (T3 - T2);
  61. message.t = ((T2 - T1) + (T3 - T4)) / 2;
  62. message.receivedLocally = received;
  63. if (!settings.resolveReference ||
  64. message.stratum !== 'secondary') {
  65. return finish(null, message);
  66. }
  67. // Resolve reference IP address
  68. Dns.reverse(message.referenceId, function (err, domains) {
  69. if (/* $lab:coverage:off$ */ !err /* $lab:coverage:on$ */) {
  70. message.referenceHost = domains[0];
  71. }
  72. return finish(null, message);
  73. });
  74. });
  75. // Set timeout
  76. if (settings.timeout) {
  77. timeoutId = setTimeout(function () {
  78. timeoutId = 0;
  79. return finish(new Error('Timeout'));
  80. }, settings.timeout);
  81. }
  82. // Construct NTP message
  83. var message = new Buffer(48);
  84. for (var i = 0; i < 48; i++) { // Zero message
  85. message[i] = 0;
  86. }
  87. message[0] = (0 << 6) + (4 << 3) + (3 << 0) // Set version number to 4 and Mode to 3 (client)
  88. sent = Date.now();
  89. internals.fromMsecs(sent, message, 40); // Set transmit timestamp (returns as originate)
  90. // Send NTP request
  91. socket.send(message, 0, message.length, settings.port, settings.host, function (err, bytes) {
  92. if (err ||
  93. bytes !== 48) {
  94. return finish(err || new Error('Could not send entire message'));
  95. }
  96. });
  97. };
  98. internals.NtpMessage = function (buffer) {
  99. this.isValid = false;
  100. // Validate
  101. if (buffer.length !== 48) {
  102. return;
  103. }
  104. // Leap indicator
  105. var li = (buffer[0] >> 6);
  106. switch (li) {
  107. case 0: this.leapIndicator = 'no-warning'; break;
  108. case 1: this.leapIndicator = 'last-minute-61'; break;
  109. case 2: this.leapIndicator = 'last-minute-59'; break;
  110. case 3: this.leapIndicator = 'alarm'; break;
  111. }
  112. // Version
  113. var vn = ((buffer[0] & 0x38) >> 3);
  114. this.version = vn;
  115. // Mode
  116. var mode = (buffer[0] & 0x7);
  117. switch (mode) {
  118. case 1: this.mode = 'symmetric-active'; break;
  119. case 2: this.mode = 'symmetric-passive'; break;
  120. case 3: this.mode = 'client'; break;
  121. case 4: this.mode = 'server'; break;
  122. case 5: this.mode = 'broadcast'; break;
  123. case 0:
  124. case 6:
  125. case 7: this.mode = 'reserved'; break;
  126. }
  127. // Stratum
  128. var stratum = buffer[1];
  129. if (stratum === 0) {
  130. this.stratum = 'death';
  131. }
  132. else if (stratum === 1) {
  133. this.stratum = 'primary';
  134. }
  135. else if (stratum <= 15) {
  136. this.stratum = 'secondary';
  137. }
  138. else {
  139. this.stratum = 'reserved';
  140. }
  141. // Poll interval (msec)
  142. this.pollInterval = Math.round(Math.pow(2, buffer[2])) * 1000;
  143. // Precision (msecs)
  144. this.precision = Math.pow(2, buffer[3]) * 1000;
  145. // Root delay (msecs)
  146. var rootDelay = 256 * (256 * (256 * buffer[4] + buffer[5]) + buffer[6]) + buffer[7];
  147. this.rootDelay = 1000 * (rootDelay / 0x10000);
  148. // Root dispersion (msecs)
  149. this.rootDispersion = ((buffer[8] << 8) + buffer[9] + ((buffer[10] << 8) + buffer[11]) / Math.pow(2, 16)) * 1000;
  150. // Reference identifier
  151. this.referenceId = '';
  152. switch (this.stratum) {
  153. case 'death':
  154. case 'primary':
  155. this.referenceId = String.fromCharCode(buffer[12]) + String.fromCharCode(buffer[13]) + String.fromCharCode(buffer[14]) + String.fromCharCode(buffer[15]);
  156. break;
  157. case 'secondary':
  158. this.referenceId = '' + buffer[12] + '.' + buffer[13] + '.' + buffer[14] + '.' + buffer[15];
  159. break;
  160. }
  161. // Reference timestamp
  162. this.referenceTimestamp = internals.toMsecs(buffer, 16);
  163. // Originate timestamp
  164. this.originateTimestamp = internals.toMsecs(buffer, 24);
  165. // Receive timestamp
  166. this.receiveTimestamp = internals.toMsecs(buffer, 32);
  167. // Transmit timestamp
  168. this.transmitTimestamp = internals.toMsecs(buffer, 40);
  169. // Validate
  170. if (this.version === 4 &&
  171. this.stratum !== 'reserved' &&
  172. this.mode === 'server' &&
  173. this.originateTimestamp &&
  174. this.receiveTimestamp &&
  175. this.transmitTimestamp) {
  176. this.isValid = true;
  177. }
  178. return this;
  179. };
  180. internals.toMsecs = function (buffer, offset) {
  181. var seconds = 0;
  182. var fraction = 0;
  183. for (var i = 0; i < 4; ++i) {
  184. seconds = (seconds * 256) + buffer[offset + i];
  185. }
  186. for (i = 4; i < 8; ++i) {
  187. fraction = (fraction * 256) + buffer[offset + i];
  188. }
  189. return ((seconds - 2208988800 + (fraction / Math.pow(2, 32))) * 1000);
  190. };
  191. internals.fromMsecs = function (ts, buffer, offset) {
  192. var seconds = Math.floor(ts / 1000) + 2208988800;
  193. var fraction = Math.round((ts % 1000) / 1000 * Math.pow(2, 32));
  194. buffer[offset + 0] = (seconds & 0xFF000000) >> 24;
  195. buffer[offset + 1] = (seconds & 0x00FF0000) >> 16;
  196. buffer[offset + 2] = (seconds & 0x0000FF00) >> 8;
  197. buffer[offset + 3] = (seconds & 0x000000FF);
  198. buffer[offset + 4] = (fraction & 0xFF000000) >> 24;
  199. buffer[offset + 5] = (fraction & 0x00FF0000) >> 16;
  200. buffer[offset + 6] = (fraction & 0x0000FF00) >> 8;
  201. buffer[offset + 7] = (fraction & 0x000000FF);
  202. };
  203. // Offset singleton
  204. internals.last = {
  205. offset: 0,
  206. expires: 0,
  207. host: '',
  208. port: 0
  209. };
  210. exports.offset = function (options, callback) {
  211. if (arguments.length !== 2) {
  212. callback = arguments[0];
  213. options = {};
  214. }
  215. var now = Date.now();
  216. var clockSyncRefresh = options.clockSyncRefresh || 24 * 60 * 60 * 1000; // Daily
  217. if (internals.last.offset &&
  218. internals.last.host === options.host &&
  219. internals.last.port === options.port &&
  220. now < internals.last.expires) {
  221. process.nextTick(function () {
  222. callback(null, internals.last.offset);
  223. });
  224. return;
  225. }
  226. exports.time(options, function (err, time) {
  227. if (err) {
  228. return callback(err, 0);
  229. }
  230. internals.last = {
  231. offset: Math.round(time.t),
  232. expires: now + clockSyncRefresh,
  233. host: options.host,
  234. port: options.port
  235. };
  236. return callback(null, internals.last.offset);
  237. });
  238. };
  239. // Now singleton
  240. internals.now = {
  241. intervalId: 0
  242. };
  243. exports.start = function (options, callback) {
  244. if (arguments.length !== 2) {
  245. callback = arguments[0];
  246. options = {};
  247. }
  248. if (internals.now.intervalId) {
  249. process.nextTick(function () {
  250. callback();
  251. });
  252. return;
  253. }
  254. exports.offset(options, function (err, offset) {
  255. internals.now.intervalId = setInterval(function () {
  256. exports.offset(options, function () { });
  257. }, options.clockSyncRefresh || 24 * 60 * 60 * 1000); // Daily
  258. return callback();
  259. });
  260. };
  261. exports.stop = function () {
  262. if (!internals.now.intervalId) {
  263. return;
  264. }
  265. clearInterval(internals.now.intervalId);
  266. internals.now.intervalId = 0;
  267. };
  268. exports.isLive = function () {
  269. return !!internals.now.intervalId;
  270. };
  271. exports.now = function () {
  272. var now = Date.now();
  273. if (!exports.isLive() ||
  274. now >= internals.last.expires) {
  275. return now;
  276. }
  277. return now + internals.last.offset;
  278. };
  279. internals.ignore = function () {
  280. };