blob: 49105dd457d2bfbde8a34f891443560afe463821 [file] [log] [blame]
/*
* Copyright 2017 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;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.net.Network;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
import android.net.wifi.hotspot2.IProvisioningCallback;
import android.net.wifi.hotspot2.OsuProvider;
import android.net.wifi.hotspot2.PasspointConfiguration;
import android.net.wifi.hotspot2.ProvisioningCallback;
import android.net.wifi.hotspot2.omadm.PpsMoParser;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.WifiMetrics;
import com.android.server.wifi.WifiNative;
import com.android.server.wifi.hotspot2.anqp.ANQPElement;
import com.android.server.wifi.hotspot2.anqp.Constants;
import com.android.server.wifi.hotspot2.anqp.HSOsuProvidersElement;
import com.android.server.wifi.hotspot2.anqp.OsuProviderInfo;
import com.android.server.wifi.hotspot2.soap.ExchangeCompleteMessage;
import com.android.server.wifi.hotspot2.soap.PostDevDataMessage;
import com.android.server.wifi.hotspot2.soap.PostDevDataResponse;
import com.android.server.wifi.hotspot2.soap.RedirectListener;
import com.android.server.wifi.hotspot2.soap.SppConstants;
import com.android.server.wifi.hotspot2.soap.SppResponseMessage;
import com.android.server.wifi.hotspot2.soap.UpdateResponseMessage;
import com.android.server.wifi.hotspot2.soap.command.BrowserUri;
import com.android.server.wifi.hotspot2.soap.command.PpsMoData;
import com.android.server.wifi.hotspot2.soap.command.SppCommand;
import com.android.server.wifi.util.Environment;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
/**
* Provides methods to carry out provisioning flow
*/
public class PasspointProvisioner {
private static final String TAG = "PasspointProvisioner";
// Indicates callback type for caller initiating provisioning
private static final int PROVISIONING_STATUS = 0;
private static final int PROVISIONING_FAILURE = 1;
// TLS version to be used for HTTPS connection with OSU server
private static final String TLS_VERSION = "TLSv1";
private final Context mContext;
private final ProvisioningStateMachine mProvisioningStateMachine;
private final OsuNetworkCallbacks mOsuNetworkCallbacks;
private final OsuNetworkConnection mOsuNetworkConnection;
private final OsuServerConnection mOsuServerConnection;
private final WfaKeyStore mWfaKeyStore;
private final PasspointObjectFactory mObjectFactory;
private final SystemInfo mSystemInfo;
private int mCurrentSessionId = 0;
private int mCallingUid;
private boolean mVerboseLoggingEnabled = false;
private WifiManager mWifiManager;
private PasspointManager mPasspointManager;
private Looper mLooper;
private final WifiMetrics mWifiMetrics;
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public PasspointProvisioner(Context context, WifiNative wifiNative,
PasspointObjectFactory objectFactory, PasspointManager passpointManager,
WifiMetrics wifiMetrics) {
mContext = context;
mOsuNetworkConnection = objectFactory.makeOsuNetworkConnection(context);
mProvisioningStateMachine = new ProvisioningStateMachine();
mOsuNetworkCallbacks = new OsuNetworkCallbacks();
mOsuServerConnection = objectFactory.makeOsuServerConnection();
mWfaKeyStore = objectFactory.makeWfaKeyStore();
mSystemInfo = objectFactory.getSystemInfo(context, wifiNative);
mObjectFactory = objectFactory;
mPasspointManager = passpointManager;
mWifiMetrics = wifiMetrics;
}
/**
* Sets up for provisioning
*
* @param looper Looper on which the Provisioning state machine will run
*/
public void init(Looper looper) {
mLooper = looper;
mWifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
mProvisioningStateMachine.start(new Handler(mLooper));
mOsuNetworkConnection.init(mProvisioningStateMachine.getHandler());
// Offload the heavy load job to another thread
mProvisioningStateMachine.getHandler().post(() -> {
mWfaKeyStore.load();
mOsuServerConnection.init(mObjectFactory.getSSLContext(TLS_VERSION),
mObjectFactory.getTrustManagerFactory(mWfaKeyStore.get()));
});
}
/**
* Enable verbose logging to help debug failures
*
* @param verbose enables verbose logging.
*/
public void enableVerboseLogging(boolean verbose) {
mVerboseLoggingEnabled = verbose;
mOsuNetworkConnection.enableVerboseLogging(verbose);
mOsuServerConnection.enableVerboseLogging(verbose);
}
/**
* Start provisioning flow with a given provider.
*
* @param callingUid calling uid.
* @param provider {@link OsuProvider} to provision with.
* @param callback {@link IProvisioningCallback} to provide provisioning status.
* @return boolean value, true if provisioning was started, false otherwise.
*
* Implements HS2.0 provisioning flow with a given HS2.0 provider.
*/
public boolean startSubscriptionProvisioning(int callingUid, OsuProvider provider,
IProvisioningCallback callback) {
mCallingUid = callingUid;
Log.v(TAG, "Provisioning started with " + provider.toString());
mProvisioningStateMachine.getHandler().post(() -> {
mProvisioningStateMachine.startProvisioning(provider, callback);
});
return true;
}
/**
* Handles the provisioning flow state transitions
*/
class ProvisioningStateMachine {
private static final String TAG = "PasspointProvisioningStateMachine";
static final int STATE_INIT = 1;
static final int STATE_AP_CONNECTING = 2;
static final int STATE_OSU_SERVER_CONNECTING = 3;
static final int STATE_WAITING_FOR_FIRST_SOAP_RESPONSE = 4;
static final int STATE_WAITING_FOR_REDIRECT_RESPONSE = 5;
static final int STATE_WAITING_FOR_SECOND_SOAP_RESPONSE = 6;
static final int STATE_WAITING_FOR_THIRD_SOAP_RESPONSE = 7;
static final int STATE_WAITING_FOR_TRUST_ROOT_CERTS = 8;
private OsuProvider mOsuProvider;
private IProvisioningCallback mProvisioningCallback;
private int mState = STATE_INIT;
private Handler mHandler;
private URL mServerUrl;
private Network mNetwork;
private String mSessionId;
private String mWebUrl;
private PasspointConfiguration mPasspointConfiguration;
private RedirectListener mRedirectListener;
private HandlerThread mRedirectHandlerThread;
private Handler mRedirectStartStopHandler;
/**
* Initializes and starts the state machine with a handler to handle incoming events
*/
public void start(Handler handler) {
mHandler = handler;
if (mRedirectHandlerThread == null) {
mRedirectHandlerThread = new HandlerThread("RedirectListenerHandler");
mRedirectHandlerThread.start();
mRedirectStartStopHandler = new Handler(mRedirectHandlerThread.getLooper());
}
}
/**
* Returns the handler on which a runnable can be posted
*
* @return Handler State Machine's handler
*/
public Handler getHandler() {
return mHandler;
}
/**
* 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) {
Log.v(TAG, "startProvisioning received in state=" + mState);
}
if (mState != STATE_INIT) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "State Machine needs to be reset before starting provisioning");
}
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
}
mProvisioningCallback = callback;
mRedirectListener = RedirectListener.createInstance(mLooper);
if (mRedirectListener == null) {
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_START_REDIRECT_LISTENER);
return;
}
if (!mOsuServerConnection.canValidateServer()) {
Log.w(TAG, "Provisioning is not possible");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_PROVISIONING_NOT_AVAILABLE);
return;
}
URL serverUrl;
try {
serverUrl = new URL(provider.getServerUri().toString());
} catch (MalformedURLException e) {
Log.e(TAG, "Invalid Server URL");
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SERVER_URL_INVALID);
return;
}
mServerUrl = serverUrl;
mOsuProvider = provider;
if (mOsuProvider.getOsuSsid() == null) {
// Find a best matching OsuProvider that has an OSU SSID from current scanResults
List<ScanResult> scanResults = mWifiManager.getScanResults();
mOsuProvider = getBestMatchingOsuProvider(scanResults, mOsuProvider);
if (mOsuProvider == null) {
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_OSU_PROVIDER_NOT_FOUND);
return;
}
}
// Register for network and wifi state events during provisioning flow
mOsuNetworkConnection.setEventCallback(mOsuNetworkCallbacks);
// Register for OSU server callbacks
mOsuServerConnection.setEventCallback(new OsuServerCallbacks(++mCurrentSessionId));
if (!mOsuNetworkConnection.connect(mOsuProvider.getOsuSsid(),
mOsuProvider.getNetworkAccessIdentifier(), mOsuProvider.getFriendlyName())) {
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
return;
}
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_AP_CONNECTING);
changeState(STATE_AP_CONNECTING);
}
/**
* Handles Wifi Disable event
*
* Note: Called on main thread (WifiService thread).
*/
public void handleWifiDisabled() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Wifi Disabled in state=" + mState);
}
if (mState == STATE_INIT) {
Log.w(TAG, "Wifi Disable unhandled in state=" + mState);
return;
}
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
}
/**
* Handles server connection status
*
* @param sessionId indicating current session ID
* @param succeeded boolean indicating success/failure of server connection
* Note: Called on main thread (WifiService thread).
*/
public void handleServerConnectionStatus(int sessionId, boolean succeeded) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Server Connection status received in " + mState);
}
if (sessionId != mCurrentSessionId) {
Log.w(TAG, "Expected server connection failure callback for currentSessionId="
+ mCurrentSessionId);
return;
}
if (mState != STATE_OSU_SERVER_CONNECTING) {
Log.wtf(TAG, "Server Validation Failure unhandled in mState=" + mState);
return;
}
if (!succeeded) {
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION);
return;
}
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_SERVER_CONNECTED);
mProvisioningStateMachine.getHandler().post(() -> initSoapExchange());
}
/**
* Handles server validation failure
*
* @param sessionId indicating current session ID
* Note: Called on main thread (WifiService thread).
*/
public void handleServerValidationFailure(int sessionId) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Server Validation failure received in " + mState);
}
if (sessionId != mCurrentSessionId) {
Log.w(TAG, "Expected server validation callback for currentSessionId="
+ mCurrentSessionId);
return;
}
if (mState != STATE_OSU_SERVER_CONNECTING) {
Log.wtf(TAG, "Server Validation Failure unhandled in mState=" + mState);
return;
}
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SERVER_VALIDATION);
}
/**
* Handles status of server validation success
*
* @param sessionId indicating current session ID
* Note: Called on main thread (WifiService thread).
*/
public void handleServerValidationSuccess(int sessionId) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Server Validation Success received in " + mState);
}
if (sessionId != mCurrentSessionId) {
Log.w(TAG, "Expected server validation callback for currentSessionId="
+ mCurrentSessionId);
return;
}
if (mState != STATE_OSU_SERVER_CONNECTING) {
Log.wtf(TAG, "Server validation success event unhandled in state=" + mState);
return;
}
if (!mOsuServerConnection.validateProvider(
mOsuProvider.getFriendlyNameList())) {
Log.e(TAG,
"OSU Server certificate does not have the one matched with the selected "
+ "Service Name: "
+ mOsuProvider.getFriendlyName());
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_SERVICE_PROVIDER_VERIFICATION);
return;
}
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_SERVER_VALIDATED);
}
/**
* 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);
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
return;
}
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_REDIRECT_RESPONSE_RECEIVED);
mRedirectListener.stopServer(mRedirectStartStopHandler);
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);
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
return;
}
mRedirectListener.stopServer(mRedirectStartStopHandler);
resetStateMachineForFailure(
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) {
Log.v(TAG, "Connected event received in state=" + mState);
}
if (mState != STATE_AP_CONNECTING) {
// Not waiting for a connection
Log.wtf(TAG, "Connection event unhandled in state=" + mState);
return;
}
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_AP_CONNECTED);
initiateServerConnection(network);
}
/**
* 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");
resetStateMachineForFailure(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());
resetStateMachineForFailure(
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());
resetStateMachineForFailure(
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");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_INVALID_URL_FORMAT_FOR_OSU);
return;
}
if (!mWebUrl.toLowerCase(Locale.US).contains(mSessionId.toLowerCase(Locale.US))) {
Log.e(TAG, "Bad or Missing session ID in webUrl");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_INVALID_URL_FORMAT_FOR_OSU);
return;
}
launchOsuWebView();
} else if (mState == STATE_WAITING_FOR_SECOND_SOAP_RESPONSE) {
if (responseMessage.getMessageType()
!= SppResponseMessage.MessageType.POST_DEV_DATA_RESPONSE) {
Log.e(TAG, "Expected a PostDevDataResponse, but got "
+ responseMessage.getMessageType());
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE);
return;
}
PostDevDataResponse devDataResponse = (PostDevDataResponse) responseMessage;
if (devDataResponse.getSppCommand() == null
|| devDataResponse.getSppCommand().getSppCommandId()
!= SppCommand.CommandId.ADD_MO) {
Log.e(TAG, "Expected a ADD_MO command, but got " + (
(devDataResponse.getSppCommand() == null) ? "null"
: devDataResponse.getSppCommand().getSppCommandId()));
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_UNEXPECTED_COMMAND_TYPE);
return;
}
mPasspointConfiguration = buildPasspointConfiguration(
(PpsMoData) devDataResponse.getSppCommand().getCommandData());
thirdSoapExchange(mPasspointConfiguration == null);
} else if (mState == STATE_WAITING_FOR_THIRD_SOAP_RESPONSE) {
if (responseMessage.getMessageType()
!= SppResponseMessage.MessageType.EXCHANGE_COMPLETE) {
Log.e(TAG, "Expected a ExchangeCompleteMessage, but got "
+ responseMessage.getMessageType());
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE);
return;
}
ExchangeCompleteMessage exchangeCompleteMessage =
(ExchangeCompleteMessage) responseMessage;
if (exchangeCompleteMessage.getStatus()
!= SppConstants.SppStatus.EXCHANGE_COMPLETE) {
Log.e(TAG, "Expected a ExchangeCompleteMessage Status, but got "
+ exchangeCompleteMessage.getStatus());
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_STATUS);
return;
}
if (exchangeCompleteMessage.getError() != SppConstants.INVALID_SPP_CONSTANT) {
Log.e(TAG,
"In the SppExchangeComplete, got error "
+ exchangeCompleteMessage.getError());
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
return;
}
if (mPasspointConfiguration == null) {
Log.e(TAG, "No PPS MO to use for retrieving TrustCerts");
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_NO_PPS_MO);
return;
}
retrieveTrustRootCerts(mPasspointConfiguration);
} else {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Received an unexpected SOAP message in state=" + mState);
}
}
}
/**
* Installs the trust root CA certificates for AAA, Remediation and Policy Server
*
* @param sessionId indicating current session ID
* @param trustRootCertificates trust root CA certificates to be installed.
*/
public void installTrustRootCertificates(int sessionId,
@NonNull Map<Integer, List<X509Certificate>> trustRootCertificates) {
if (sessionId != mCurrentSessionId) {
Log.w(TAG, "Expected TrustRootCertificates callback for currentSessionId="
+ mCurrentSessionId);
return;
}
if (mState != STATE_WAITING_FOR_TRUST_ROOT_CERTS) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Received an unexpected TrustRootCertificates in state=" + mState);
}
return;
}
if (trustRootCertificates.isEmpty()) {
Log.e(TAG, "fails to retrieve trust root certificates");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_RETRIEVE_TRUST_ROOT_CERTIFICATES);
return;
}
List<X509Certificate> certificates = trustRootCertificates.get(
OsuServerConnection.TRUST_CERT_TYPE_AAA);
if (certificates == null || certificates.isEmpty()) {
Log.e(TAG, "fails to retrieve trust root certificate for AAA server");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_NO_AAA_TRUST_ROOT_CERTIFICATE);
return;
}
// Save the service friendly names from OsuProvider to keep this in the profile.
mPasspointConfiguration.setServiceFriendlyNames(mOsuProvider.getFriendlyNameList());
mPasspointConfiguration.getCredential().setCaCertificates(
certificates.toArray(new X509Certificate[0]));
certificates = trustRootCertificates.get(
OsuServerConnection.TRUST_CERT_TYPE_REMEDIATION);
if (certificates == null || certificates.isEmpty()) {
Log.e(TAG, "fails to retrieve trust root certificate for Remediation");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_RETRIEVE_TRUST_ROOT_CERTIFICATES);
return;
}
if (mPasspointConfiguration.getSubscriptionUpdate() != null) {
mPasspointConfiguration.getSubscriptionUpdate().setCaCertificate(
certificates.get(0));
}
try {
mWifiManager.addOrUpdatePasspointConfiguration(mPasspointConfiguration);
} catch (IllegalArgumentException e) {
Log.e(TAG, "fails to add a new PasspointConfiguration: " + e);
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_ADD_PASSPOINT_CONFIGURATION);
return;
}
invokeProvisioningCompleteCallback();
if (mVerboseLoggingEnabled) {
Log.i(TAG, "Provisioning is complete for "
+ mPasspointConfiguration.getHomeSp().getFqdn());
}
resetStateMachine();
}
/**
* Disconnect event received
*
* Note: Called on main thread (WifiService thread).
*/
public void handleDisconnect() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Connection failed in state=" + mState);
}
if (mState == STATE_INIT) {
Log.w(TAG, "Disconnect event unhandled in state=" + mState);
return;
}
mNetwork = null;
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
}
/**
* Establishes TLS session to the server(OSU Server, Remediation or Policy Server).
*
* @param network current {@link Network} associated with the target AP.
*/
private void initiateServerConnection(Network network) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Initiating server connection in state=" + mState);
}
if (!mOsuServerConnection.connect(mServerUrl, network)) {
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION);
return;
}
mNetwork = network;
changeState(STATE_OSU_SERVER_CONNECTING);
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_SERVER_CONNECTING);
}
private void invokeProvisioningCallback(int callbackType, int status) {
if (mProvisioningCallback == null) {
Log.e(TAG, "Provisioning callback " + callbackType + " with status " + status
+ " not invoked");
return;
}
try {
if (callbackType == PROVISIONING_STATUS) {
mProvisioningCallback.onProvisioningStatus(status);
} else {
mProvisioningCallback.onProvisioningFailure(status);
}
} catch (RemoteException e) {
Log.e(TAG, "Remote Exception while posting callback type=" + callbackType
+ " status=" + status);
}
}
private void invokeProvisioningCompleteCallback() {
mWifiMetrics.incrementPasspointProvisionSuccess();
if (mProvisioningCallback == null) {
Log.e(TAG, "No provisioning complete callback registered");
return;
}
try {
mProvisioningCallback.onProvisioningComplete();
} catch (RemoteException e) {
Log.e(TAG, "Remote Exception while posting provisioning complete");
}
}
/**
* Initiates the SOAP message exchange with sending the sppPostDevData message.
*/
private void initSoapExchange() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Initiates soap message exchange in state =" + mState);
}
if (mState != STATE_OSU_SERVER_CONNECTING) {
Log.e(TAG, "Initiates soap message exchange in wrong state=" + mState);
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
return;
}
// Redirect uri used for signal of completion for registration process.
final URL redirectUri = mRedirectListener.getServerUrl();
// Sending the first sppPostDevDataRequest message.
if (mOsuServerConnection.exchangeSoapMessage(
PostDevDataMessage.serializeToSoapEnvelope(mContext, mSystemInfo,
redirectUri.toString(),
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");
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE);
return;
}
}
/**
* Launches OsuLogin Application for users to register a new subscription.
*/
private void launchOsuWebView() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "launch Osu webview in state =" + mState);
}
if (mState != STATE_WAITING_FOR_FIRST_SOAP_RESPONSE) {
Log.e(TAG, "launch Osu webview in wrong state =" + mState);
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
return;
}
// 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();
}
}, mRedirectStartStopHandler)) {
Log.e(TAG, "fails to start redirect listener");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_START_REDIRECT_LISTENER);
return;
}
Intent intent = new Intent(WifiManager.ACTION_PASSPOINT_LAUNCH_OSU_VIEW);
intent.putExtra(WifiManager.EXTRA_OSU_NETWORK, mNetwork);
intent.putExtra(WifiManager.EXTRA_URL, mWebUrl);
intent.setFlags(Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
List<ResolveInfo> resolveInfos = mContext.getPackageManager()
.queryIntentActivities(
intent,
PackageManager.MATCH_DEFAULT_ONLY | PackageManager.MATCH_SYSTEM_ONLY);
if (resolveInfos == null || resolveInfos.isEmpty()) {
Log.e(TAG, "can't resolve the activity for the intent");
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_NO_OSU_ACTIVITY_FOUND);
return;
}
if (resolveInfos.size() > 1) {
if (mVerboseLoggingEnabled) {
Log.i(TAG, "Multiple OsuLogin apps found: "
+ resolveInfos.stream()
.map(info -> info.activityInfo.applicationInfo.packageName)
.collect(Collectors.joining(", ")));
}
// if multiple apps are found, filter out the default implementation supplied
// in the Wifi apex and let other implementations override.
resolveInfos.removeIf(info ->
Environment.isAppInWifiApex(info.activityInfo.applicationInfo));
}
// forcefully resolve to the first one
String packageName = resolveInfos.get(0).activityInfo.applicationInfo.packageName;
intent.setPackage(packageName);
if (mVerboseLoggingEnabled) {
Log.i(TAG, "Opening OsuLogin app: " + packageName);
}
mContext.startActivityAsUser(intent, UserHandle.CURRENT);
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_WAITING_FOR_REDIRECT_RESPONSE);
changeState(STATE_WAITING_FOR_REDIRECT_RESPONSE);
}
/**
* 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 (mState != STATE_WAITING_FOR_REDIRECT_RESPONSE) {
Log.e(TAG, "Initiates the second soap message exchange in wrong state=" + mState);
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
return;
}
// Sending the second sppPostDevDataRequest message.
if (mOsuServerConnection.exchangeSoapMessage(
PostDevDataMessage.serializeToSoapEnvelope(mContext, mSystemInfo,
mRedirectListener.getServerUrl().toString(),
SppConstants.SppReason.USER_INPUT_COMPLETED, mSessionId))) {
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_SECOND_SOAP_EXCHANGE);
changeState(STATE_WAITING_FOR_SECOND_SOAP_RESPONSE);
} else {
Log.e(TAG, "HttpsConnection is not established for soap message exchange");
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE);
return;
}
}
/**
* Initiates the third SOAP message exchange with sending the sppUpdateResponse message.
*/
private void thirdSoapExchange(boolean isError) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Initiates the third soap message exchange in state =" + mState);
}
if (mState != STATE_WAITING_FOR_SECOND_SOAP_RESPONSE) {
Log.e(TAG, "Initiates the third soap message exchange in wrong state=" + mState);
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
return;
}
// Sending the sppUpdateResponse message.
if (mOsuServerConnection.exchangeSoapMessage(
UpdateResponseMessage.serializeToSoapEnvelope(mSessionId, isError))) {
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_THIRD_SOAP_EXCHANGE);
changeState(STATE_WAITING_FOR_THIRD_SOAP_RESPONSE);
} else {
Log.e(TAG, "HttpsConnection is not established for soap message exchange");
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE);
return;
}
}
/**
* Builds {@link PasspointConfiguration} object from PPS(PerProviderSubscription)
* MO(Management Object).
*/
private PasspointConfiguration buildPasspointConfiguration(@NonNull PpsMoData moData) {
String moTree = moData.getPpsMoTree();
PasspointConfiguration passpointConfiguration = PpsMoParser.parseMoText(moTree);
if (passpointConfiguration == null) {
Log.e(TAG, "fails to parse the MoTree");
return null;
}
if (!passpointConfiguration.validateForR2()) {
Log.e(TAG, "PPS MO received is invalid: " + passpointConfiguration);
return null;
}
if (mVerboseLoggingEnabled) {
Log.d(TAG, "The parsed PasspointConfiguration: " + passpointConfiguration);
}
return passpointConfiguration;
}
/**
* Retrieves Trust Root CA Certificates from server url defined in PPS
* (PerProviderSubscription) MO(Management Object).
*/
private void retrieveTrustRootCerts(@NonNull PasspointConfiguration passpointConfig) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Initiates retrieving trust root certs in state =" + mState);
}
Map<String, byte[]> trustCertInfo = passpointConfig.getTrustRootCertList();
if (trustCertInfo == null || trustCertInfo.isEmpty()) {
Log.e(TAG, "no AAATrustRoot Node found");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_NO_AAA_SERVER_TRUST_ROOT_NODE);
return;
}
Map<Integer, Map<String, byte[]>> allTrustCerts = new HashMap<>();
allTrustCerts.put(OsuServerConnection.TRUST_CERT_TYPE_AAA, trustCertInfo);
// SubscriptionUpdate is a required node.
if (passpointConfig.getSubscriptionUpdate() != null
&& passpointConfig.getSubscriptionUpdate().getTrustRootCertUrl() != null) {
trustCertInfo = new HashMap<>();
trustCertInfo.put(
passpointConfig.getSubscriptionUpdate().getTrustRootCertUrl(),
passpointConfig.getSubscriptionUpdate()
.getTrustRootCertSha256Fingerprint());
allTrustCerts.put(OsuServerConnection.TRUST_CERT_TYPE_REMEDIATION, trustCertInfo);
} else {
Log.e(TAG, "no TrustRoot Node for remediation server found");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_NO_REMEDIATION_SERVER_TRUST_ROOT_NODE);
return;
}
// Policy is an optional node
if (passpointConfig.getPolicy() != null) {
if (passpointConfig.getPolicy().getPolicyUpdate() != null
&& passpointConfig.getPolicy().getPolicyUpdate().getTrustRootCertUrl()
!= null) {
trustCertInfo = new HashMap<>();
trustCertInfo.put(
passpointConfig.getPolicy().getPolicyUpdate()
.getTrustRootCertUrl(),
passpointConfig.getPolicy().getPolicyUpdate()
.getTrustRootCertSha256Fingerprint());
allTrustCerts.put(OsuServerConnection.TRUST_CERT_TYPE_POLICY, trustCertInfo);
} else {
Log.e(TAG, "no TrustRoot Node for policy server found");
resetStateMachineForFailure(
ProvisioningCallback.OSU_FAILURE_NO_POLICY_SERVER_TRUST_ROOT_NODE);
return;
}
}
if (mOsuServerConnection.retrieveTrustRootCerts(allTrustCerts)) {
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_RETRIEVING_TRUST_ROOT_CERTS);
changeState(STATE_WAITING_FOR_TRUST_ROOT_CERTS);
} else {
Log.e(TAG, "HttpsConnection is not established for retrieving trust root certs");
resetStateMachineForFailure(ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION);
return;
}
}
private void changeState(int nextState) {
if (nextState != mState) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Changing state from " + mState + " -> " + nextState);
}
mState = nextState;
}
}
private void resetStateMachineForFailure(int failureCode) {
mWifiMetrics.incrementPasspointProvisionFailure(failureCode);
invokeProvisioningCallback(PROVISIONING_FAILURE, failureCode);
resetStateMachine();
}
private void resetStateMachine() {
if (mRedirectListener != null) {
mRedirectListener.stopServer(mRedirectStartStopHandler);
}
mOsuNetworkConnection.setEventCallback(null);
mOsuNetworkConnection.disconnectIfNeeded();
mOsuServerConnection.setEventCallback(null);
mOsuServerConnection.cleanup();
mPasspointConfiguration = null;
mProvisioningCallback = null;
changeState(STATE_INIT);
}
/**
* Get a best matching osuProvider from scanResults with provided osuProvider
*
* @param scanResults a list of {@link ScanResult} to find a best osuProvider
* @param osuProvider an instance of {@link OsuProvider} used to match with scanResults
* @return a best matching {@link OsuProvider}, {@code null} when an invalid scanResults are
* provided or no match is found.
*/
private OsuProvider getBestMatchingOsuProvider(
List<ScanResult> scanResults,
OsuProvider osuProvider) {
if (scanResults == null) {
Log.e(TAG, "Attempt to retrieve OSU providers for a null ScanResult");
return null;
}
if (osuProvider == null) {
Log.e(TAG, "Attempt to retrieve best OSU provider for a null osuProvider");
return null;
}
// Clear the OSU SSID to compare it with other OsuProviders only about service
// provider information.
osuProvider.setOsuSsid(null);
// Filter non-Passpoint AP out and sort it by descending order of signal strength.
scanResults = scanResults.stream()
.filter((scanResult) -> scanResult.isPasspointNetwork())
.sorted((sr1, sr2) -> sr2.level - sr1.level)
.collect(Collectors.toList());
for (ScanResult scanResult : scanResults) {
// Lookup OSU Providers ANQP element by ANQPNetworkKey.
// It might have same ANQP element with another one which has same ANQP domain id.
Map<Constants.ANQPElementType, ANQPElement> anqpElements =
mPasspointManager.getANQPElements(
scanResult);
HSOsuProvidersElement element =
(HSOsuProvidersElement) anqpElements.get(
Constants.ANQPElementType.HSOSUProviders);
if (element == null) continue;
for (OsuProviderInfo info : element.getProviders()) {
OsuProvider candidate = new OsuProvider(
(WifiSsid) null, info.getFriendlyNames(),
info.getServiceDescription(), info.getServerUri(),
info.getNetworkAccessIdentifier(), info.getMethodList());
if (candidate.equals(osuProvider)) {
// Found a matching candidate and then set OSU SSID for the OSU provider.
candidate.setOsuSsid(element.getOsuSsid());
return candidate;
}
}
}
return null;
}
}
/**
* Callbacks for network and wifi events
*
* Note: Called on main thread (WifiService thread).
*/
class OsuNetworkCallbacks implements OsuNetworkConnection.Callbacks {
@Override
public void onConnected(Network network) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "onConnected to " + network);
}
if (network == null) {
mProvisioningStateMachine.handleDisconnect();
} else {
mProvisioningStateMachine.handleConnectedEvent(network);
}
}
@Override
public void onDisconnected() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "onDisconnected");
}
mProvisioningStateMachine.handleDisconnect();
}
@Override
public void onTimeOut() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Timed out waiting for connection to OSU AP");
}
mProvisioningStateMachine.handleDisconnect();
}
@Override
public void onWifiEnabled() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "onWifiEnabled");
}
}
@Override
public void onWifiDisabled() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "onWifiDisabled");
}
mProvisioningStateMachine.handleWifiDisabled();
}
}
/**
* Defines the callbacks expected from OsuServerConnection
*
* Note: Called on main thread (WifiService thread).
*/
public class OsuServerCallbacks {
private final int mSessionId;
OsuServerCallbacks(int sessionId) {
mSessionId = sessionId;
}
/**
* Returns the session ID corresponding to this callback
*
* @return int sessionID
*/
public int getSessionId() {
return mSessionId;
}
/**
* Callback when a TLS connection to the server is failed.
*
* @param sessionId indicating current session ID
* @param succeeded boolean indicating success/failure of server connection
*/
public void onServerConnectionStatus(int sessionId, boolean succeeded) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "OSU Server connection status=" + succeeded + " sessionId=" + sessionId);
}
mProvisioningStateMachine.getHandler().post(() ->
mProvisioningStateMachine.handleServerConnectionStatus(sessionId, succeeded));
}
/**
* 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
*/
public void onServerValidationStatus(int sessionId, boolean succeeded) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "OSU Server Validation status=" + succeeded + " sessionId=" + sessionId);
}
if (succeeded) {
mProvisioningStateMachine.getHandler().post(() -> {
mProvisioningStateMachine.handleServerValidationSuccess(sessionId);
});
} else {
mProvisioningStateMachine.getHandler().post(() -> {
mProvisioningStateMachine.handleServerValidationFailure(sessionId);
});
}
}
/**
* 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));
}
/**
* Callback when trust root certificates are retrieved from server.
*
* @param sessionId indicating current session ID
* @param trustRootCertificates trust root CA certificates retrieved from server
* Note: Called on different thread (OsuServer Thread)!
*/
public void onReceivedTrustRootCertificates(int sessionId,
@NonNull Map<Integer, List<X509Certificate>> trustRootCertificates) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "onReceivedTrustRootCertificates with sessionId=" + sessionId);
}
mProvisioningStateMachine.getHandler().post(() ->
mProvisioningStateMachine.installTrustRootCertificates(sessionId,
trustRootCertificates));
}
}
}