/* 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.sync.stage;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.NodeAuthenticationException;
import org.mozilla.gecko.sync.NullClusterURLException;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
import ch.boye.httpclientandroidlib.HttpEntity;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
public class EnsureClusterURLStage extends AbstractNonRepositorySyncStage {
public EnsureClusterURLStage(GlobalSession session) {
super(session);
}
public interface ClusterURLFetchDelegate {
/**
* 200 - Success.
* @param url The node/weave cluster URL returned by the server.
*/
public void handleSuccess(URI url);
/**
* 200 - Success, but the server returned 'null', meaning no node can be
* assigned at this time, probably due to registration throttling.
*/
public void handleThrottled();
/**
* 404 - User not found.
* 503 - Service unavailable.
* @param response The server's response.
*/
public void handleFailure(HttpResponse response);
/**
* An unexpected error occurred.
*/
public void handleError(Exception e);
}
protected static final String LOG_TAG = "EnsureClusterURLStage";
// TODO: if cluster URL has changed since last time, we need to ensure that we do
// a fresh start. This takes place at the GlobalSession level. Verify!
/**
* Fetch a node/weave cluster URL from a server.
*
* @param nodeWeaveURL
* Where to request the cluster URL from, usually something like:
* <code>https://server/pathname/version/username/node/weave</code>.
* @throws URISyntaxException
*/
public static void fetchClusterURL(final String nodeWeaveURL,
final ClusterURLFetchDelegate delegate) throws URISyntaxException {
Logger.info(LOG_TAG, "In fetchClusterURL: node/weave is " + nodeWeaveURL);
BaseResource resource = new BaseResource(nodeWeaveURL);
resource.delegate = new SyncResourceDelegate(resource) {
/**
* Handle the response for GET https://server/pathname/version/username/node/weave.
*
* Returns the Sync Node that the client is located on.
* Storage operations should be directed to that node.
*
* Return value: the node URL, an unadorned (not JSON) string.
*
* node may be 'null' if no node can be assigned at this time, probably
* due to registration throttling.
*
* Possible errors:
*
* 503: there was an error getting a node | empty body
*
* 400: for historical reasons treated as 404.
*
* 404: user not found | empty body
*
* {@link http://docs.services.mozilla.com/reg/apis.html}
*/
@Override
public void handleHttpResponse(HttpResponse response) {
try {
int status = response.getStatusLine().getStatusCode();
switch (status) {
case 200:
Logger.info(LOG_TAG, "Got 200 for node/weave cluster URL request (user found; succeeding).");
HttpEntity entity = response.getEntity();
if (entity == null) {
delegate.handleThrottled();
BaseResource.consumeEntity(response);
return;
}
String output = null;
try {
InputStream content = entity.getContent();
BufferedReader reader = new BufferedReader(new InputStreamReader(content, "UTF-8"), 1024);
output = reader.readLine();
BaseResource.consumeReader(reader);
reader.close();
} catch (IllegalStateException e) {
delegate.handleError(e);
BaseResource.consumeEntity(response);
return;
} catch (IOException e) {
delegate.handleError(e);
BaseResource.consumeEntity(response);
return;
}
if (output == null || output.equals("null")) {
delegate.handleThrottled();
return;
}
try {
URI uri = new URI(output);
delegate.handleSuccess(uri);
} catch (URISyntaxException e) {
delegate.handleError(e);
}
break;
case 400:
case 404:
Logger.info(LOG_TAG, "Got " + status + " for node/weave cluster URL request (user not found; failing).");
delegate.handleFailure(response);
break;
case 503:
Logger.info(LOG_TAG, "Got 503 for node/weave cluster URL request (error fetching node; failing).");
delegate.handleFailure(response);
break;
default:
Logger.warn(LOG_TAG, "Got " + status + " for node/weave cluster URL request (unexpected HTTP status; failing).");
delegate.handleFailure(response);
}
} finally {
BaseResource.consumeEntity(response);
}
BaseResource.consumeEntity(response);
}
@Override
public void handleHttpProtocolException(ClientProtocolException e) {
delegate.handleError(e);
}
@Override
public void handleHttpIOException(IOException e) {
delegate.handleError(e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
delegate.handleError(e);
}
};
resource.get();
}
public void execute() throws NoSuchStageException {
final URI oldClusterURL = session.config.getClusterURL();
final boolean wantNodeAssignment = session.callback.wantNodeAssignment();
if (!wantNodeAssignment && oldClusterURL != null) {
Logger.info(LOG_TAG, "Cluster URL is already set and not stale. Continuing with sync.");
session.advance();
return;
}
Logger.info(LOG_TAG, "Fetching cluster URL.");
final ClusterURLFetchDelegate delegate = new ClusterURLFetchDelegate() {
@Override
public void handleSuccess(final URI url) {
Logger.info(LOG_TAG, "Node assignment pointed us to " + url);
if (oldClusterURL != null && oldClusterURL.equals(url)) {
// Our cluster URL is marked as stale and the fresh cluster URL is the same -- this is the user's problem.
session.callback.informNodeAuthenticationFailed(session, url);
session.abort(new NodeAuthenticationException(), "User password has changed.");
return;
}
session.callback.informNodeAssigned(session, oldClusterURL, url); // No matter what, we're getting a new node/weave clusterURL.
session.config.setClusterURL(url);
ThreadPool.run(new Runnable() {
@Override
public void run() {
session.advance();
}
});
}
@Override
public void handleThrottled() {
session.abort(new NullClusterURLException(), "Got 'null' cluster URL. Aborting.");
}
@Override
public void handleFailure(HttpResponse response) {
int statusCode = response.getStatusLine().getStatusCode();
Logger.warn(LOG_TAG, "Got HTTP failure fetching node assignment: " + statusCode);
if (statusCode == 404) {
URI serverURL = session.config.serverURL;
if (serverURL != null) {
Logger.info(LOG_TAG, "Using serverURL <" + serverURL.toASCIIString() + "> as clusterURL.");
session.config.setClusterURL(serverURL);
session.advance();
return;
}
Logger.warn(LOG_TAG, "No serverURL set to use as fallback cluster URL. Aborting sync.");
// Fallthrough to abort.
} else {
session.interpretHTTPFailure(response);
}
session.abort(new Exception("HTTP failure."), "Got failure fetching cluster URL.");
}
@Override
public void handleError(Exception e) {
session.abort(e, "Got exception fetching cluster URL.");
}
};
ThreadPool.run(new Runnable() {
@Override
public void run() {
try {
fetchClusterURL(session.config.nodeWeaveURL(), delegate);
} catch (URISyntaxException e) {
session.abort(e, "Invalid URL for node/weave.");
}
}});
}
}