blob: 2d77ac1c1f8719ba53a88d11611aa0b340fd7335 [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.content.Context;
import android.net.Network;
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.util.Log;
import java.net.MalformedURLException;
import java.net.URL;
/**
* 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 int mCurrentSessionId = 0;
private int mCallingUid;
private boolean mVerboseLoggingEnabled = false;
PasspointProvisioner(Context context, PasspointObjectFactory objectFactory) {
mContext = context;
mOsuNetworkConnection = objectFactory.makeOsuNetworkConnection(context);
mProvisioningStateMachine = new ProvisioningStateMachine();
mOsuNetworkCallbacks = new OsuNetworkCallbacks();
mOsuServerConnection = objectFactory.makeOsuServerConnection();
mWfaKeyStore = objectFactory.makeWfaKeyStore();
mObjectFactory = objectFactory;
}
/**
* Sets up for provisioning
* @param looper Looper on which the Provisioning state machine will run
*/
public void init(Looper looper) {
mProvisioningStateMachine.start(new Handler(looper));
mOsuNetworkConnection.init(mProvisioningStateMachine.getHandler());
// Offload the heavy load job to another thread
mProvisioningStateMachine.getHandler().post(() -> {
mWfaKeyStore.load();
mOsuServerConnection.init(mObjectFactory.getSSLContext(TLS_VERSION),
mObjectFactory.getTrustManagerImpl(mWfaKeyStore.get()));
});
}
/**
* Enable verbose logging to help debug failures
* @param level integer indicating verbose logging enabled if > 0
*/
public void enableVerboseLogging(int level) {
mVerboseLoggingEnabled = (level > 0) ? true : false;
mOsuNetworkConnection.enableVerboseLogging(level);
mOsuServerConnection.enableVerboseLogging(level);
}
/**
* 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 = "ProvisioningStateMachine";
private static final int INITIAL_STATE = 1;
private static final int WAITING_TO_CONNECT = 2;
private static final int OSU_AP_CONNECTED = 3;
private static final int OSU_SERVER_CONNECTED = 4;
private static final int OSU_SERVER_VALIDATED = 5;
private static final int OSU_PROVIDER_VERIFIED = 6;
private OsuProvider mOsuProvider;
private IProvisioningCallback mProvisioningCallback;
private int mState = INITIAL_STATE;
private Handler mHandler;
private URL mServerUrl;
/**
* Initializes and starts the state machine with a handler to handle incoming events
*/
public void start(Handler handler) {
mHandler = handler;
}
/**
* 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
*/
public void startProvisioning(OsuProvider provider, IProvisioningCallback callback) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "startProvisioning received in state=" + mState);
}
if (mState != INITIAL_STATE) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "State Machine needs to be reset before starting provisioning");
}
resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_ABORTED);
}
if (!mOsuServerConnection.canValidateServer()) {
Log.w(TAG, "Provisioning is not possible");
mProvisioningCallback = callback;
resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVISIONING_NOT_AVAILABLE);
return;
}
URL serverUrl = null;
try {
serverUrl = new URL(provider.getServerUri().toString());
} catch (MalformedURLException e) {
Log.e(TAG, "Invalid Server URL");
mProvisioningCallback = callback;
resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_URL_INVALID);
return;
}
mServerUrl = serverUrl;
mProvisioningCallback = callback;
mOsuProvider = provider;
// 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())) {
resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
return;
}
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_AP_CONNECTING);
changeState(WAITING_TO_CONNECT);
}
/**
* Handle Wifi Disable event
*/
public void handleWifiDisabled() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Wifi Disabled in state=" + mState);
}
if (mState == INITIAL_STATE) {
Log.w(TAG, "Wifi Disable unhandled in state=" + mState);
return;
}
resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
}
/**
* Handle server validation failure
*/
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 != OSU_SERVER_CONNECTED) {
Log.wtf(TAG, "Server Validation Failure unhandled in mState=" + mState);
return;
}
resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_VALIDATION);
}
/**
* Handle status of server validation success
*/
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 != OSU_SERVER_CONNECTED) {
Log.wtf(TAG, "Server validation success event unhandled in state=" + mState);
return;
}
changeState(OSU_SERVER_VALIDATED);
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_SERVER_VALIDATED);
validateProvider();
}
private void validateProvider() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Validating provider in state=" + mState);
}
if (!mOsuServerConnection.validateProvider(mOsuProvider.getFriendlyName())) {
resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVIDER_VERIFICATION);
return;
}
changeState(OSU_PROVIDER_VERIFIED);
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_PROVIDER_VERIFIED);
// TODO : send Initial SOAP Exchange
}
/**
* Connected event received
* @param network Network object for this connection
*/
public void handleConnectedEvent(Network network) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Connected event received in state=" + mState);
}
if (mState != WAITING_TO_CONNECT) {
// Not waiting for a connection
Log.wtf(TAG, "Connection event unhandled in state=" + mState);
return;
}
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_AP_CONNECTED);
changeState(OSU_AP_CONNECTED);
initiateServerConnection(network);
}
private void initiateServerConnection(Network network) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Initiating server connection in state=" + mState);
}
if (mState != OSU_AP_CONNECTED) {
Log.wtf(TAG , "Initiating server connection aborted in invalid state=" + mState);
return;
}
if (!mOsuServerConnection.connect(mServerUrl, network)) {
resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION);
return;
}
changeState(OSU_SERVER_CONNECTED);
invokeProvisioningCallback(PROVISIONING_STATUS,
ProvisioningCallback.OSU_STATUS_SERVER_CONNECTED);
}
/**
* Disconnect event received
*/
public void handleDisconnect() {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Connection failed in state=" + mState);
}
if (mState == INITIAL_STATE) {
Log.w(TAG, "Disconnect event unhandled in state=" + mState);
return;
}
resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION);
}
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 changeState(int nextState) {
if (nextState != mState) {
if (mVerboseLoggingEnabled) {
Log.v(TAG, "Changing state from " + mState + " -> " + nextState);
}
mState = nextState;
}
}
private void resetStateMachine(int failureCode) {
invokeProvisioningCallback(PROVISIONING_FAILURE, failureCode);
mOsuNetworkConnection.setEventCallback(null);
mOsuNetworkConnection.disconnectIfNeeded();
mOsuServerConnection.setEventCallback(null);
mOsuServerConnection.cleanup();
changeState(INITIAL_STATE);
}
}
/**
* Callbacks for network and wifi events
*/
class OsuNetworkCallbacks implements OsuNetworkConnection.Callbacks {
OsuNetworkCallbacks() {}
@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
*/
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;
}
/**
* 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);
});
}
}
}
}