diff options
author | Ecco Park <eccopark@google.com> | 2018-07-06 16:25:04 -0700 |
---|---|---|
committer | Ecco Park <eccopark@google.com> | 2018-08-01 17:12:52 +0000 |
commit | e7effff4ab2cb785096e1908ae9861ab9756358f (patch) | |
tree | f46a2849d769945b6e96aeedb15b3a2fa2fd34d3 /service | |
parent | c630bd543106df72cc22eca1fe3e58fea4107090 (diff) |
passpoint-r2: Implement the TX/RX routine for SOAP message
Bug: 74244324
Test: ./frameworks/opt/net/wifi/tests/wifitests/runtests.sh
Test: live test with Passpoint R2 service provider AP and verified getting PostDevDataReponse
message.
Change-Id: I2503fc9a1defa51a5c9ca49fdb8b244943cd8b03
Signed-off-by: Ecco Park <eccopark@google.com>
Diffstat (limited to 'service')
14 files changed, 1121 insertions, 70 deletions
diff --git a/service/java/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParser.java b/service/java/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParser.java index 4d32e17a1..33e865b78 100644 --- a/service/java/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParser.java +++ b/service/java/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParser.java @@ -34,7 +34,7 @@ import java.util.List; import java.util.Locale; /** - * Provides parsing of SubjectAltNames extensions from X509Certificate + * Utility to provide parsing of SubjectAltNames extensions from X509Certificate */ public class ASN1SubjectAltNamesParser { private static final int OTHER_NAME = 0; @@ -53,19 +53,6 @@ public class ASN1SubjectAltNamesParser { */ @VisibleForTesting public static final String ID_WFA_OID_HOTSPOT_FRIENDLYNAME = "1.3.6.1.4.1.40808.1.1.1"; - private static ASN1SubjectAltNamesParser sASN1SubjectAltNamesParser = null; - - private ASN1SubjectAltNamesParser() {} - - /** - * Obtain an instance of the {@link ASN1SubjectAltNamesParser} as a singleton object. - */ - public static ASN1SubjectAltNamesParser getInstance() { - if (sASN1SubjectAltNamesParser == null) { - sASN1SubjectAltNamesParser = new ASN1SubjectAltNamesParser(); - } - return sASN1SubjectAltNamesParser; - } /** * Extracts provider names from a certificate by parsing subjectAltName extensions field @@ -79,7 +66,7 @@ public class ASN1SubjectAltNamesParser { * @return List of Pair representing {@Locale} and friendly Name for Operator found in the * certificate. */ - public List<Pair<Locale, String>> getProviderNames(X509Certificate providerCert) { + public static List<Pair<Locale, String>> getProviderNames(X509Certificate providerCert) { List<Pair<Locale, String>> providerNames = new ArrayList<>(); Pair<Locale, String> providerName; if (providerCert == null) { @@ -180,7 +167,7 @@ public class ASN1SubjectAltNamesParser { /** * Extract the language code and friendly Name from the alternativeName. */ - private Pair<Locale, String> getFriendlyName(String alternativeName) { + private static Pair<Locale, String> getFriendlyName(String alternativeName) { // Check for the minimum required length. if (TextUtils.isEmpty(alternativeName) || alternativeName.length() < LANGUAGE_CODE_LENGTH) { diff --git a/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java b/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java index b50a4f0a9..f0a114186 100644 --- a/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java +++ b/service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java @@ -16,13 +16,21 @@ package com.android.server.wifi.hotspot2; +import android.annotation.NonNull; import android.net.Network; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.android.org.conscrypt.TrustManagerImpl; -import com.android.server.wifi.hotspot2.anqp.I18Name; +import com.android.server.wifi.hotspot2.soap.HttpsServiceConnection; +import com.android.server.wifi.hotspot2.soap.HttpsTransport; +import com.android.server.wifi.hotspot2.soap.SoapParser; +import com.android.server.wifi.hotspot2.soap.SppResponseMessage; + +import org.ksoap2.serialization.AttributeInfo; +import org.ksoap2.serialization.SoapObject; +import org.ksoap2.serialization.SoapSerializationEnvelope; import java.io.IOException; import java.net.URL; @@ -36,6 +44,7 @@ import java.util.Locale; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; @@ -53,10 +62,13 @@ public class OsuServerConnection { private URL mUrl; private Network mNetwork; private WFATrustManager mTrustManager; + private HttpsTransport mHttpsTransport; + private HttpsServiceConnection mServiceConnection = null; private HttpsURLConnection mUrlConnection = null; private PasspointProvisioner.OsuServerCallbacks mOsuServerCallbacks; private boolean mSetupComplete = false; private boolean mVerboseLoggingEnabled = false; + /** * Sets up callback for event * @param callbacks OsuServerCallbacks to be invoked for server related events @@ -132,8 +144,6 @@ public class OsuServerConnection { * Validate the service provider by comparing its identities found in OSU Server cert * to the friendlyName obtained from ANQP exchange that is displayed to the user. * - * @param parser {@link ASN1SubjectAltNamesParser} to extract provider identities from - * X509Certificate * @param locale a {@link Locale} object used for matching the friendly name in * subjectAltName section of the certificate along with * {@param friendlyName}. @@ -141,14 +151,14 @@ public class OsuServerConnection { * subjectAltName section of the certificate. * @return boolean true if friendlyName shows up as one of the identities in the cert */ - public boolean validateProvider(ASN1SubjectAltNamesParser parser, Locale locale, + public boolean validateProvider(Locale locale, String friendlyName) { if (locale == null || TextUtils.isEmpty(friendlyName)) { return false; } - for (Pair<Locale, String> identity : parser.getProviderNames( + for (Pair<Locale, String> identity : ASN1SubjectAltNamesParser.getProviderNames( mTrustManager.getProviderCert())) { if (identity.first == null) continue; // Compare the language code for ISO-639. @@ -165,10 +175,117 @@ public class OsuServerConnection { } /** + * The helper method to exchange a SOAP message. + * + * @param url server's URL + * @param soapEnvelope the soap message to be sent. + * @return {@link SppResponseMessage} parsed, {@code null} in any failure + */ + public SppResponseMessage exchangeSoapMessage(@NonNull URL url, + @NonNull SoapSerializationEnvelope soapEnvelope) { + if (mNetwork == null) { + Log.e(TAG, "Network is not established"); + return null; + } + if (soapEnvelope == null) { + Log.e(TAG, "soapEnvelope is null"); + return null; + } + if (mUrlConnection == null) { + Log.e(TAG, "Server certificate is not validated"); + return null; + } + + if (mServiceConnection != null) { + mServiceConnection.disconnect(); + } + + mServiceConnection = getServiceConnection(url, mNetwork); + if (mServiceConnection == null) { + Log.e(TAG, "ServiceConnection for https is null"); + return null; + } + + mUrl = url; + 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 (!(response instanceof SoapObject)) { + Log.e(TAG, "Not a SoapObject instance"); + return null; + } + + SoapObject soapResponse = (SoapObject) response; + if (mVerboseLoggingEnabled) { + for (int i = 0; i < soapResponse.getAttributeCount(); i++) { + AttributeInfo attributeInfo = new AttributeInfo(); + soapResponse.getAttributeInfo(i, attributeInfo); + Log.v(TAG, "Attribute : " + attributeInfo.toString()); + } + Log.v(TAG, "response : " + soapResponse.toString()); + } + // Get the parsed SOAP SPP Response message + sppResponse = SoapParser.getResponse(soapResponse); + + } catch (Exception e) { + if (e instanceof SSLHandshakeException) { + Log.e(TAG, "Failed to make TLS connection"); + } else { + Log.e(TAG, "Failed to exchange the SOAP message"); + } + return null; + } finally { + mServiceConnection.disconnect(); + mServiceConnection = null; + } + + return sppResponse; + } + + /** + * Get the HTTPS service connection used for SOAP message exchange. + * + * @param url target address that the device connect to + * @param network {@link Network} for current wifi connection + * @return {@link HttpsServiceConnection} + */ + private HttpsServiceConnection getServiceConnection(@NonNull URL url, + @NonNull Network network) { + HttpsServiceConnection serviceConnection; + + try { + mHttpsTransport = new HttpsTransport(network, url); + serviceConnection = (HttpsServiceConnection) mHttpsTransport.getServiceConnection(); + if (serviceConnection != null) { + serviceConnection.setSSLSocketFactory(mSocketFactory); + } + } catch (IOException e) { + Log.e(TAG, "Unable to establish a URL connection"); + return null; + } + return serviceConnection; + } + + /** * Clean up */ public void cleanup() { - mUrlConnection.disconnect(); + if (mUrlConnection != null) { + mUrlConnection.disconnect(); + mUrlConnection = null; + } + + if (mServiceConnection != null) { + mServiceConnection.disconnect(); + mServiceConnection = null; + } } private class WFATrustManager implements X509TrustManager { @@ -221,7 +338,7 @@ public class OsuServerConnection { /** * Returns the OSU certificate matching the FQDN of the OSU server - * @return X509Certificate OSU certificate matching FQDN of OSU server + * @return {@link X509Certificate} OSU certificate matching FQDN of OSU server */ public X509Certificate getProviderCert() { if (mServerCerts == null || mServerCerts.size() <= 0) { diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java index cc651269f..2f6dc05dc 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointManager.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointManager.java @@ -58,7 +58,6 @@ import com.android.server.wifi.util.ScanResultUtil; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -217,7 +216,7 @@ public class PasspointManager { mProviderIndex = 0; wifiConfigStore.registerStoreData(objectFactory.makePasspointConfigStoreData( mKeyStore, mSimAccessor, new DataSourceHandler())); - mPasspointProvisioner = objectFactory.makePasspointProvisioner(context); + mPasspointProvisioner = objectFactory.makePasspointProvisioner(context, wifiNative); sPasspointManager = this; } diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java b/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java index abe069f24..953107cf7 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java @@ -106,11 +106,12 @@ public class PasspointObjectFactory{ /** * Create an instance of {@link PasspointProvisioner}. * - * @param context + * @param context Instance of {@link Context} + * @param wifiNative Instance of {@link WifiNative} * @return {@link PasspointProvisioner} */ - public PasspointProvisioner makePasspointProvisioner(Context context) { - return new PasspointProvisioner(context, this); + public PasspointProvisioner makePasspointProvisioner(Context context, WifiNative wifiNative) { + return new PasspointProvisioner(context, wifiNative, this); } /** @@ -169,15 +170,6 @@ public class PasspointObjectFactory{ } /** - * Create an instance of ASN1SubjectAltNamesParser - * - * @return ASN1SubjectAltNamesParser an instance of the parser - */ - public ASN1SubjectAltNamesParser getASN1SubjectAltNamesParser() { - return ASN1SubjectAltNamesParser.getInstance(); - } - - /** * Create an instance of {@link SystemInfo}. * * @param context Instance of {@link Context} diff --git a/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java b/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java index 7ae68df3f..80dc79952 100644 --- a/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java +++ b/service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java @@ -26,6 +26,15 @@ import android.os.Looper; import android.os.RemoteException; import android.util.Log; +import com.android.server.wifi.WifiNative; +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.command.BrowserUri; +import com.android.server.wifi.hotspot2.soap.command.SppCommand; + import java.net.MalformedURLException; import java.net.URL; import java.util.Locale; @@ -50,18 +59,22 @@ public class PasspointProvisioner { private final OsuServerConnection mOsuServerConnection; private final WfaKeyStore mWfaKeyStore; private final PasspointObjectFactory mObjectFactory; - + private final SystemInfo mSystemInfo; + private final RedirectListener mRedirectListener; private int mCurrentSessionId = 0; private int mCallingUid; private boolean mVerboseLoggingEnabled = false; - PasspointProvisioner(Context context, PasspointObjectFactory objectFactory) { + PasspointProvisioner(Context context, WifiNative wifiNative, + PasspointObjectFactory objectFactory) { mContext = context; mOsuNetworkConnection = objectFactory.makeOsuNetworkConnection(context); mProvisioningStateMachine = new ProvisioningStateMachine(); mOsuNetworkCallbacks = new OsuNetworkCallbacks(); mOsuServerConnection = objectFactory.makeOsuServerConnection(); mWfaKeyStore = objectFactory.makeWfaKeyStore(); + mSystemInfo = objectFactory.getSystemInfo(context, wifiNative); + mRedirectListener = RedirectListener.createInstance(); mObjectFactory = objectFactory; } @@ -101,6 +114,10 @@ public class PasspointProvisioner { */ public boolean startSubscriptionProvisioning(int callingUid, OsuProvider provider, IProvisioningCallback callback) { + if (mRedirectListener == null) { + Log.e(TAG, "RedirectListener is not possible to run"); + return false; + } mCallingUid = callingUid; Log.v(TAG, "Provisioning started with " + provider.toString()); @@ -118,18 +135,20 @@ public class PasspointProvisioner { 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; + static final int STATE_INIT = 1; + static final int STATE_WAITING_TO_CONNECT = 2; + 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; private OsuProvider mOsuProvider; private IProvisioningCallback mProvisioningCallback; - private int mState = INITIAL_STATE; + private int mState = STATE_INIT; private Handler mHandler; private URL mServerUrl; + private Network mNetwork; + private String mSessionId; + private String mWebUrl; /** * Initializes and starts the state machine with a handler to handle incoming events @@ -155,7 +174,7 @@ public class PasspointProvisioner { if (mVerboseLoggingEnabled) { Log.v(TAG, "startProvisioning received in state=" + mState); } - if (mState != INITIAL_STATE) { + if (mState != STATE_INIT) { if (mVerboseLoggingEnabled) { Log.v(TAG, "State Machine needs to be reset before starting provisioning"); } @@ -192,7 +211,7 @@ public class PasspointProvisioner { } invokeProvisioningCallback(PROVISIONING_STATUS, ProvisioningCallback.OSU_STATUS_AP_CONNECTING); - changeState(WAITING_TO_CONNECT); + changeState(STATE_WAITING_TO_CONNECT); } /** @@ -202,7 +221,7 @@ public class PasspointProvisioner { if (mVerboseLoggingEnabled) { Log.v(TAG, "Wifi Disabled in state=" + mState); } - if (mState == INITIAL_STATE) { + if (mState == STATE_INIT) { Log.w(TAG, "Wifi Disable unhandled in state=" + mState); return; } @@ -221,7 +240,7 @@ public class PasspointProvisioner { + mCurrentSessionId); return; } - if (mState != OSU_SERVER_CONNECTED) { + if (mState != STATE_OSU_SERVER_CONNECTED) { Log.wtf(TAG, "Server Validation Failure unhandled in mState=" + mState); return; } @@ -240,30 +259,108 @@ public class PasspointProvisioner { + mCurrentSessionId); return; } - if (mState != OSU_SERVER_CONNECTED) { + if (mState != STATE_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(); + validateServiceProvider(); } - private void validateProvider() { + /** + * Validate the OSU Server certificate based on the procedure in 7.3.2.2 of Hotspot2.0 + * rel2 spec. + */ + private void validateServiceProvider() { if (mVerboseLoggingEnabled) { - Log.v(TAG, "Validating provider in state=" + mState); + Log.v(TAG, "Validating the service provider of OSU Server certificate in state=" + + mState); } if (!mOsuServerConnection.validateProvider( - mObjectFactory.getASN1SubjectAltNamesParser(), Locale.getDefault(), mOsuProvider.getFriendlyName())) { - resetStateMachine(ProvisioningCallback.OSU_FAILURE_PROVIDER_VERIFICATION); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVICE_PROVIDER_VERIFICATION); return; } - changeState(OSU_PROVIDER_VERIFIED); invokeProvisioningCallback(PROVISIONING_STATUS, - ProvisioningCallback.OSU_STATUS_PROVIDER_VERIFIED); - // TODO : send Initial SOAP Exchange + 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()); + } + + /** + * 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_WAITING_FOR_FIRST_SOAP_RESPONSE) { + 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; + } + + // Sending the first sppPostDevDataRequest message. + SppResponseMessage sppResponse = mOsuServerConnection.exchangeSoapMessage(mServerUrl, + PostDevDataMessage.serializeToSoapEnvelope(mContext, mSystemInfo, + redirectUri.toString(), + SppConstants.SppReason.SUBSCRIPTION_REGISTRATION, + null)); + if (sppResponse == null) { + Log.e(TAG, "failed to send the sppPostDevData message"); + resetStateMachine(ProvisioningCallback.OSU_FAILURE_SOAP_MESSAGE_EXCHANGE); + return; + } + + 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); + 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); + 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; + } } /** @@ -274,14 +371,14 @@ public class PasspointProvisioner { if (mVerboseLoggingEnabled) { Log.v(TAG, "Connected event received in state=" + mState); } - if (mState != WAITING_TO_CONNECT) { + if (mState != STATE_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); + changeState(STATE_OSU_AP_CONNECTED); initiateServerConnection(network); } @@ -289,7 +386,7 @@ public class PasspointProvisioner { if (mVerboseLoggingEnabled) { Log.v(TAG, "Initiating server connection in state=" + mState); } - if (mState != OSU_AP_CONNECTED) { + if (mState != STATE_OSU_AP_CONNECTED) { Log.wtf(TAG , "Initiating server connection aborted in invalid state=" + mState); return; } @@ -297,7 +394,8 @@ public class PasspointProvisioner { resetStateMachine(ProvisioningCallback.OSU_FAILURE_SERVER_CONNECTION); return; } - changeState(OSU_SERVER_CONNECTED); + mNetwork = network; + changeState(STATE_OSU_SERVER_CONNECTED); invokeProvisioningCallback(PROVISIONING_STATUS, ProvisioningCallback.OSU_STATUS_SERVER_CONNECTED); } @@ -309,10 +407,11 @@ public class PasspointProvisioner { if (mVerboseLoggingEnabled) { Log.v(TAG, "Connection failed in state=" + mState); } - if (mState == INITIAL_STATE) { + if (mState == STATE_INIT) { Log.w(TAG, "Disconnect event unhandled in state=" + mState); return; } + mNetwork = null; resetStateMachine(ProvisioningCallback.OSU_FAILURE_AP_CONNECTION); } @@ -349,7 +448,7 @@ public class PasspointProvisioner { mOsuNetworkConnection.disconnectIfNeeded(); mOsuServerConnection.setEventCallback(null); mOsuServerConnection.cleanup(); - changeState(INITIAL_STATE); + changeState(STATE_INIT); } } @@ -445,3 +544,5 @@ public class PasspointProvisioner { } } + + diff --git a/service/java/com/android/server/wifi/hotspot2/soap/HttpsServiceConnection.java b/service/java/com/android/server/wifi/hotspot2/soap/HttpsServiceConnection.java new file mode 100644 index 000000000..8f22589e8 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/soap/HttpsServiceConnection.java @@ -0,0 +1,140 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2.soap; + +import android.net.Network; +import android.text.TextUtils; + +import org.ksoap2.HeaderProperty; +import org.ksoap2.transport.ServiceConnection; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLSocketFactory; + +/** + * A wrapper class for {@link HttpsURLConnection} that requires {@link Network} to open the + * https connection for SOAP message. + */ +public class HttpsServiceConnection implements ServiceConnection { + private HttpsURLConnection mConnection; + + public HttpsServiceConnection(Network network, URL url) throws IOException { + mConnection = (HttpsURLConnection) network.openConnection(url); + } + + @Override + public void connect() throws IOException { + mConnection.connect(); + } + + @Override + public void disconnect() { + mConnection.disconnect(); + } + + @Override + public List<HeaderProperty> getResponseProperties() { + Map<String, List<String>> properties = mConnection.getHeaderFields(); + Set<String> keys = properties.keySet(); + List<HeaderProperty> retList = new ArrayList<>(); + + keys.forEach(key -> { + List<String> values = properties.get(key); + values.forEach(value -> retList.add(new HeaderProperty(key, value))); + }); + + return retList; + } + + @Override + public int getResponseCode() throws IOException { + return mConnection.getResponseCode(); + } + + @Override + public void setRequestProperty(String propertyName, String value) { + // Ignore any settings of "the Connection: close" as android network uses the keep alive + // by default. + if (!TextUtils.equals("Connection", propertyName) || !TextUtils.equals("close", value)) { + mConnection.setRequestProperty(propertyName, value); + } + } + + @Override + public void setRequestMethod(String requestMethodType) throws IOException { + mConnection.setRequestMethod(requestMethodType); + } + + @Override + public void setFixedLengthStreamingMode(int contentLength) { + mConnection.setFixedLengthStreamingMode(contentLength); + } + + @Override + public void setChunkedStreamingMode() { + mConnection.setChunkedStreamingMode(0); + } + + @Override + public OutputStream openOutputStream() throws IOException { + return mConnection.getOutputStream(); + } + + @Override + public InputStream openInputStream() throws IOException { + return mConnection.getInputStream(); + } + + @Override + public InputStream getErrorStream() { + return mConnection.getErrorStream(); + } + + @Override + public String getHost() { + return mConnection.getURL().getHost(); + } + + @Override + public int getPort() { + return mConnection.getURL().getPort(); + } + + + @Override + public String getPath() { + return mConnection.getURL().getPath(); + } + + /** + * Wrapper function for {@link HttpsURLConnection#setSSLSocketFactory(SSLSocketFactory)} + * + * @param sslSocketFactory SSL Socket factory + */ + public void setSSLSocketFactory(SSLSocketFactory sslSocketFactory) { + mConnection.setSSLSocketFactory(sslSocketFactory); + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/soap/HttpsTransport.java b/service/java/com/android/server/wifi/hotspot2/soap/HttpsTransport.java new file mode 100644 index 000000000..e04af2182 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/soap/HttpsTransport.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2.soap; + +import android.annotation.NonNull; +import android.net.Network; + +import org.ksoap2.transport.HttpTransportSE; +import org.ksoap2.transport.ServiceConnection; + +import java.io.IOException; +import java.net.URL; + +/** + * Https Transport Layer for SOAP message over the {@link HttpsServiceConnection}. + */ +public class HttpsTransport extends HttpTransportSE { + private Network mNetwork; + private URL mUrl; + private ServiceConnection mServiceConnection; + + public HttpsTransport(@NonNull Network network, @NonNull URL url) { + super(url.toString()); + mNetwork = network; + mUrl = url; + } + + @Override + public ServiceConnection getServiceConnection() throws IOException { + if (mServiceConnection == null) { + mServiceConnection = new HttpsServiceConnection(mNetwork, mUrl); + } + return mServiceConnection; + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/soap/PostDevDataResponse.java b/service/java/com/android/server/wifi/hotspot2/soap/PostDevDataResponse.java new file mode 100644 index 000000000..df842e8f0 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/soap/PostDevDataResponse.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2.soap; + +import android.annotation.NonNull; +import android.util.Log; + +import com.android.server.wifi.hotspot2.soap.command.SppCommand; + +import org.ksoap2.serialization.PropertyInfo; +import org.ksoap2.serialization.SoapObject; + +import java.util.Objects; + +/** + * Represents the sppPostDevDataResponse message sent by the server. + * For the details, refer to A.3.2 section in Hotspot2.0 rel2 specification. + */ +public class PostDevDataResponse extends SppResponseMessage { + private static final String TAG = "PostDevDataResponse"; + private static final int MAX_COMMAND_COUNT = 1; + private final SppCommand mSppCommand; + + private PostDevDataResponse(@NonNull SoapObject response) throws IllegalArgumentException { + super(response, MessageType.POST_DEV_DATA_RESPONSE); + if (getStatus() == SppConstants.SppStatus.ERROR) { + mSppCommand = null; + return; + } + + PropertyInfo propertyInfo = new PropertyInfo(); + response.getPropertyInfo(0, propertyInfo); + // Get SPP(Subscription Provisioning Protocol) command from the original message. + mSppCommand = SppCommand.createInstance(propertyInfo); + } + + /** + * create an instance of {@link PostDevDataResponse} + * + * @param response SOAP response message received from server. + * @return Instance of {@link PostDevDataResponse}, {@code null} in any failure. + */ + public static PostDevDataResponse createInstance(@NonNull SoapObject response) { + if (response.getPropertyCount() != MAX_COMMAND_COUNT) { + Log.e(TAG, "max command count exceeds: " + response.getPropertyCount()); + return null; + } + + PostDevDataResponse postDevDataResponse; + try { + postDevDataResponse = new PostDevDataResponse(response); + } catch (IllegalArgumentException e) { + Log.e(TAG, "fails to create an Instance: " + e); + return null; + } + + return postDevDataResponse; + } + + /** + * Get a SppCommand for the current {@code PostDevDataResponse} instance. + * + * @return {@link SppCommand} + */ + public SppCommand getSppCommand() { + return mSppCommand; + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), mSppCommand); + } + + @Override + public boolean equals(Object thatObject) { + if (this == thatObject) return true; + if (!(thatObject instanceof PostDevDataResponse)) return false; + if (!super.equals(thatObject)) return false; + PostDevDataResponse that = (PostDevDataResponse) thatObject; + return Objects.equals(mSppCommand, that.getSppCommand()); + } + + @Override + public String toString() { + return super.toString() + ", commands " + mSppCommand; + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java b/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java new file mode 100644 index 000000000..6b7873d3e --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2.soap; + +import android.util.Log; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.URL; +import java.util.Random; + +/** + * Redirect message listener to listen for the redirect message from server. + */ +public class RedirectListener extends Thread { + private static final String TAG = "RedirectListener"; + private final ServerSocket mServerSocket; + private final String mPath; + private final URL mURL; + + 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); + } + + /** + * Create an instance of {@link RedirectListener} + * + * @return Instance of {@link RedirectListener}, {@code null} in any failure. + */ + public static RedirectListener createInstance() { + RedirectListener redirectListener; + try { + redirectListener = new RedirectListener(); + } catch (IOException e) { + Log.e(TAG, "fails to create an instance: " + e); + return null; + } + return redirectListener; + } + + public URL getURL() { + return mURL; + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/soap/SoapParser.java b/service/java/com/android/server/wifi/hotspot2/soap/SoapParser.java new file mode 100644 index 000000000..cb7270b48 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/soap/SoapParser.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2.soap; + +import android.annotation.NonNull; + +import org.ksoap2.serialization.SoapObject; + +/** + * Utility to parse the raw soap SPP (Subscription Provisioning Protocol) response message + * sent by server and make the instance of {@link SppResponseMessage} + */ +public class SoapParser { + /** + * Get a SppResponseMessage from the original SOAP response. + * + * @param response original SOAP response sent by server + * @return {@link SppResponseMessage}, or {@code null} in any failure + */ + public static SppResponseMessage getResponse(@NonNull SoapObject response) { + SppResponseMessage responseMessage; + switch (response.getName()) { + case "sppPostDevDataResponse": + responseMessage = PostDevDataResponse.createInstance(response); + break; + default: + responseMessage = null; + } + return responseMessage; + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/soap/SppConstants.java b/service/java/com/android/server/wifi/hotspot2/soap/SppConstants.java index c44226050..01cec0a7d 100644 --- a/service/java/com/android/server/wifi/hotspot2/soap/SppConstants.java +++ b/service/java/com/android/server/wifi/hotspot2/soap/SppConstants.java @@ -21,6 +21,7 @@ import android.util.SparseArray; import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; /** @@ -57,6 +58,8 @@ public class SppConstants { public static final String ATTRIBUTE_MO_URN = "moURN"; public static final String ATTRIBUTE_ERROR_CODE = "errorCode"; + public static final int INVALID_SPP_CONSTANT = -1; + private static final SparseArray<String> sStatusStrings = new SparseArray<>(); private static final Map<String, Integer> sStatusEnums = new HashMap<>(); static { @@ -109,6 +112,7 @@ public class SppConstants { /** * Convert the {@link SppStatus} to <code>String</code> + * * @param status value of {@link SppStatus} * @return string of the status */ @@ -118,15 +122,19 @@ public class SppConstants { /** * Convert the status string to {@link SppStatus} + * * @param status string of the status - * @return <code>int</code> value of {@link SppStatus} + * @return <code>int</code> value of {@link SppStatus} if found; {@link #INVALID_SPP_CONSTANT} + * otherwise. */ public static int mapStatusStringToInt(String status) { - return sStatusEnums.get(status.toLowerCase()); + Integer value = sStatusEnums.get(status.toLowerCase(Locale.US)); + return (value == null) ? INVALID_SPP_CONSTANT : value; } /** * Convert the {@link SppError} to <code>String</code> + * * @param error value of {@link SppError} * @return string of error */ @@ -136,11 +144,14 @@ public class SppConstants { /** * Convert the error string to {@link SppError} + * * @param error string of the error - * @return <code>int</code> value of {@link SppError} + * @return <code>int</code> value of {@link SppError} if found; {@link #INVALID_SPP_CONSTANT} + * otherwise. */ public static int mapErrorStringToInt(String error) { - return sErrorEnums.get(error.toLowerCase()); + Integer value = sErrorEnums.get(error.toLowerCase()); + return (value == null) ? INVALID_SPP_CONSTANT : value; } // Request reasons for sppPostDevData requests. diff --git a/service/java/com/android/server/wifi/hotspot2/soap/SppResponseMessage.java b/service/java/com/android/server/wifi/hotspot2/soap/SppResponseMessage.java new file mode 100644 index 000000000..72f97ba9c --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/soap/SppResponseMessage.java @@ -0,0 +1,157 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2.soap; + +import android.annotation.NonNull; +import android.text.TextUtils; + +import org.ksoap2.serialization.AttributeInfo; +import org.ksoap2.serialization.SoapObject; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Base SOAP SPP (Subscription Provisioning Protocol) response message sent by server + */ +public class SppResponseMessage { + static final String SPPVersionAttribute = "sppVersion"; + static final String SPPStatusAttribute = "sppStatus"; + static final String SPPSessionIDAttribute = "sessionID"; + static final String SPPErrorCodeAttribute = "errorCode"; + static final String SPPErrorProperty = "sppError"; + + private final int mMessageType; + private final String mVersion; + private final String mSessionID; + private int mStatus; + private int mError = SppConstants.INVALID_SPP_CONSTANT; + private Map<String, String> mAttributes; + + /** + * Message types of SOAP SPP response. + */ + public static class MessageType { + /* SOAP method response from the subscription server */ + public static final int POST_DEV_DATA_RESPONSE = 0; + + /* Message exchange sequence has been completed and the TLS connection should be released */ + public static final int EXCHANGE_COMPLETE = 1; + } + + protected SppResponseMessage(@NonNull SoapObject response, int messageType) + throws IllegalArgumentException { + if (!response.hasAttribute(SPPStatusAttribute)) { + throw new IllegalArgumentException("Missing status"); + } + + mMessageType = messageType; + mStatus = SppConstants.mapStatusStringToInt( + response.getAttributeAsString(SPPStatusAttribute)); + if (!response.hasAttribute(SPPVersionAttribute) || !response.hasAttribute( + SPPSessionIDAttribute) || mStatus == SppConstants.INVALID_SPP_CONSTANT) { + throw new IllegalArgumentException("Incomplete request: " + messageType); + } + + // Validation check for error status + if (mStatus == SppConstants.SppStatus.ERROR) { + if (!response.hasProperty(SPPErrorProperty)) { + throw new IllegalArgumentException("Missing sppError"); + } + } + + if (response.hasProperty(SPPErrorProperty)) { + SoapObject errorInfo = (SoapObject) response.getProperty(SPPErrorProperty); + if (!errorInfo.hasAttribute(SPPErrorCodeAttribute)) { + throw new IllegalArgumentException("Missing errorCode"); + } + mError = SppConstants.mapErrorStringToInt( + errorInfo.getAttributeAsString(SPPErrorCodeAttribute)); + } + + mSessionID = response.getAttributeAsString(SPPSessionIDAttribute); + mVersion = response.getAttributeAsString(SPPVersionAttribute); + if (response.getAttributeCount() > 0) { + mAttributes = new HashMap<>(); + for (int i = 0; i < response.getAttributeCount(); i++) { + AttributeInfo attributeInfo = new AttributeInfo(); + response.getAttributeInfo(i, attributeInfo); + mAttributes.put(attributeInfo.getName(), + response.getAttributeAsString(attributeInfo.getName())); + + } + } + } + + public int getMessageType() { + return mMessageType; + } + + public String getVersion() { + return mVersion; + } + + public String getSessionID() { + return mSessionID; + } + + public int getStatus() { + return mStatus; + } + + public int getError() { + return mError; + } + + protected final Map<String, String> getAttributes() { + return Collections.unmodifiableMap(mAttributes); + } + + @Override + public int hashCode() { + return Objects.hash(mMessageType, mVersion, mSessionID, mStatus, mError, mAttributes); + } + + @Override + public boolean equals(Object thatObject) { + if (this == thatObject) return true; + if (!(thatObject instanceof SppResponseMessage)) return false; + SppResponseMessage that = (SppResponseMessage) thatObject; + return mMessageType == that.mMessageType + && mStatus == that.mStatus + && mError == that.mError + && TextUtils.equals(mVersion, that.mVersion) + && TextUtils.equals(mSessionID, that.mSessionID) + && ((mAttributes == null) ? (that.mAttributes == null) : mAttributes.equals( + that.mAttributes)); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(mMessageType); + sb.append(", version ").append(mVersion); + sb.append(", status ").append(mStatus); + sb.append(", session-id ").append(mSessionID); + if (mError != SppConstants.INVALID_SPP_CONSTANT) { + sb.append(", error ").append(mError); + } + return sb.toString(); + } +} + diff --git a/service/java/com/android/server/wifi/hotspot2/soap/command/BrowserUri.java b/service/java/com/android/server/wifi/hotspot2/soap/command/BrowserUri.java new file mode 100644 index 000000000..65a28c354 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/soap/command/BrowserUri.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2.soap.command; + +import android.text.TextUtils; +import android.util.Log; + +import org.ksoap2.serialization.PropertyInfo; + +import java.util.Objects; + +/** + * Represents URI of LaunchBrowser command defined by SPP (Subscription Provisioning Protocol). + */ +public class BrowserUri implements SppCommand.SppCommandData { + private static final String TAG = "BrowserUri"; + private final String mUri; + + private BrowserUri(PropertyInfo command) { + mUri = command.getValue().toString(); + } + + /** + * Create an instance of {@link BrowserUri} + * + * @param command command message embedded in SOAP sppPostDevDataResponse. + * @return instance of {@link BrowserUri}, {@code null} in any failure. + */ + public static BrowserUri createInstance(PropertyInfo command) { + if (!TextUtils.equals(command.getName(), "launchBrowserToURI")) { + Log.e(TAG, "received wrong command : " + ((command == null) ? "" : command.getName())); + return null; + } + return new BrowserUri(command); + } + + public String getUri() { + return mUri; + } + + @Override + public boolean equals(Object thatObject) { + if (this == thatObject) return true; + if (!(thatObject instanceof BrowserUri)) return false; + BrowserUri that = (BrowserUri) thatObject; + return TextUtils.equals(mUri, that.mUri); + } + + @Override + public int hashCode() { + return Objects.hash(mUri); + } + + @Override + public String toString() { + return "BrowserUri{mUri: " + mUri + "}"; + } +} diff --git a/service/java/com/android/server/wifi/hotspot2/soap/command/SppCommand.java b/service/java/com/android/server/wifi/hotspot2/soap/command/SppCommand.java new file mode 100644 index 000000000..ffc8a5ab3 --- /dev/null +++ b/service/java/com/android/server/wifi/hotspot2/soap/command/SppCommand.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.wifi.hotspot2.soap.command; + +import android.annotation.NonNull; +import android.util.Log; + +import org.ksoap2.serialization.PropertyInfo; +import org.ksoap2.serialization.SoapObject; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * Commands that the mobile device is being requested to execute, which is defined in SPP + * (Subscription Provisioning Protocol). + * + * For the details, refer to A.3.2 of Hotspot 2.0 rel2 technical specification. + */ +public class SppCommand { + private static final String TAG = "SppCommand"; + private int mSppCommandId; + private int mExecCommandId = -1; + private SppCommandData mCommandData; + + /** + * Marker interface to indicate data used for a SPP(Subscription Provisioning Protocol) command + */ + public interface SppCommandData { + + } + + /** + * Commands embedded in sppPostDevDataResponse message for client to take an action. + */ + public class CommandId { + public static final int EXEC = 0; + public static final int ADD_MO = 1; + public static final int UPDATE_NODE = 2; + public static final int NO_MO_UPDATE = 3; + } + + private static final Map<String, Integer> sCommands = new HashMap<>(); + static { + sCommands.put("exec", CommandId.EXEC); + sCommands.put("addMO", CommandId.ADD_MO); + sCommands.put("updateNode", CommandId.UPDATE_NODE); + sCommands.put("noMOUpdate", CommandId.NO_MO_UPDATE); + } + + /** + * Execution types embedded in exec command for client to execute it. + */ + public class ExecCommandId { + public static final int BROWSER = 0; + public static final int GET_CERT = 1; + public static final int USE_CLIENT_CERT_TLS = 2; + public static final int UPLOAD_MO = 3; + } + + private static final Map<String, Integer> sExecs = new HashMap<>(); + static { + sExecs.put("launchBrowserToURI", ExecCommandId.BROWSER); + sExecs.put("getCertificate", ExecCommandId.GET_CERT); + sExecs.put("useClientCertTLS", ExecCommandId.USE_CLIENT_CERT_TLS); + sExecs.put("uploadMO", ExecCommandId.UPLOAD_MO); + } + + private SppCommand(PropertyInfo soapResponse) throws IllegalArgumentException { + if (!sCommands.containsKey(soapResponse.getName())) { + throw new IllegalArgumentException("can't find the command: " + soapResponse.getName()); + } + mSppCommandId = sCommands.get(soapResponse.getName()); + + Log.i(TAG, "command name: " + soapResponse.getName()); + + switch(mSppCommandId) { + case CommandId.EXEC: + /* + * Receipt of this element by a mobile device causes the following command + * to be executed. + */ + SoapObject subCommand = (SoapObject) soapResponse.getValue(); + if (subCommand.getPropertyCount() != 1) { + throw new IllegalArgumentException( + "more than one child element found for exec command: " + + subCommand.getPropertyCount()); + } + + PropertyInfo commandInfo = new PropertyInfo(); + subCommand.getPropertyInfo(0, commandInfo); + if (!sExecs.containsKey(commandInfo.getName())) { + throw new IllegalArgumentException( + "Unrecognized exec command: " + commandInfo.getName()); + } + mExecCommandId = sExecs.get(commandInfo.getName()); + Log.i(TAG, "exec command: " + commandInfo.getName()); + + switch (mExecCommandId) { + case ExecCommandId.BROWSER: + /* + * When the mobile device receives this command, it launches its default + * browser to the URI contained in this element. The URI must use HTTPS as + * the protocol and must contain a FQDN. + */ + mCommandData = BrowserUri.createInstance(commandInfo); + break; + case ExecCommandId.GET_CERT: //fall-through + case ExecCommandId.UPLOAD_MO: //fall-through + case ExecCommandId.USE_CLIENT_CERT_TLS: //fall-through + /* + * Command to mobile to re-negotiate the TLS connection using a client + * certificate of the accepted type or Issuer to authenticate with the + * Subscription server. + */ + default: + mCommandData = null; + break; + } + break; + case CommandId.ADD_MO: + /* + * This command causes an management object in the mobile devices management tree + * at the specified location to be added. + * If there is already a management object at that location, the object is replaced. + */ + break; + case CommandId.UPDATE_NODE: + /* + * This command causes the update of an interior node and its child nodes (if any) + * at the location specified in the management tree URI attribute. The content of + * this element is the MO node XML. + */ + break; + case CommandId.NO_MO_UPDATE: + /* + * This response is used when there is no command to be executed nor update of + * any MO required. + */ + break; + default: + mExecCommandId = -1; + mCommandData = null; + break; + } + } + + /** + * Create an instance of {@link SppCommand} + * + * @param soapResponse SOAP Response received from server. + * @return instance of {@link SppCommand} + */ + public static SppCommand createInstance(@NonNull PropertyInfo soapResponse) { + SppCommand sppCommand; + try { + sppCommand = new SppCommand(soapResponse); + } catch (IllegalArgumentException e) { + Log.e(TAG, "fails to create an instance: " + e); + return null; + } + return sppCommand; + } + + public int getSppCommandId() { + return mSppCommandId; + } + + public int getExecCommandId() { + return mExecCommandId; + } + + public SppCommandData getCommandData() { + return mCommandData; + } + + @Override + public boolean equals(Object thatObject) { + if (this == thatObject) return true; + if (!(thatObject instanceof SppCommand)) return false; + SppCommand that = (SppCommand) thatObject; + return (mSppCommandId == that.getSppCommandId()) + && (mExecCommandId == that.getExecCommandId()) + && Objects.equals(mCommandData, that.getCommandData()); + } + + @Override + public int hashCode() { + return Objects.hash(mSppCommandId, mExecCommandId, mCommandData); + } + + @Override + public String toString() { + return "SppCommand{" + + "mSppCommandId=" + mSppCommandId + + ", mExecCommandId=" + mExecCommandId + + ", mCommandData=" + mCommandData + + "}"; + } +} |