diff options
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(""); } } |