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.

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

mercurial