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