1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 package org.mozilla.gecko.fxa.devices; 6 7 import android.content.Context; 8 import android.content.Intent; 9 import android.support.annotation.NonNull; 10 import android.support.annotation.Nullable; 11 import android.support.annotation.VisibleForTesting; 12 import android.text.TextUtils; 13 import android.util.Log; 14 15 import org.mozilla.gecko.background.common.log.Logger; 16 import org.mozilla.gecko.background.fxa.FxAccountClient; 17 import org.mozilla.gecko.background.fxa.FxAccountClient20; 18 import org.mozilla.gecko.background.fxa.FxAccountClient20.AccountStatusResponse; 19 import org.mozilla.gecko.background.fxa.FxAccountClient20.RequestDelegate; 20 import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException; 21 import org.mozilla.gecko.background.fxa.FxAccountRemoteError; 22 import org.mozilla.gecko.background.fxa.FxAccountUtils; 23 import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount; 24 import org.mozilla.gecko.fxa.login.State; 25 import org.mozilla.gecko.sync.SharedPreferencesClientsDataDelegate; 26 import org.mozilla.gecko.util.BundleEventListener; 27 import org.mozilla.gecko.util.EventCallback; 28 import org.mozilla.gecko.util.GeckoBundle; 29 30 import java.io.IOException; 31 import java.io.UnsupportedEncodingException; 32 import java.lang.ref.WeakReference; 33 import java.lang.reflect.InvocationTargetException; 34 import java.lang.reflect.Method; 35 import java.security.GeneralSecurityException; 36 import java.util.concurrent.ExecutorService; 37 import java.util.concurrent.Executors; 38 39 /* This class provides a way to register the current device against FxA 40 * and also stores the registration details in the Android FxAccount. 41 * This should be used in a state where we possess a sessionToken, most likely the Engaged/Married states. 42 */ 43 public class FxAccountDeviceRegistrator implements BundleEventListener { 44 private static final String LOG_TAG = "FxADeviceRegistrator"; 45 46 // The autopush endpoint expires stale channel subscriptions every 30 days (at a set time during 47 // the month, although we don't depend on this). To avoid the FxA service channel silently 48 // expiring from underneath us, we unsubscribe and resubscribe every 21 days. 49 // Note that this simple schedule means that we might unsubscribe perfectly valid (but old) 50 // subscriptions. This will be improved as part of Bug 1345651. 51 @VisibleForTesting 52 static final long TIME_BETWEEN_CHANNEL_REGISTRATION_IN_MILLIS = 21 * 24 * 60 * 60 * 1000L; 53 54 @VisibleForTesting 55 static final long RETRY_TIME_AFTER_GCM_DISABLED_ERROR = 15 * 24 * 60 * 60 * 1000L; 56 57 58 public static final String PUSH_SUBSCRIPTION_REPLY_BUNDLE_KEY_ERROR = "error"; 59 @VisibleForTesting 60 static final long ERROR_GCM_DISABLED = 2154627078L; // = NS_ERROR_DOM_PUSH_GCM_DISABLED 61 62 // The current version of the device registration, we use this to re-register 63 // devices after we update what we send on device registration. 64 @VisibleForTesting 65 static final Integer DEVICE_REGISTRATION_VERSION = 2; 66 67 private static FxAccountDeviceRegistrator instance; 68 private final WeakReference<Context> context; 69 FxAccountDeviceRegistrator(Context appContext)70 private FxAccountDeviceRegistrator(Context appContext) { 71 this.context = new WeakReference<>(appContext); 72 } 73 getInstance(Context appContext)74 private static FxAccountDeviceRegistrator getInstance(Context appContext) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException { 75 if (instance == null) { 76 final FxAccountDeviceRegistrator tempInstance = new FxAccountDeviceRegistrator(appContext); 77 tempInstance.setupListeners(); // Set up listener for FxAccountPush:Subscribe:Response 78 instance = tempInstance; 79 } 80 return instance; 81 } 82 shouldRegister(final AndroidFxAccount fxAccount)83 public static boolean shouldRegister(final AndroidFxAccount fxAccount) { 84 if (fxAccount.getDeviceRegistrationVersion() != FxAccountDeviceRegistrator.DEVICE_REGISTRATION_VERSION || 85 TextUtils.isEmpty(fxAccount.getDeviceId())) { 86 return true; 87 } 88 // At this point, we have a working up-to-date registration, but it might be a partial one 89 // (no push registration). 90 return fxAccount.getDevicePushRegistrationError() == ERROR_GCM_DISABLED && 91 (System.currentTimeMillis() - fxAccount.getDevicePushRegistrationErrorTime()) > RETRY_TIME_AFTER_GCM_DISABLED_ERROR; 92 } 93 shouldRenewRegistration(final AndroidFxAccount fxAccount)94 public static boolean shouldRenewRegistration(final AndroidFxAccount fxAccount) { 95 final long deviceRegistrationTimestamp = fxAccount.getDeviceRegistrationTimestamp(); 96 // NB: we're comparing wall clock to wall clock, at different points in time. 97 // It's possible that wall clocks have changed, and our comparison will be meaningless. 98 // However, this happens in the context of a sync, and we won't be able to sync anyways if our 99 // wall clock deviates too much from time on the server. 100 return (System.currentTimeMillis() - deviceRegistrationTimestamp) > TIME_BETWEEN_CHANNEL_REGISTRATION_IN_MILLIS; 101 } 102 register(Context context)103 public static void register(Context context) { 104 final Context appContext = context.getApplicationContext(); 105 try { 106 getInstance(appContext).beginRegistration(appContext); 107 } catch (Exception e) { 108 Log.e(LOG_TAG, "Could not start FxA device registration", e); 109 } 110 } 111 renewRegistration(Context context)112 public static void renewRegistration(Context context) { 113 final Context appContext = context.getApplicationContext(); 114 try { 115 getInstance(appContext).beginRegistrationRenewal(appContext); 116 } catch (Exception e) { 117 Log.e(LOG_TAG, "Could not start FxA device re-registration", e); 118 } 119 } 120 beginRegistration(Context context)121 private void beginRegistration(Context context) { 122 // Fire up gecko and send event 123 // We create the Intent ourselves instead of using GeckoService.getIntentToCreateServices 124 // because we can't import these modules (circular dependency between browser and services) 125 final Intent geckoIntent = buildCreatePushServiceIntent(context, "android-fxa-subscribe"); 126 context.startService(geckoIntent); 127 // -> handleMessage() 128 } 129 beginRegistrationRenewal(Context context)130 private void beginRegistrationRenewal(Context context) { 131 // Same as registration, but unsubscribe first to get a fresh subscription. 132 final Intent geckoIntent = buildCreatePushServiceIntent(context, "android-fxa-resubscribe"); 133 context.startService(geckoIntent); 134 // -> handleMessage() 135 } 136 buildCreatePushServiceIntent(final Context context, final String data)137 private Intent buildCreatePushServiceIntent(final Context context, final String data) { 138 final Intent intent = new Intent(); 139 intent.setAction("create-services"); 140 intent.setClassName(context, "org.mozilla.gecko.GeckoService"); 141 intent.putExtra("category", "android-push-service"); 142 intent.putExtra("data", data); 143 final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context); 144 intent.putExtra("org.mozilla.gecko.intent.PROFILE_NAME", fxAccount.getProfile()); 145 return intent; 146 } 147 148 @Override handleMessage(String event, GeckoBundle message, EventCallback callback)149 public void handleMessage(String event, GeckoBundle message, EventCallback callback) { 150 if ("FxAccountsPush:Subscribe:Response".equals(event)) { 151 handlePushSubscriptionResponse(message); 152 } else { 153 Log.e(LOG_TAG, "No action defined for " + event); 154 } 155 } 156 handlePushSubscriptionResponse(final GeckoBundle message)157 private void handlePushSubscriptionResponse(final GeckoBundle message) { 158 // Make sure the context has not been gc'd during the push registration 159 // and the FxAccount still exists. 160 final Context context = this.context.get(); 161 if (context == null) { 162 throw new IllegalStateException("Application context has been gc'ed"); 163 } 164 final AndroidFxAccount fxAccount = AndroidFxAccount.fromContext(context); 165 if (fxAccount == null) { 166 Log.e(LOG_TAG, "AndroidFxAccount is null"); 167 return; 168 } 169 170 fxAccount.resetDevicePushRegistrationError(); 171 final long error = getSubscriptionReplyError(message); 172 173 final FxAccountDevice device; 174 if (error == 0L) { 175 Log.i(LOG_TAG, "Push registration succeeded. Beginning normal FxA Registration."); 176 device = buildFxAccountDevice(context, fxAccount, message.getBundle("subscription")); 177 } else { 178 fxAccount.setDevicePushRegistrationError(error, System.currentTimeMillis()); 179 Log.i(LOG_TAG, "Push registration failed. Beginning degraded FxA Registration."); 180 device = buildFxAccountDevice(context, fxAccount); 181 } 182 183 doFxaRegistration(context, fxAccount, device, true); 184 } 185 getSubscriptionReplyError(final GeckoBundle message)186 private long getSubscriptionReplyError(final GeckoBundle message) { 187 String errorStr = message.getString(PUSH_SUBSCRIPTION_REPLY_BUNDLE_KEY_ERROR); 188 if (TextUtils.isEmpty(errorStr)) { 189 return 0L; 190 } 191 return Long.parseLong(errorStr); 192 } 193 doFxaRegistration(final Context context, final AndroidFxAccount fxAccount, final FxAccountDevice device, final boolean allowRecursion)194 private static void doFxaRegistration(final Context context, final AndroidFxAccount fxAccount, 195 final FxAccountDevice device, final boolean allowRecursion) { 196 final byte[] sessionToken; 197 try { 198 sessionToken = fxAccount.getState().getSessionToken(); 199 } catch (State.NotASessionTokenState e) { 200 Log.e(LOG_TAG, "Could not get a session token", e); 201 return; 202 } 203 204 if (device.id == null) { 205 Log.i(LOG_TAG, "Attempting registration for a new device"); 206 } else { 207 Log.i(LOG_TAG, "Attempting registration for an existing device"); 208 } 209 210 final ExecutorService executor = Executors.newSingleThreadExecutor(); // Not called often, it's okay to spawn another thread 211 final FxAccountClient20 fxAccountClient = 212 new FxAccountClient20(fxAccount.getAccountServerURI(), executor); 213 fxAccountClient.registerOrUpdateDevice(sessionToken, device, new RequestDelegate<FxAccountDevice>() { 214 @Override 215 public void handleError(Exception e) { 216 Log.e(LOG_TAG, "Error while updating a device registration: ", e); 217 fxAccount.setDeviceRegistrationTimestamp(0L); 218 } 219 220 @Override 221 public void handleFailure(FxAccountClientRemoteException error) { 222 Log.e(LOG_TAG, "Error while updating a device registration: ", error); 223 224 fxAccount.setDeviceRegistrationTimestamp(0L); 225 226 if (error.httpStatusCode == 400) { 227 if (error.apiErrorNumber == FxAccountRemoteError.UNKNOWN_DEVICE) { 228 recoverFromUnknownDevice(fxAccount); 229 } else if (error.apiErrorNumber == FxAccountRemoteError.DEVICE_SESSION_CONFLICT) { 230 // This can happen if a device was already registered using our session token, and we 231 // tried to create a new one (no id field). 232 recoverFromDeviceSessionConflict(error, fxAccountClient, sessionToken, fxAccount, device, 233 context, allowRecursion); 234 } 235 } else 236 if (error.httpStatusCode == 401 237 && error.apiErrorNumber == FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN) { 238 handleTokenError(error, fxAccountClient, fxAccount); 239 } else { 240 logErrorAndResetDeviceRegistrationVersionAndTimestamp(error, fxAccount); 241 } 242 } 243 244 @Override 245 public void handleSuccess(FxAccountDevice result) { 246 Log.i(LOG_TAG, "Device registration complete"); 247 Logger.pii(LOG_TAG, "Registered device ID: " + result.id); 248 Log.i(LOG_TAG, "Setting DEVICE_REGISTRATION_VERSION to " + DEVICE_REGISTRATION_VERSION); 249 fxAccount.setFxAUserData(result.id, DEVICE_REGISTRATION_VERSION, System.currentTimeMillis()); 250 } 251 }); 252 } 253 buildFxAccountDevice(Context context, AndroidFxAccount fxAccount)254 private static FxAccountDevice buildFxAccountDevice(Context context, AndroidFxAccount fxAccount) { 255 return makeFxADeviceCommonBuilder(context, fxAccount).build(); 256 } 257 buildFxAccountDevice(Context context, AndroidFxAccount fxAccount, @NonNull GeckoBundle subscription)258 private static FxAccountDevice buildFxAccountDevice(Context context, AndroidFxAccount fxAccount, @NonNull GeckoBundle subscription) { 259 final FxAccountDevice.Builder builder = makeFxADeviceCommonBuilder(context, fxAccount); 260 final String pushCallback = subscription.getString("pushCallback"); 261 final String pushPublicKey = subscription.getString("pushPublicKey"); 262 final String pushAuthKey = subscription.getString("pushAuthKey"); 263 if (!TextUtils.isEmpty(pushCallback) && !TextUtils.isEmpty(pushPublicKey) && 264 !TextUtils.isEmpty(pushAuthKey)) { 265 builder.pushCallback(pushCallback); 266 builder.pushPublicKey(pushPublicKey); 267 builder.pushAuthKey(pushAuthKey); 268 } 269 return builder.build(); 270 } 271 272 // Do not call this directly, use buildFxAccountDevice instead. makeFxADeviceCommonBuilder(Context context, AndroidFxAccount fxAccount)273 private static FxAccountDevice.Builder makeFxADeviceCommonBuilder(Context context, AndroidFxAccount fxAccount) { 274 final String deviceId = fxAccount.getDeviceId(); 275 final String clientName = getClientName(fxAccount, context); 276 277 final FxAccountDevice.Builder builder = new FxAccountDevice.Builder(); 278 builder.name(clientName); 279 builder.type("mobile"); 280 if (!TextUtils.isEmpty(deviceId)) { 281 builder.id(deviceId); 282 } 283 return builder; 284 } 285 logErrorAndResetDeviceRegistrationVersionAndTimestamp( final FxAccountClientRemoteException error, final AndroidFxAccount fxAccount)286 private static void logErrorAndResetDeviceRegistrationVersionAndTimestamp( 287 final FxAccountClientRemoteException error, final AndroidFxAccount fxAccount) { 288 Log.e(LOG_TAG, "Device registration failed", error); 289 fxAccount.resetDeviceRegistrationVersion(); 290 fxAccount.setDeviceRegistrationTimestamp(0L); 291 } 292 getClientName(final AndroidFxAccount fxAccount, final Context context)293 private static String getClientName(final AndroidFxAccount fxAccount, final Context context) { 294 try { 295 final SharedPreferencesClientsDataDelegate clientsDataDelegate = 296 new SharedPreferencesClientsDataDelegate(fxAccount.getSyncPrefs(), context); 297 return clientsDataDelegate.getClientName(); 298 } catch (Exception e) { 299 Log.e(LOG_TAG, "Unable to get client name.", e); 300 // It's possible we're racing against account pickler. 301 // In either case, it should be always safe to perform registration using our default name. 302 return FxAccountUtils.defaultClientName(context); 303 } 304 } 305 handleTokenError(final FxAccountClientRemoteException error, final FxAccountClient fxAccountClient, final AndroidFxAccount fxAccount)306 private static void handleTokenError(final FxAccountClientRemoteException error, 307 final FxAccountClient fxAccountClient, 308 final AndroidFxAccount fxAccount) { 309 Log.i(LOG_TAG, "Recovering from invalid token error: ", error); 310 logErrorAndResetDeviceRegistrationVersionAndTimestamp(error, fxAccount); 311 fxAccountClient.accountStatus(fxAccount.getState().uid, 312 new RequestDelegate<AccountStatusResponse>() { 313 @Override 314 public void handleError(Exception e) { 315 } 316 317 @Override 318 public void handleFailure(FxAccountClientRemoteException e) { 319 } 320 321 @Override 322 public void handleSuccess(AccountStatusResponse result) { 323 final State doghouseState = fxAccount.getState().makeDoghouseState(); 324 if (!result.exists) { 325 Log.i(LOG_TAG, "token invalidated because the account no longer exists"); 326 // TODO: Should be in a "I have an Android account, but the FxA is gone." State. 327 // This will do for now.. 328 fxAccount.setState(doghouseState); 329 return; 330 } 331 Log.e(LOG_TAG, "sessionToken invalid"); 332 fxAccount.setState(doghouseState); 333 } 334 }); 335 } 336 recoverFromUnknownDevice(final AndroidFxAccount fxAccount)337 private static void recoverFromUnknownDevice(final AndroidFxAccount fxAccount) { 338 Log.i(LOG_TAG, "unknown device id, clearing the cached device id"); 339 fxAccount.setDeviceId(null); 340 } 341 342 /** 343 * Will call delegate#complete in all cases 344 */ recoverFromDeviceSessionConflict(final FxAccountClientRemoteException error, final FxAccountClient fxAccountClient, final byte[] sessionToken, final AndroidFxAccount fxAccount, final FxAccountDevice device, final Context context, final boolean allowRecursion)345 private static void recoverFromDeviceSessionConflict(final FxAccountClientRemoteException error, 346 final FxAccountClient fxAccountClient, 347 final byte[] sessionToken, 348 final AndroidFxAccount fxAccount, 349 final FxAccountDevice device, 350 final Context context, 351 final boolean allowRecursion) { 352 // Recovery strategy: re-try a registration, UPDATING (instead of creating) the device. 353 // We do that by finding the device ID who conflicted with us and try a registration update 354 // using that id. 355 Log.w(LOG_TAG, "device session conflict, attempting to ascertain the correct device id"); 356 fxAccountClient.deviceList(sessionToken, new RequestDelegate<FxAccountDevice[]>() { 357 private void onError() { 358 Log.e(LOG_TAG, "failed to recover from device-session conflict"); 359 logErrorAndResetDeviceRegistrationVersionAndTimestamp(error, fxAccount); 360 } 361 362 @Override 363 public void handleError(Exception e) { 364 onError(); 365 } 366 367 @Override 368 public void handleFailure(FxAccountClientRemoteException e) { 369 onError(); 370 } 371 372 @Override 373 public void handleSuccess(FxAccountDevice[] devices) { 374 for (final FxAccountDevice fxaDevice : devices) { 375 if (!fxaDevice.isCurrentDevice) { 376 continue; 377 } 378 fxAccount.setFxAUserData(fxaDevice.id, 0, 0L); // Reset device registration version/timestamp 379 if (!allowRecursion) { 380 Log.d(LOG_TAG, "Failure to register a device on the second try"); 381 break; 382 } 383 final FxAccountDevice updatedDevice = new FxAccountDevice(device.name, fxaDevice.id, device.type, 384 null, null, 385 device.pushCallback, device.pushPublicKey, 386 device.pushAuthKey, null); 387 doFxaRegistration(context, fxAccount, updatedDevice, false); 388 return; 389 } 390 onError(); 391 } 392 }); 393 } 394 setupListeners()395 private void setupListeners() throws ClassNotFoundException, NoSuchMethodException, 396 InvocationTargetException, IllegalAccessException { 397 // We have no choice but to use reflection here, sorry :( 398 final Class<?> eventDispatcher = Class.forName("org.mozilla.gecko.EventDispatcher"); 399 final Method getInstance = eventDispatcher.getMethod("getInstance"); 400 final Object instance = getInstance.invoke(null); 401 final Method registerBackgroundThreadListener = eventDispatcher.getMethod("registerBackgroundThreadListener", 402 BundleEventListener.class, String[].class); 403 registerBackgroundThreadListener.invoke(instance, this, new String[] { "FxAccountsPush:Subscribe:Response" }); 404 } 405 } 406