diff options
Diffstat (limited to 'toolkit/modules/Sntp.jsm')
-rw-r--r-- | toolkit/modules/Sntp.jsm | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/toolkit/modules/Sntp.jsm b/toolkit/modules/Sntp.jsm new file mode 100644 index 000000000..6041c5f15 --- /dev/null +++ b/toolkit/modules/Sntp.jsm @@ -0,0 +1,334 @@ +/* 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) {}; +} |