diff -r 000000000000 -r 6474c204b198 mobile/android/base/tokenserver/TokenServerClient.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mobile/android/base/tokenserver/TokenServerClient.java Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,330 @@ +/* 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/. */ + +package org.mozilla.gecko.tokenserver; + +import java.io.IOException; +import java.net.URI; +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +import org.json.simple.JSONObject; +import org.mozilla.gecko.background.common.log.Logger; +import org.mozilla.gecko.background.fxa.SkewHandler; +import org.mozilla.gecko.sync.ExtendedJSONObject; +import org.mozilla.gecko.sync.NonArrayJSONException; +import org.mozilla.gecko.sync.NonObjectJSONException; +import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException; +import org.mozilla.gecko.sync.net.AuthHeaderProvider; +import org.mozilla.gecko.sync.net.BaseResource; +import org.mozilla.gecko.sync.net.BaseResourceDelegate; +import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider; +import org.mozilla.gecko.sync.net.SyncResponse; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException; +import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException; + +import ch.boye.httpclientandroidlib.Header; +import ch.boye.httpclientandroidlib.HttpHeaders; +import ch.boye.httpclientandroidlib.HttpResponse; +import ch.boye.httpclientandroidlib.client.ClientProtocolException; +import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase; +import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient; +import ch.boye.httpclientandroidlib.message.BasicHeader; + +/** + * HTTP client for interacting with the Mozilla Services Token Server API v1.0, + * as documented at + * http://docs.services.mozilla.com/token/apis.html. + *
+ * A token server accepts some authorization credential and returns a different
+ * authorization credential. Usually, it used to exchange a public-key
+ * authorization token that is expensive to validate for a symmetric-key
+ * authorization that is cheap to validate. For example, we might exchange a
+ * BrowserID assertion for a HAWK id and key pair.
+ */
+public class TokenServerClient {
+ protected static final String LOG_TAG = "TokenServerClient";
+
+ public static final String JSON_KEY_API_ENDPOINT = "api_endpoint";
+ public static final String JSON_KEY_CONDITION_URLS = "condition_urls";
+ public static final String JSON_KEY_DURATION = "duration";
+ public static final String JSON_KEY_ERRORS = "errors";
+ public static final String JSON_KEY_ID = "id";
+ public static final String JSON_KEY_KEY = "key";
+ public static final String JSON_KEY_UID = "uid";
+
+ public static final String HEADER_CONDITIONS_ACCEPTED = "X-Conditions-Accepted";
+ public static final String HEADER_CLIENT_STATE = "X-Client-State";
+
+ protected final Executor executor;
+ protected final URI uri;
+
+ public TokenServerClient(URI uri, Executor executor) {
+ if (uri == null) {
+ throw new IllegalArgumentException("uri must not be null");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("executor must not be null");
+ }
+ this.uri = uri;
+ this.executor = executor;
+ }
+
+ protected void invokeHandleSuccess(final TokenServerClientDelegate delegate, final TokenServerToken token) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleSuccess(token);
+ }
+ });
+ }
+
+ protected void invokeHandleFailure(final TokenServerClientDelegate delegate, final TokenServerException e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleFailure(e);
+ }
+ });
+ }
+
+ /**
+ * Notify the delegate that some kind of backoff header (X-Backoff,
+ * X-Weave-Backoff, Retry-After) was received and should be acted upon.
+ *
+ * This method is non-terminal, and will be followed by a separate
+ * invoke*
call.
+ *
+ * @param delegate
+ * the delegate to inform.
+ * @param backoffSeconds
+ * the number of seconds for which the system should wait before
+ * making another token server request to this server.
+ */
+ protected void notifyBackoff(final TokenServerClientDelegate delegate, final int backoffSeconds) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleBackoff(backoffSeconds);
+ }
+ });
+ }
+
+ protected void invokeHandleError(final TokenServerClientDelegate delegate, final Exception e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleError(e);
+ }
+ });
+ }
+
+ public TokenServerToken processResponse(SyncResponse res) throws TokenServerException {
+ int statusCode = res.getStatusCode();
+
+ Logger.debug(LOG_TAG, "Got token response with status code " + statusCode + ".");
+
+ // Responses should *always* be JSON, even in the case of 4xx and 5xx
+ // errors. If we don't see JSON, the server is likely very unhappy.
+ final Header contentType = res.getContentType();
+ if (contentType == null) {
+ throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
+ }
+
+ final String type = contentType.getValue();
+ if (!type.equals("application/json") &&
+ !type.startsWith("application/json;")) {
+ Logger.warn(LOG_TAG, "Got non-JSON response with Content-Type " +
+ contentType + ". Misconfigured server?");
+ throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
+ }
+
+ // Responses should *always* be a valid JSON object.
+ // It turns out that right now they're not always, but that's a server bug...
+ ExtendedJSONObject result;
+ try {
+ result = res.jsonObjectBody();
+ } catch (Exception e) {
+ Logger.debug(LOG_TAG, "Malformed token response.", e);
+ throw new TokenServerMalformedResponseException(null, e);
+ }
+
+ // The service shouldn't have any 3xx, so we don't need to handle those.
+ if (res.getStatusCode() != 200) {
+ // We should have a (Cornice) error report in the JSON. We log that to
+ // help with debugging.
+ List