/* 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.jpake.stage;
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.Timer;
import java.util.TimerTask;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.jpake.JPakeClient;
import org.mozilla.gecko.sync.jpake.JPakeResponse;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.Resource;
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
import org.mozilla.gecko.sync.setup.Constants;
import ch.boye.httpclientandroidlib.Header;
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;
public class GetRequestStage extends JPakeStage {
private Timer timerScheduler = new Timer();
private int pollTries;
private GetStepTimerTask getStepTimerTask;
private interface GetRequestStageDelegate {
public void handleSuccess(HttpResponse response);
public void handleFailure(String error);
public void handleError(Exception e);
}
@Override
public void execute(final JPakeClient jClient) {
Logger.debug(LOG_TAG, "Retrieving next message.");
final GetRequestStageDelegate callbackDelegate = new GetRequestStageDelegate() {
@Override
public void handleSuccess(HttpResponse response) {
if (jClient.finished) {
Logger.debug(LOG_TAG, "Finished; returning.");
return;
}
JPakeResponse res = new JPakeResponse(response);
Header etagHeader = response.getFirstHeader("etag");
if (etagHeader == null) {
Logger.error(LOG_TAG, "Server did not supply ETag.");
jClient.abort(Constants.JPAKE_ERROR_SERVER);
return;
}
jClient.theirEtag = etagHeader.getValue();
try {
jClient.jIncoming = res.jsonObjectBody();
} catch (Exception e) {
Logger.error(LOG_TAG, "Illegal state.", e);
jClient.abort(Constants.JPAKE_ERROR_INVALID);
return;
}
Logger.debug(LOG_TAG, "incoming message: " + jClient.jIncoming.toJSONString());
jClient.runNextStage();
}
@Override
public void handleFailure(String error) {
Logger.error(LOG_TAG, "Got HTTP failure: " + error);
jClient.abort(error);
}
@Override
public void handleError(Exception e) {
Logger.error(LOG_TAG, "Threw HTTP exception.", e);
jClient.abort(Constants.JPAKE_ERROR_NETWORK);
}
};
Resource httpRequest;
try {
httpRequest = createGetRequest(callbackDelegate, jClient);
} catch (URISyntaxException e) {
Logger.error(LOG_TAG, "Incorrect URI syntax.", e);
jClient.abort(Constants.JPAKE_ERROR_INVALID);
return;
}
Logger.debug(LOG_TAG, "Scheduling GET request.");
getStepTimerTask = new GetStepTimerTask(httpRequest);
timerScheduler.schedule(getStepTimerTask, jClient.jpakePollInterval);
}
private Resource createGetRequest(final GetRequestStageDelegate callbackDelegate, final JPakeClient jpakeClient) throws URISyntaxException {
BaseResource httpResource = new BaseResource(jpakeClient.channelUrl);
httpResource.delegate = new SyncResourceDelegate(httpResource) {
@Override
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
request.setHeader(new BasicHeader("X-KeyExchange-Id", jpakeClient.clientId));
if (jpakeClient.myEtag != null) {
request.setHeader(new BasicHeader("If-None-Match", jpakeClient.myEtag));
}
}
@Override
public void handleHttpResponse(HttpResponse response) {
try {
int statusCode = response.getStatusLine().getStatusCode();
switch (statusCode) {
case 200:
jpakeClient.pollTries = 0; // Reset pollTries for next GET.
callbackDelegate.handleSuccess(response);
break;
case 304:
Logger.debug(LOG_TAG, "Channel hasn't been updated yet. Will try again later");
if (pollTries >= jpakeClient.jpakeMaxTries) {
Logger.error(LOG_TAG, "Tried for " + pollTries + " times, maxTries " + jpakeClient.jpakeMaxTries + ", aborting");
callbackDelegate.handleFailure(Constants.JPAKE_ERROR_TIMEOUT);
break;
}
jpakeClient.pollTries += 1;
if (!jpakeClient.finished) {
Logger.debug(LOG_TAG, "Scheduling next GET request.");
scheduleGetRequest(jpakeClient.jpakePollInterval, jpakeClient);
} else {
Logger.debug(LOG_TAG, "Resetting pollTries");
jpakeClient.pollTries = 0;
}
break;
case 404:
Logger.error(LOG_TAG, "No data found in channel.");
callbackDelegate.handleFailure(Constants.JPAKE_ERROR_NODATA);
break;
case 412: // "Precondition failed"
Logger.debug(LOG_TAG, "Message already replaced on server by other party.");
callbackDelegate.handleSuccess(response);
break;
default:
Logger.error(LOG_TAG, "Could not retrieve data. Server responded with HTTP " + statusCode);
callbackDelegate.handleFailure(Constants.JPAKE_ERROR_SERVER);
break;
}
} finally {
// Clean up.
BaseResource.consumeEntity(response);
}
}
@Override
public void handleHttpProtocolException(ClientProtocolException e) {
callbackDelegate.handleError(e);
}
@Override
public void handleHttpIOException(IOException e) {
callbackDelegate.handleError(e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
callbackDelegate.handleError(e);
}
@Override
public int connectionTimeout() {
return JPakeClient.REQUEST_TIMEOUT;
}
};
return httpResource;
}
/**
* TimerTask for use with delayed GET requests.
*
*/
public class GetStepTimerTask extends TimerTask {
private Resource request;
public GetStepTimerTask(Resource request) {
this.request = request;
}
@Override
public void run() {
request.get();
}
}
/*
* Helper method to schedule a GET request with some delay.
* Basically, run another GetRequestStage.
*/
private void scheduleGetRequest(int delay, final JPakeClient jClient) {
timerScheduler.schedule(new TimerTask() {
@Override
public void run() {
new GetRequestStage().execute(jClient);
}
}, delay);
}
}