From 937ad30df0977c7ae4077c69c71afa7e813e50a5 Mon Sep 17 00:00:00 2001 From: Ecco Park Date: Mon, 30 Jul 2018 17:28:49 -0700 Subject: passpoint-r2: redirect listener and launch an OSU app 1) In the provisioning flow, OSU application will be launched with the OSU url provided for subscription. Once user completes the subscription in OSU app, OSU server will send HTTP redirect response as completion of user input. 2) Creates OsuServer Handler thread to take care of exchaging soap message to prevent caller(WifiService Thread) from blocking until getting a SOAP response. 3) add @SmallTest annotation. Bug: 74244324 Test: ./frameworks/opt/net/wifi/tests/wifitests/runtests.sh Test: live test with Passpoint R2 service provider AP Change-Id: Ib0999fd2290e9f7bcb5dd30f1ac81c31c3f7c503 Signed-off-by: Ecco Park --- service/Android.mk | 3 +- .../server/wifi/hotspot2/OsuNetworkConnection.java | 8 +- .../server/wifi/hotspot2/OsuServerConnection.java | 80 +++++-- .../wifi/hotspot2/PasspointObjectFactory.java | 2 +- .../server/wifi/hotspot2/PasspointProvisioner.java | 255 +++++++++++++++++---- .../wifi/hotspot2/soap/RedirectListener.java | 143 ++++++++++-- .../wifi/hotspot2/OsuServerConnectionTest.java | 32 ++- .../wifi/hotspot2/PasspointProvisionerTest.java | 132 ++++++++++- .../wifi/hotspot2/soap/PostDevDataMessageTest.java | 1 - .../hotspot2/soap/PostDevDataResponseTest.java | 3 + .../wifi/hotspot2/soap/RedirectListenerTest.java | 172 ++++++++++++++ .../server/wifi/hotspot2/soap/SoapParserTest.java | 3 + .../wifi/hotspot2/soap/SppResponseMessageTest.java | 3 + 13 files changed, 735 insertions(+), 102 deletions(-) create mode 100644 tests/wifitests/src/com/android/server/wifi/hotspot2/soap/RedirectListenerTest.java diff --git a/service/Android.mk b/service/Android.mk index 21d776855..cf5cd8d1d 100644 --- a/service/Android.mk +++ b/service/Android.mk @@ -70,7 +70,8 @@ LOCAL_STATIC_JAVA_LIBRARIES := \ android.hardware.wifi.hostapd-V1.0-java \ android.hardware.wifi.supplicant-V1.0-java \ android.hardware.wifi.supplicant-V1.1-java \ - ksoap2 + ksoap2 \ + libnanohttpd LOCAL_REQUIRED_MODULES := \ services \ diff --git a/service/java/com/android/server/wifi/hotspot2/OsuNetworkConnection.java b/service/java/com/android/server/wifi/hotspot2/OsuNetworkConnection.java index ecfcc5a51..73dcca505 100644 --- a/service/java/com/android/server/wifi/hotspot2/OsuNetworkConnection.java +++ b/service/java/com/android/server/wifi/hotspot2/OsuNetworkConnection.java @@ -69,7 +69,7 @@ public class OsuNetworkConnection { void onDisconnected(); /** - * Invoked when a timer tracking connection request is not reset by successfull connection. + * Invoked when a timer tracking connection request is not reset by successful connection. */ void onTimeOut(); @@ -84,10 +84,6 @@ public class OsuNetworkConnection { void onWifiDisabled(); } - /** - * Create an instance of {@link NetworkConnection} for the specified Wi-Fi network. - * @param context The application context - */ public OsuNetworkConnection(Context context) { mContext = context; } @@ -146,6 +142,7 @@ public class OsuNetworkConnection { /** * Register for network and Wifi state events + * * @param callbacks The callbacks to be invoked on network change events */ public void setEventCallback(Callbacks callbacks) { @@ -206,6 +203,7 @@ public class OsuNetworkConnection { /** * Method to update logging level in this class + * * @param verbose more than 0 enables verbose logging */ public void enableVerboseLogging(int verbose) { diff --git a/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java b/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java index ca05cd017..fd993e599 100644 --- a/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java +++ b/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java @@ -18,10 +18,14 @@ package com.android.server.wifi.hotspot2; import android.annotation.NonNull; import android.net.Network; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; import android.text.TextUtils; import android.util.Log; import android.util.Pair; +import com.android.internal.annotations.VisibleForTesting; import com.android.org.conscrypt.TrustManagerImpl; import com.android.server.wifi.hotspot2.soap.HttpsServiceConnection; import com.android.server.wifi.hotspot2.soap.HttpsTransport; @@ -65,12 +69,21 @@ public class OsuServerConnection { private HttpsTransport mHttpsTransport; private HttpsServiceConnection mServiceConnection = null; private HttpsURLConnection mUrlConnection = null; + private HandlerThread mOsuServerHandlerThread; + private Handler mHandler; private PasspointProvisioner.OsuServerCallbacks mOsuServerCallbacks; private boolean mSetupComplete = false; private boolean mVerboseLoggingEnabled = false; + private Looper mLooper; + + @VisibleForTesting + /* package */ OsuServerConnection(Looper looper) { + mLooper = looper; + } /** * Sets up callback for event + * * @param callbacks OsuServerCallbacks to be invoked for server related events */ public void setEventCallback(PasspointProvisioner.OsuServerCallbacks callbacks) { @@ -79,6 +92,7 @@ public class OsuServerConnection { /** * Initialize socket factory for server connection using HTTPS + * * @param tlsContext SSLContext that will be used for HTTPS connection * @param trustManagerImpl TrustManagerImpl delegate to validate certs */ @@ -96,10 +110,19 @@ public class OsuServerConnection { return; } mSetupComplete = true; + + // If mLooper is already set by unit test, don't overwrite it. + if (mLooper == null) { + mOsuServerHandlerThread = new HandlerThread("OsuServerHandler"); + mOsuServerHandlerThread.start(); + mLooper = mOsuServerHandlerThread.getLooper(); + } + mHandler = new Handler(mLooper); } /** * Provides the capability to run OSU server validation + * * @return boolean true if capability available */ public boolean canValidateServer() { @@ -108,6 +131,7 @@ public class OsuServerConnection { /** * Enables verbose logging + * * @param verbose a value greater than zero enables verbose logging */ public void enableVerboseLogging(int verbose) { @@ -116,11 +140,12 @@ public class OsuServerConnection { /** * Connect to the OSU server + * * @param url Osu Server's URL * @param network current network connection * @return boolean value, true if connection was successful * - * Relies on the caller to ensure that the capability to validate the OSU + * Note: Relies on the caller to ensure that the capability to validate the OSU * Server is available. */ public boolean connect(URL url, Network network) { @@ -180,24 +205,30 @@ public class OsuServerConnection { * The helper method to exchange a SOAP message. * * @param soapEnvelope the soap message to be sent. - * @return {@link SppResponseMessage} parsed, {@code null} in any failure + * @return {@code true} if {@link Network} is valid and {@code soapEnvelope} is not null, + * {@code false} otherwise. */ - public SppResponseMessage exchangeSoapMessage(@NonNull SoapSerializationEnvelope soapEnvelope) { + public boolean exchangeSoapMessage(@NonNull SoapSerializationEnvelope soapEnvelope) { if (mNetwork == null) { Log.e(TAG, "Network is not established"); - return null; + return false; + } + + if (mUrlConnection == null) { + Log.e(TAG, "Server certificate is not validated"); + return false; } if (soapEnvelope == null) { Log.e(TAG, "soapEnvelope is null"); - return null; + return false; } - if (mUrlConnection == null) { - Log.e(TAG, "Server certificate is not validated"); - return null; - } + mHandler.post(() -> performSoapMessageExchange(soapEnvelope)); + return true; + } + private void performSoapMessageExchange(@NonNull SoapSerializationEnvelope soapEnvelope) { if (mServiceConnection != null) { mServiceConnection.disconnect(); } @@ -205,21 +236,32 @@ public class OsuServerConnection { mServiceConnection = getServiceConnection(); if (mServiceConnection == null) { Log.e(TAG, "ServiceConnection for https is null"); - return null; + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), null); + return; + } } - SppResponseMessage sppResponse; + SppResponseMessage sppResponse = null; try { // Sending the SOAP message mHttpsTransport.call("", soapEnvelope); Object response = soapEnvelope.bodyIn; if (response == null) { Log.e(TAG, "SoapObject is null"); - return null; + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + null); + return; + } } if (!(response instanceof SoapObject)) { Log.e(TAG, "Not a SoapObject instance"); - return null; + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + null); + return; + } } SoapObject soapResponse = (SoapObject) response; if (mVerboseLoggingEnabled) { @@ -239,12 +281,19 @@ public class OsuServerConnection { } else { Log.e(TAG, "Failed to exchange the SOAP message"); } - return null; + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), null); + return; + } } finally { mServiceConnection.disconnect(); mServiceConnection = null; } - return sppResponse; + + if (mOsuServerCallbacks != null) { + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + sppResponse); + } } /** @@ -331,6 +380,7 @@ public class OsuServerConnection { /** * Returns the OSU certificate matching the FQDN of the OSU server + * * @return {@link X509Certificate} OSU certificate matching FQDN of OSU server */ public X509Certificate getProviderCert() { diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java b/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java index 953107cf7..f743882e3 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java @@ -130,7 +130,7 @@ public class PasspointObjectFactory{ * @return {@link OsuServerConnection} */ public OsuServerConnection makeOsuServerConnection() { - return new OsuServerConnection(); + return new OsuServerConnection(null); } diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java b/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java index 0c8a6a72e..d47f5eece 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java @@ -16,14 +16,18 @@ package com.android.server.wifi.hotspot2; +import android.annotation.Nullable; import android.content.Context; +import android.content.Intent; import android.net.Network; +import android.net.wifi.WifiManager; import android.net.wifi.hotspot2.IProvisioningCallback; import android.net.wifi.hotspot2.OsuProvider; import android.net.wifi.hotspot2.ProvisioningCallback; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; +import android.os.UserHandle; import android.util.Log; import com.android.server.wifi.WifiNative; @@ -51,6 +55,7 @@ public class PasspointProvisioner { // TLS version to be used for HTTPS connection with OSU server private static final String TLS_VERSION = "TLSv1"; + private static final String OSU_APP_PACKAGE = "com.android.hotspot2"; private final Context mContext; private final ProvisioningStateMachine mProvisioningStateMachine; @@ -86,7 +91,7 @@ public class PasspointProvisioner { mOsuNetworkConnection.init(mProvisioningStateMachine.getHandler()); // Offload the heavy load job to another thread mProvisioningStateMachine.getHandler().post(() -> { - mRedirectListener = RedirectListener.createInstance(); + mRedirectListener = RedirectListener.createInstance(looper); mWfaKeyStore.load(); mOsuServerConnection.init(mObjectFactory.getSSLContext(TLS_VERSION), mObjectFactory.getTrustManagerImpl(mWfaKeyStore.get())); @@ -140,6 +145,7 @@ public class PasspointProvisioner { static final int STATE_OSU_AP_CONNECTED = 3; static final int STATE_OSU_SERVER_CONNECTED = 4; static final int STATE_WAITING_FOR_FIRST_SOAP_RESPONSE = 5; + static final int STATE_WAITING_FOR_REDIRECT_RESPONSE = 6; private OsuProvider mOsuProvider; private IProvisioningCallback mProvisioningCallback; @@ -159,6 +165,7 @@ public class PasspointProvisioner { /** * Returns the handler on which a runnable can be posted + * * @return Handler State Machine's handler */ public Handler getHandler() { @@ -167,8 +174,10 @@ public class PasspointProvisioner { /** * Start Provisioning with the Osuprovider and invoke callbacks + * * @param provider OsuProvider to provision with * @param callback IProvisioningCallback to invoke callbacks on + * Note: Called on main thread (WifiService thread). */ public void startProvisioning(OsuProvider provider, IProvisioningCallback callback) { if (mVerboseLoggingEnabled) { @@ -198,6 +207,7 @@ public class PasspointProvisioner { mServerUrl = serverUrl; mProvisioningCallback = callback; mOsuProvider = provider; + // Register for network and wifi state events during provisioning flow mOsuNetworkConnection.setEventCallback(mOsuNetworkCallbacks); @@ -216,6 +226,8 @@ public class PasspointProvisioner { /** * Handle Wifi Disable event + * + * Note: Called on main thread (WifiService thread). */ public void handleWifiDisabled() { if (mVerboseLoggingEnabled) { @@ -230,6 +242,8 @@ public class PasspointProvisioner { /** * Handle server validation failure + * + * Note: Called on main thread (WifiService thread). */ public void handleServerValidationFailure(int sessionId) { if (mVerboseLoggingEnabled) { @@ -249,6 +263,8 @@ public class PasspointProvisioner { /** * Handle status of server validation success + * + * Note: Called on main thread (WifiService thread). */ public void handleServerValidationSuccess(int sessionId) { if (mVerboseLoggingEnabled) { @@ -284,12 +300,6 @@ public class PasspointProvisioner { } invokeProvisioningCallback(PROVISIONING_STATUS, ProvisioningCallback.OSU_STATUS_SERVICE_PROVIDER_VERIFIED); - - invokeProvisioningCallback(PROVISIONING_STATUS, - ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); - - // Move to initiate soap exchange - changeState(STATE_WAITING_FOR_FIRST_SOAP_RESPONSE); mProvisioningStateMachine.getHandler().post(() -> initSoapExchange()); } @@ -301,71 +311,207 @@ public class PasspointProvisioner { Log.v(TAG, "Initiates soap message exchange in state =" + mState); } - if (mState != STATE_WAITING_FOR_FIRST_SOAP_RESPONSE) { + if (mState != STATE_OSU_SERVER_CONNECTED) { Log.e(TAG, "Initiates soap message exchange in wrong state=" + mState); resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } // Redirect uri used for signal of completion for registration process. - final URL redirectUri = mRedirectListener.getURL(); - if (redirectUri == null) { - Log.e(TAG, "redirectUri is not valid"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); - return; - } + final URL redirectUri = mRedirectListener.getServerUrl(); // Sending the first sppPostDevDataRequest message. - SppResponseMessage sppResponse = mOsuServerConnection.exchangeSoapMessage( + if (mOsuServerConnection.exchangeSoapMessage( PostDevDataMessage.serializeToSoapEnvelope(mContext, mSystemInfo, redirectUri.toString(), - SppConstants.SppReason.SUBSCRIPTION_REGISTRATION, - null)); - if (sppResponse == null) { - Log.e(TAG, "failed to send the sppPostDevData message"); + SppConstants.SppReason.SUBSCRIPTION_REGISTRATION, null))) { + invokeProvisioningCallback(PROVISIONING_STATUS, + ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); + // Move to initiate soap exchange + changeState(STATE_WAITING_FOR_FIRST_SOAP_RESPONSE); + } else { + Log.e(TAG, "HttpsConnection is not established for soap message exchange"); resetStateMachine(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); return; } + } + + private void launchOsuWebView() { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "launch Osu webview in state =" + mState); + } - if (sppResponse.getMessageType() - != SppResponseMessage.MessageType.POST_DEV_DATA_RESPONSE) { - Log.e(TAG, "Expected a PostDevDataResponse, but got " - + sppResponse.getMessageType()); - resetStateMachine( - ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE); + if (mState != STATE_WAITING_FOR_FIRST_SOAP_RESPONSE) { + Log.e(TAG, "launch Osu webview in wrong state =" + mState); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } - PostDevDataResponse devDataResponse = (PostDevDataResponse) sppResponse; - mSessionId = devDataResponse.getSessionID(); - if (devDataResponse.getSppCommand().getExecCommandId() - != SppCommand.ExecCommandId.BROWSER) { - Log.e(TAG, "Expected a launchBrowser command, but got " - + devDataResponse.getSppCommand().getExecCommandId()); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_UNEXPECTED_COMMAND_TYPE); + // Start the redirect server to listen the HTTP redirect response from server + // as completion of user input. + if (!mRedirectListener.startServer(new RedirectListener.RedirectCallback() { + /** Called on different thread (RedirectListener thread). */ + @Override + public void onRedirectReceived() { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "Received HTTP redirect response"); + } + mProvisioningStateMachine.getHandler().post(() -> handleRedirectResponse()); + } + + /** Called on main thread (WifiService thread). */ + @Override + public void onRedirectTimedOut() { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "Timed out to receive a HTTP redirect response"); + } + mProvisioningStateMachine.handleTimeOutForRedirectResponse(); + } + })) { + Log.e(TAG, "fails to start redirect listener"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_START_REDIRECT_LISTENER); return; } - Log.d(TAG, "Exec: " + devDataResponse.getSppCommand().getExecCommandId() + ", for '" - + devDataResponse.getSppCommand().getCommandData() + "'"); + Intent intent = new Intent(WifiManager.ACTION_PASSPOINT_LAUNCH_OSU_VIEW); + intent.setPackage(OSU_APP_PACKAGE); + intent.putExtra(WifiManager.EXTRA_OSU_NETWORK, mNetwork); + intent.putExtra(WifiManager.EXTRA_URL, mWebUrl); - mWebUrl = ((BrowserUri) devDataResponse.getSppCommand().getCommandData()).getUri(); - if (mWebUrl == null) { - Log.e(TAG, "No Web-Url"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + intent.setFlags( + Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); + + // Verify that the intent will resolve to an activity + if (intent.resolveActivity(mContext.getPackageManager()) != null) { + mContext.startActivityAsUser(intent, UserHandle.CURRENT); + invokeProvisioningCallback(PROVISIONING_STATUS, + ProvisioningCallback.OSU_STATUS_WAITING_FOR_REDIRECT_RESPONSE); + changeState(STATE_WAITING_FOR_REDIRECT_RESPONSE); + } else { + Log.e(TAG, "can't resolve the activity for the intent"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_NO_OSU_ACTIVITY_FOUND); return; } + } + + /** + * Initiates the second SOAP message exchange with sending the sppPostDevData message. + */ + private void secondSoapExchange() { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "Initiates the second soap message exchange in state =" + mState); + } - if (!mWebUrl.toLowerCase(Locale.US).contains(mSessionId.toLowerCase(Locale.US))) { - Log.e(TAG, "Bad or Missing session ID in webUrl"); - resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + if (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) { + Log.e(TAG, "Initiates the second soap message exchange in wrong state=" + mState); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); return; } + // TODO(b/74244324): Implement a routine to transmit second SOAP message. + } + + /** + * Handles SOAP message response sent by server + * + * @param sessionId indicating current session ID + * @param responseMessage SOAP SPP response, or {@code null} in any failure. + * Note: Called on main thread (WifiService thread). + */ + public void handleSoapMessageResponse(int sessionId, + @Nullable SppResponseMessage responseMessage) { + if (sessionId != mCurrentSessionId) { + Log.w(TAG, "Expected soapMessageResponse callback for currentSessionId=" + + mCurrentSessionId); + return; + } + + if (responseMessage == null) { + Log.e(TAG, "failed to send the sppPostDevData message"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); + return; + } + + if (mState == STATE_WAITING_FOR_FIRST_SOAP_RESPONSE) { + if (responseMessage.getMessageType() + != SppResponseMessage.MessageType.POST_DEV_DATA_RESPONSE) { + Log.e(TAG, "Expected a PostDevDataResponse, but got " + + responseMessage.getMessageType()); + resetStateMachine( + ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE); + return; + } + + PostDevDataResponse devDataResponse = (PostDevDataResponse) responseMessage; + mSessionId = devDataResponse.getSessionID(); + if (devDataResponse.getSppCommand().getExecCommandId() + != SppCommand.ExecCommandId.BROWSER) { + Log.e(TAG, "Expected a launchBrowser command, but got " + + devDataResponse.getSppCommand().getExecCommandId()); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_UNEXPECTED_COMMAND_TYPE); + return; + } + + Log.d(TAG, "Exec: " + devDataResponse.getSppCommand().getExecCommandId() + ", for '" + + devDataResponse.getSppCommand().getCommandData() + "'"); + + mWebUrl = ((BrowserUri) devDataResponse.getSppCommand().getCommandData()).getUri(); + if (mWebUrl == null) { + Log.e(TAG, "No Web-Url"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + return; + } + + if (!mWebUrl.toLowerCase(Locale.US).contains(mSessionId.toLowerCase(Locale.US))) { + Log.e(TAG, "Bad or Missing session ID in webUrl"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_INVALID_SERVER_URL); + return; + } + launchOsuWebView(); + } + } + + /** + * Handles next step once receiving a HTTP redirect response. + * + * Note: Called on main thread (WifiService thread). + */ + public void handleRedirectResponse() { + if (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) { + Log.e(TAG, "Received redirect request in wrong state=" + mState); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + return; + } + + invokeProvisioningCallback(PROVISIONING_STATUS, + ProvisioningCallback.OSU_STATUS_REDIRECT_RESPONSE_RECEIVED); + mRedirectListener.stopServer(); + secondSoapExchange(); + } + + /** + * Handles next step when timeout occurs because {@link RedirectListener} doesn't + * receive a HTTP redirect response. + * + * Note: Called on main thread (WifiService thread). + */ + public void handleTimeOutForRedirectResponse() { + Log.e(TAG, "Timed out for HTTP redirect response"); + + if (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) { + Log.e(TAG, "Received timeout error for HTTP redirect response in wrong state=" + + mState); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED); + return; + } + mRedirectListener.stopServer(); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_TIMED_OUT_REDIRECT_LISTENER); } /** * Connected event received + * * @param network Network object for this connection + * Note: Called on main thread (WifiService thread). */ public void handleConnectedEvent(Network network) { if (mVerboseLoggingEnabled) { @@ -402,6 +548,8 @@ public class PasspointProvisioner { /** * Disconnect event received + * + * Note: Called on main thread (WifiService thread). */ public void handleDisconnect() { if (mVerboseLoggingEnabled) { @@ -444,6 +592,7 @@ public class PasspointProvisioner { private void resetStateMachine(int failureCode) { invokeProvisioningCallback(PROVISIONING_FAILURE, failureCode); + mRedirectListener.stopServer(); mOsuNetworkConnection.setEventCallback(null); mOsuNetworkConnection.disconnectIfNeeded(); mOsuServerConnection.setEventCallback(null); @@ -454,6 +603,8 @@ public class PasspointProvisioner { /** * Callbacks for network and wifi events + * + * Note: Called on main thread (WifiService thread). */ class OsuNetworkCallbacks implements OsuNetworkConnection.Callbacks { @@ -505,6 +656,8 @@ public class PasspointProvisioner { /** * Defines the callbacks expected from OsuServerConnection + * + * Note: Called on main thread (WifiService thread). */ public class OsuServerCallbacks { private final int mSessionId; @@ -515,6 +668,7 @@ public class PasspointProvisioner { /** * Returns the session ID corresponding to this callback + * * @return int sessionID */ public int getSessionId() { @@ -523,6 +677,7 @@ public class PasspointProvisioner { /** * Provides a server validation status for the session ID + * * @param sessionId integer indicating current session ID * @param succeeded boolean indicating success/failure of server validation */ @@ -541,8 +696,22 @@ public class PasspointProvisioner { } } + /** + * Callback when soap message is received from server. + * + * @param sessionId indicating current session ID + * @param responseMessage SOAP SPP response parsed or {@code null} in any failure + * Note: Called on different thread (OsuServer Thread)! + */ + public void onReceivedSoapMessage(int sessionId, + @Nullable SppResponseMessage responseMessage) { + if (mVerboseLoggingEnabled) { + Log.v(TAG, "onReceivedSoapMessage with sessionId=" + sessionId); + } + mProvisioningStateMachine.getHandler().post(() -> + mProvisioningStateMachine.handleSoapMessageResponse(sessionId, + responseMessage)); + } } } - - diff --git a/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java b/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java index 6b7873d3e..5c9391150 100644 --- a/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java +++ b/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java @@ -16,42 +16,91 @@ package com.android.server.wifi.hotspot2.soap; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; + import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.URL; import java.util.Random; +import fi.iki.elonen.NanoHTTPD; + /** - * Redirect message listener to listen for the redirect message from server. + * Server for listening for redirect request from the OSU server to indicate the completion + * of user input. + * + * A HTTP server will be started in the {@link RedirectListener#startServer} of {@link + * RedirectListener}, so the caller will need to invoke {@link RedirectListener#stop} once the + * redirect server no longer needed. */ -public class RedirectListener extends Thread { +public class RedirectListener extends NanoHTTPD { + // 4 minutes for the maximum wait time. + @VisibleForTesting + static final int USER_TIMEOUT_MILLIS = 4 * 60 * 1000; + private static final String TAG = "RedirectListener"; - private final ServerSocket mServerSocket; + private final String mPath; - private final URL mURL; + private final URL mServerUrl; + private final Handler mStartStopHandler; + private final Handler mHandler; + private Runnable mTimeOutTask; + private RedirectCallback mRedirectCallback; + + /** + * Listener interface for handling redirect events. + */ + public interface RedirectCallback { + + /** + * Invoked when HTTP redirect response is received. + */ + void onRedirectReceived(); + + /** + * Invoked when timeout occurs on receiving HTTP redirect response. + */ + void onRedirectTimedOut(); + } + + @VisibleForTesting + /* package */ RedirectListener(Looper looper, @Nullable Looper startStopLooper, int port) + throws IOException { + super(InetAddress.getLocalHost().getHostAddress(), port); - private RedirectListener() throws IOException { - mServerSocket = new ServerSocket(0, 5, InetAddress.getLocalHost()); Random rnd = new Random(System.currentTimeMillis()); + mPath = "rnd" + Integer.toString(Math.abs(rnd.nextInt()), Character.MAX_RADIX); - mURL = new URL("http", mServerSocket.getInetAddress().getHostAddress(), - mServerSocket.getLocalPort(), mPath); - setName("HS20-Redirect-Listener"); - setDaemon(true); + mServerUrl = new URL("http", getHostname(), port, mPath); + mHandler = new Handler(looper); + mTimeOutTask = () -> mRedirectCallback.onRedirectTimedOut(); + if (startStopLooper == null) { + HandlerThread redirectHandlerThread = new HandlerThread("RedirectListenerHandler"); + redirectHandlerThread.start(); + startStopLooper = redirectHandlerThread.getLooper(); + } + mStartStopHandler = new Handler(startStopLooper); } /** * Create an instance of {@link RedirectListener} * + * @param looper Looper on which the {@link RedirectCallback} will be called. * @return Instance of {@link RedirectListener}, {@code null} in any failure. */ - public static RedirectListener createInstance() { + public static RedirectListener createInstance(@NonNull Looper looper) { RedirectListener redirectListener; try { - redirectListener = new RedirectListener(); + redirectListener = new RedirectListener(looper, null, + new ServerSocket(0, 1, InetAddress.getLocalHost()).getLocalPort()); } catch (IOException e) { Log.e(TAG, "fails to create an instance: " + e); return null; @@ -59,7 +108,73 @@ public class RedirectListener extends Thread { return redirectListener; } - public URL getURL() { - return mURL; + /** + * Start redirect listener + * + * @param callback to be notified when the redirect request is received or timed out. + * @return {@code true} in success, {@code false} if the {@code callback} is {@code null} or the + * server is already running. + */ + public boolean startServer(@NonNull RedirectCallback callback) { + if (callback == null) { + return false; + } + + if (isAlive()) { + Log.e(TAG, "redirect listener is already running"); + return false; + } + mRedirectCallback = callback; + + mStartStopHandler.post(() -> { + try { + start(); + } catch (IOException e) { + Log.e(TAG, "unable to start redirect listener: " + e); + } + }); + mHandler.postDelayed(mTimeOutTask, USER_TIMEOUT_MILLIS); + return true; + } + + /** + * Stop redirect listener + */ + public void stopServer() { + if (isServerAlive()) { + mStartStopHandler.post(() -> stop()); + } + } + + /** + * Check if the server is alive or not. + * + * @return {@code true} if the server is alive. + */ + public boolean isServerAlive() { + return isAlive(); + } + + /** + * Get URL to which the local redirect server listens + * + * @return The URL for the local redirect server. + */ + public URL getServerUrl() { + return mServerUrl; + } + + @Override + public Response serve(IHTTPSession session) { + + // Ignore all other requests except for a HTTP request that has the server url path with + // GET method. + if (session.getMethod() != Method.GET || !mServerUrl.getPath().equals(session.getUri())) { + return newFixedLengthResponse(Response.Status.NOT_FOUND, NanoHTTPD.MIME_HTML, ""); + } + + mHandler.removeCallbacks(mTimeOutTask); + mRedirectCallback.onRedirectReceived(); + return newFixedLengthResponse(""); } } diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java index c91b1bcb5..8a123c2f2 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/OsuServerConnectionTest.java @@ -16,9 +16,7 @@ package com.android.server.wifi.hotspot2; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isNull; @@ -30,6 +28,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.net.Network; +import android.os.test.TestLooper; import android.support.test.filters.SmallTest; import android.util.Pair; @@ -78,6 +77,7 @@ public class OsuServerConnectionTest { private static final int ENABLE_VERBOSE_LOGGING = 1; private static final int TEST_SESSION_ID = 1; + private TestLooper mLooper = new TestLooper(); private OsuServerConnection mOsuServerConnection; private URL mValidServerUrl; private List> mProviderIdentities = new ArrayList<>(); @@ -98,7 +98,7 @@ public class OsuServerConnectionTest { @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); - mOsuServerConnection = new OsuServerConnection(); + mOsuServerConnection = new OsuServerConnection(mLooper.getLooper()); mOsuServerConnection.enableVerboseLogging(ENABLE_VERBOSE_LOGGING); mProviderIdentities.add(Pair.create(Locale.US, PROVIDER_NAME_VALID)); mValidServerUrl = new URL(TEST_VALID_URL); @@ -249,16 +249,16 @@ public class OsuServerConnectionTest { } /** - * Verifies {@code ExchangeSoapMessage} should return {@code null} if there is no connection. + * Verifies {@code ExchangeSoapMessage} should return {@code false} if there is no connection. */ @Test public void verifyExchangeSoapMessageWithoutConnection() { - assertNull(mOsuServerConnection.exchangeSoapMessage( + assertFalse(mOsuServerConnection.exchangeSoapMessage( new SoapSerializationEnvelope(SoapEnvelope.VER12))); } /** - * Verifies {@code ExchangeSoapMessage} should return {@code null} if {@code soapEnvelope} is + * Verifies {@code ExchangeSoapMessage} should return {@code false} if {@code soapEnvelope} is * {@code null} */ @Test @@ -267,12 +267,12 @@ public class OsuServerConnectionTest { mOsuServerConnection.setEventCallback(mOsuServerCallbacks); assertTrue(mOsuServerConnection.connect(mValidServerUrl, mNetwork)); - assertNull(mOsuServerConnection.exchangeSoapMessage(null)); + assertFalse(mOsuServerConnection.exchangeSoapMessage(null)); } /** - * Verifies {@code ExchangeSoapMessage} should return {@code null} if exception occurs during - * soap exchange. + * Verifies {@code ExchangeSoapMessage} should get {@code null} message if exception occurs + * during soap exchange. */ @Test public void verifyExchangeSoapMessageWithException() throws Exception { @@ -280,14 +280,19 @@ public class OsuServerConnectionTest { MockitoSession session = ExtendedMockito.mockitoSession().mockStatic( HttpsTransport.class).startMocking(); try { + mOsuServerConnection.init(mTlsContext, mDelegate); + mOsuServerConnection.setEventCallback(mOsuServerCallbacks); when(HttpsTransport.createInstance(any(Network.class), any(URL.class))).thenReturn( mHttpsTransport); doThrow(new IOException()).when(mHttpsTransport).call(any(String.class), any(SoapSerializationEnvelope.class)); assertTrue(mOsuServerConnection.connect(mValidServerUrl, mNetwork)); - assertNull(mOsuServerConnection.exchangeSoapMessage( + assertTrue(mOsuServerConnection.exchangeSoapMessage( new SoapSerializationEnvelope(SoapEnvelope.VER12))); + + mLooper.dispatchAll(); + verify(mOsuServerCallbacks).onReceivedSoapMessage(anyInt(), isNull()); } finally { session.finishMocking(); } @@ -302,6 +307,8 @@ public class OsuServerConnectionTest { MockitoSession session = ExtendedMockito.mockitoSession().mockStatic( HttpsTransport.class).startMocking(); try { + mOsuServerConnection.init(mTlsContext, mDelegate); + mOsuServerConnection.setEventCallback(mOsuServerCallbacks); when(HttpsTransport.createInstance(any(Network.class), any(URL.class))).thenReturn( mHttpsTransport); assertTrue(mOsuServerConnection.connect(mValidServerUrl, mNetwork)); @@ -314,7 +321,10 @@ public class OsuServerConnectionTest { envelope.bodyIn = new SoapObject(); when(SoapParser.getResponse(any(SoapObject.class))).thenReturn(mSppResponseMessage); - assertEquals(mSppResponseMessage, mOsuServerConnection.exchangeSoapMessage(envelope)); + assertTrue(mOsuServerConnection.exchangeSoapMessage(envelope)); + + mLooper.dispatchAll(); + verify(mOsuServerCallbacks).onReceivedSoapMessage(anyInt(), eq(mSppResponseMessage)); } finally { session.finishMocking(); } diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java index 161bd2a86..5e73cd652 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/PasspointProvisionerTest.java @@ -29,6 +29,11 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.net.Network; import android.net.wifi.WifiManager; import android.net.wifi.WifiSsid; @@ -42,19 +47,23 @@ import android.os.test.TestLooper; import android.support.test.filters.SmallTest; import android.telephony.TelephonyManager; +import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.org.conscrypt.TrustManagerImpl; import com.android.server.wifi.WifiNative; import com.android.server.wifi.hotspot2.soap.PostDevDataResponse; +import com.android.server.wifi.hotspot2.soap.RedirectListener; import com.android.server.wifi.hotspot2.soap.SppResponseMessage; import com.android.server.wifi.hotspot2.soap.command.BrowserUri; import com.android.server.wifi.hotspot2.soap.command.SppCommand; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.ksoap2.serialization.SoapSerializationEnvelope; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; import java.net.URL; import java.security.KeyStore; @@ -71,6 +80,7 @@ public class PasspointProvisionerTest { private static final int STEP_INIT = 0; private static final int STEP_AP_CONNECT = 1; private static final int STEP_SERVER_CONNECT = 2; + private static final int STEP_WAIT_FOR_REDIRECT_RESPONSE = 3; private static final String TEST_DEV_ID = "12312341"; private static final String TEST_MANUFACTURER = Build.MANUFACTURER; @@ -84,20 +94,27 @@ public class PasspointProvisionerTest { private static final String TEST_SW_VERSION = "Android Test 1.0"; private static final String TEST_FW_VERSION = "Test FW 1.0"; private static final String TEST_REDIRECT_URL = "http://127.0.0.1:12345/index.htm"; + private static final String OSU_APP_PACKAGE = "com.android.hotspot2"; + private static final String OSU_APP_NAME = "OsuLogin"; private PasspointProvisioner mPasspointProvisioner; private TestLooper mLooper = new TestLooper(); private Handler mHandler; private OsuNetworkConnection.Callbacks mOsuNetworkCallbacks; private PasspointProvisioner.OsuServerCallbacks mOsuServerCallbacks; + private RedirectListener.RedirectCallback mRedirectReceivedListener; private ArgumentCaptor mOsuNetworkCallbacksCaptor = ArgumentCaptor.forClass(OsuNetworkConnection.Callbacks.class); private ArgumentCaptor mOsuServerCallbacksCaptor = ArgumentCaptor.forClass(PasspointProvisioner.OsuServerCallbacks.class); + private ArgumentCaptor + mOnRedirectReceivedArgumentCaptor = + ArgumentCaptor.forClass(RedirectListener.RedirectCallback.class); private ArgumentCaptor mHandlerCaptor = ArgumentCaptor.forClass(Handler.class); private OsuProvider mOsuProvider; private TrustManagerImpl mDelegate; private URL mTestUrl; + private MockitoSession mSession; @Mock PasspointObjectFactory mObjectFactory; @Mock Context mContext; @@ -110,17 +127,27 @@ public class PasspointProvisionerTest { @Mock KeyStore mKeyStore; @Mock SSLContext mTlsContext; @Mock WifiNative mWifiNative; - @Mock SoapSerializationEnvelope mSoapEnvelope; @Mock PostDevDataResponse mSppResponseMessage; @Mock SystemInfo mSystemInfo; @Mock TelephonyManager mTelephonyManager; @Mock SppCommand mSppCommand; @Mock BrowserUri mBrowserUri; + @Mock RedirectListener mRedirectListener; + @Mock PackageManager mPackageManager; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); mTestUrl = new URL(TEST_REDIRECT_URL); + mSession = ExtendedMockito.mockitoSession().mockStatic( + RedirectListener.class).startMocking(); + + when(RedirectListener.createInstance(mLooper.getLooper())).thenReturn( + mRedirectListener); + when(mRedirectListener.getServerUrl()).thenReturn(new URL(TEST_REDIRECT_URL)); + when(mRedirectListener.startServer( + any(RedirectListener.RedirectCallback.class))).thenReturn(true); + when(mRedirectListener.isAlive()).thenReturn(true); when(mWifiManager.isWifiEnabled()).thenReturn(true); when(mObjectFactory.makeOsuNetworkConnection(any(Context.class))) .thenReturn(mOsuNetworkConnection); @@ -162,8 +189,20 @@ public class PasspointProvisionerTest { when(mSppCommand.getCommandData()).thenReturn(mBrowserUri); when(mBrowserUri.getUri()).thenReturn(TEST_URL); when(mOsuServerConnection.exchangeSoapMessage( - any(SoapSerializationEnvelope.class))).thenReturn( - mSppResponseMessage); + any(SoapSerializationEnvelope.class))).thenReturn(true); + when(mContext.getPackageManager()).thenReturn(mPackageManager); + ResolveInfo resolveInfo = new ResolveInfo(); + resolveInfo.activityInfo = new ActivityInfo(); + resolveInfo.activityInfo.applicationInfo = new ApplicationInfo(); + resolveInfo.activityInfo.name = OSU_APP_NAME; + resolveInfo.activityInfo.applicationInfo.packageName = OSU_APP_PACKAGE; + when(mPackageManager.resolveActivity(any(Intent.class), + eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(resolveInfo); + } + + @After + public void cleanUp() { + mSession.finishMocking(); } private void initAndStartProvisioning() { @@ -205,6 +244,30 @@ public class PasspointProvisionerTest { } else if (step == STEP_SERVER_CONNECT) { verify(mCallback).onProvisioningStatus( ProvisioningCallback.OSU_STATUS_SERVER_CONNECTED); + } else if (step == STEP_WAIT_FOR_REDIRECT_RESPONSE) { + // Server validation passed + mOsuServerCallbacks.onServerValidationStatus(mOsuServerCallbacks.getSessionId(), + true); + mLooper.dispatchAll(); + + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_SERVER_VALIDATED); + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_SERVICE_PROVIDER_VERIFIED); + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); + + // Received soapMessageResponse + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + mSppResponseMessage); + mLooper.dispatchAll(); + + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_WAITING_FOR_REDIRECT_RESPONSE); + verify(mRedirectListener, atLeastOnce()) + .startServer(mOnRedirectReceivedArgumentCaptor.capture()); + mRedirectReceivedListener = mOnRedirectReceivedArgumentCaptor.getValue(); + verifyNoMoreInteractions(mCallback); } } } @@ -426,7 +489,7 @@ public class PasspointProvisionerTest { public void verifyExchangingSoapMessageFailure() throws RemoteException { // Fail to exchange the SOAP message when(mOsuServerConnection.exchangeSoapMessage( - any(SoapSerializationEnvelope.class))).thenReturn(null); + any(SoapSerializationEnvelope.class))).thenReturn(false); stopAfterStep(STEP_SERVER_CONNECT); // Server validation passed @@ -436,19 +499,21 @@ public class PasspointProvisionerTest { verify(mCallback).onProvisioningStatus(ProvisioningCallback.OSU_STATUS_SERVER_VALIDATED); verify(mCallback).onProvisioningStatus( ProvisioningCallback.OSU_STATUS_SERVICE_PROVIDER_VERIFIED); - verify(mCallback).onProvisioningStatus(ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); verify(mCallback).onProvisioningFailure( ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); - // Osu provider verification is the last current step in the flow, no more runnables posted. + // No further runnables posted verifyNoMoreInteractions(mCallback); } /** - * Verifies that the right provisioning callbacks are invoked as the provisioner progresses - * to the end as successful case. + * Verifies that the right provisioning callbacks are invoked when there is no OSU activity for + * the intent */ @Test - public void verifyProvisioningFlowForSuccessfulCase() throws RemoteException { + public void verifyNoOsuActivityFoundFailure() throws RemoteException { + // There is no activity found for the intent + when(mPackageManager.resolveActivity(any(Intent.class), + eq(PackageManager.MATCH_DEFAULT_ONLY))).thenReturn(null); stopAfterStep(STEP_SERVER_CONNECT); // Server validation passed @@ -459,8 +524,53 @@ public class PasspointProvisionerTest { verify(mCallback).onProvisioningStatus( ProvisioningCallback.OSU_STATUS_SERVICE_PROVIDER_VERIFIED); verify(mCallback).onProvisioningStatus(ProvisioningCallback.OSU_STATUS_INIT_SOAP_EXCHANGE); - // Osu provider verification is the last current step in the flow, no more runnables posted. + + // Received soapMessageResponse + mOsuServerCallbacks.onReceivedSoapMessage(mOsuServerCallbacks.getSessionId(), + mSppResponseMessage); + mLooper.dispatchAll(); + + verify(mCallback).onProvisioningFailure( + ProvisioningCallback.OSU_FAILURE_NO_OSU_ACTIVITY_FOUND); + // No further runnables posted verifyNoMoreInteractions(mCallback); } -} + /** + * Verifies that the right provisioning callbacks are invoked when timeout occurs for HTTP + * redirect response. + */ + @Test + public void verifyRedirectResponseTimeout() throws RemoteException { + stopAfterStep(STEP_WAIT_FOR_REDIRECT_RESPONSE); + + // Timed out for HTTP redirect response. + mRedirectReceivedListener.onRedirectTimedOut(); + mLooper.dispatchAll(); + + verify(mRedirectListener, atLeastOnce()).stopServer(); + verify(mCallback).onProvisioningFailure( + ProvisioningCallback.OSU_FAILURE_TIMED_OUT_REDIRECT_LISTENER); + // No further runnables posted + verifyNoMoreInteractions(mCallback); + } + + /** + * Verifies that the right provisioning callbacks are invoked as the provisioner progresses + * to the end as successful case. + */ + @Test + public void verifyProvisioningFlowForSuccessfulCase() throws RemoteException { + stopAfterStep(STEP_WAIT_FOR_REDIRECT_RESPONSE); + + // Received HTTP redirect response. + mRedirectReceivedListener.onRedirectReceived(); + mLooper.dispatchAll(); + + verify(mRedirectListener, atLeastOnce()).stopServer(); + verify(mCallback).onProvisioningStatus( + ProvisioningCallback.OSU_STATUS_REDIRECT_RESPONSE_RECEIVED); + // No further runnables posted + verifyNoMoreInteractions(mCallback); + } +} diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataMessageTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataMessageTest.java index de95cc8ca..179ac68a6 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataMessageTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataMessageTest.java @@ -43,7 +43,6 @@ import org.ksoap2.serialization.SoapPrimitive; import org.ksoap2.serialization.SoapSerializationEnvelope; import org.mockito.Mock; - /** * Unit tests for {@link PostDevDataMessage}. */ diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataResponseTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataResponseTest.java index 25e91cba8..d5b827d8f 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataResponseTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/PostDevDataResponseTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.MockitoAnnotations.initMocks; +import android.support.test.filters.SmallTest; + import com.android.server.wifi.hotspot2.soap.command.SppCommand; import org.junit.Before; @@ -30,6 +32,7 @@ import org.ksoap2.serialization.SoapObject; /** * Unit tests for {@link PostDevDataResponse}. */ +@SmallTest public class PostDevDataResponseTest { private static final String EXEC = "exec"; private static final String BROWSER_COMMAND = "launchBrowserToURI"; diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/RedirectListenerTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/RedirectListenerTest.java new file mode 100644 index 000000000..c4fb3fd71 --- /dev/null +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/RedirectListenerTest.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2.soap; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import android.os.Looper; +import android.os.test.TestLooper; +import android.support.test.filters.SmallTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import java.io.IOException; +import java.net.URL; + +import fi.iki.elonen.NanoHTTPD; + +/** + * Unit tests for {@link RedirectListener}. + */ +@SmallTest +public class RedirectListenerTest { + private static final int TEST_PORT = 1010; + + private RedirectListenerSpy mRedirectListener; + private URL mServerUrl; + private TestLooper mLooper = new TestLooper(); + + @Mock RedirectListener.RedirectCallback mListener; + @Mock NanoHTTPD.IHTTPSession mIHTTPSession; + + /** Spy class to avoid start/stop {@link NanoHTTPD} server */ + private class RedirectListenerSpy extends RedirectListener { + boolean mIsStart = false; + RedirectListenerSpy(Looper looper, int port) throws IOException { + super(looper, looper, port); + } + + @Override + public void start() { + mIsStart = true; + } + + @Override + public void stop() { + mIsStart = false; + } + + @Override + public boolean isServerAlive() { + return mIsStart; + } + } + + /** + * Sets up test. + */ + @Before + public void setUp() throws Exception { + initMocks(this); + + mRedirectListener = new RedirectListenerSpy(mLooper.getLooper(), TEST_PORT); + mServerUrl = mRedirectListener.getServerUrl(); + } + + private void verifyStartServer() { + mRedirectListener.startServer(mListener); + mLooper.dispatchAll(); + + assertTrue(mRedirectListener.mIsStart); + } + + private void verifyStopServer() { + mRedirectListener.stopServer(); + mLooper.dispatchAll(); + + assertFalse(mRedirectListener.mIsStart); + } + + /** + * Verifies that Timeout handler will be invoked when There is no a known GET request received + * in a {@link RedirectListener#USER_TIMEOUT_MILLIS}. + */ + @Test + public void timeOutForKnownGetRequest() { + when(mIHTTPSession.getMethod()).thenReturn(NanoHTTPD.Method.PUT); + verifyStartServer(); + mRedirectListener.serve(mIHTTPSession); + + verify(mListener, never()).onRedirectReceived(); + + // Timeout has expired. + mLooper.moveTimeForward(RedirectListener.USER_TIMEOUT_MILLIS); + mLooper.dispatchAll(); + + verify(mListener).onRedirectTimedOut(); + verifyStopServer(); + } + + /** + * Verifies that {@link RedirectListener.RedirectCallback#onRedirectReceived()} will not be + * invoked when receiving a GET request with an unexpected path. + */ + @Test + public void receiveUnknownGetRequest() { + when(mIHTTPSession.getMethod()).thenReturn(NanoHTTPD.Method.GET); + when(mIHTTPSession.getUri()).thenReturn("/test"); + verifyStartServer(); + + mRedirectListener.serve(mIHTTPSession); + + verify(mListener, never()).onRedirectReceived(); + + // Timeout has expired. + mLooper.moveTimeForward(RedirectListener.USER_TIMEOUT_MILLIS); + mLooper.dispatchAll(); + + verify(mListener).onRedirectTimedOut(); + verifyStopServer(); + } + + /** + * Verifies that a {@link RedirectListener.RedirectCallback#onRedirectReceived()} callback will + * be invoked when receiving a GET request with an expected path. + */ + @Test + public void receiveKnownGetRequest() { + when(mIHTTPSession.getMethod()).thenReturn(NanoHTTPD.Method.GET); + when(mIHTTPSession.getUri()).thenReturn(mServerUrl.getPath()); + verifyStartServer(); + + mRedirectListener.serve(mIHTTPSession); + + verify(mListener).onRedirectReceived(); + + mLooper.moveTimeForward(RedirectListener.USER_TIMEOUT_MILLIS); + mLooper.dispatchAll(); + + // TimeoutTask is cancelled once receiving HTTP redirect response. + verify(mListener, never()).onRedirectTimedOut(); + verifyStopServer(); + } +} + + + + + + + + diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SoapParserTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SoapParserTest.java index 344d1f93f..3bb63a157 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SoapParserTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SoapParserTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.MockitoAnnotations.initMocks; +import android.support.test.filters.SmallTest; + import org.junit.Before; import org.junit.Test; import org.ksoap2.serialization.PropertyInfo; @@ -28,6 +30,7 @@ import org.ksoap2.serialization.SoapObject; /** * Unit tests for {@link SoapParser}. */ +@SmallTest public class SoapParserTest { private static final String EXEC = "exec"; private static final String BROWSER_COMMAND = "launchBrowserToURI"; diff --git a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SppResponseMessageTest.java b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SppResponseMessageTest.java index 5253946b2..5e6ed070c 100644 --- a/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SppResponseMessageTest.java +++ b/tests/wifitests/src/com/android/server/wifi/hotspot2/soap/SppResponseMessageTest.java @@ -20,6 +20,8 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.MockitoAnnotations.initMocks; +import android.support.test.filters.SmallTest; + import org.junit.Before; import org.junit.Test; import org.ksoap2.serialization.SoapObject; @@ -30,6 +32,7 @@ import java.util.Map; /** * Unit tests for {@link SppResponseMessage}. */ +@SmallTest public class SppResponseMessageTest { private static final String TEST_STATUS = "OK"; private static final String TEST_ERROR_STATUS = "Error occurred"; -- cgit v1.2.3