diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 01:07:19 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2023-07-07 01:07:19 +0000 |
commit | 7efac1ff123a747f8818f1de48306bcdd2693c36 (patch) | |
tree | d53c9c8b58b0e475e5774751728d52fa88f8259b | |
parent | 582b06d05eed0aa0dfd3bd9b37580891743810cb (diff) | |
parent | 1a392f75763d5679f669b26d97269f275491e79e (diff) | |
download | service_entitlement-aml_wif_341011010.tar.gz |
Snap for 10447354 from 1a392f75763d5679f669b26d97269f275491e79e to mainline-wifi-releaseaml_wif_341711020aml_wif_341610000aml_wif_341510000aml_wif_341410080aml_wif_341310010aml_wif_341110010aml_wif_341011010aml_wif_340913010android14-mainline-wifi-release
Change-Id: Id0612a0390c71ca601c11df80f1f7b901aebe684
-rw-r--r-- | Android.bp | 3 | ||||
-rw-r--r-- | OWNERS | 4 | ||||
-rw-r--r-- | TEST_MAPPING | 5 | ||||
-rw-r--r-- | java/com/android/libraries/entitlement/EsimOdsaOperation.java | 49 | ||||
-rw-r--r-- | java/com/android/libraries/entitlement/ServiceEntitlement.java | 78 | ||||
-rw-r--r-- | java/com/android/libraries/entitlement/ServiceEntitlementException.java | 5 | ||||
-rw-r--r-- | java/com/android/libraries/entitlement/ServiceEntitlementRequest.java | 29 | ||||
-rw-r--r-- | java/com/android/libraries/entitlement/eapaka/EapAkaApi.java | 199 | ||||
-rw-r--r-- | java/com/android/libraries/entitlement/http/HttpClient.java | 35 | ||||
-rw-r--r-- | java/com/android/libraries/entitlement/http/HttpResponse.java | 7 | ||||
-rw-r--r-- | tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java | 354 | ||||
-rw-r--r-- | tests/src/com/android/libraries/entitlement/http/HttpClientTest.java | 39 |
12 files changed, 746 insertions, 61 deletions
@@ -96,6 +96,9 @@ java_library { "java/com/android/libraries/entitlement/ServiceEntitlementException.java", "java/com/android/libraries/entitlement/ServiceEntitlementRequest.java", ], + static_libs: [ + "guava", + ], apex_available: [ "//apex_available:platform", "com.android.wifi", @@ -1,3 +1,3 @@ mewan@google.com -samalin@google.com -danielwbhuang@google.com +kiwonp@google.com +akaustubh@google.com diff --git a/TEST_MAPPING b/TEST_MAPPING index ec047b7..2f98cca 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -3,10 +3,5 @@ { "name": "service-entitlement-tests" } - ], - "postsubmit": [ - { - "name": "service-entitlement-tests" - } ] } diff --git a/java/com/android/libraries/entitlement/EsimOdsaOperation.java b/java/com/android/libraries/entitlement/EsimOdsaOperation.java index 20f4fa3..9a3eae6 100644 --- a/java/com/android/libraries/entitlement/EsimOdsaOperation.java +++ b/java/com/android/libraries/entitlement/EsimOdsaOperation.java @@ -17,6 +17,7 @@ package com.android.libraries.entitlement; import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableList; /** * HTTP request parameters specific to on device service actiavation (ODSA). See GSMA spec TS.43 @@ -40,6 +41,10 @@ public abstract class EsimOdsaOperation { * OSDA operation: AcquireConfiguration. */ public static final String OPERATION_ACQUIRE_CONFIGURATION = "AcquireConfiguration"; + /** + * OSDA operation: AcquireTemporaryToken. + */ + public static final String OPERATION_ACQUIRE_TEMPORARY_TOKEN = "AcquireTemporaryToken"; /** * Indicates that operation_type is not set. @@ -97,6 +102,12 @@ public abstract class EsimOdsaOperation { public abstract int operationType(); /** + * Returns the comma separated list of operation targets used with temporary token from + * AcquireTemporaryToken operation. Used by HTTP parameter "operation_targets". + */ + public abstract ImmutableList<String> operationTargets(); + + /** * Returns the unique identifier of the companion device, like IMEI. Used by HTTP parameter * "companion_terminal_id". */ @@ -170,6 +181,18 @@ public abstract class EsimOdsaOperation { */ public abstract String targetTerminalEid(); + + /** + * Returns the unique identifier of the old device eSIM, like the IMEI associated with the + * eSIM. Used by HTTP parameter "old_terminal_id". + */ + public abstract String oldTerminalId(); + + /** + * Returns the ICCID of old device eSIM. Used by HTTP parameter "old_terminal_iccid". + */ + public abstract String oldTerminalIccid(); + /** * Returns a new {@link Builder} object. */ @@ -177,6 +200,7 @@ public abstract class EsimOdsaOperation { return new AutoValue_EsimOdsaOperation.Builder() .setOperation("") .setOperationType(OPERATION_TYPE_NOT_SET) + .setOperationTargets(ImmutableList.of()) .setCompanionTerminalId("") .setCompanionTerminalVendor("") .setCompanionTerminalModel("") @@ -189,7 +213,9 @@ public abstract class EsimOdsaOperation { .setTerminalEid("") .setTargetTerminalId("") .setTargetTerminalIccid("") - .setTargetTerminalEid(""); + .setTargetTerminalEid("") + .setOldTerminalId("") + .setOldTerminalIccid(""); } /** @@ -230,6 +256,12 @@ public abstract class EsimOdsaOperation { public abstract Builder setOperationType(int value); /** + * Sets the operation targets to be used with temporary token from AcquireTemporaryToken + * operation. Used by HTTP parameter "operation_targets" if set. + */ + public abstract Builder setOperationTargets(ImmutableList<String> value); + + /** * Sets the unique identifier of the companion device, like IMEI. Used by HTTP parameter * "companion_terminal_id" if set. * @@ -336,6 +368,21 @@ public abstract class EsimOdsaOperation { */ public abstract Builder setTargetTerminalEid(String value); + /** + * Sets the unique identifier of the old device eSIM, like the IMEI associated with the + * eSIM. Used by HTTP parameter "old_terminal_id" if set. + * + * <p>Used by primary device ODSA operation. + */ + public abstract Builder setOldTerminalId(String value); + + /** + * Sets the ICCID old device eSIM. Used by HTTP parameter "old_terminal_iccid" if set. + * + * <p>Used by primary device ODSA operation. + */ + public abstract Builder setOldTerminalIccid(String value); + public abstract EsimOdsaOperation build(); } } diff --git a/java/com/android/libraries/entitlement/ServiceEntitlement.java b/java/com/android/libraries/entitlement/ServiceEntitlement.java index d723e4c..c0d6d55 100644 --- a/java/com/android/libraries/entitlement/ServiceEntitlement.java +++ b/java/com/android/libraries/entitlement/ServiceEntitlement.java @@ -25,6 +25,8 @@ import com.android.libraries.entitlement.eapaka.EapAkaApi; import com.google.common.collect.ImmutableList; +import java.util.List; + /** * Implemnets protocol for carrier service entitlement configuration query and operation, based on * GSMA TS.43 spec. @@ -50,6 +52,10 @@ public class ServiceEntitlement { * App ID for on device service activation (OSDA) for primary device. */ public static final String APP_ODSA_PRIMARY = "ap2009"; + /** + * App ID for data plan information entitlement. + */ + public static final String APP_DATA_PLAN_BOOST = "ap2010"; private final CarrierConfig carrierConfig; private final EapAkaApi eapAkaApi; @@ -67,8 +73,63 @@ public class ServiceEntitlement { * for how to get the subscroption ID. */ public ServiceEntitlement(Context context, CarrierConfig carrierConfig, int simSubscriptionId) { + this( + context, + carrierConfig, + simSubscriptionId, + /* saveHttpHistory= */ false, + /* bypassEapAkaResponse= */ ""); + } + + /** + * Creates an instance for service entitlement configuration query and operation for the + * carrier. + * + * @param context context of application + * @param carrierConfig carrier specific configs used in the queries and operations. + * @param simSubscriptionId the subscroption ID of the carrier's SIM on device. This indicates + * which SIM to retrieve IMEI/IMSI from and perform EAP-AKA authentication with. See {@link + * android.telephony.SubscriptionManager} for how to get the subscroption ID. + * @param saveHttpHistory set to {@code true} to save the history of request and response which + * can later be retrieved by {@code getHistory()}. Intended for debugging. + */ + public ServiceEntitlement( + Context context, + CarrierConfig carrierConfig, + int simSubscriptionId, + boolean saveHttpHistory) { + this( + context, + carrierConfig, + simSubscriptionId, + saveHttpHistory, + /* bypassEapAkaResponse= */ ""); + } + + /** + * Creates an instance for service entitlement configuration query and operation for the + * carrier. + * + * @param context context of application + * @param carrierConfig carrier specific configs used in the queries and operations. + * @param simSubscriptionId the subscroption ID of the carrier's SIM on device. This indicates + * which SIM to retrieve IMEI/IMSI from and perform EAP-AKA authentication with. See {@link + * android.telephony.SubscriptionManager} for how to get the subscroption ID. + * @param saveHttpHistory set to {@code true} to save the history of request and response which + * can later be retrieved by {@code getHistory()}. Intended for debugging. + * @param bypassEapAkaResponse set to non empty string to bypass EAP-AKA authentication. + * The client will accept any challenge from the server and return this string as a + * response. Must not be {@code null}. Intended for testing. + */ + public ServiceEntitlement( + Context context, + CarrierConfig carrierConfig, + int simSubscriptionId, + boolean saveHttpHistory, + String bypassEapAkaResponse) { this.carrierConfig = carrierConfig; - this.eapAkaApi = new EapAkaApi(context, simSubscriptionId); + this.eapAkaApi = + new EapAkaApi(context, simSubscriptionId, saveHttpHistory, bypassEapAkaResponse); } @VisibleForTesting @@ -151,4 +212,19 @@ public class ServiceEntitlement { throws ServiceEntitlementException { return eapAkaApi.performEsimOdsaOperation(appId, carrierConfig, request, operation); } + + /** + * Retrieves the history of past HTTP request and responses if {@code saveHttpHistory} was set + * in constructor. + */ + public List<String> getHistory() { + return eapAkaApi.getHistory(); + } + + /** + * Clears the history of past HTTP request and responses. + */ + public void clearHistory() { + eapAkaApi.clearHistory(); + } } diff --git a/java/com/android/libraries/entitlement/ServiceEntitlementException.java b/java/com/android/libraries/entitlement/ServiceEntitlementException.java index 45b1b9b..b1cb50f 100644 --- a/java/com/android/libraries/entitlement/ServiceEntitlementException.java +++ b/java/com/android/libraries/entitlement/ServiceEntitlementException.java @@ -43,6 +43,11 @@ public class ServiceEntitlementException extends Exception { * synchronization" procedure as defined in RFC 4187. */ public static final int ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE = 21; + /** + * EAP-AKA failure that happens when the client fails to authenticate within the maximum number + * of attempts + */ + public static final int ERROR_EAP_AKA_FAILURE = 21; // HTTP related failures /** diff --git a/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java index c7b0ad3..e0ecbf7 100644 --- a/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java +++ b/java/com/android/libraries/entitlement/ServiceEntitlementRequest.java @@ -62,6 +62,11 @@ public abstract class ServiceEntitlementRequest { public abstract String authenticationToken(); /** + * Returns the temporary token. Used by HTTP parameter "temporary_token". + */ + public abstract String temporaryToken(); + + /** * Returns the unique identifier of the device like IMEI. Used by HTTP parameter "terminal_id". */ public abstract String terminalId(); @@ -118,6 +123,11 @@ public abstract class ServiceEntitlementRequest { public abstract String acceptContentType(); /** + * Returns the boost type for premium network. Used for premium network slice entitlement. + */ + public abstract String boostType(); + + /** * Returns a new {@link Builder} object. */ public static Builder builder() { @@ -125,6 +135,7 @@ public abstract class ServiceEntitlementRequest { .setConfigurationVersion(DEFAULT_CONFIGURATION_VERSION) .setEntitlementVersion(DEFAULT_ENTITLEMENT_VERSION) .setAuthenticationToken("") + .setTemporaryToken("") .setTerminalId("") .setTerminalVendor(Build.MANUFACTURER) .setTerminalModel(Build.MODEL) @@ -133,7 +144,8 @@ public abstract class ServiceEntitlementRequest { .setAppVersion("") .setNotificationToken("") .setNotificationAction(NOTICATION_ACTION_ENABLE_FCM) - .setAcceptContentType(ACCEPT_CONTENT_TYPE_JSON_AND_XML); + .setAcceptContentType(ACCEPT_CONTENT_TYPE_JSON_AND_XML) + .setBoostType(""); } /** @@ -167,6 +179,13 @@ public abstract class ServiceEntitlementRequest { public abstract Builder setAuthenticationToken(String value); /** + * Sets the temporary token. Used by HTTP parameter "temporary_token". + * + * <p>Optional. + */ + public abstract Builder setTemporaryToken(String value); + + /** * Sets the unique identifier of the device like IMEI. Used by HTTP parameter * "terminal_id". * @@ -243,6 +262,14 @@ public abstract class ServiceEntitlementRequest { */ public abstract Builder setAcceptContentType(String contentType); + /** + * Sets the boost type for premium network. Used by HTTP parameter + * "boost_type" in case of premium network slice entitlement. + * + * <p>Optional. + */ + public abstract Builder setBoostType(String value); + public abstract ServiceEntitlementRequest build(); } } diff --git a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java index 4482bf7..be41ca7 100644 --- a/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java +++ b/java/com/android/libraries/entitlement/eapaka/EapAkaApi.java @@ -16,6 +16,7 @@ package com.android.libraries.entitlement.eapaka; +import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_EAP_AKA_FAILURE; import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE; import static com.android.libraries.entitlement.ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE; @@ -33,6 +34,7 @@ import com.android.libraries.entitlement.EsimOdsaOperation; import com.android.libraries.entitlement.ServiceEntitlementException; import com.android.libraries.entitlement.ServiceEntitlementRequest; import com.android.libraries.entitlement.http.HttpClient; +import com.android.libraries.entitlement.http.HttpConstants.ContentType; import com.android.libraries.entitlement.http.HttpConstants.RequestMethod; import com.android.libraries.entitlement.http.HttpRequest; import com.android.libraries.entitlement.http.HttpResponse; @@ -43,6 +45,8 @@ import com.google.common.net.HttpHeaders; import org.json.JSONException; import org.json.JSONObject; +import java.util.List; + public class EapAkaApi { private static final String TAG = "ServiceEntitlement"; @@ -58,6 +62,7 @@ public class EapAkaApi { private static final String EAP_ID = "EAP_ID"; private static final String IMSI = "IMSI"; private static final String TOKEN = "token"; + private static final String TEMPORARY_TOKEN = "temporary_token"; private static final String NOTIF_ACTION = "notif_action"; private static final String NOTIF_TOKEN = "notif_token"; private static final String APP_VERSION = "app_version"; @@ -65,6 +70,7 @@ public class EapAkaApi { private static final String OPERATION = "operation"; private static final String OPERATION_TYPE = "operation_type"; + private static final String OPERATION_TARGETS = "operation_targets"; private static final String COMPANION_TERMINAL_ID = "companion_terminal_id"; private static final String COMPANION_TERMINAL_VENDOR = "companion_terminal_vendor"; private static final String COMPANION_TERMINAL_MODEL = "companion_terminal_model"; @@ -82,22 +88,38 @@ public class EapAkaApi { private static final String TARGET_TERMINAL_ICCID = "target_terminal_iccid"; private static final String TARGET_TERMINAL_EID = "target_terminal_eid"; - // In case of EAP-AKA synchronization failure, we try to recover for at most two times. - private static final int FOLLOW_SYNC_FAILURE_MAX_COUNT = 2; + private static final String OLD_TERMINAL_ID = "old_terminal_id"; + private static final String OLD_TERMINAL_ICCID = "old_terminal_iccid"; + + private static final String BOOST_TYPE = "boost_type"; + + // In case of EAP-AKA synchronization failure or another challenge, we try to authenticate for + // at most three times. + private static final int MAX_EAP_AKA_ATTEMPTS = 3; private final Context mContext; private final int mSimSubscriptionId; private final HttpClient mHttpClient; - - public EapAkaApi(Context context, int simSubscriptionId) { - this(context, simSubscriptionId, new HttpClient()); + private final String mBypassEapAkaResponse; + + public EapAkaApi( + Context context, + int simSubscriptionId, + boolean saveHistory, + String bypassEapAkaResponse) { + this(context, simSubscriptionId, new HttpClient(saveHistory), bypassEapAkaResponse); } @VisibleForTesting - EapAkaApi(Context context, int simSubscriptionId, HttpClient httpClient) { + EapAkaApi( + Context context, + int simSubscriptionId, + HttpClient httpClient, + String bypassEapAkaResponse) { this.mContext = context; this.mSimSubscriptionId = simSubscriptionId; this.mHttpClient = httpClient; + this.mBypassEapAkaResponse = bypassEapAkaResponse; } /** @@ -126,10 +148,17 @@ public class EapAkaApi { urlBuilder.toString(), carrierConfig, ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON); + String eapAkaChallenge = getEapAkaChallenge(challengeResponse); + if (eapAkaChallenge == null) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, + "Failed to parse EAP-AKA challenge: " + challengeResponse.body()); + } return respondToEapAkaChallenge( carrierConfig, - challengeResponse, - FOLLOW_SYNC_FAILURE_MAX_COUNT, + eapAkaChallenge, + challengeResponse.cookies(), + MAX_EAP_AKA_ATTEMPTS, request.acceptContentType()) .body(); } @@ -139,55 +168,79 @@ public class EapAkaApi { * Sends a follow-up HTTP request to the HTTP {@code response} using the same cookie, and * returns the follow-up HTTP response. * - * <p>The {@code response} should contain a EAP-AKA challenge from server, and the - * follow-up request could contain: + * <p>The {@code eapAkaChallenge} should be the EAP-AKA challenge from server, and the follow-up + * request could contain: * * <ul> - * <li>The EAP-AKA response message, and the follow-up response should contain the - * service entitlement configuration; or, - * <li>The EAP-AKA synchronization failure message, and the follow-up response should - * contain the new EAP-AKA challenge. Then this method calls itself to follow-up - * the new challenge and return a new response, if {@code followSyncFailureCount} - * is greater than zero. When this method call itself {@code followSyncFailureCount} is - * reduced by one to prevent infinite loop (unlikely in practice, but just in case). + * <li>The EAP-AKA response message, and the follow-up response should contain the service + * entitlement configuration, or another EAP-AKA challenge in which case the method calls + * if {@code remainingAttempts} is greater than zero (If {@code remainingAttempts} reaches + * 0, the method will throw ServiceEntitlementException) ; or + * <li>The EAP-AKA synchronization failure message, and the follow-up response should contain + * the new EAP-AKA challenge. Then this method calls itself to follow-up the new challenge + * and return a new response, as long as {@code remainingAttempts} is greater than zero. * </ul> * * @param response Challenge response from server which its content type is JSON */ private HttpResponse respondToEapAkaChallenge( CarrierConfig carrierConfig, - HttpResponse response, - int followSyncFailureCount, + String eapAkaChallenge, + ImmutableList<String> cookies, + int remainingAttempts, String contentType) throws ServiceEntitlementException { - String eapAkaChallenge; - try { - eapAkaChallenge = new JSONObject(response.body()).getString(EAP_CHALLENGE_RESPONSE); - } catch (JSONException jsonException) { - throw new ServiceEntitlementException( - ERROR_MALFORMED_HTTP_RESPONSE, "Failed to parse json object", jsonException); + if (!mBypassEapAkaResponse.isEmpty()) { + return challengeResponse(mBypassEapAkaResponse, carrierConfig, cookies, contentType); } + EapAkaChallenge challenge = EapAkaChallenge.parseEapAkaChallenge(eapAkaChallenge); EapAkaResponse eapAkaResponse = EapAkaResponse.respondToEapAkaChallenge(mContext, mSimSubscriptionId, challenge); - // This could be a successful authentication, or synchronization failure. - if (eapAkaResponse.response() != null) { // successful authentication - return challengeResponse( - eapAkaResponse.response(), - carrierConfig, - response.cookies(), - contentType); + // This could be a successful authentication, another challenge, or synchronization failure. + if (eapAkaResponse.response() != null) { + HttpResponse response = + challengeResponse( + eapAkaResponse.response(), carrierConfig, cookies, contentType); + String nextEapAkaChallenge = getEapAkaChallenge(response); + // successful authentication + if (nextEapAkaChallenge == null) { + return response; + } + // another challenge + Log.d(TAG, "Received another challenge"); + if (remainingAttempts > 0) { + return respondToEapAkaChallenge( + carrierConfig, + nextEapAkaChallenge, + cookies, + remainingAttempts - 1, + contentType); + } else { + throw new ServiceEntitlementException( + ERROR_EAP_AKA_FAILURE, "Unable to EAP-AKA authenticate"); + } } else if (eapAkaResponse.synchronizationFailureResponse() != null) { Log.d(TAG, "synchronization failure"); HttpResponse newChallenge = challengeResponse( eapAkaResponse.synchronizationFailureResponse(), carrierConfig, - response.cookies(), + cookies, ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON); - if (followSyncFailureCount > 0) { + String nextEapAkaChallenge = getEapAkaChallenge(newChallenge); + if (nextEapAkaChallenge == null) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, + "Failed to parse EAP-AKA challenge: " + newChallenge.body()); + } + if (remainingAttempts > 0) { return respondToEapAkaChallenge( - carrierConfig, newChallenge, followSyncFailureCount - 1, contentType); + carrierConfig, + nextEapAkaChallenge, + cookies, + remainingAttempts - 1, + contentType); } else { throw new ServiceEntitlementException( ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE, @@ -241,7 +294,8 @@ public class EapAkaApi { appendParametersForServiceEntitlementRequest(urlBuilder, ImmutableList.of(appId), request); appendParametersForEsimOdsaOperation(urlBuilder, odsaOperation); - if (!TextUtils.isEmpty(request.authenticationToken())) { + if (!TextUtils.isEmpty(request.authenticationToken()) + || !TextUtils.isEmpty(request.temporaryToken())) { // Fast Re-Authentication flow with pre-existing auth token Log.d(TAG, "Fast Re-Authentication"); return httpGet( @@ -254,10 +308,17 @@ public class EapAkaApi { urlBuilder.toString(), carrierConfig, ServiceEntitlementRequest.ACCEPT_CONTENT_TYPE_JSON); + String eapAkaChallenge = getEapAkaChallenge(challengeResponse); + if (eapAkaChallenge == null) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, + "Failed to parse EAP-AKA challenge: " + challengeResponse.body()); + } return respondToEapAkaChallenge( carrierConfig, - challengeResponse, - FOLLOW_SYNC_FAILURE_MAX_COUNT, + eapAkaChallenge, + challengeResponse.cookies(), + MAX_EAP_AKA_ATTEMPTS, request.acceptContentType()) .body(); } @@ -268,17 +329,20 @@ public class EapAkaApi { ServiceEntitlementRequest request) { TelephonyManager telephonyManager = mContext.getSystemService( TelephonyManager.class).createForSubscriptionId(mSimSubscriptionId); - if (TextUtils.isEmpty(request.authenticationToken())) { + if (!TextUtils.isEmpty(request.authenticationToken())) { + // IMSI and token required for fast AuthN. + urlBuilder + .appendQueryParameter(IMSI, telephonyManager.getSubscriberId()) + .appendQueryParameter(TOKEN, request.authenticationToken()); + } else if (!TextUtils.isEmpty(request.temporaryToken())) { + // temporary_token required for fast AuthN. + urlBuilder.appendQueryParameter(TEMPORARY_TOKEN, request.temporaryToken()); + } else { // EAP_ID required for initial AuthN urlBuilder.appendQueryParameter( EAP_ID, getImsiEap(telephonyManager.getSimOperator(), telephonyManager.getSubscriberId())); - } else { - // IMSI and token required for fast AuthN. - urlBuilder - .appendQueryParameter(IMSI, telephonyManager.getSubscriberId()) - .appendQueryParameter(TOKEN, request.authenticationToken()); } if (!TextUtils.isEmpty(request.notificationToken())) { @@ -298,6 +362,7 @@ public class EapAkaApi { // Optional query parameters, append them if not empty appendOptionalQueryParameter(urlBuilder, APP_VERSION, request.appVersion()); appendOptionalQueryParameter(urlBuilder, APP_NAME, request.appName()); + appendOptionalQueryParameter(urlBuilder, BOOST_TYPE, request.boostType()); for (String appId : appIds) { urlBuilder.appendQueryParameter(APP, appId); @@ -320,6 +385,10 @@ public class EapAkaApi { urlBuilder.appendQueryParameter(OPERATION_TYPE, Integer.toString(odsaOperation.operationType())); } + appendOptionalQueryParameter( + urlBuilder, + OPERATION_TARGETS, + TextUtils.join(",", odsaOperation.operationTargets())); appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_ID, odsaOperation.companionTerminalId()); appendOptionalQueryParameter(urlBuilder, COMPANION_TERMINAL_VENDOR, @@ -345,6 +414,10 @@ public class EapAkaApi { odsaOperation.targetTerminalIccid()); appendOptionalQueryParameter(urlBuilder, TARGET_TERMINAL_EID, odsaOperation.targetTerminalEid()); + appendOptionalQueryParameter(urlBuilder, OLD_TERMINAL_ICCID, + odsaOperation.oldTerminalIccid()); + appendOptionalQueryParameter(urlBuilder, OLD_TERMINAL_ID, + odsaOperation.oldTerminalId()); } private HttpResponse httpGet(String url, CarrierConfig carrierConfig, String contentType) @@ -366,6 +439,30 @@ public class EapAkaApi { } } + @Nullable + private String getEapAkaChallenge(HttpResponse response) throws ServiceEntitlementException { + String eapAkaChallenge = null; + String responseBody = response.body(); + if (response.contentType() == ContentType.JSON) { + try { + eapAkaChallenge = + new JSONObject(responseBody).optString(EAP_CHALLENGE_RESPONSE, null); + } catch (JSONException jsonException) { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, + "Failed to parse json object", + jsonException); + } + } else if (response.contentType() == ContentType.XML) { + // TODO: possibly support parsing eap-relay-packet in XML format + return null; + } else { + throw new ServiceEntitlementException( + ERROR_MALFORMED_HTTP_RESPONSE, "Unknown HTTP content type"); + } + return eapAkaChallenge; + } + /** * Returns the IMSI EAP value. The resulting realm part of the Root NAI in 3GPP TS 23.003 clause * 19.3.2 will be in the form: @@ -385,4 +482,18 @@ public class EapAkaApi { } return "0" + imsi + "@nai.epc.mnc" + mnc + ".mcc" + mcc + ".3gppnetwork.org"; } + + /** + * Retrieves the history of past HTTP request and responses. + */ + public List<String> getHistory() { + return mHttpClient.getHistory(); + } + + /** + * Clears the history of past HTTP request and responses. + */ + public void clearHistory() { + mHttpClient.clearHistory(); + } } diff --git a/java/com/android/libraries/entitlement/http/HttpClient.java b/java/com/android/libraries/entitlement/http/HttpClient.java index 9ccb5ee..f2b394d 100644 --- a/java/com/android/libraries/entitlement/http/HttpClient.java +++ b/java/com/android/libraries/entitlement/http/HttpClient.java @@ -47,6 +47,7 @@ import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLConnection; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -55,9 +56,19 @@ public class HttpClient { private static final String TAG = "ServiceEntitlement"; private HttpURLConnection mConnection; + private boolean mSaveHistory; + private ArrayList<String> mHistory; + + public HttpClient(boolean saveHistory) { + mSaveHistory = saveHistory; + mHistory = new ArrayList<>(); + } @WorkerThread public HttpResponse request(HttpRequest request) throws ServiceEntitlementException { + if (mSaveHistory) { + mHistory.add(request.toString()); + } logPii("HttpClient.request url: " + request.url()); createConnection(request); logPii("HttpClient.request headers (partial): " + mConnection.getRequestProperties()); @@ -73,18 +84,38 @@ public class HttpClient { } mConnection.connect(); // This is to trigger SocketTimeoutException early HttpResponse response = getHttpResponse(mConnection); - Log.d(TAG, "HttpClient.response : " + response); + Log.d(TAG, "HttpClient.response : " + response.toShortDebugString()); + if (mSaveHistory) { + mHistory.add(response.toString()); + } return response; } catch (IOException ioe) { throw new ServiceEntitlementException( ERROR_HTTP_STATUS_NOT_SUCCESS, - StreamUtils.inputStreamToStringSafe(mConnection.getErrorStream()), + "Connection error stream: " + + StreamUtils.inputStreamToStringSafe(mConnection.getErrorStream()) + + " IOException: " + + ioe.toString(), ioe); } finally { closeConnection(); } } + /** + * Retrieves the history of past HTTP request and responses. + */ + public List<String> getHistory() { + return mHistory; + } + + /** + * Clears the history of past HTTP request and responses. + */ + public void clearHistory() { + mHistory.clear(); + } + private void createConnection(HttpRequest request) throws ServiceEntitlementException { try { URL url = new URL(request.url()); diff --git a/java/com/android/libraries/entitlement/http/HttpResponse.java b/java/com/android/libraries/entitlement/http/HttpResponse.java index f495578..142639e 100644 --- a/java/com/android/libraries/entitlement/http/HttpResponse.java +++ b/java/com/android/libraries/entitlement/http/HttpResponse.java @@ -74,8 +74,11 @@ public abstract class HttpResponse { .setCookies(ImmutableList.of()); } - @Override - public final String toString() { + /** + * Returns a short string representation for debugging purposes. Doesn't include the cookie or + * full body to prevent leaking sensitive data. + */ + public String toShortDebugString() { return new StringBuilder("HttpResponse{") .append("contentType=") .append(contentType()) diff --git a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java index b837695..aff8f4b 100644 --- a/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java +++ b/tests/src/com/android/libraries/entitlement/eapaka/EapAkaApiTest.java @@ -24,6 +24,7 @@ import static com.android.libraries.entitlement.eapaka.EapAkaResponseTest.EAP_AK import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -66,6 +67,8 @@ public class EapAkaApiTest { private static final String TEST_URL = "https://test.url/test-path"; private static final String EAP_AKA_CHALLENGE = "{\"eap-relay-packet\":\"" + EAP_AKA_CHALLENGE_REQUEST + "\"}"; + private static final String INVALID_EAP_AKA_CHALLENGE = + "{\"invalid-eap-relay-packet\":\"" + EAP_AKA_CHALLENGE_REQUEST + "\"}"; // com.google.common.net.HttpHeaders.COOKIE private static final String HTTP_HEADER_COOKIE = "Cookie"; private static final String COOKIE_VALUE = "COOKIE=abcdefg"; @@ -94,6 +97,7 @@ public class EapAkaApiTest { private static final int SUB_ID = 1; private static final String ACCEPT_CONTENT_TYPE_JSON_AND_XML = "application/vnd.gsma.eap-relay.v1.0+json, text/vnd.wap.connectivity-xml"; + private static final String BYPASS_EAP_AKA_RESPONSE = "abc"; @Rule public final MockitoRule rule = MockitoJUnit.rule(); @@ -105,11 +109,14 @@ public class EapAkaApiTest { private Context mContext; private EapAkaApi mEapAkaApi; + private EapAkaApi mEapAkaApiBypassAuthentication; @Before public void setUp() { mContext = spy(ApplicationProvider.getApplicationContext()); - mEapAkaApi = new EapAkaApi(mContext, SUB_ID, mMockHttpClient); + mEapAkaApi = new EapAkaApi(mContext, SUB_ID, mMockHttpClient, ""); + mEapAkaApiBypassAuthentication = + new EapAkaApi(mContext, SUB_ID, mMockHttpClient, BYPASS_EAP_AKA_RESPONSE); when(mContext.getSystemService(TelephonyManager.class)) .thenReturn(mMockTelephonyManager); when(mMockTelephonyManager.createForSubscriptionId(SUB_ID)) @@ -164,8 +171,8 @@ public class EapAkaApiTest { ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); assertThat(respopnse).isEqualTo(RESPONSE_XML); - // Verify that the 2nd request has cookies set by the 1st response verify(mMockHttpClient, times(2)).request(mHttpRequestCaptor.capture()); + // Verify that the 2nd request has cookies set by the 1st response assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) .containsAtLeast(HTTP_HEADER_COOKIE, COOKIE_VALUE, HTTP_HEADER_COOKIE, COOKIE_VALUE_1); @@ -178,6 +185,180 @@ public class EapAkaApiTest { } @Test + public void queryEntitlementStatus_noAuthenticationToken_invalidChallenge() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(INVALID_EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse).thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + carrierConfig, + request)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); + assertThat(exception.getMessage()) + .isEqualTo("Failed to parse EAP-AKA challenge: " + INVALID_EAP_AKA_CHALLENGE); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_secondChallenge() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + String respopnse = + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + assertThat(respopnse).isEqualTo(RESPONSE_XML); + // Verify that the subsequent requests have cookies set by the 1st response + verify(mMockHttpClient, times(3)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) + .containsAtLeast(HTTP_HEADER_COOKIE, COOKIE_VALUE, + HTTP_HEADER_COOKIE, COOKIE_VALUE_1); + assertThat(mHttpRequestCaptor.getAllValues().get(0).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(0).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(1).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(1).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(2).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(2).network()).isNull(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_thirdChallenge() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + String respopnse = + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + assertThat(respopnse).isEqualTo(RESPONSE_XML); + // Verify that the subsequent requests have cookies set by the 1st response + verify(mMockHttpClient, times(4)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) + .containsAtLeast(HTTP_HEADER_COOKIE, COOKIE_VALUE, + HTTP_HEADER_COOKIE, COOKIE_VALUE_1); + assertThat(mHttpRequestCaptor.getAllValues().get(0).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(0).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(1).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(1).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(2).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(2).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(3).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(3).network()).isNull(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_fourthChallenge_throwException() + throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + carrierConfig, + request)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_EAP_AKA_FAILURE); + assertThat(exception.getMessage()).isEqualTo("Unable to EAP-AKA authenticate"); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); + } + + @Test public void queryEntitlementStatus_hasAuthenticationToken_multipleAppIds() throws Exception { HttpResponse response = HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) @@ -261,6 +442,131 @@ public class EapAkaApiTest { } @Test + public void queryEntitlementStatus_noAuthenticationToken_eapAkaSyncFailure_invalidChallenge() + throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE); + HttpResponse eapChallengeResponse = + HttpResponse + .builder().setContentType(ContentType.JSON).setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)).build(); + HttpResponse invalidEapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(INVALID_EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(invalidEapChallengeResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + carrierConfig, + request)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); + assertThat(exception.getMessage()) + .isEqualTo("Failed to parse EAP-AKA challenge: " + INVALID_EAP_AKA_CHALLENGE); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); + } + + @Test + public void queryEntitlementStatus_noAuthenticationToken_fourthEapAkaSyncFailure() + throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SYNC_FAILURE); + HttpResponse eapChallengeResponse = + HttpResponse + .builder().setContentType(ContentType.JSON).setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)).build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse) + .thenReturn(eapChallengeResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), + carrierConfig, + request)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_EAP_AKA_SYNCHRONIZATION_FAILURE); + assertThat(exception.getMessage()) + .isEqualTo("Unable to recover from EAP-AKA synchroinization failure"); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); + } + + @Test + public void queryEntitlementStatus_hasNoAuthenticationToken_bypassAuthentication() + throws Exception { + HttpResponse eapChallengeResponse = + HttpResponse + .builder().setContentType(ContentType.JSON).setBody(EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE, COOKIE_VALUE_1)).build(); + HttpResponse xmlResponse = + HttpResponse.builder().setContentType(ContentType.XML).setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse).thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + + String respopnse = + mEapAkaApiBypassAuthentication.queryEntitlementStatus( + ImmutableList.of(ServiceEntitlement.APP_VOWIFI), carrierConfig, request); + + assertThat(respopnse).isEqualTo(RESPONSE_XML); + // Verify that the 2nd request has cookies set by the 1st response + verify(mMockHttpClient, times(2)).request(mHttpRequestCaptor.capture()); + assertThat(mHttpRequestCaptor.getAllValues().get(1).requestProperties()) + .containsAtLeast(HTTP_HEADER_COOKIE, COOKIE_VALUE, + HTTP_HEADER_COOKIE, COOKIE_VALUE_1); + assertThat(mHttpRequestCaptor.getAllValues().get(0).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(0).network()).isNull(); + assertThat(mHttpRequestCaptor.getAllValues().get(1).timeoutInSec()) + .isEqualTo(CarrierConfig.DEFAULT_TIMEOUT_IN_SEC); + assertThat(mHttpRequestCaptor.getAllValues().get(1).network()).isNull(); + verify(mMockTelephonyManagerForSubId, times(0)) + .getIccAuthentication(anyInt(), anyInt(), any()); + assertThat( + mHttpRequestCaptor + .getAllValues() + .get(1) + .postData() + .get(EapAkaApi.EAP_CHALLENGE_RESPONSE)) + .isEqualTo(BYPASS_EAP_AKA_RESPONSE); + } + + @Test public void queryEntitlementStatus_acceptContentTypeSpecified_verfityAcceptContentType() throws Exception { HttpResponse response = HttpResponse.builder().setBody(RESPONSE_XML).build(); @@ -351,4 +657,48 @@ public class EapAkaApiTest { assertThat(response).isEqualTo(RESPONSE_XML); verify(mMockHttpClient, times(1)).request(any()); } + + @Test + public void performEsimOdsaOperation_noAuthenticationToken_invalidChallenge() throws Exception { + when(mMockTelephonyManagerForSubId.getIccAuthentication( + TelephonyManager.APPTYPE_USIM, + TelephonyManager.AUTHTYPE_EAP_AKA, + EAP_AKA_SECURITY_CONTEXT_REQUEST_EXPECTED)) + .thenReturn(EAP_AKA_SECURITY_CONTEXT_RESPONSE_SUCCESS); + HttpResponse eapChallengeResponse = + HttpResponse.builder() + .setContentType(ContentType.JSON) + .setBody(INVALID_EAP_AKA_CHALLENGE) + .setCookies(ImmutableList.of(COOKIE_VALUE)) + .build(); + HttpResponse xmlResponse = + HttpResponse.builder() + .setContentType(ContentType.XML) + .setBody(RESPONSE_XML) + .build(); + when(mMockHttpClient.request(any())) + .thenReturn(eapChallengeResponse) + .thenReturn(xmlResponse); + CarrierConfig carrierConfig = CarrierConfig.builder().setServerUrl(TEST_URL).build(); + ServiceEntitlementRequest request = ServiceEntitlementRequest.builder().build(); + EsimOdsaOperation operation = EsimOdsaOperation.builder().build(); + + ServiceEntitlementException exception = + expectThrows( + ServiceEntitlementException.class, + () -> + mEapAkaApi.performEsimOdsaOperation( + ServiceEntitlement.APP_ODSA_COMPANION, + carrierConfig, + request, + operation)); + + assertThat(exception.getErrorCode()) + .isEqualTo(ServiceEntitlementException.ERROR_MALFORMED_HTTP_RESPONSE); + assertThat(exception.getMessage()) + .isEqualTo("Failed to parse EAP-AKA challenge: " + INVALID_EAP_AKA_CHALLENGE); + assertThat(exception.getCause()).isNull(); + assertThat(exception.getHttpStatus()).isEqualTo(0); + assertThat(exception.getRetryAfter()).isEmpty(); + } } diff --git a/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java b/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java index 505e8b5..9f05828 100644 --- a/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java +++ b/tests/src/com/android/libraries/entitlement/http/HttpClientTest.java @@ -46,6 +46,7 @@ import org.junit.runner.RunWith; import java.net.HttpURLConnection; import java.net.URL; +import java.util.List; import java.util.Map; @RunWith(AndroidJUnit4.class) @@ -70,7 +71,7 @@ public class HttpClientTest { // Reset sFakeURLStreamHandler sFakeURLStreamHandler.stubResponse(ImmutableMap.of()); - mHttpClient = new HttpClient(); + mHttpClient = new HttpClient(true); } @Test @@ -244,4 +245,40 @@ public class HttpClientTest { assertThat(exception.getHttpStatus()).isEqualTo(0); assertThat(exception.getRetryAfter()).isEmpty(); } + + @Test + public void history() throws Exception { + FakeResponse responseContent = + FakeResponse.builder() + .setResponseCode(HttpURLConnection.HTTP_OK) + .setResponseLocation(null) + .setResponseBody(TEST_RESPONSE_BODY.getBytes(UTF_8)) + .setContentType(CONTENT_TYPE_STRING_JSON) + .build(); + Map<String, FakeResponse> response = ImmutableMap.of(TEST_URL, responseContent); + sFakeURLStreamHandler.stubResponse(response); + HttpRequest request = + HttpRequest.builder() + .setUrl(TEST_URL) + .setRequestMethod(RequestMethod.GET) + .setTimeoutInSec(70) + .build(); + + HttpResponse httpResponse0 = mHttpClient.request(request); + HttpResponse httpResponse1 = mHttpClient.request(request); + List<String> history = mHttpClient.getHistory(); + + assertThat(history) + .containsExactly( + request.toString(), + httpResponse0.toString(), + request.toString(), + httpResponse1.toString()) + .inOrder(); + + mHttpClient.clearHistory(); + history = mHttpClient.getHistory(); + + assertThat(history).isEmpty(); + } } |