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 +}