/* 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