/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; this.EXPORTED_SYMBOLS = [ "Sntp", ]; const {classes: Cc, interfaces: Ci, utils: Cu} = Components; // Set to true to see debug messages. var DEBUG = false; /** * Constructor of Sntp. * * @param dataAvailableCb * Callback function gets called when SNTP offset available. Signature * is function dataAvailableCb(offsetInMS). * @param maxRetryCount * Maximum retry count when SNTP failed to connect to server; set to * zero to disable the retry. * @param refreshPeriodInSecs * Refresh period; set to zero to disable refresh. * @param timeoutInSecs * Timeout value used for connection. * @param pools * SNTP server lists separated by ';'. * @param port * SNTP port. */ this.Sntp = function Sntp(dataAvailableCb, maxRetryCount, refreshPeriodInSecs, timeoutInSecs, pools, port) { if (dataAvailableCb != null) { this._dataAvailableCb = dataAvailableCb; } if (maxRetryCount != null) { this._maxRetryCount = maxRetryCount; } if (refreshPeriodInSecs != null) { this._refreshPeriodInMS = refreshPeriodInSecs * 1000; } if (timeoutInSecs != null) { this._timeoutInMS = timeoutInSecs * 1000; } if (pools != null && Array.isArray(pools) && pools.length > 0) { this._pools = pools; } if (port != null) { this._port = port; } } Sntp.prototype = { isAvailable: function isAvailable() { return this._cachedOffset != null; }, isExpired: function isExpired() { let valid = this._cachedOffset != null && this._cachedTimeInMS != null; if (this._refreshPeriodInMS > 0) { valid = valid && Date.now() < this._cachedTimeInMS + this._refreshPeriodInMS; } return !valid; }, request: function request() { this._request(); }, getOffset: function getOffset() { return this._cachedOffset; }, /** * Indicates the system clock has been changed by [offset]ms so we need to * adjust the stored value. */ updateOffset: function updateOffset(offset) { if (this._cachedOffset != null) { this._cachedOffset -= offset; } }, /** * Used to schedule a retry or periodic updates. */ _schedule: function _schedule(timeInMS) { if (this._updateTimer == null) { this._updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); } this._updateTimer.initWithCallback(this._request.bind(this), timeInMS, Ci.nsITimer.TYPE_ONE_SHOT); debug("Scheduled SNTP request in " + timeInMS + "ms"); }, /** * Handle the SNTP response. */ _handleSntp: function _handleSntp(originateTimeInMS, receiveTimeInMS, transmitTimeInMS, respondTimeInMS) { let clockOffset = Math.floor(((receiveTimeInMS - originateTimeInMS) + (transmitTimeInMS - respondTimeInMS)) / 2); debug("Clock offset: " + clockOffset); // We've succeeded so clear the retry status. this._retryCount = 0; this._retryPeriodInMS = 0; // Cache the latest SNTP offset whenever receiving it. this._cachedOffset = clockOffset; this._cachedTimeInMS = respondTimeInMS; if (this._dataAvailableCb != null) { this._dataAvailableCb(clockOffset); } this._schedule(this._refreshPeriodInMS); }, /** * Used for retry SNTP requests. */ _retry: function _retry() { this._retryCount++; if (this._retryCount > this._maxRetryCount) { debug("stop retrying SNTP"); // Clear so we can start with clean status next time we have network. this._retryCount = 0; this._retryPeriodInMS = 0; return; } this._retryPeriodInMS = Math.max(1000, this._retryPeriodInMS * 2); this._schedule(this._retryPeriodInMS); }, /** * Request SNTP. */ _request: function _request() { function GetRequest() { let NTP_PACKET_SIZE = 48; let NTP_MODE_CLIENT = 3; let NTP_VERSION = 3; let TRANSMIT_TIME_OFFSET = 40; // Send the NTP request. let requestTimeInMS = Date.now(); let s = requestTimeInMS / 1000; let ms = requestTimeInMS % 1000; // NTP time is relative to 1900. s += OFFSET_1900_TO_1970; let f = ms * 0x100000000 / 1000; s = Math.floor(s); f = Math.floor(f); let buffer = new ArrayBuffer(NTP_PACKET_SIZE); let data = new DataView(buffer); data.setUint8(0, NTP_MODE_CLIENT | (NTP_VERSION << 3)); data.setUint32(TRANSMIT_TIME_OFFSET, s, false); data.setUint32(TRANSMIT_TIME_OFFSET + 4, f, false); return String.fromCharCode.apply(null, new Uint8Array(buffer)); } function SNTPListener() {} SNTPListener.prototype = { onStartRequest: function onStartRequest(request, context) { }, onStopRequest: function onStopRequest(request, context, status) { if (!Components.isSuccessCode(status)) { debug("Connection failed"); this._requesting = false; this._retry(); } }.bind(this), onDataAvailable: function onDataAvailable(request, context, inputStream, offset, count) { function GetTimeStamp(binaryInputStream) { let s = binaryInputStream.read32(); let f = binaryInputStream.read32(); return Math.floor( ((s - OFFSET_1900_TO_1970) * 1000) + ((f * 1000) / 0x100000000) ); } debug("Data available: " + count + " bytes"); try { let binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"] .createInstance(Ci.nsIBinaryInputStream); binaryInputStream.setInputStream(inputStream); // We don't need first 24 bytes. for (let i = 0; i < 6; i++) { binaryInputStream.read32(); } // Offset 24: originate time. let originateTimeInMS = GetTimeStamp(binaryInputStream); // Offset 32: receive time. let receiveTimeInMS = GetTimeStamp(binaryInputStream); // Offset 40: transmit time. let transmitTimeInMS = GetTimeStamp(binaryInputStream); let respondTimeInMS = Date.now(); this._handleSntp(originateTimeInMS, receiveTimeInMS, transmitTimeInMS, respondTimeInMS); this._requesting = false; } catch (e) { debug("SNTPListener Error: " + e.message); this._requesting = false; this._retry(); } inputStream.close(); }.bind(this) }; function SNTPRequester() {} SNTPRequester.prototype = { onOutputStreamReady: function(stream) { try { let data = GetRequest(); let bytes_write = stream.write(data, data.length); debug("SNTP: sent " + bytes_write + " bytes"); stream.close(); } catch (e) { debug("SNTPRequester Error: " + e.message); this._requesting = false; this._retry(); } }.bind(this) }; // Number of seconds between Jan 1, 1900 and Jan 1, 1970. // 70 years plus 17 leap days. let OFFSET_1900_TO_1970 = ((365 * 70) + 17) * 24 * 60 * 60; if (this._requesting) { return; } if (this._pools.length < 1) { debug("No server defined"); return; } if (this._updateTimer) { this._updateTimer.cancel(); } debug("Making request"); this._requesting = true; let currentThread = Cc["@mozilla.org/thread-manager;1"] .getService().currentThread; let socketTransportService = Cc["@mozilla.org/network/socket-transport-service;1"] .getService(Ci.nsISocketTransportService); let pump = Cc["@mozilla.org/network/input-stream-pump;1"] .createInstance(Ci.nsIInputStreamPump); let transport = socketTransportService .createTransport(["udp"], 1, this._pools[Math.floor(this._pools.length * Math.random())], this._port, null); transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, this._timeoutInMS); transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, this._timeoutInMS); let outStream = transport.openOutputStream(0, 0, 0) .QueryInterface(Ci.nsIAsyncOutputStream); let inStream = transport.openInputStream(0, 0, 0); pump.init(inStream, -1, -1, 0, 0, false); pump.asyncRead(new SNTPListener(), null); outStream.asyncWait(new SNTPRequester(), 0, 0, currentThread); }, // Callback function. _dataAvailableCb: null, // Sntp servers. _pools: [ "0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org", "3.pool.ntp.org" ], // The SNTP port. _port: 123, // Maximum retry count allowed when request failed. _maxRetryCount: 0, // Refresh period. _refreshPeriodInMS: 0, // Timeout value used for connecting. _timeoutInMS: 30 * 1000, // Cached SNTP offset. _cachedOffset: null, // The time point when we cache the offset. _cachedTimeInMS: null, // Flag to avoid redundant requests. _requesting: false, // Retry counter. _retryCount: 0, // Retry time offset (in seconds). _retryPeriodInMS: 0, // Timer used for retries & daily updates. _updateTimer: null }; var debug; if (DEBUG) { debug = function(s) { dump("-*- Sntp: " + s + "\n"); }; } else { debug = function(s) {}; }