Bug 705767 - Validate credentials before completing setup. r=rnewman

This commit is contained in:
Chenxia Liu 2012-06-05 19:35:26 -04:00
parent b921ba52c1
commit 0f04823a37
19 changed files with 739 additions and 62 deletions

File diff suppressed because one or more lines are too long

View File

@ -78,4 +78,7 @@
<!ENTITY sync.notification.oneaccount.label 'Only one &syncBrand.fullName.label; account is supported.'>
<!-- Incorrect settings and changing credentials. -->
<!ENTITY sync.invalidcreds.label 'Incorrect account name or password.'>
<!ENTITY sync.invalidserver.label 'Please enter a valid server URL'>
<!ENTITY sync.verifying.label 'Verifying…'>
<!ENTITY sync.new.recoverykey.status.incorrect 'Recovery Key incorrect. Please try again.'>

View File

@ -43,6 +43,10 @@
<EditText android:id="@+id/keyInput"
style="@style/SyncEditItem"
android:hint="@string/sync_input_key" />
<TextView android:id="@+id/cred_error"
style="@style/SyncTextError"
android:text="@string/sync_invalidcreds_label" />
<CheckBox android:id="@+id/checkbox_server"
android:layout_width="wrap_content"
@ -56,6 +60,10 @@
android:visibility="gone"
android:hint="@string/sync_input_server" />
<TextView android:id="@+id/server_error"
style="@style/SyncTextError"
android:text="@string/sync_invalidserver_label" />
</LinearLayout>
</ScrollView>
@ -65,6 +73,7 @@
android:orientation="horizontal" >
<Button
android:id="@+id/accountCancelButton"
style="@style/SyncButton"
android:onClick="cancelClickHandler"
android:text="@string/sync_button_cancel" />

View File

@ -99,8 +99,7 @@
style="@style/SyncBottom"
android:orientation="horizontal" >
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/SyncButton"
android:onClick="cancelClickHandler"
android:text="@string/sync_button_cancel" />
</LinearLayout>

View File

@ -27,22 +27,17 @@
android:orientation="horizontal" >
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/SyncButton"
android:onClick="tryAgainClickHandler"
android:text="@string/sync_button_tryagain" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/SyncButton"
android:onClick="manualClickHandler"
android:text="@string/sync_button_manual" />
<Button
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
style="@style/SyncButton"
android:onClick="cancelClickHandler"
android:text="@string/sync_button_cancel" />
</LinearLayout>

View File

@ -66,7 +66,7 @@
android:visibility="invisible" >
<TextView
style="@style/SyncTextItem"
style="@style/SyncTextError"
android:layout_margin="10dp"
android:text="@string/sync_pair_tryagain"
android:textSize="10dp" />

View File

@ -28,7 +28,12 @@
<item name="android:layout_height">wrap_content</item>
<item name="android:textSize">15dp</item>
</style>
<style name="SyncLinkItem" parent="SyncTextItem">
<style name="SyncTextError" parent="@style/SyncTextItem">
<item name="android:layout_margin">10dp</item>
<item name="android:textSize">15dp</item>
<item name="android:visibility">gone</item>
</style>
<style name="SyncLinkItem" parent="@style/SyncTextItem">
<item name="android:clickable">true</item>
<item name="android:textColor">#ACC4D5</item>
</style>
@ -67,6 +72,7 @@
<style name="SyncBottom">
<item name="android:layout_width">fill_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">center</item>
<item name="android:gravity">center</item>
<item name="android:layout_alignParentBottom">true</item>
<item name="android:background">@android:drawable/bottom_bar</item>

View File

@ -12,6 +12,7 @@ import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.jpake.JPakeClient;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
import org.mozilla.gecko.sync.setup.auth.AccountAuthenticator;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
@ -85,7 +86,7 @@ public class DeleteChannel {
}
};
JPakeClient.runOnThread(new Runnable() {
AccountAuthenticator.runOnThread(new Runnable() {
@Override
public void run() {
httpResource.delete();

View File

@ -26,6 +26,17 @@ public class Constants {
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT |
Intent.FLAG_ACTIVITY_NO_ANIMATION;
// Constants for Account Authentication.
public static final String AUTH_NODE_DEFAULT = "https://auth.services.mozilla.com/";
public static final String AUTH_NODE_PATHNAME = "user/";
public static final String AUTH_NODE_VERSION = "1.0/";
public static final String AUTH_NODE_SUFFIX = "node/weave";
public static final String AUTH_SERVER_VERSION = "1.1/";
public static final String AUTH_SERVER_SUFFIX = "info/collections/";
// Account Authentication Errors.
public static final String AUTH_ERROR_NOUSER = "auth.error.badcredentials";
// Links for J-PAKE setup help pages.
public static final String LINK_FIND_CODE = "https://support.mozilla.org/kb/find-code-to-add-device-to-firefox-sync";
public static final String LINK_FIND_ADD_DEVICE = "https://support.mozilla.org/kb/add-a-device-to-firefox-sync";

View File

@ -12,19 +12,22 @@ import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.InvalidSyncKeyException;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.setup.SyncAccounts.SyncAccountParameters;
import org.mozilla.gecko.sync.setup.auth.AccountAuthenticator;
import org.mozilla.gecko.sync.setup.auth.AuthenticationResult;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorActivity;
import android.accounts.AccountManager;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
@ -33,14 +36,14 @@ import android.widget.EditText;
import android.widget.Toast;
public class AccountActivity extends AccountAuthenticatorActivity {
private final static String LOG_TAG = "AccountActivity";
private final static String LOG_TAG = "AccountActivity";
private AccountManager mAccountManager;
private Context mContext;
private String username;
private String password;
private String key;
private String server;
private String server = Constants.AUTH_NODE_DEFAULT;
// UI elements.
private EditText serverInput;
@ -49,6 +52,10 @@ public class AccountActivity extends AccountAuthenticatorActivity {
private EditText synckeyInput;
private CheckBox serverCheckbox;
private Button connectButton;
private Button cancelButton;
private ProgressDialog progressDialog;
private AccountAuthenticator accountAuthenticator;
@Override
public void onCreate(Bundle savedInstanceState) {
@ -56,7 +63,7 @@ public class AccountActivity extends AccountAuthenticatorActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.sync_account);
mContext = getApplicationContext();
Log.d(LOG_TAG, "AccountManager.get(" + mContext + ")");
Logger.debug(LOG_TAG, "AccountManager.get(" + mContext + ")");
mAccountManager = AccountManager.get(mContext);
// Set "screen on" flag.
@ -78,18 +85,20 @@ public class AccountActivity extends AccountAuthenticatorActivity {
serverInput.addTextChangedListener(inputValidator);
connectButton = (Button) findViewById(R.id.accountConnectButton);
cancelButton = (Button) findViewById(R.id.accountCancelButton);
serverCheckbox = (CheckBox) findViewById(R.id.checkbox_server);
serverCheckbox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
Log.i(LOG_TAG, "Toggling checkbox: " + isChecked);
// Hack for pre-3.0 Android: can enter text into disabled EditText.
Logger.info(LOG_TAG, "Toggling checkbox: " + isChecked);
if (!isChecked) { // Clear server input.
serverInput.setVisibility(View.GONE);
findViewById(R.id.server_error).setVisibility(View.GONE);
serverInput.setText("");
} else {
serverInput.setVisibility(View.VISIBLE);
serverInput.setEnabled(true);
}
// Activate connectButton if necessary.
activateView(connectButton, validateInputs());
@ -101,12 +110,32 @@ public class AccountActivity extends AccountAuthenticatorActivity {
public void onResume() {
super.onResume();
clearCredentials();
usernameInput.requestFocus();
cancelButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
cancelClickHandler(v);
}
});
}
public void cancelClickHandler(View target) {
finish();
}
public void cancelConnectHandler(View target) {
if (accountAuthenticator != null) {
accountAuthenticator.isCanceled = true;
accountAuthenticator = null;
}
displayVerifying(false);
activateView(connectButton, true);
clearCredentials();
usernameInput.requestFocus();
}
private void clearCredentials() {
// Only clear password. Re-typing the sync key or email is annoying.
passwordInput.setText("");
@ -116,13 +145,11 @@ public class AccountActivity extends AccountAuthenticatorActivity {
* accessed by Fennec and Sync Service.
*/
public void connectClickHandler(View target) {
Log.d(LOG_TAG, "connectClickHandler for view " + target);
enableCredEntry(false);
Logger.debug(LOG_TAG, "connectClickHandler for view " + target);
// Validate sync key format.
try {
key = ActivityUtils.validateSyncKey(synckeyInput.getText().toString());
} catch (InvalidSyncKeyException e) {
enableCredEntry(true);
// Toast: invalid sync key format.
Toast toast = Toast.makeText(mContext, R.string.sync_new_recoverykey_status_incorrect, Toast.LENGTH_LONG);
toast.show();
@ -130,26 +157,42 @@ public class AccountActivity extends AccountAuthenticatorActivity {
}
username = usernameInput.getText().toString().toLowerCase(Locale.US);
password = passwordInput.getText().toString();
key = synckeyInput.getText().toString();
server = Constants.AUTH_NODE_DEFAULT;
if (serverCheckbox.isChecked()) {
server = serverInput.getText().toString();
String userServer = serverInput.getText().toString();
if (userServer != null) {
userServer = userServer.trim();
if (userServer.length() != 0) {
if (!userServer.startsWith("https://") &&
!userServer.startsWith("http://")) {
// Assume HTTPS if not specified.
userServer = "https://" + userServer;
serverInput.setText(userServer);
}
server = userServer;
}
}
}
// TODO : Authenticate with Sync Service, once implemented, with
// onAuthSuccess as callback
clearErrors();
displayVerifying(true);
cancelButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
cancelConnectHandler(v);
// Set cancel click handler to leave account setup.
cancelButton.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
cancelClickHandler(v);
}
});
}
});
authCallback();
}
/* Helper UI functions */
private void enableCredEntry(boolean toEnable) {
usernameInput.setEnabled(toEnable);
passwordInput.setEnabled(toEnable);
synckeyInput.setEnabled(toEnable);
if (!toEnable) {
serverInput.setEnabled(toEnable);
} else {
serverInput.setEnabled(serverCheckbox.isChecked());
}
accountAuthenticator = new AccountAuthenticator(this);
accountAuthenticator.authenticate(server, username, password);
}
private TextWatcher makeInputValidator() {
@ -185,13 +228,18 @@ public class AccountActivity extends AccountAuthenticatorActivity {
/*
* Callback that handles auth based on success/failure
*/
private void authCallback() {
// Create and add account to AccountManager
// TODO: only allow one account to be added?
final SyncAccountParameters syncAccount = new SyncAccountParameters(mContext, mAccountManager,
username, key, password, server);
public void authCallback(final AuthenticationResult result) {
displayVerifying(false);
if (result != AuthenticationResult.SUCCESS) {
Logger.debug(LOG_TAG, "displayFailure()");
displayFailure(result);
return;
}
// Successful authentication. Create and add account to AccountManager.
final SyncAccountParameters syncAccount = new SyncAccountParameters(
mContext, mAccountManager, username, key, password, server);
final Account account = SyncAccounts.createSyncAccount(syncAccount);
final boolean result = (account != null);
final boolean accountResult = (account != null);
final Intent intent = new Intent(); // The intent to return.
intent.putExtra(AccountManager.KEY_ACCOUNT_NAME, syncAccount.username);
@ -199,39 +247,77 @@ public class AccountActivity extends AccountAuthenticatorActivity {
intent.putExtra(AccountManager.KEY_AUTHTOKEN, Constants.ACCOUNTTYPE_SYNC);
setAccountAuthenticatorResult(intent.getExtras());
if (!result) {
if (!accountResult) {
// Failed to add account!
setResult(RESULT_CANCELED, intent);
runOnUiThread(new Runnable() {
@Override
public void run() {
authFailure();
// Use default error.
// TODO: Display more accurate error (Account failed to be created).
Logger.debug(LOG_TAG, "displayFailure()");
displayFailure(result);
}
});
return;
}
// TODO: Currently, we do not actually authenticate username/pass against
// Moz sync server.
clearErrors();
if (intent != null) {
setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
// Successfully added account.
setResult(RESULT_OK, intent);
runOnUiThread(new Runnable() {
@Override
public void run() {
authSuccess();
}
});
return;
}
}
private void displayVerifying(final boolean isVerifying) {
if (isVerifying) {
progressDialog = ProgressDialog.show(AccountActivity.this, "", getString(R.string.sync_verifying_label), true);
} else {
progressDialog.dismiss();
}
}
private void displayFailure(final AuthenticationResult result) {
runOnUiThread(new Runnable() {
@Override
public void run() {
authSuccess();
switch (result) {
case FAILURE_USERNAME:
// No such username. Don't leak whether the username exists.
case FAILURE_PASSWORD:
findViewById(R.id.cred_error).setVisibility(View.VISIBLE);
usernameInput.requestFocus();
break;
case FAILURE_SERVER:
findViewById(R.id.server_error).setVisibility(View.VISIBLE);
serverInput.requestFocus();
break;
case FAILURE_OTHER:
default:
// Display default error screen.
Logger.debug(LOG_TAG, "displaying default failure.");
Intent intent = new Intent(mContext, SetupFailureActivity.class);
intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
startActivity(intent);
}
}
});
return;
}
private void authFailure() {
enableCredEntry(true);
Intent intent = new Intent(mContext, SetupFailureActivity.class);
intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
startActivity(intent);
}
private void authSuccess() {
/**
* Feedback to user of account setup success.
*/
public void authSuccess() {
// Display feedback of successful account setup.
Intent intent = new Intent(mContext, SetupSuccessActivity.class);
intent.setFlags(Constants.FLAG_ACTIVITY_REORDER_TO_FRONT_NO_ANIMATION);
startActivity(intent);
@ -242,4 +328,14 @@ public class AccountActivity extends AccountAuthenticatorActivity {
view.setEnabled(toActivate);
view.setClickable(toActivate);
}
private void clearErrors() {
runOnUiThread(new Runnable() {
@Override
public void run() {
findViewById(R.id.cred_error).setVisibility(View.GONE);
findViewById(R.id.server_error).setVisibility(View.GONE);
}
});
}
}

View File

@ -36,6 +36,7 @@ public class SetupFailureActivity extends Activity {
public void cancelClickHandler(View target) {
setResult(RESULT_CANCELED);
moveTaskToBack(true);
finish();
}
}

View File

@ -0,0 +1,108 @@
/* 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.setup.auth;
import java.util.LinkedList;
import java.util.Queue;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.setup.activities.AccountActivity;
import android.util.Log;
public class AccountAuthenticator {
private final String LOG_TAG = "AccountAuthenticator";
private AccountActivity activityCallback;
private Queue<AuthenticatorStage> stages;
// Values for authentication.
public String password;
public String username;
public String authServer;
public String nodeServer;
public boolean isSuccess = false;
public boolean isCanceled = false;
public AccountAuthenticator(AccountActivity activity) {
activityCallback = activity;
prepareStages();
}
private void prepareStages() {
stages = new LinkedList<AuthenticatorStage>();
stages.add(new EnsureUserExistenceStage());
stages.add(new FetchUserNodeStage());
stages.add(new AuthenticateAccountStage());
}
public void authenticate(String server, String account, String password) {
// Set authentication values.
if (!server.endsWith("/")) {
server += "/";
}
nodeServer = server;
this.password = password;
// Calculate and save username hash.
try {
username = KeyBundle.usernameFromAccount(account);
} catch (Exception e) {
abort(AuthenticationResult.FAILURE_OTHER, e);
return;
}
Logger.debug(LOG_TAG, "username:" + username);
Log.d(LOG_TAG, "running first stage.");
// Start first stage of authentication.
runNextStage();
}
/**
* Run next stage of authentication.
*/
public void runNextStage() {
if (isCanceled) {
return;
}
if (stages.size() == 0) {
Logger.debug(LOG_TAG, "Authentication completed.");
activityCallback.authCallback(isSuccess ? AuthenticationResult.SUCCESS : AuthenticationResult.FAILURE_PASSWORD);
return;
}
AuthenticatorStage nextStage = stages.remove();
try {
nextStage.execute(this);
} catch (Exception e) {
Logger.warn(LOG_TAG, "Unhandled exception in stage " + nextStage);
abort(AuthenticationResult.FAILURE_OTHER, e);
}
}
/**
* Abort authentication.
*
* @param e
* Exception causing abort.
* @param reason
* Reason for abort.
*/
public void abort(AuthenticationResult result, Exception e) {
if (isCanceled) {
return;
}
Logger.warn(LOG_TAG, "Authentication failed.", e);
activityCallback.authCallback(result);
}
/* Helper functions */
public static void runOnThread(Runnable run) {
ThreadPool.run(run);
}
}

View File

@ -0,0 +1,175 @@
/* 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.setup.auth;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import org.mozilla.apache.commons.codec.binary.Base64;
import org.mozilla.gecko.sync.GlobalConstants;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
import org.mozilla.gecko.sync.setup.Constants;
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 AuthenticateAccountStage implements AuthenticatorStage {
private final String LOG_TAG = "AuthAccountStage";
private HttpRequestBase httpRequest = null;
public interface AuthenticateAccountStageDelegate {
public void handleSuccess(boolean isSuccess);
public void handleFailure(HttpResponse response);
public void handleError(Exception e);
}
@Override
public void execute(final AccountAuthenticator aa) throws URISyntaxException, UnsupportedEncodingException {
AuthenticateAccountStageDelegate callbackDelegate = new AuthenticateAccountStageDelegate() {
@Override
public void handleSuccess(boolean isSuccess) {
aa.isSuccess = isSuccess;
aa.runNextStage();
}
@Override
public void handleFailure(HttpResponse response) {
Logger.debug(LOG_TAG, "handleFailure");
aa.abort(AuthenticationResult.FAILURE_OTHER, new Exception(response.getStatusLine().getStatusCode() + " error."));
if (response.getEntity() == null) {
// No cleanup necessary.
return;
}
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
Logger.warn(LOG_TAG, "content: " + reader.readLine());
BaseResource.consumeReader(reader);
} catch (IllegalStateException e) {
Logger.debug(LOG_TAG, "Error reading content.", e);
} catch (RuntimeException e) {
Logger.debug(LOG_TAG, "Unexpected exception.", e);
if (httpRequest != null) {
httpRequest.abort();
}
} catch (IOException e) {
Logger.debug(LOG_TAG, "Error reading content.", e);
}
}
@Override
public void handleError(Exception e) {
Logger.debug(LOG_TAG, "handleError", e);
aa.abort(AuthenticationResult.FAILURE_OTHER, e);
}
};
// Calculate BasicAuth hash of username/password.
String authHeader = makeAuthHeader(aa.username, aa.password);
String authRequestUrl = makeAuthRequestUrl(aa.authServer, aa.username);
Logger.trace(LOG_TAG, "Making auth request to: " + authRequestUrl);
authenticateAccount(callbackDelegate, authRequestUrl, authHeader);
}
/**
* Makes an authentication request to the server and passes appropriate response back to callback.
* @param callbackDelegate
* Delegate to deal with HTTP response.
* @param authRequestUrl
* @param authHeader
* @throws URISyntaxException
*/
// Made public for testing.
public void authenticateAccount(final AuthenticateAccountStageDelegate callbackDelegate, final String authRequestUrl, final String authHeader) throws URISyntaxException {
final BaseResource httpResource = new BaseResource(authRequestUrl);
httpResource.delegate = new SyncResourceDelegate(httpResource) {
@Override
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
// Make reference to request, to abort if necessary.
httpRequest = request;
client.log.enableDebug(true);
request.setHeader(new BasicHeader("User-Agent", GlobalConstants.USER_AGENT));
// Host header is not set for some reason, so do it explicitly.
try {
URI authServerUri = new URI(authRequestUrl);
request.setHeader(new BasicHeader("Host", authServerUri.getHost()));
} catch (URISyntaxException e) {
Logger.error(LOG_TAG, "Malformed uri, will be caught elsewhere.", e);
}
request.setHeader(new BasicHeader("Authorization", authHeader));
}
@Override
public void handleHttpResponse(HttpResponse response) {
int statusCode = response.getStatusLine().getStatusCode();
try {
switch (statusCode) {
case 200:
callbackDelegate.handleSuccess(true);
break;
case 401:
callbackDelegate.handleSuccess(false);
break;
default:
callbackDelegate.handleFailure(response);
}
} finally {
BaseResource.consumeEntity(response.getEntity());
Logger.info(LOG_TAG, "Released entity.");
}
}
@Override
public void handleHttpProtocolException(ClientProtocolException e) {
Logger.error(LOG_TAG, "Client protocol error.", e);
callbackDelegate.handleError(e);
}
@Override
public void handleHttpIOException(IOException e) {
Logger.error(LOG_TAG, "I/O exception.");
callbackDelegate.handleError(e);
}
@Override
public void handleTransportException(GeneralSecurityException e) {
Logger.error(LOG_TAG, "Transport exception.");
callbackDelegate.handleError(e);
}
};
AccountAuthenticator.runOnThread(new Runnable() {
@Override
public void run() {
httpResource.get();
}
});
}
public String makeAuthHeader(String usernameHash, String password) {
try {
return "Basic " + Base64.encodeBase64String((usernameHash + ":" + password).getBytes("UTF-8"));
} catch (UnsupportedEncodingException e) {
Logger.debug(LOG_TAG, "Unsupported encoding: UTF-8.");
return null;
}
}
public String makeAuthRequestUrl(String authServer, String usernameHash) {
return authServer + Constants.AUTH_SERVER_VERSION + usernameHash + "/" + Constants.AUTH_SERVER_SUFFIX;
}
}

View File

@ -0,0 +1,9 @@
/* 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.setup.auth;
public enum AuthenticationResult {
SUCCESS, FAILURE_USERNAME, FAILURE_PASSWORD, FAILURE_SERVER, FAILURE_OTHER
}

View File

@ -0,0 +1,12 @@
/* 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.setup.auth;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
public interface AuthenticatorStage {
public void execute(AccountAuthenticator aa) throws URISyntaxException, UnsupportedEncodingException;
}

View File

@ -0,0 +1,117 @@
/* 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.setup.auth;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
import org.mozilla.gecko.sync.setup.Constants;
import android.util.Log;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
public class EnsureUserExistenceStage implements AuthenticatorStage {
private final String LOG_TAG = "EnsureUserExistence";
public interface EnsureUserExistenceStageDelegate {
public void handleSuccess();
public void handleFailure(AuthenticationResult result);
public void handleError(Exception e);
}
@Override
public void execute(final AccountAuthenticator aa) throws URISyntaxException,
UnsupportedEncodingException {
final EnsureUserExistenceStageDelegate callbackDelegate = new EnsureUserExistenceStageDelegate() {
@Override
public void handleSuccess() {
// User exists; now determine auth node.
Log.d(LOG_TAG, "handleSuccess()");
aa.runNextStage();
}
@Override
public void handleFailure(AuthenticationResult result) {
aa.abort(result, new Exception("Failure in EnsureUser"));
}
@Override
public void handleError(Exception e) {
Logger.info(LOG_TAG, "Error checking for user existence.");
aa.abort(AuthenticationResult.FAILURE_SERVER, e);
}
};
String userRequestUrl = aa.nodeServer + Constants.AUTH_NODE_PATHNAME + Constants.AUTH_NODE_VERSION + aa.username;
final BaseResource httpResource = new BaseResource(userRequestUrl);
httpResource.delegate = new SyncResourceDelegate(httpResource) {
@Override
public void handleHttpResponse(HttpResponse response) {
int statusCode = response.getStatusLine().getStatusCode();
switch(statusCode) {
case 200:
try {
InputStream content = response.getEntity().getContent();
BufferedReader reader = new BufferedReader(new InputStreamReader(content, "UTF-8"), 1024);
String inUse = reader.readLine();
BaseResource.consumeReader(reader);
reader.close();
// Removed Logger.debug inUse, because stalling.
if (inUse.equals("1")) { // Username exists.
callbackDelegate.handleSuccess();
} else { // User does not exist.
Logger.info(LOG_TAG, "No such user.");
callbackDelegate.handleFailure(AuthenticationResult.FAILURE_USERNAME);
}
} catch (Exception e) {
Logger.error(LOG_TAG, "Failure in content parsing.", e);
callbackDelegate.handleFailure(AuthenticationResult.FAILURE_OTHER);
}
break;
default: // No other response is acceptable.
callbackDelegate.handleFailure(AuthenticationResult.FAILURE_OTHER);
}
Logger.debug(LOG_TAG, "Consuming entity.");
BaseResource.consumeEntity(response.getEntity());
}
@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);
}
};
// Make request.
AccountAuthenticator.runOnThread(new Runnable() {
@Override
public void run() {
httpResource.get();
}
});
}
}

View File

@ -0,0 +1,126 @@
/* 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.setup.auth;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.SyncResourceDelegate;
import org.mozilla.gecko.sync.setup.Constants;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
public class FetchUserNodeStage implements AuthenticatorStage {
private final String LOG_TAG = "FetchUserNodeStage";
public interface FetchNodeStageDelegate {
public void handleSuccess(String url);
public void handleFailure(HttpResponse response);
public void handleError(Exception e);
}
@Override
public void execute(final AccountAuthenticator aa) throws URISyntaxException {
FetchNodeStageDelegate callbackDelegate = new FetchNodeStageDelegate() {
@Override
public void handleSuccess(String server) {
if (server == null) { // No separate auth node; use server url.
Logger.debug(LOG_TAG, "Using server as auth node.");
aa.authServer = aa.nodeServer;
aa.runNextStage();
return;
}
if (!server.endsWith("/")) {
server += "/";
}
aa.authServer = server;
aa.runNextStage();
}
@Override
public void handleFailure(HttpResponse response) {
int statusCode = response.getStatusLine().getStatusCode();
Logger.debug(LOG_TAG, "Failed to fetch user node, with status " + statusCode);
aa.abort(AuthenticationResult.FAILURE_OTHER, new Exception("HTTP " + statusCode + " error."));
}
@Override
public void handleError(Exception e) {
Logger.debug(LOG_TAG, "Error in fetching node.");
aa.abort(AuthenticationResult.FAILURE_OTHER, e);
}
};
String nodeRequestUrl = aa.nodeServer + Constants.AUTH_NODE_PATHNAME + Constants.AUTH_NODE_VERSION + aa.username + "/" + Constants.AUTH_NODE_SUFFIX;
Logger.debug(LOG_TAG, "nodeUrl: " + nodeRequestUrl);
final BaseResource httpResource = makeFetchNodeRequest(callbackDelegate, nodeRequestUrl);
// Make request on separate thread.
AccountAuthenticator.runOnThread(new Runnable() {
@Override
public void run() {
httpResource.get();
}
});
}
private BaseResource makeFetchNodeRequest(final FetchNodeStageDelegate callbackDelegate, String fetchNodeUrl) throws URISyntaxException {
// Fetch node containing user.
final BaseResource httpResource = new BaseResource(fetchNodeUrl);
httpResource.delegate = new SyncResourceDelegate(httpResource) {
@Override
public void handleHttpResponse(HttpResponse response) {
int statusCode = response.getStatusLine().getStatusCode();
switch(statusCode) {
case 200:
try {
InputStream content = response.getEntity().getContent();
BufferedReader reader = new BufferedReader(new InputStreamReader(content, "UTF-8"), 1024);
String server = reader.readLine();
callbackDelegate.handleSuccess(server);
BaseResource.consumeReader(reader);
reader.close();
} catch (IllegalStateException e) {
callbackDelegate.handleError(e);
} catch (IOException e) {
callbackDelegate.handleError(e);
}
break;
case 404: // Does not support auth nodes, use server instead.
callbackDelegate.handleSuccess(null);
break;
default:
// No other acceptable states.
callbackDelegate.handleFailure(response);
}
BaseResource.consumeEntity(response.getEntity());
}
@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);
}
};
return httpResource;
}
}

View File

@ -168,6 +168,12 @@ sync/setup/activities/ActivityUtils.java
sync/setup/activities/SetupFailureActivity.java
sync/setup/activities/SetupSuccessActivity.java
sync/setup/activities/SetupSyncActivity.java
sync/setup/auth/AccountAuthenticator.java
sync/setup/auth/AuthenticateAccountStage.java
sync/setup/auth/AuthenticationResult.java
sync/setup/auth/AuthenticatorStage.java
sync/setup/auth/EnsureUserExistenceStage.java
sync/setup/auth/FetchUserNodeStage.java
sync/setup/Constants.java
sync/setup/InvalidSyncKeyException.java
sync/setup/SyncAccounts.java

View File

@ -73,4 +73,7 @@
<string name="sync_new_tab">&new_tab;</string>
<!-- Incorrect settings and changing credentials. -->
<string name="sync.new.recoverykey.status.incorrect">&sync.new.recoverykey.status.incorrect;</string>
<string name="sync_invalidcreds_label">&sync.invalidcreds.label;</string>
<string name="sync_invalidserver_label">&sync.invalidserver.label;</string>
<string name="sync_verifying_label">&sync.verifying.label;</string>
<string name="sync_new_recoverykey_status_incorrect">&sync.new.recoverykey.status.incorrect;</string>