Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | this.EXPORTED_SYMBOLS = [ |
michael@0 | 8 | "TokenServerClient", |
michael@0 | 9 | "TokenServerClientError", |
michael@0 | 10 | "TokenServerClientNetworkError", |
michael@0 | 11 | "TokenServerClientServerError", |
michael@0 | 12 | ]; |
michael@0 | 13 | |
michael@0 | 14 | const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
michael@0 | 15 | |
michael@0 | 16 | Cu.import("resource://gre/modules/Preferences.jsm"); |
michael@0 | 17 | Cu.import("resource://gre/modules/Log.jsm"); |
michael@0 | 18 | Cu.import("resource://services-common/rest.js"); |
michael@0 | 19 | Cu.import("resource://services-common/utils.js"); |
michael@0 | 20 | Cu.import("resource://services-common/observers.js"); |
michael@0 | 21 | |
michael@0 | 22 | const Prefs = new Preferences("services.common.tokenserverclient."); |
michael@0 | 23 | |
michael@0 | 24 | /** |
michael@0 | 25 | * Represents a TokenServerClient error that occurred on the client. |
michael@0 | 26 | * |
michael@0 | 27 | * This is the base type for all errors raised by client operations. |
michael@0 | 28 | * |
michael@0 | 29 | * @param message |
michael@0 | 30 | * (string) Error message. |
michael@0 | 31 | */ |
michael@0 | 32 | this.TokenServerClientError = function TokenServerClientError(message) { |
michael@0 | 33 | this.name = "TokenServerClientError"; |
michael@0 | 34 | this.message = message || "Client error."; |
michael@0 | 35 | } |
michael@0 | 36 | TokenServerClientError.prototype = new Error(); |
michael@0 | 37 | TokenServerClientError.prototype.constructor = TokenServerClientError; |
michael@0 | 38 | TokenServerClientError.prototype._toStringFields = function() { |
michael@0 | 39 | return {message: this.message}; |
michael@0 | 40 | } |
michael@0 | 41 | TokenServerClientError.prototype.toString = function() { |
michael@0 | 42 | return this.name + "(" + JSON.stringify(this._toStringFields()) + ")"; |
michael@0 | 43 | } |
michael@0 | 44 | |
michael@0 | 45 | /** |
michael@0 | 46 | * Represents a TokenServerClient error that occurred in the network layer. |
michael@0 | 47 | * |
michael@0 | 48 | * @param error |
michael@0 | 49 | * The underlying error thrown by the network layer. |
michael@0 | 50 | */ |
michael@0 | 51 | this.TokenServerClientNetworkError = |
michael@0 | 52 | function TokenServerClientNetworkError(error) { |
michael@0 | 53 | this.name = "TokenServerClientNetworkError"; |
michael@0 | 54 | this.error = error; |
michael@0 | 55 | } |
michael@0 | 56 | TokenServerClientNetworkError.prototype = new TokenServerClientError(); |
michael@0 | 57 | TokenServerClientNetworkError.prototype.constructor = |
michael@0 | 58 | TokenServerClientNetworkError; |
michael@0 | 59 | TokenServerClientNetworkError.prototype._toStringFields = function() { |
michael@0 | 60 | return {error: this.error}; |
michael@0 | 61 | } |
michael@0 | 62 | |
michael@0 | 63 | /** |
michael@0 | 64 | * Represents a TokenServerClient error that occurred on the server. |
michael@0 | 65 | * |
michael@0 | 66 | * This type will be encountered for all non-200 response codes from the |
michael@0 | 67 | * server. The type of error is strongly enumerated and is stored in the |
michael@0 | 68 | * `cause` property. This property can have the following string values: |
michael@0 | 69 | * |
michael@0 | 70 | * conditions-required -- The server is requesting that the client |
michael@0 | 71 | * agree to service conditions before it can obtain a token. The |
michael@0 | 72 | * conditions that must be presented to the user and agreed to are in |
michael@0 | 73 | * the `urls` mapping on the instance. Keys of this mapping are |
michael@0 | 74 | * identifiers. Values are string URLs. |
michael@0 | 75 | * |
michael@0 | 76 | * invalid-credentials -- A token could not be obtained because |
michael@0 | 77 | * the credentials presented by the client were invalid. |
michael@0 | 78 | * |
michael@0 | 79 | * unknown-service -- The requested service was not found. |
michael@0 | 80 | * |
michael@0 | 81 | * malformed-request -- The server rejected the request because it |
michael@0 | 82 | * was invalid. If you see this, code in this file is likely wrong. |
michael@0 | 83 | * |
michael@0 | 84 | * malformed-response -- The response from the server was not what was |
michael@0 | 85 | * expected. |
michael@0 | 86 | * |
michael@0 | 87 | * general -- A general server error has occurred. Clients should |
michael@0 | 88 | * interpret this as an opaque failure. |
michael@0 | 89 | * |
michael@0 | 90 | * @param message |
michael@0 | 91 | * (string) Error message. |
michael@0 | 92 | */ |
michael@0 | 93 | this.TokenServerClientServerError = |
michael@0 | 94 | function TokenServerClientServerError(message, cause="general") { |
michael@0 | 95 | this.now = new Date().toISOString(); // may be useful to diagnose time-skew issues. |
michael@0 | 96 | this.name = "TokenServerClientServerError"; |
michael@0 | 97 | this.message = message || "Server error."; |
michael@0 | 98 | this.cause = cause; |
michael@0 | 99 | } |
michael@0 | 100 | TokenServerClientServerError.prototype = new TokenServerClientError(); |
michael@0 | 101 | TokenServerClientServerError.prototype.constructor = |
michael@0 | 102 | TokenServerClientServerError; |
michael@0 | 103 | |
michael@0 | 104 | TokenServerClientServerError.prototype._toStringFields = function() { |
michael@0 | 105 | let fields = { |
michael@0 | 106 | now: this.now, |
michael@0 | 107 | message: this.message, |
michael@0 | 108 | cause: this.cause, |
michael@0 | 109 | }; |
michael@0 | 110 | if (this.response) { |
michael@0 | 111 | fields.response_body = this.response.body; |
michael@0 | 112 | fields.response_headers = this.response.headers; |
michael@0 | 113 | fields.response_status = this.response.status; |
michael@0 | 114 | } |
michael@0 | 115 | return fields; |
michael@0 | 116 | }; |
michael@0 | 117 | |
michael@0 | 118 | /** |
michael@0 | 119 | * Represents a client to the Token Server. |
michael@0 | 120 | * |
michael@0 | 121 | * http://docs.services.mozilla.com/token/index.html |
michael@0 | 122 | * |
michael@0 | 123 | * The Token Server supports obtaining tokens for arbitrary apps by |
michael@0 | 124 | * constructing URI paths of the form <app>/<app_version>. However, the service |
michael@0 | 125 | * discovery mechanism emphasizes the use of full URIs and tries to not force |
michael@0 | 126 | * the client to manipulate URIs. This client currently enforces this practice |
michael@0 | 127 | * by not implementing an API which would perform URI manipulation. |
michael@0 | 128 | * |
michael@0 | 129 | * If you are tempted to implement this API in the future, consider this your |
michael@0 | 130 | * warning that you may be doing it wrong and that you should store full URIs |
michael@0 | 131 | * instead. |
michael@0 | 132 | * |
michael@0 | 133 | * Areas to Improve: |
michael@0 | 134 | * |
michael@0 | 135 | * - The server sends a JSON response on error. The client does not currently |
michael@0 | 136 | * parse this. It might be convenient if it did. |
michael@0 | 137 | * - Currently most non-200 status codes are rolled into one error type. It |
michael@0 | 138 | * might be helpful if callers had a richer API that communicated who was |
michael@0 | 139 | * at fault (e.g. differentiating a 503 from a 401). |
michael@0 | 140 | */ |
michael@0 | 141 | this.TokenServerClient = function TokenServerClient() { |
michael@0 | 142 | this._log = Log.repository.getLogger("Common.TokenServerClient"); |
michael@0 | 143 | this._log.level = Log.Level[Prefs.get("logger.level")]; |
michael@0 | 144 | } |
michael@0 | 145 | TokenServerClient.prototype = { |
michael@0 | 146 | /** |
michael@0 | 147 | * Logger instance. |
michael@0 | 148 | */ |
michael@0 | 149 | _log: null, |
michael@0 | 150 | |
michael@0 | 151 | /** |
michael@0 | 152 | * Obtain a token from a BrowserID assertion against a specific URL. |
michael@0 | 153 | * |
michael@0 | 154 | * This asynchronously obtains the token. The callback receives 2 arguments: |
michael@0 | 155 | * |
michael@0 | 156 | * (TokenServerClientError | null) If no token could be obtained, this |
michael@0 | 157 | * will be a TokenServerClientError instance describing why. The |
michael@0 | 158 | * type seen defines the type of error encountered. If an HTTP response |
michael@0 | 159 | * was seen, a RESTResponse instance will be stored in the `response` |
michael@0 | 160 | * property of this object. If there was no error and a token is |
michael@0 | 161 | * available, this will be null. |
michael@0 | 162 | * |
michael@0 | 163 | * (map | null) On success, this will be a map containing the results from |
michael@0 | 164 | * the server. If there was an error, this will be null. The map has the |
michael@0 | 165 | * following properties: |
michael@0 | 166 | * |
michael@0 | 167 | * id (string) HTTP MAC public key identifier. |
michael@0 | 168 | * key (string) HTTP MAC shared symmetric key. |
michael@0 | 169 | * endpoint (string) URL where service can be connected to. |
michael@0 | 170 | * uid (string) user ID for requested service. |
michael@0 | 171 | * duration (string) the validity duration of the issued token. |
michael@0 | 172 | * |
michael@0 | 173 | * Terms of Service Acceptance |
michael@0 | 174 | * --------------------------- |
michael@0 | 175 | * |
michael@0 | 176 | * Some services require users to accept terms of service before they can |
michael@0 | 177 | * obtain a token. If a service requires ToS acceptance, the error passed |
michael@0 | 178 | * to the callback will be a `TokenServerClientServerError` with the |
michael@0 | 179 | * `cause` property set to "conditions-required". The `urls` property of that |
michael@0 | 180 | * instance will be a map of string keys to string URL values. The user-agent |
michael@0 | 181 | * should prompt the user to accept the content at these URLs. |
michael@0 | 182 | * |
michael@0 | 183 | * Clients signify acceptance of the terms of service by sending a token |
michael@0 | 184 | * request with additional metadata. This is controlled by the |
michael@0 | 185 | * `conditionsAccepted` argument to this function. Clients only need to set |
michael@0 | 186 | * this flag once per service and the server remembers acceptance. If |
michael@0 | 187 | * the conditions for the service change, the server may request |
michael@0 | 188 | * clients agree to terms again. Therefore, clients should always be |
michael@0 | 189 | * prepared to handle a conditions required response. |
michael@0 | 190 | * |
michael@0 | 191 | * Clients should not blindly send acceptance to conditions. Instead, clients |
michael@0 | 192 | * should set `conditionsAccepted` if and only if the server asks for |
michael@0 | 193 | * acceptance, the conditions are displayed to the user, and the user agrees |
michael@0 | 194 | * to them. |
michael@0 | 195 | * |
michael@0 | 196 | * Example Usage |
michael@0 | 197 | * ------------- |
michael@0 | 198 | * |
michael@0 | 199 | * let client = new TokenServerClient(); |
michael@0 | 200 | * let assertion = getBrowserIDAssertionFromSomewhere(); |
michael@0 | 201 | * let url = "https://token.services.mozilla.com/1.0/sync/2.0"; |
michael@0 | 202 | * |
michael@0 | 203 | * client.getTokenFromBrowserIDAssertion(url, assertion, |
michael@0 | 204 | * function onResponse(error, result) { |
michael@0 | 205 | * if (error) { |
michael@0 | 206 | * if (error.cause == "conditions-required") { |
michael@0 | 207 | * promptConditionsAcceptance(error.urls, function onAccept() { |
michael@0 | 208 | * client.getTokenFromBrowserIDAssertion(url, assertion, |
michael@0 | 209 | * onResponse, true); |
michael@0 | 210 | * } |
michael@0 | 211 | * return; |
michael@0 | 212 | * } |
michael@0 | 213 | * |
michael@0 | 214 | * // Do other error handling. |
michael@0 | 215 | * return; |
michael@0 | 216 | * } |
michael@0 | 217 | * |
michael@0 | 218 | * let { |
michael@0 | 219 | * id: id, key: key, uid: uid, endpoint: endpoint, duration: duration |
michael@0 | 220 | * } = result; |
michael@0 | 221 | * // Do stuff with data and carry on. |
michael@0 | 222 | * }); |
michael@0 | 223 | * |
michael@0 | 224 | * @param url |
michael@0 | 225 | * (string) URL to fetch token from. |
michael@0 | 226 | * @param assertion |
michael@0 | 227 | * (string) BrowserID assertion to exchange token for. |
michael@0 | 228 | * @param cb |
michael@0 | 229 | * (function) Callback to be invoked with result of operation. |
michael@0 | 230 | * @param conditionsAccepted |
michael@0 | 231 | * (bool) Whether to send acceptance to service conditions. |
michael@0 | 232 | */ |
michael@0 | 233 | getTokenFromBrowserIDAssertion: |
michael@0 | 234 | function getTokenFromBrowserIDAssertion(url, assertion, cb, addHeaders={}) { |
michael@0 | 235 | if (!url) { |
michael@0 | 236 | throw new TokenServerClientError("url argument is not valid."); |
michael@0 | 237 | } |
michael@0 | 238 | |
michael@0 | 239 | if (!assertion) { |
michael@0 | 240 | throw new TokenServerClientError("assertion argument is not valid."); |
michael@0 | 241 | } |
michael@0 | 242 | |
michael@0 | 243 | if (!cb) { |
michael@0 | 244 | throw new TokenServerClientError("cb argument is not valid."); |
michael@0 | 245 | } |
michael@0 | 246 | |
michael@0 | 247 | this._log.debug("Beginning BID assertion exchange: " + url); |
michael@0 | 248 | |
michael@0 | 249 | let req = this.newRESTRequest(url); |
michael@0 | 250 | req.setHeader("Accept", "application/json"); |
michael@0 | 251 | req.setHeader("Authorization", "BrowserID " + assertion); |
michael@0 | 252 | |
michael@0 | 253 | for (let header in addHeaders) { |
michael@0 | 254 | req.setHeader(header, addHeaders[header]); |
michael@0 | 255 | } |
michael@0 | 256 | |
michael@0 | 257 | let client = this; |
michael@0 | 258 | req.get(function onResponse(error) { |
michael@0 | 259 | if (error) { |
michael@0 | 260 | cb(new TokenServerClientNetworkError(error), null); |
michael@0 | 261 | return; |
michael@0 | 262 | } |
michael@0 | 263 | |
michael@0 | 264 | let self = this; |
michael@0 | 265 | function callCallback(error, result) { |
michael@0 | 266 | if (!cb) { |
michael@0 | 267 | self._log.warn("Callback already called! Did it throw?"); |
michael@0 | 268 | return; |
michael@0 | 269 | } |
michael@0 | 270 | |
michael@0 | 271 | try { |
michael@0 | 272 | cb(error, result); |
michael@0 | 273 | } catch (ex) { |
michael@0 | 274 | self._log.warn("Exception when calling user-supplied callback: " + |
michael@0 | 275 | CommonUtils.exceptionStr(ex)); |
michael@0 | 276 | } |
michael@0 | 277 | |
michael@0 | 278 | cb = null; |
michael@0 | 279 | } |
michael@0 | 280 | |
michael@0 | 281 | try { |
michael@0 | 282 | client._processTokenResponse(this.response, callCallback); |
michael@0 | 283 | } catch (ex) { |
michael@0 | 284 | this._log.warn("Error processing token server response: " + |
michael@0 | 285 | CommonUtils.exceptionStr(ex)); |
michael@0 | 286 | |
michael@0 | 287 | let error = new TokenServerClientError(ex); |
michael@0 | 288 | error.response = this.response; |
michael@0 | 289 | callCallback(error, null); |
michael@0 | 290 | } |
michael@0 | 291 | }); |
michael@0 | 292 | }, |
michael@0 | 293 | |
michael@0 | 294 | /** |
michael@0 | 295 | * Handler to process token request responses. |
michael@0 | 296 | * |
michael@0 | 297 | * @param response |
michael@0 | 298 | * RESTResponse from token HTTP request. |
michael@0 | 299 | * @param cb |
michael@0 | 300 | * The original callback passed to the public API. |
michael@0 | 301 | */ |
michael@0 | 302 | _processTokenResponse: function processTokenResponse(response, cb) { |
michael@0 | 303 | this._log.debug("Got token response: " + response.status); |
michael@0 | 304 | |
michael@0 | 305 | // Responses should *always* be JSON, even in the case of 4xx and 5xx |
michael@0 | 306 | // errors. If we don't see JSON, the server is likely very unhappy. |
michael@0 | 307 | let ct = response.headers["content-type"] || ""; |
michael@0 | 308 | if (ct != "application/json" && !ct.startsWith("application/json;")) { |
michael@0 | 309 | this._log.warn("Did not receive JSON response. Misconfigured server?"); |
michael@0 | 310 | this._log.debug("Content-Type: " + ct); |
michael@0 | 311 | this._log.debug("Body: " + response.body); |
michael@0 | 312 | |
michael@0 | 313 | let error = new TokenServerClientServerError("Non-JSON response.", |
michael@0 | 314 | "malformed-response"); |
michael@0 | 315 | error.response = response; |
michael@0 | 316 | cb(error, null); |
michael@0 | 317 | return; |
michael@0 | 318 | } |
michael@0 | 319 | |
michael@0 | 320 | let result; |
michael@0 | 321 | try { |
michael@0 | 322 | result = JSON.parse(response.body); |
michael@0 | 323 | } catch (ex) { |
michael@0 | 324 | this._log.warn("Invalid JSON returned by server: " + response.body); |
michael@0 | 325 | let error = new TokenServerClientServerError("Malformed JSON.", |
michael@0 | 326 | "malformed-response"); |
michael@0 | 327 | error.response = response; |
michael@0 | 328 | cb(error, null); |
michael@0 | 329 | return; |
michael@0 | 330 | } |
michael@0 | 331 | |
michael@0 | 332 | // Any response status can have X-Backoff or X-Weave-Backoff headers. |
michael@0 | 333 | this._maybeNotifyBackoff(response, "x-weave-backoff"); |
michael@0 | 334 | this._maybeNotifyBackoff(response, "x-backoff"); |
michael@0 | 335 | |
michael@0 | 336 | // The service shouldn't have any 3xx, so we don't need to handle those. |
michael@0 | 337 | if (response.status != 200) { |
michael@0 | 338 | // We /should/ have a Cornice error report in the JSON. We log that to |
michael@0 | 339 | // help with debugging. |
michael@0 | 340 | if ("errors" in result) { |
michael@0 | 341 | // This could throw, but this entire function is wrapped in a try. If |
michael@0 | 342 | // the server is sending something not an array of objects, it has |
michael@0 | 343 | // failed to keep its contract with us and there is little we can do. |
michael@0 | 344 | for (let error of result.errors) { |
michael@0 | 345 | this._log.info("Server-reported error: " + JSON.stringify(error)); |
michael@0 | 346 | } |
michael@0 | 347 | } |
michael@0 | 348 | |
michael@0 | 349 | let error = new TokenServerClientServerError(); |
michael@0 | 350 | error.response = response; |
michael@0 | 351 | |
michael@0 | 352 | if (response.status == 400) { |
michael@0 | 353 | error.message = "Malformed request."; |
michael@0 | 354 | error.cause = "malformed-request"; |
michael@0 | 355 | } else if (response.status == 401) { |
michael@0 | 356 | // Cause can be invalid-credentials, invalid-timestamp, or |
michael@0 | 357 | // invalid-generation. |
michael@0 | 358 | error.message = "Authentication failed."; |
michael@0 | 359 | error.cause = result.status; |
michael@0 | 360 | } |
michael@0 | 361 | |
michael@0 | 362 | // 403 should represent a "condition acceptance needed" response. |
michael@0 | 363 | // |
michael@0 | 364 | // The extra validation of "urls" is important. We don't want to signal |
michael@0 | 365 | // conditions required unless we are absolutely sure that is what the |
michael@0 | 366 | // server is asking for. |
michael@0 | 367 | else if (response.status == 403) { |
michael@0 | 368 | if (!("urls" in result)) { |
michael@0 | 369 | this._log.warn("403 response without proper fields!"); |
michael@0 | 370 | this._log.warn("Response body: " + response.body); |
michael@0 | 371 | |
michael@0 | 372 | error.message = "Missing JSON fields."; |
michael@0 | 373 | error.cause = "malformed-response"; |
michael@0 | 374 | } else if (typeof(result.urls) != "object") { |
michael@0 | 375 | error.message = "urls field is not a map."; |
michael@0 | 376 | error.cause = "malformed-response"; |
michael@0 | 377 | } else { |
michael@0 | 378 | error.message = "Conditions must be accepted."; |
michael@0 | 379 | error.cause = "conditions-required"; |
michael@0 | 380 | error.urls = result.urls; |
michael@0 | 381 | } |
michael@0 | 382 | } else if (response.status == 404) { |
michael@0 | 383 | error.message = "Unknown service."; |
michael@0 | 384 | error.cause = "unknown-service"; |
michael@0 | 385 | } |
michael@0 | 386 | |
michael@0 | 387 | // A Retry-After header should theoretically only appear on a 503, but |
michael@0 | 388 | // we'll look for it on any error response. |
michael@0 | 389 | this._maybeNotifyBackoff(response, "retry-after"); |
michael@0 | 390 | |
michael@0 | 391 | cb(error, null); |
michael@0 | 392 | return; |
michael@0 | 393 | } |
michael@0 | 394 | |
michael@0 | 395 | for (let k of ["id", "key", "api_endpoint", "uid", "duration"]) { |
michael@0 | 396 | if (!(k in result)) { |
michael@0 | 397 | let error = new TokenServerClientServerError("Expected key not " + |
michael@0 | 398 | " present in result: " + |
michael@0 | 399 | k); |
michael@0 | 400 | error.cause = "malformed-response"; |
michael@0 | 401 | error.response = response; |
michael@0 | 402 | cb(error, null); |
michael@0 | 403 | return; |
michael@0 | 404 | } |
michael@0 | 405 | } |
michael@0 | 406 | |
michael@0 | 407 | this._log.debug("Successful token response: " + result.id); |
michael@0 | 408 | cb(null, { |
michael@0 | 409 | id: result.id, |
michael@0 | 410 | key: result.key, |
michael@0 | 411 | endpoint: result.api_endpoint, |
michael@0 | 412 | uid: result.uid, |
michael@0 | 413 | duration: result.duration, |
michael@0 | 414 | }); |
michael@0 | 415 | }, |
michael@0 | 416 | |
michael@0 | 417 | /* |
michael@0 | 418 | * The prefix used for all notifications sent by this module. This |
michael@0 | 419 | * allows the handler of notifications to be sure they are handling |
michael@0 | 420 | * notifications for the service they expect. |
michael@0 | 421 | * |
michael@0 | 422 | * If not set, no notifications will be sent. |
michael@0 | 423 | */ |
michael@0 | 424 | observerPrefix: null, |
michael@0 | 425 | |
michael@0 | 426 | // Given an optional header value, notify that a backoff has been requested. |
michael@0 | 427 | _maybeNotifyBackoff: function (response, headerName) { |
michael@0 | 428 | if (!this.observerPrefix) { |
michael@0 | 429 | return; |
michael@0 | 430 | } |
michael@0 | 431 | let headerVal = response.headers[headerName]; |
michael@0 | 432 | if (!headerVal) { |
michael@0 | 433 | return; |
michael@0 | 434 | } |
michael@0 | 435 | let backoffInterval; |
michael@0 | 436 | try { |
michael@0 | 437 | backoffInterval = parseInt(headerVal, 10); |
michael@0 | 438 | } catch (ex) { |
michael@0 | 439 | this._log.error("TokenServer response had invalid backoff value in '" + |
michael@0 | 440 | headerName + "' header: " + headerVal); |
michael@0 | 441 | return; |
michael@0 | 442 | } |
michael@0 | 443 | Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval); |
michael@0 | 444 | }, |
michael@0 | 445 | |
michael@0 | 446 | // override points for testing. |
michael@0 | 447 | newRESTRequest: function(url) { |
michael@0 | 448 | return new RESTRequest(url); |
michael@0 | 449 | } |
michael@0 | 450 | }; |