summaryrefslogtreecommitdiff
path: root/service
diff options
context:
space:
mode:
authorEcco Park <eccopark@google.com>2018-07-06 16:25:04 -0700
committerEcco Park <eccopark@google.com>2018-08-01 17:12:52 +0000
commite7effff4ab2cb785096e1908ae9861ab9756358f (patch)
treef46a2849d769945b6e96aeedb15b3a2fa2fd34d3 /service
parentc630bd543106df72cc22eca1fe3e58fea4107090 (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')
-rw-r--r--service/java/com/android/server/wifi/hotspot2/ASN1SubjectAltNamesParser.java19
-rw-r--r--service/java/com/android/server/wifi/hotspot2/OsuServerConnection.java131
-rw-r--r--service/java/com/android/server/wifi/hotspot2/PasspointManager.java3
-rw-r--r--service/java/com/android/server/wifi/hotspot2/PasspointObjectFactory.java16
-rw-r--r--service/java/com/android/server/wifi/hotspot2/PasspointProvisioner.java159
-rw-r--r--service/java/com/android/server/wifi/hotspot2/soap/HttpsServiceConnection.java140
-rw-r--r--service/java/com/android/server/wifi/hotspot2/soap/HttpsTransport.java49
-rw-r--r--service/java/com/android/server/wifi/hotspot2/soap/PostDevDataResponse.java101
-rw-r--r--service/java/com/android/server/wifi/hotspot2/soap/RedirectListener.java65
-rw-r--r--service/java/com/android/server/wifi/hotspot2/soap/SoapParser.java45
-rw-r--r--service/java/com/android/server/wifi/hotspot2/soap/SppConstants.java19
-rw-r--r--service/java/com/android/server/wifi/hotspot2/soap/SppResponseMessage.java157
-rw-r--r--service/java/com/android/server/wifi/hotspot2/soap/command/BrowserUri.java72
-rw-r--r--service/java/com/android/server/wifi/hotspot2/soap/command/SppCommand.java215
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
+ + "}";
+ }
+}