sponses from our host. this._localtimeOffsetMsec = 0; }; HawkClient.prototype = { /** * Construct an error message for a response. Private. * * @param restResponse * A RESTResponse object from a RESTRequest * * @param error * A string or object describing the error */ _constructError(restResponse, error) { let errorObj = { error, // This object is likely to be JSON.stringify'd, but neither Error() // objects nor Components.Exception objects do the right thing there, // so we add a new element which is simply the .toString() version of // the error object, so it does appear in JSON'd values. errorString: error.toString(), message: restResponse.statusText, code: restResponse.status, errno: restResponse.status, toString() { return this.code + ": " + this.message; }, }; let retryAfter = restResponse.headers && restResponse.headers["retry-after"]; retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter; if (retryAfter) { errorObj.retryAfter = retryAfter; // and notify observers of the retry interval if (this.observerPrefix) { Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter); } } return errorObj; }, /** * * Update clock offset by determining difference from date gives in the (RFC * 1123) Date header of a server response. Because HAWK tolerates a window * of one minute of clock skew (so two minutes total since the skew can be * positive or negative), the simple method of calculating offset here is * probably good enough. We keep the value in milliseconds to make life * easier, even though the value will not have millisecond accuracy. * * @param dateString * An RFC 1123 date string (e.g., "Mon, 13 Jan 2014 21:45:06 GMT") * * For HAWK clock skew and replay protection, see * https://github.com/hueniverse/hawk#replay-protection */ _updateClockOffset(dateString) { try { let serverDateMsec = Date.parse(dateString); this._localtimeOffsetMsec = serverDateMsec - this.now(); lazy.log.debug( "Clock offset vs " + this.host + ": " + this._localtimeOffsetMsec ); } catch (err) { lazy.log.warn("Bad date header in server response: " + dateString); } }, /* * Get the current clock offset in milliseconds. * * The offset is the number of milliseconds that must be added to the client * clock to make it equal to the server clock. For example, if the client is * five minutes ahead of the server, the localtimeOffsetMsec will be -300000. */ get localtimeOffsetMsec() { return this._localtimeOffsetMsec; }, /* * return current time in milliseconds */ now() { return Date.now(); }, /** * A general method for sending raw RESTRequest calls authorized using HAWK. * * @param path * API endpoint path * @param method * The HTTP request method * @param credentials * Hawk credentials * @param payloadObj * An object that can be encodable as JSON as the payload of the * request * @param extraHeaders * An object with header/value pairs to send with the request. * @return Promise * Returns a promise that resolves to the response of the API call, * or is rejected with an error. If the server response can be parsed * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ async request( path, method, credentials = null, payloadObj = {}, extraHeaders = {}, retryOK = true ) { method = method.toLowerCase(); let uri = this.host + path; let extra = { now: this.now(), localtimeOffsetMsec: this.localtimeOffsetMsec, headers: extraHeaders, }; let request = this.newHAWKAuthenticatedRESTRequest(uri, credentials, extra); let error; let restResponse = await request[method](payloadObj).catch(e => { // Keep a reference to the error, log a message about it, and return the // response anyway. error = e; lazy.log.warn("hawk request error", error); return request.response; }); // This shouldn't happen anymore, but it's not exactly difficult to handle. if (!restResponse) { throw error; } let status = restResponse.status; lazy.log.debug( "(Response) " + path + ": code: " + status + " - Status text: " + restResponse.statusText ); if (lazy.logPII) { lazy.log.debug("Response text", restResponse.body); } // All responses may have backoff headers, which are a server-side safety // valve to allow slowing down clients without hurting performance. this._maybeNotifyBackoff(restResponse, "x-weave-backoff"); this._maybeNotifyBackoff(restResponse, "x-backoff"); if (error) { // When things really blow up, reconstruct an error object that follows // the general format of the server on error responses. throw this._constructError(restResponse, error); } this._updateClockOffset(restResponse.headers.date); if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) { // Retry once if we were rejected due to a bad timestamp. // Clock offset is adjusted already in the top of this function. lazy.log.debug("Received 401 for " + path + ": retrying"); return this.request( path, method, credentials, payloadObj, extraHeaders, false ); } // If the server returned a json error message, use it in the rejection // of the promise. // // In the case of a 401, in which we are probably being rejected for a // bad timestamp, retry exactly once, during which time clock offset will // be adjusted. let jsonResponse = {}; try { jsonResponse = JSON.parse(restResponse.body); } catch (notJSON) {} let okResponse = 200 <= status && status < 300; if (!okResponse || jsonResponse.error) { if (jsonResponse.error) { throw jsonResponse; } throw this._constructError(restResponse, "Request failed"); } // It's up to the caller to know how to decode the response. // We just return the whole response. return restResponse; }, /* * The prefix used for all notifications sent by this module. This * allows the handler of notifications to be sure they are handling * notifications for the service they expect. * * If not set, no notifications will be sent. */ observerPrefix: null, // Given an optional header value, notify that a backoff has been requested. _maybeNotifyBackoff(response, headerName) { if (!this.observerPrefix || !response.headers) { return; } let headerVal = response.headers[headerName]; if (!headerVal) { return; } let backoffInterval; try { backoffInterval = parseInt(headerVal, 10); } catch (ex) { lazy.log.error( "hawkclient response had invalid backoff value in '" + headerName + "' header: " + headerVal ); return; } Observers.notify( this.observerPrefix + ":backoff:interval", backoffInterval ); }, // override points for testing. newHAWKAuthenticatedRESTRequest(uri, credentials, extra) { return new HAWKAuthenticatedRESTRequest(uri, credentials, extra); }, }; PK