mobile/android/base/tokenserver/TokenServerClient.java

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.tokenserver;
     7 import java.io.IOException;
     8 import java.net.URI;
     9 import java.security.GeneralSecurityException;
    10 import java.util.ArrayList;
    11 import java.util.List;
    12 import java.util.concurrent.Executor;
    14 import org.json.simple.JSONObject;
    15 import org.mozilla.gecko.background.common.log.Logger;
    16 import org.mozilla.gecko.background.fxa.SkewHandler;
    17 import org.mozilla.gecko.sync.ExtendedJSONObject;
    18 import org.mozilla.gecko.sync.NonArrayJSONException;
    19 import org.mozilla.gecko.sync.NonObjectJSONException;
    20 import org.mozilla.gecko.sync.UnexpectedJSONException.BadRequiredFieldJSONException;
    21 import org.mozilla.gecko.sync.net.AuthHeaderProvider;
    22 import org.mozilla.gecko.sync.net.BaseResource;
    23 import org.mozilla.gecko.sync.net.BaseResourceDelegate;
    24 import org.mozilla.gecko.sync.net.BrowserIDAuthHeaderProvider;
    25 import org.mozilla.gecko.sync.net.SyncResponse;
    26 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerConditionsRequiredException;
    27 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerInvalidCredentialsException;
    28 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedRequestException;
    29 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerMalformedResponseException;
    30 import org.mozilla.gecko.tokenserver.TokenServerException.TokenServerUnknownServiceException;
    32 import ch.boye.httpclientandroidlib.Header;
    33 import ch.boye.httpclientandroidlib.HttpHeaders;
    34 import ch.boye.httpclientandroidlib.HttpResponse;
    35 import ch.boye.httpclientandroidlib.client.ClientProtocolException;
    36 import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
    37 import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
    38 import ch.boye.httpclientandroidlib.message.BasicHeader;
    40 /**
    41  * HTTP client for interacting with the Mozilla Services Token Server API v1.0,
    42  * as documented at
    43  * <a href="http://docs.services.mozilla.com/token/apis.html">http://docs.services.mozilla.com/token/apis.html</a>.
    44  * <p>
    45  * A token server accepts some authorization credential and returns a different
    46  * authorization credential. Usually, it used to exchange a public-key
    47  * authorization token that is expensive to validate for a symmetric-key
    48  * authorization that is cheap to validate. For example, we might exchange a
    49  * BrowserID assertion for a HAWK id and key pair.
    50  */
    51 public class TokenServerClient {
    52   protected static final String LOG_TAG = "TokenServerClient";
    54   public static final String JSON_KEY_API_ENDPOINT = "api_endpoint";
    55   public static final String JSON_KEY_CONDITION_URLS = "condition_urls";
    56   public static final String JSON_KEY_DURATION = "duration";
    57   public static final String JSON_KEY_ERRORS = "errors";
    58   public static final String JSON_KEY_ID = "id";
    59   public static final String JSON_KEY_KEY = "key";
    60   public static final String JSON_KEY_UID = "uid";
    62   public static final String HEADER_CONDITIONS_ACCEPTED = "X-Conditions-Accepted";
    63   public static final String HEADER_CLIENT_STATE = "X-Client-State";
    65   protected final Executor executor;
    66   protected final URI uri;
    68   public TokenServerClient(URI uri, Executor executor) {
    69     if (uri == null) {
    70       throw new IllegalArgumentException("uri must not be null");
    71     }
    72     if (executor == null) {
    73       throw new IllegalArgumentException("executor must not be null");
    74     }
    75     this.uri = uri;
    76     this.executor = executor;
    77   }
    79   protected void invokeHandleSuccess(final TokenServerClientDelegate delegate, final TokenServerToken token) {
    80     executor.execute(new Runnable() {
    81       @Override
    82       public void run() {
    83         delegate.handleSuccess(token);
    84       }
    85     });
    86   }
    88   protected void invokeHandleFailure(final TokenServerClientDelegate delegate, final TokenServerException e) {
    89     executor.execute(new Runnable() {
    90       @Override
    91       public void run() {
    92         delegate.handleFailure(e);
    93       }
    94     });
    95   }
    97   /**
    98    * Notify the delegate that some kind of backoff header (X-Backoff,
    99    * X-Weave-Backoff, Retry-After) was received and should be acted upon.
   100    *
   101    * This method is non-terminal, and will be followed by a separate
   102    * <code>invoke*</code> call.
   103    *
   104    * @param delegate
   105    *          the delegate to inform.
   106    * @param backoffSeconds
   107    *          the number of seconds for which the system should wait before
   108    *          making another token server request to this server.
   109    */
   110   protected void notifyBackoff(final TokenServerClientDelegate delegate, final int backoffSeconds) {
   111     executor.execute(new Runnable() {
   112       @Override
   113       public void run() {
   114         delegate.handleBackoff(backoffSeconds);
   115       }
   116     });
   117   }
   119   protected void invokeHandleError(final TokenServerClientDelegate delegate, final Exception e) {
   120     executor.execute(new Runnable() {
   121       @Override
   122       public void run() {
   123         delegate.handleError(e);
   124       }
   125     });
   126   }
   128   public TokenServerToken processResponse(SyncResponse res) throws TokenServerException {
   129     int statusCode = res.getStatusCode();
   131     Logger.debug(LOG_TAG, "Got token response with status code " + statusCode + ".");
   133     // Responses should *always* be JSON, even in the case of 4xx and 5xx
   134     // errors. If we don't see JSON, the server is likely very unhappy.
   135     final Header contentType = res.getContentType();
   136     if (contentType == null) {
   137       throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
   138     }
   140     final String type = contentType.getValue();
   141     if (!type.equals("application/json") &&
   142         !type.startsWith("application/json;")) {
   143       Logger.warn(LOG_TAG, "Got non-JSON response with Content-Type " +
   144           contentType + ". Misconfigured server?");
   145       throw new TokenServerMalformedResponseException(null, "Non-JSON response Content-Type.");
   146     }
   148     // Responses should *always* be a valid JSON object.
   149     // It turns out that right now they're not always, but that's a server bug...
   150     ExtendedJSONObject result;
   151     try {
   152       result = res.jsonObjectBody();
   153     } catch (Exception e) {
   154       Logger.debug(LOG_TAG, "Malformed token response.", e);
   155       throw new TokenServerMalformedResponseException(null, e);
   156     }
   158     // The service shouldn't have any 3xx, so we don't need to handle those.
   159     if (res.getStatusCode() != 200) {
   160       // We should have a (Cornice) error report in the JSON. We log that to
   161       // help with debugging.
   162       List<ExtendedJSONObject> errorList = new ArrayList<ExtendedJSONObject>();
   164       if (result.containsKey(JSON_KEY_ERRORS)) {
   165         try {
   166           for (Object error : result.getArray(JSON_KEY_ERRORS)) {
   167             Logger.warn(LOG_TAG, "" + error);
   169             if (error instanceof JSONObject) {
   170               errorList.add(new ExtendedJSONObject((JSONObject) error));
   171             }
   172           }
   173         } catch (NonArrayJSONException e) {
   174           Logger.warn(LOG_TAG, "Got non-JSON array '" + result.getString(JSON_KEY_ERRORS) + "'.", e);
   175         }
   176       }
   178       if (statusCode == 400) {
   179         throw new TokenServerMalformedRequestException(errorList, result.toJSONString());
   180       }
   182       if (statusCode == 401) {
   183         throw new TokenServerInvalidCredentialsException(errorList, result.toJSONString());
   184       }
   186       // 403 should represent a "condition acceptance needed" response.
   187       //
   188       // The extra validation of "urls" is important. We don't want to signal
   189       // conditions required unless we are absolutely sure that is what the
   190       // server is asking for.
   191       if (statusCode == 403) {
   192         // Bug 792674 and Bug 783598: make this testing simpler. For now, we
   193         // check that errors is an array, and take any condition_urls from the
   194         // first element.
   196         try {
   197           if (errorList == null || errorList.isEmpty()) {
   198             throw new TokenServerMalformedResponseException(errorList, "403 response without proper fields.");
   199           }
   201           ExtendedJSONObject error = errorList.get(0);
   203           ExtendedJSONObject condition_urls = error.getObject(JSON_KEY_CONDITION_URLS);
   204           if (condition_urls != null) {
   205             throw new TokenServerConditionsRequiredException(condition_urls);
   206           }
   207         } catch (NonObjectJSONException e) {
   208           Logger.warn(LOG_TAG, "Got non-JSON error object.");
   209         }
   211         throw new TokenServerMalformedResponseException(errorList, "403 response without proper fields.");
   212       }
   214       if (statusCode == 404) {
   215         throw new TokenServerUnknownServiceException(errorList);
   216       }
   218       // We shouldn't ever get here...
   219       throw new TokenServerException(errorList);
   220     }
   222     try {
   223       result.throwIfFieldsMissingOrMisTyped(new String[] { JSON_KEY_ID, JSON_KEY_KEY, JSON_KEY_API_ENDPOINT }, String.class);
   224       result.throwIfFieldsMissingOrMisTyped(new String[] { JSON_KEY_UID }, Long.class);
   225     } catch (BadRequiredFieldJSONException e ) {
   226       throw new TokenServerMalformedResponseException(null, e);
   227     }
   229     Logger.debug(LOG_TAG, "Successful token response: " + result.getString(JSON_KEY_ID));
   231     return new TokenServerToken(result.getString(JSON_KEY_ID),
   232         result.getString(JSON_KEY_KEY),
   233         result.get(JSON_KEY_UID).toString(),
   234         result.getString(JSON_KEY_API_ENDPOINT));
   235   }
   237   public static class TokenFetchResourceDelegate extends BaseResourceDelegate {
   238     private final TokenServerClient         client;
   239     private final TokenServerClientDelegate delegate;
   240     private final String                    assertion;
   241     private final String                    clientState;
   242     private final BaseResource              resource;
   243     private final boolean                   conditionsAccepted;
   245     public TokenFetchResourceDelegate(TokenServerClient client,
   246                                       BaseResource resource,
   247                                       TokenServerClientDelegate delegate,
   248                                       String assertion, String clientState,
   249                                       boolean conditionsAccepted) {
   250       super(resource);
   251       this.client = client;
   252       this.delegate = delegate;
   253       this.assertion = assertion;
   254       this.clientState = clientState;
   255       this.resource = resource;
   256       this.conditionsAccepted = conditionsAccepted;
   257     }
   259     @Override
   260     public String getUserAgent() {
   261       return delegate.getUserAgent();
   262     }
   264     @Override
   265     public void handleHttpResponse(HttpResponse response) {
   266       // Skew.
   267       SkewHandler skewHandler = SkewHandler.getSkewHandlerForResource(resource);
   268       skewHandler.updateSkew(response, System.currentTimeMillis());
   270       // Extract backoff regardless of whether this was an error response, and
   271       // Retry-After for 503 responses. The error will be handled elsewhere.)
   272       SyncResponse res = new SyncResponse(response);
   273       final boolean includeRetryAfter = res.getStatusCode() == 503;
   274       int backoffInSeconds = res.totalBackoffInSeconds(includeRetryAfter);
   275       if (backoffInSeconds > -1) {
   276         client.notifyBackoff(delegate, backoffInSeconds);
   277       }
   279       try {
   280         TokenServerToken token = client.processResponse(res);
   281         client.invokeHandleSuccess(delegate, token);
   282       } catch (TokenServerException e) {
   283         client.invokeHandleFailure(delegate, e);
   284       }
   285     }
   287     @Override
   288     public void handleTransportException(GeneralSecurityException e) {
   289       client.invokeHandleError(delegate, e);
   290     }
   292     @Override
   293     public void handleHttpProtocolException(ClientProtocolException e) {
   294       client.invokeHandleError(delegate, e);
   295     }
   297     @Override
   298     public void handleHttpIOException(IOException e) {
   299       client.invokeHandleError(delegate, e);
   300     }
   302     @Override
   303     public AuthHeaderProvider getAuthHeaderProvider() {
   304       return new BrowserIDAuthHeaderProvider(assertion);
   305     }
   307     @Override
   308     public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
   309       String host = request.getURI().getHost();
   310       request.setHeader(new BasicHeader(HttpHeaders.HOST, host));
   311       if (clientState != null) {
   312         request.setHeader(new BasicHeader(HEADER_CLIENT_STATE, clientState));
   313       }
   314       if (conditionsAccepted) {
   315         request.addHeader(HEADER_CONDITIONS_ACCEPTED, "1");
   316       }
   317     }
   318   }
   320   public void getTokenFromBrowserIDAssertion(final String assertion,
   321                                              final boolean conditionsAccepted,
   322                                              final String clientState,
   323                                              final TokenServerClientDelegate delegate) {
   324     final BaseResource resource = new BaseResource(this.uri);
   325     resource.delegate = new TokenFetchResourceDelegate(this, resource, delegate,
   326                                                        assertion, clientState,
   327                                                        conditionsAccepted);
   328     resource.get();
   329   }
   330 }

mercurial