summaryrefslogtreecommitdiffstats
path: root/toolkit/modules/Sntp.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/modules/Sntp.jsm')
-rw-r--r--toolkit/modules/Sntp.jsm334
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) {};
+}