diff options
author | Ecco Park <eccopark@google.com> | 2018-07-30 17:28:49 -0700 |
---|---|---|
committer | Ecco Park <eccopark@google.com> | 2018-09-04 09:35:53 -0700 |
commit | 937ad30df0977c7ae4077c69c71afa7e813e50a5 (patch) | |
tree | 1367a583e840ca9dcecb6d94f73e0a2126603a10 /service | |
parent | e6a42731c0fa1792db3b8d6c22b5913322ed08e8 (diff) |
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 <eccopark@google.com>
Diffstat (limited to 'service')
6 files changed, 412 insertions, 79 deletions
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(""); } } |