toolkit/modules/Sntp.jsm

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/modules/Sntp.jsm	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,334 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +"use strict";
     1.9 +
    1.10 +this.EXPORTED_SYMBOLS = [
    1.11 +  "Sntp",
    1.12 +];
    1.13 +
    1.14 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    1.15 +
    1.16 +// Set to true to see debug messages.
    1.17 +let DEBUG = false;
    1.18 +
    1.19 +/**
    1.20 + * Constructor of Sntp.
    1.21 + *
    1.22 + * @param dataAvailableCb
    1.23 + *        Callback function gets called when SNTP offset available. Signature
    1.24 + *        is function dataAvailableCb(offsetInMS).
    1.25 + * @param maxRetryCount
    1.26 + *        Maximum retry count when SNTP failed to connect to server; set to
    1.27 + *        zero to disable the retry.
    1.28 + * @param refreshPeriodInSecs
    1.29 + *        Refresh period; set to zero to disable refresh.
    1.30 + * @param timeoutInSecs
    1.31 + *        Timeout value used for connection.
    1.32 + * @param pools
    1.33 + *        SNTP server lists separated by ';'.
    1.34 + * @param port
    1.35 + *        SNTP port.
    1.36 + */
    1.37 +this.Sntp = function Sntp(dataAvailableCb, maxRetryCount, refreshPeriodInSecs,
    1.38 +                          timeoutInSecs, pools, port) {
    1.39 +  if (dataAvailableCb != null) {
    1.40 +    this._dataAvailableCb = dataAvailableCb;
    1.41 +  }
    1.42 +  if (maxRetryCount != null) {
    1.43 +    this._maxRetryCount = maxRetryCount;
    1.44 +  }
    1.45 +  if (refreshPeriodInSecs != null) {
    1.46 +    this._refreshPeriodInMS = refreshPeriodInSecs * 1000;
    1.47 +  }
    1.48 +  if (timeoutInSecs != null) {
    1.49 +    this._timeoutInMS = timeoutInSecs * 1000;
    1.50 +  }
    1.51 +  if (pools != null && Array.isArray(pools) && pools.length > 0) {
    1.52 +    this._pools = pools;
    1.53 +  }
    1.54 +  if (port != null) {
    1.55 +    this._port = port;
    1.56 +  }
    1.57 +}
    1.58 +
    1.59 +Sntp.prototype = {
    1.60 +  isAvailable: function isAvailable() {
    1.61 +    return this._cachedOffset != null;
    1.62 +  },
    1.63 +
    1.64 +  isExpired: function isExpired() {
    1.65 +    let valid = this._cachedOffset != null && this._cachedTimeInMS != null;
    1.66 +    if (this._refreshPeriodInMS > 0) {
    1.67 +      valid = valid && Date.now() < this._cachedTimeInMS +
    1.68 +                                    this._refreshPeriodInMS;
    1.69 +    }
    1.70 +    return !valid;
    1.71 +  },
    1.72 +
    1.73 +  request: function request() {
    1.74 +    this._request();
    1.75 +  },
    1.76 +
    1.77 +  getOffset: function getOffset() {
    1.78 +    return this._cachedOffset;
    1.79 +  },
    1.80 +
    1.81 +  /**
    1.82 +   * Indicates the system clock has been changed by [offset]ms so we need to
    1.83 +   * adjust the stored value.
    1.84 +   */
    1.85 +  updateOffset: function updateOffset(offset) {
    1.86 +    if (this._cachedOffset != null) {
    1.87 +      this._cachedOffset -= offset;
    1.88 +    }
    1.89 +  },
    1.90 +
    1.91 +  /**
    1.92 +   * Used to schedule a retry or periodic updates.
    1.93 +   */
    1.94 +  _schedule: function _schedule(timeInMS) {
    1.95 +    if (this._updateTimer == null) {
    1.96 +      this._updateTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    1.97 +    }
    1.98 +
    1.99 +    this._updateTimer.initWithCallback(this._request.bind(this),
   1.100 +                                       timeInMS,
   1.101 +                                       Ci.nsITimer.TYPE_ONE_SHOT);
   1.102 +    debug("Scheduled SNTP request in " + timeInMS + "ms");
   1.103 +  },
   1.104 +
   1.105 +  /**
   1.106 +   * Handle the SNTP response.
   1.107 +   */
   1.108 +  _handleSntp: function _handleSntp(originateTimeInMS, receiveTimeInMS,
   1.109 +                                    transmitTimeInMS, respondTimeInMS) {
   1.110 +    let clockOffset = Math.floor(((receiveTimeInMS - originateTimeInMS) +
   1.111 +                                 (transmitTimeInMS - respondTimeInMS)) / 2);
   1.112 +    debug("Clock offset: " + clockOffset);
   1.113 +
   1.114 +    // We've succeeded so clear the retry status.
   1.115 +    this._retryCount = 0;
   1.116 +    this._retryPeriodInMS = 0;
   1.117 +
   1.118 +    // Cache the latest SNTP offset whenever receiving it.
   1.119 +    this._cachedOffset = clockOffset;
   1.120 +    this._cachedTimeInMS = respondTimeInMS;
   1.121 +
   1.122 +    if (this._dataAvailableCb != null) {
   1.123 +      this._dataAvailableCb(clockOffset);
   1.124 +    }
   1.125 +
   1.126 +    this._schedule(this._refreshPeriodInMS);
   1.127 +  },
   1.128 +
   1.129 +  /**
   1.130 +   * Used for retry SNTP requests.
   1.131 +   */
   1.132 +  _retry: function _retry() {
   1.133 +    this._retryCount++;
   1.134 +    if (this._retryCount > this._maxRetryCount) {
   1.135 +      debug ("stop retrying SNTP");
   1.136 +      // Clear so we can start with clean status next time we have network.
   1.137 +      this._retryCount = 0;
   1.138 +      this._retryPeriodInMS = 0;
   1.139 +      return;
   1.140 +    }
   1.141 +    this._retryPeriodInMS = Math.max(1000, this._retryPeriodInMS * 2);
   1.142 +
   1.143 +    this._schedule(this._retryPeriodInMS);
   1.144 +  },
   1.145 +
   1.146 +  /**
   1.147 +   * Request SNTP.
   1.148 +   */
   1.149 +  _request: function _request() {
   1.150 +    function GetRequest() {
   1.151 +      let NTP_PACKET_SIZE = 48;
   1.152 +      let NTP_MODE_CLIENT = 3;
   1.153 +      let NTP_VERSION = 3;
   1.154 +      let TRANSMIT_TIME_OFFSET = 40;
   1.155 +
   1.156 +      // Send the NTP request.
   1.157 +      let requestTimeInMS = Date.now();
   1.158 +      let s = requestTimeInMS / 1000;
   1.159 +      let ms = requestTimeInMS % 1000;
   1.160 +      // NTP time is relative to 1900.
   1.161 +      s += OFFSET_1900_TO_1970;
   1.162 +      let f = ms * 0x100000000 / 1000;
   1.163 +      s = Math.floor(s);
   1.164 +      f = Math.floor(f);
   1.165 +
   1.166 +      let buffer = new ArrayBuffer(NTP_PACKET_SIZE);
   1.167 +      let data = new DataView(buffer);
   1.168 +      data.setUint8(0, NTP_MODE_CLIENT | (NTP_VERSION << 3));
   1.169 +      data.setUint32(TRANSMIT_TIME_OFFSET, s, false);
   1.170 +      data.setUint32(TRANSMIT_TIME_OFFSET + 4, f, false);
   1.171 +
   1.172 +      return String.fromCharCode.apply(null, new Uint8Array(buffer));
   1.173 +    }
   1.174 +
   1.175 +    function SNTPListener() {};
   1.176 +    SNTPListener.prototype = {
   1.177 +      onStartRequest: function onStartRequest(request, context) {
   1.178 +      },
   1.179 +
   1.180 +      onStopRequest: function onStopRequest(request, context, status) {
   1.181 +        if (!Components.isSuccessCode(status)) {
   1.182 +          debug ("Connection failed");
   1.183 +          this._requesting = false;
   1.184 +          this._retry();
   1.185 +        }
   1.186 +      }.bind(this),
   1.187 +
   1.188 +      onDataAvailable: function onDataAvailable(request, context, inputStream,
   1.189 +                                                offset, count) {
   1.190 +        function GetTimeStamp(binaryInputStream) {
   1.191 +          let s = binaryInputStream.read32();
   1.192 +          let f = binaryInputStream.read32();
   1.193 +          return Math.floor(
   1.194 +            ((s - OFFSET_1900_TO_1970) * 1000) + ((f * 1000) / 0x100000000)
   1.195 +          );
   1.196 +        }
   1.197 +        debug ("Data available: " + count + " bytes");
   1.198 +
   1.199 +        try {
   1.200 +          let binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"]
   1.201 +                                    .createInstance(Ci.nsIBinaryInputStream);
   1.202 +          binaryInputStream.setInputStream(inputStream);
   1.203 +          // We don't need first 24 bytes.
   1.204 +          for (let i = 0; i < 6; i++) {
   1.205 +            binaryInputStream.read32();
   1.206 +          }
   1.207 +          // Offset 24: originate time.
   1.208 +          let originateTimeInMS = GetTimeStamp(binaryInputStream);
   1.209 +          // Offset 32: receive time.
   1.210 +          let receiveTimeInMS = GetTimeStamp(binaryInputStream);
   1.211 +          // Offset 40: transmit time.
   1.212 +          let transmitTimeInMS = GetTimeStamp(binaryInputStream);
   1.213 +          let respondTimeInMS = Date.now();
   1.214 +
   1.215 +          this._handleSntp(originateTimeInMS, receiveTimeInMS,
   1.216 +                           transmitTimeInMS, respondTimeInMS);
   1.217 +          this._requesting = false;
   1.218 +        } catch (e) {
   1.219 +          debug ("SNTPListener Error: " + e.message);
   1.220 +          this._requesting = false;
   1.221 +          this._retry();
   1.222 +        }
   1.223 +        inputStream.close();
   1.224 +      }.bind(this)
   1.225 +    };
   1.226 +
   1.227 +    function SNTPRequester() {}
   1.228 +    SNTPRequester.prototype = {
   1.229 +      onOutputStreamReady: function(stream) {
   1.230 +        try {
   1.231 +          let data = GetRequest();
   1.232 +          let bytes_write = stream.write(data, data.length);
   1.233 +          debug ("SNTP: sent " + bytes_write + " bytes");
   1.234 +          stream.close();
   1.235 +        } catch (e) {
   1.236 +          debug ("SNTPRequester Error: " + e.message);
   1.237 +          this._requesting = false;
   1.238 +          this._retry();
   1.239 +        }
   1.240 +      }.bind(this)
   1.241 +    };
   1.242 +
   1.243 +    // Number of seconds between Jan 1, 1900 and Jan 1, 1970.
   1.244 +    // 70 years plus 17 leap days.
   1.245 +    let OFFSET_1900_TO_1970 = ((365 * 70) + 17) * 24 * 60 * 60;
   1.246 +
   1.247 +    if (this._requesting) {
   1.248 +      return;
   1.249 +    }
   1.250 +    if (this._pools.length < 1) {
   1.251 +      debug("No server defined");
   1.252 +      return;
   1.253 +    }
   1.254 +    if (this._updateTimer) {
   1.255 +      this._updateTimer.cancel();
   1.256 +    }
   1.257 +
   1.258 +    debug ("Making request");
   1.259 +    this._requesting = true;
   1.260 +
   1.261 +    let currentThread = Cc["@mozilla.org/thread-manager;1"]
   1.262 +                          .getService().currentThread;
   1.263 +    let socketTransportService =
   1.264 +      Cc["@mozilla.org/network/socket-transport-service;1"]
   1.265 +        .getService(Ci.nsISocketTransportService);
   1.266 +    let pump = Cc["@mozilla.org/network/input-stream-pump;1"]
   1.267 +                 .createInstance(Ci.nsIInputStreamPump);
   1.268 +    let transport = socketTransportService
   1.269 +      .createTransport(["udp"],
   1.270 +                       1,
   1.271 +                       this._pools[Math.floor(this._pools.length * Math.random())],
   1.272 +                       this._port,
   1.273 +                       null);
   1.274 +
   1.275 +    transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, this._timeoutInMS);
   1.276 +    transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, this._timeoutInMS);
   1.277 +
   1.278 +    let outStream = transport.openOutputStream(0, 0, 0)
   1.279 +                             .QueryInterface(Ci.nsIAsyncOutputStream);
   1.280 +    let inStream = transport.openInputStream(0, 0, 0);
   1.281 +
   1.282 +    pump.init(inStream, -1, -1, 0, 0, false);
   1.283 +    pump.asyncRead(new SNTPListener(), null);
   1.284 +
   1.285 +    outStream.asyncWait(new SNTPRequester(), 0, 0, currentThread);
   1.286 +  },
   1.287 +
   1.288 +  // Callback function.
   1.289 +  _dataAvailableCb: null,
   1.290 +
   1.291 +  // Sntp servers.
   1.292 +  _pools: [
   1.293 +    "0.pool.ntp.org",
   1.294 +    "1.pool.ntp.org",
   1.295 +    "2.pool.ntp.org",
   1.296 +    "3.pool.ntp.org"
   1.297 +  ],
   1.298 +
   1.299 +  // The SNTP port.
   1.300 +  _port: 123,
   1.301 +
   1.302 +  // Maximum retry count allowed when request failed.
   1.303 +  _maxRetryCount: 0,
   1.304 +
   1.305 +  // Refresh period.
   1.306 +  _refreshPeriodInMS: 0,
   1.307 +
   1.308 +  // Timeout value used for connecting.
   1.309 +  _timeoutInMS: 30 * 1000,
   1.310 +
   1.311 +  // Cached SNTP offset.
   1.312 +  _cachedOffset: null,
   1.313 +
   1.314 +  // The time point when we cache the offset.
   1.315 +  _cachedTimeInMS: null,
   1.316 +
   1.317 +  // Flag to avoid redundant requests.
   1.318 +  _requesting: false,
   1.319 +
   1.320 +  // Retry counter.
   1.321 +  _retryCount: 0,
   1.322 +
   1.323 +  // Retry time offset (in seconds).
   1.324 +  _retryPeriodInMS: 0,
   1.325 +
   1.326 +  // Timer used for retries & daily updates.
   1.327 +  _updateTimer: null
   1.328 +};
   1.329 +
   1.330 +let debug;
   1.331 +if (DEBUG) {
   1.332 +  debug = function (s) {
   1.333 +    dump("-*- Sntp: " + s + "\n");
   1.334 +  };
   1.335 +} else {
   1.336 +  debug = function (s) {};
   1.337 +}

mercurial