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