pax_global_header00006660000000000000000000000064136060042740014514gustar00rootroot0000000000000052 comment=c37746e8fc43de1aff987aa82dde7498d61839c2 sntp-4.0.0/000077500000000000000000000000001360600427400125015ustar00rootroot00000000000000sntp-4.0.0/.gitignore000077500000000000000000000001541360600427400144740ustar00rootroot00000000000000**/node_modules **/package-lock.json coverage.* **/.DS_Store **/._* **/*.pem **/.vs **/.vscode **/.idea sntp-4.0.0/.travis.yml000077500000000000000000000002021360600427400146070ustar00rootroot00000000000000language: node_js node_js: - "12" - "node" sudo: false install: - "npm install" os: - "linux" - "osx" - "windows" sntp-4.0.0/API.md000066400000000000000000000036531360600427400134430ustar00rootroot00000000000000 ## Introduction An SNTP v4 client (RFC4330) for node. Simpy connects to the NTP or SNTP server requested and returns the server time along with the roundtrip duration and clock offset. To adjust the local time to the NTP time, add the returned `t` offset to the local time. ## Usage ```javascript const Sntp = require('@hapi/sntp'); // All options are optional const options = { host: 'nist1-sj.ustiming.org', // Defaults to pool.ntp.org port: 123, // Defaults to 123 (NTP) resolveReference: true, // Default to false (not resolving) timeout: 1000 // Defaults to zero (no timeout) }; // Request server time const exec = async function () { try { const time = await Sntp.time(options); console.log('Local clock is off by: ' + time.t + ' milliseconds'); process.exit(0); } catch (err) { console.log('Failed: ' + err.message); process.exit(1); } }; exec(); ``` If an application needs to maintain continuous time synchronization, the module provides a stateful method for querying the current offset only when the last one is too old (defaults to daily). ```javascript // Request offset once const exec = async function () { const offset1 = await Sntp.offset(); console.log(offset1); // New (served fresh) // Request offset again const offset2 = await Sntp.offset(); console.log(offset2); // Identical (served from cache) }; exec(); ``` To set a background offset refresh, start the interval and use the provided now() method. If for any reason the client fails to obtain an up-to-date offset, the current system clock is used. ```javascript const before = Sntp.now(); // System time without offset const exec = async function () { await Sntp.start(); const now = Sntp.now(); // With offset Sntp.stop(); }; exec(); ``` sntp-4.0.0/LICENSE.md000077500000000000000000000026751360600427400141220ustar00rootroot00000000000000Copyright (c) 2012-2020, Sideway Inc, and project contributors All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * The names of any contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS OFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. sntp-4.0.0/README.md000077500000000000000000000017701360600427400137700ustar00rootroot00000000000000 # @hapi/sntp #### SNTP client (REF4330) for node. **sntp** is part of the **hapi** ecosystem and was designed to work seamlessly with the [hapi web framework](https://hapi.dev) and its other components (but works great on its own or with other frameworks). If you are using a different web framework and find this module useful, check out [hapi](https://hapi.dev) – they work even better together. ### Visit the [hapi.dev](https://hapi.dev) Developer Portal for tutorials, documentation, and support ## Useful resources - [Documentation and API](https://hapi.dev/family/sntp/) - [Versions status](https://hapi.dev/resources/status/#sntp) (builds, dependencies, node versions, licenses, eol) - [Changelog](https://hapi.dev/family/sntp/changelog/) - [Project policies](https://hapi.dev/policies/) - [Free and commercial support options](https://hapi.dev/support/) sntp-4.0.0/lib/000077500000000000000000000000001360600427400132475ustar00rootroot00000000000000sntp-4.0.0/lib/index.js000077500000000000000000000221011360600427400147130ustar00rootroot00000000000000'use strict'; const Dgram = require('dgram'); const Dns = require('dns'); const Boom = require('@hapi/boom'); const Bounce = require('@hapi/bounce'); const Hoek = require('@hapi/hoek'); const Teamwork = require('@hapi/teamwork'); const internals = {}; exports.time = async function (options = {}) { const settings = Hoek.clone(options); settings.host = settings.host || 'time.google.com'; settings.port = settings.port || 123; settings.resolveReference = settings.resolveReference || false; const team = new Teamwork.Team(); // Set timeout const timeoutId = settings.timeout ? setTimeout(() => team.attend(new Boom.Boom('Timeout')), settings.timeout) : null; // Create UDP socket const socket = Dgram.createSocket('udp4'); socket.once('error', (err) => team.attend(err)); // Listen to incoming messages socket.on('message', (buffer, rinfo) => { const received = Date.now(); const message = new internals.NtpMessage(buffer); if (!message.isValid) { const error = new Boom.Boom('Invalid server response'); error.time = message; return team.attend(error); } if (message.originateTimestamp !== sent) { const error = new Boom.Boom('Wrong originate timestamp'); error.time = message; return team.attend(error); } // 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 const T1 = message.originateTimestamp; const T2 = message.receiveTimestamp; const T3 = message.transmitTimestamp; const T4 = received; message.d = (T4 - T1) - (T3 - T2); message.t = ((T2 - T1) + (T3 - T4)) / 2; message.receivedLocally = received; if (message.stratum !== 'secondary' || !settings.resolveReference) { return team.attend(message); } // Resolve reference IP address Dns.reverse(message.referenceId, (err, domains) => { if (/* $lab:coverage:off$ */ !err /* $lab:coverage:on$ */) { message.referenceHost = domains[0]; } return team.attend(message); }); }); // Construct NTP message const message = Buffer.alloc(48); // Zero-filled message message[0] = (0 << 6) + (4 << 3) + (3 << 0); // Set version number to 4 and Mode to 3 (client) const 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, (err, bytes) => { if (err || bytes !== 48) { return team.attend(err || new Boom.Boom('Could not send entire message')); } }); try { return await team.work; } finally { clearTimeout(timeoutId); socket.removeAllListeners(); socket.once('error', Hoek.ignore); try { socket.close(); } catch (ignoreErr) { } // Ignore errors if the socket is already closed } }; internals.NtpMessage = function (buffer) { this.isValid = false; // Validate if (buffer.length !== 48) { return; } // Leap indicator const 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 const vn = ((buffer[0] & 0x38) >> 3); this.version = vn; // Mode const 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 const 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) const 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 this.isValid = (this.version === 4 && this.stratum !== 'reserved' && this.mode === 'server' && this.originateTimestamp && this.receiveTimestamp && this.transmitTimestamp); return this; }; internals.toMsecs = function (buffer, offset) { let seconds = 0; let fraction = 0; for (let i = 0; i < 4; ++i) { seconds = (seconds * 256) + buffer[offset + i]; } for (let 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) { const seconds = Math.floor(ts / 1000) + 2208988800; const 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 = async function (options = {}) { const now = Date.now(); const clockSyncRefresh = options.clockSyncRefresh || 24 * 60 * 60 * 1000; // Daily if (now < internals.last.expires && internals.last.host === options.host && internals.last.port === options.port) { return internals.last.offset; } const time = await exports.time(options); internals.last = { offset: Math.round(time.t), expires: now + clockSyncRefresh, host: options.host, port: options.port }; return internals.last.offset; }; // Now singleton internals.now = { started: false, intervalId: null }; exports.start = async function (options = {}) { if (internals.now.started) { return; } const tick = async () => { try { await exports.offset(options); } catch (err) { if (options.onError) { options.onError(err); } Bounce.rethrow(err, 'system'); } }; internals.now.started = true; internals.now.intervalId = setInterval(tick, options.clockSyncRefresh || 24 * 60 * 60 * 1000); // Daily await exports.offset(options); }; exports.stop = function () { if (!internals.now.started) { return; } clearInterval(internals.now.intervalId); internals.now.started = false; internals.now.intervalId = null; }; exports.isLive = function () { return internals.now.started; }; exports.now = function () { const now = Date.now(); if (exports.isLive()) { return now + internals.last.offset; } return now; }; sntp-4.0.0/package.json000077500000000000000000000011711360600427400147720ustar00rootroot00000000000000{ "name": "@hapi/sntp", "description": "SNTP Client", "version": "4.0.0", "repository": "git://github.com/hapijs/sntp", "main": "lib/index.js", "files": [ "lib" ], "keywords": [ "sntp", "ntp", "time" ], "dependencies": { "@hapi/boom": "9.x.x", "@hapi/bounce": "2.x.x", "@hapi/hoek": "9.x.x", "@hapi/teamwork": "4.x.x" }, "devDependencies": { "@hapi/code": "8.x.x", "@hapi/lab": "22.x.x" }, "scripts": { "test": "lab -a @hapi/code -t 100 -L -m 20000", "test-cov-html": "lab -a @hapi/code -r html -o coverage.html -m 20000" }, "license": "BSD-3-Clause" } sntp-4.0.0/test/000077500000000000000000000000001360600427400134605ustar00rootroot00000000000000sntp-4.0.0/test/index.js000077500000000000000000000257021360600427400151360ustar00rootroot00000000000000'use strict'; const Dgram = require('dgram'); const Code = require('@hapi/code'); const Hoek = require('@hapi/hoek'); const Lab = require('@hapi/lab'); const Sntp = require('..'); const Teamwork = require('@hapi/teamwork'); const internals = {}; const { describe, it } = exports.lab = Lab.script(); const expect = Code.expect; describe('SNTP', () => { const origDate = Date.now; Date.now = () => { return origDate() - 5; }; describe('time()', () => { it('returns consistent result over multiple tries', async () => { const time1 = await Sntp.time(); expect(time1).to.exist(); const t1 = time1.t; const time2 = await Sntp.time(); expect(time2).to.exist(); const t2 = time2.t; expect(Math.abs(t1 - t2)).to.be.below(200); }); it('resolves reference IP', async () => { const time = await Sntp.time({ host: 'ntp.exnet.com', resolveReference: true }); expect(time).to.exist(); expect(time.referenceHost).to.exist(); }); it('times out on no response', async () => { await expect(Sntp.time({ port: 124, timeout: 100 })).to.reject('Timeout'); }); it('errors on error event', async () => { const orig = Dgram.createSocket; Dgram.createSocket = function (type) { Dgram.createSocket = orig; const socket = Dgram.createSocket(type); setImmediate(() => { socket.emit('error', new Error('Fake')); }); return socket; }; await expect(Sntp.time()).to.reject('Fake'); }); it('errors on incorrect sent size', async () => { const orig = Dgram.Socket.prototype.send; Dgram.Socket.prototype.send = function (buf, offset, length, port, address, callback) { Dgram.Socket.prototype.send = orig; return callback(null, 40); }; await expect(Sntp.time()).to.reject('Could not send entire message'); }); it('times out on invalid host', async () => { await expect(Sntp.time({ host: 'no-such-hostname' })).to.reject(/getaddrinfo/); }); it('fails on bad response buffer size', async (flags) => { const server = Dgram.createSocket('udp4'); flags.onCleanup = (next) => server.close(next); server.on('message', (message, remote) => { const msg = Buffer.alloc(10); server.send(msg, 0, msg.length, remote.port, remote.address, Hoek.ignore); }); server.bind(49123); await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject('Invalid server response'); }); const messup = function (bytes, flags) { const server = Dgram.createSocket('udp4'); flags.onCleanup = (next) => server.close(next); server.on('message', (message, remote) => { const msg = Buffer.from([ 0x24, 0x01, 0x00, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x43, 0x54, 0x53, 0xd4, 0xa8, 0x2d, 0xc7, 0x1c, 0x5d, 0x49, 0x1b, 0xd4, 0xa8, 0x2d, 0xe6, 0x67, 0xef, 0x9d, 0xb2, 0xd4, 0xa8, 0x2d, 0xe6, 0x71, 0xed, 0xb5, 0xfb, 0xd4, 0xa8, 0x2d, 0xe6, 0x71, 0xee, 0x6c, 0xc5 ]); for (let i = 0; i < bytes.length; ++i) { msg[bytes[i][0]] = bytes[i][1]; } server.send(msg, 0, msg.length, remote.port, remote.address, Hoek.ignore); }); server.bind(49123); }; it('fails on bad version', async (flags) => { messup([[0, (0 << 6) + (3 << 3) + (4 << 0)]], flags); const err = await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject('Invalid server response'); expect(err.time.version).to.equal(3); }); it('fails on bad originateTimestamp', async (flags) => { messup([[24, 0x83], [25, 0xaa], [26, 0x7e], [27, 0x80], [28, 0], [29, 0], [30, 0], [31, 0]], flags); await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject('Invalid server response'); }); it('fails on bad receiveTimestamp', async (flags) => { messup([[32, 0x83], [33, 0xaa], [34, 0x7e], [35, 0x80], [36, 0], [37, 0], [38, 0], [39, 0]], flags); await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject('Invalid server response'); }); it('fails on bad originate timestamp and alarm li', async (flags) => { messup([[0, (3 << 6) + (4 << 3) + (4 << 0)]], flags); const err = await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject('Wrong originate timestamp'); expect(err.time.leapIndicator).to.equal('alarm'); }); it('returns time with death stratum and last61 li', async (flags) => { messup([[0, (1 << 6) + (4 << 3) + (4 << 0)], [1, 0]], flags); const err = await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject(); expect(err.time.stratum).to.equal('death'); expect(err.time.leapIndicator).to.equal('last-minute-61'); }); it('returns time with reserved stratum and last59 li', async (flags) => { messup([[0, (2 << 6) + (4 << 3) + (4 << 0)], [1, 0x1f]], flags); const err = await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject(); expect(err.time.stratum).to.equal('reserved'); expect(err.time.leapIndicator).to.equal('last-minute-59'); }); it('fails on bad mode (symmetric-active)', async (flags) => { messup([[0, (0 << 6) + (4 << 3) + (1 << 0)]], flags); const err = await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject(); expect(err.time.mode).to.equal('symmetric-active'); }); it('fails on bad mode (symmetric-passive)', async (flags) => { messup([[0, (0 << 6) + (4 << 3) + (2 << 0)]], flags); const err = await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject(); expect(err.time.mode).to.equal('symmetric-passive'); }); it('fails on bad mode (client)', async (flags) => { messup([[0, (0 << 6) + (4 << 3) + (3 << 0)]], flags); const err = await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject(); expect(err.time.mode).to.equal('client'); }); it('fails on bad mode (broadcast)', async (flags) => { messup([[0, (0 << 6) + (4 << 3) + (5 << 0)]], flags); const err = await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject(); expect(err.time.mode).to.equal('broadcast'); }); it('fails on bad mode (reserved)', async (flags) => { messup([[0, (0 << 6) + (4 << 3) + (6 << 0)]], flags); const err = await expect(Sntp.time({ host: 'localhost', port: 49123 })).to.reject(); expect(err.time.mode).to.equal('reserved'); }); }); describe('offset()', () => { it('gets the current offset', async () => { const offset = await Sntp.offset(); expect(offset).to.not.equal(0); }); it('gets the current offset from cache', async () => { const offset1 = await Sntp.offset(); expect(offset1).to.not.equal(0); const offset2 = await Sntp.offset({}); expect(offset2).to.equal(offset1); }); it('gets the new offset on different server (host)', async (flags) => { const offset1 = await Sntp.offset(); expect(offset1).to.not.equal(0); const offset2 = await Sntp.offset({ host: 'us.pool.ntp.org' }); expect(offset2).to.not.equal(0); }); it('gets the new offset on different server (port)', async (flags) => { const offset1 = await Sntp.offset(); expect(offset1).to.not.equal(0); const offset2 = await Sntp.offset({ port: 123 }); expect(offset2).to.not.equal(0); }); it('fails getting the current offset on invalid server', async () => { await expect(Sntp.offset({ host: 'no-such-host-error', timeout: 100 })).to.reject(); }); }); describe('start()', () => { it('returns error (direct)', async (flags) => { Sntp.stop(); await expect(Sntp.start({ host: 'no-such-host-error', onError: Hoek.ignore, timeout: 10 })).to.reject(); Sntp.stop(); }); it('returns error (handler)', async (flags) => { Sntp.stop(); const team = new Teamwork.Team(); const onError = (err) => { expect(err).to.be.an.error(); Sntp.stop(); team.attend(); }; const orig = Sntp.offset; Sntp.offset = () => { Sntp.offset = orig; }; await Sntp.start({ host: 'no-such-host-error', onError, clockSyncRefresh: 100, timeout: 10 }); await team.work; }); it('ignores errors', async (flags) => { Sntp.stop(); const orig = Sntp.offset; Sntp.offset = () => { Sntp.offset = orig; }; await expect(Sntp.start({ host: 'no-such-host-error', clockSyncRefresh: 100, timeout: 10 })).to.not.reject(); await Hoek.wait(110); }); }); describe('now()', () => { it('starts auto-sync, gets now, then stops', async (flags) => { Sntp.stop(); const before = Sntp.now(); expect(before).to.be.about(Date.now(), 5); await Sntp.start(); const now = Sntp.now(); expect(now).to.not.equal(Date.now()); Sntp.stop(); }); it('starts twice', async (flags) => { Sntp.stop(); await Sntp.start(); await Sntp.start(); const now = Sntp.now(); expect(now).to.not.equal(Date.now()); Sntp.stop(); }); it('starts auto-sync, gets now, waits, gets again after timeout', async () => { Sntp.stop(); const before = Sntp.now(); expect(before).to.be.about(Date.now(), 5); await Sntp.start({ clockSyncRefresh: 100 }); const now = Sntp.now(); expect(now).to.not.equal(Date.now()); expect(now).to.be.about(Sntp.now(), 5); await Hoek.wait(110); expect(Sntp.now()).to.not.equal(now); Sntp.stop(); }); }); });