FlowService turned into separate process.

Introduced new much improved remediation handler.
Current incomplete code check-in.
Changing package name to osu.
Much improved and separated flow process.
Adding in-process web-view.

Change-Id: I08e6a19cad88b37f9a01571ea69de23214d97db1
This commit is contained in:
Jan Nordqvist
2016-03-18 16:02:45 -07:00
parent 91b43d50fd
commit 0701952aaa
38 changed files with 3023 additions and 1823 deletions

View File

@@ -6,6 +6,9 @@ LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_SRC_FILES += \
src/com/android/hotspot2/app/IOSUAccessor.aidl \
src/com/android/hotspot2/flow/IFlowService.aidl
LOCAL_JAVA_LIBRARIES := telephony-common ims-common bouncycastle conscrypt

View File

@@ -1,6 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
/*
* Copyright (C) 2014 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.
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android">
package="com.android.hotspot2">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
@@ -12,13 +29,12 @@
<uses-permission android:name="android.permission.INTERNET" />
<application
android:enabled="false"
android:enabled="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:persistent="true"
android:supportsRtl="true">
<activity android:name=".MainActivity">
<activity android:name="com.android.hotspot2.app.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@@ -28,30 +44,12 @@
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<receiver android:name="com.android.MainActivity$WifiReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" android:enabled="true"/>
</intent-filter>
<intent-filter>
<action android:name="android.net.wifi.SCAN_RESULTS" android:enabled="true"/>
</intent-filter>
<intent-filter>
<action android:name="android.net.wifi.PASSPOINT_WNM_FRAME_RECEIVED" android:enabled="true"/>
</intent-filter>
<intent-filter>
<action android:name="android.net.wifi.PASSPOINT_ICON_RECEIVED" android:enabled="true"/>
</intent-filter>
<intent-filter>
<action android:name="android.net.wifi.CONFIGURED_NETWORKS_CHANGE" android:enabled="true"/>
</intent-filter>
<intent-filter>
<action android:name="android.net.wifi.WIFI_STATE_CHANGED" android:enabled="true"/>
</intent-filter>
<intent-filter>
<action android:name="android.net.wifi.STATE_CHANGE" android:enabled="true"/>
</intent-filter>
</receiver>
<service android:name="com.android.MainActivity$OSUService" />
<activity android:name="com.android.hotspot2.osu.OSUWebView">
</activity>
<service android:name=".app.OSUService">
</service>
<service android:name=".flow.FlowService" android:process=":osuflow">
</service>
</application>
</manifest>

View File

@@ -0,0 +1,13 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true" />
</FrameLayout>

View File

@@ -1,442 +0,0 @@
package com.android;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.IntentService;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.android.anqp.OSUProvider;
import com.android.hotspot2.AppBridge;
import com.android.hotspot2.PasspointMatch;
import com.android.hotspot2.osu.OSUInfo;
import com.android.hotspot2.osu.OSUManager;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
//import com.android.Osu.R;
/**
* Main activity.
*/
public class MainActivity extends Activity {
private static final int NOTIFICATION_ID = 0; // Used for OSU count
private static final int NOTIFICATION_MESSAGE_ID = 1; // Used for other messages
private static final Locale LOCALE = java.util.Locale.getDefault();
private static volatile OSUService sOsuService;
private ListView osuListView;
private OsuListAdapter2 osuListAdapter;
private String message;
public MainActivity() {
}
@Override
protected void onResume() {
super.onResume();
if (message != null) {
showDialog(message);
message = null;
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
Bundle bundle = intent.getExtras();
if (bundle == null) { // User interaction
if (sOsuService == null) {
Intent serviceIntent = new Intent(this, OSUService.class);
serviceIntent.putExtra(ACTION_KEY, "dummy-key");
startService(serviceIntent);
return;
}
List<OSUInfo> osuInfos = sOsuService.getOsuInfos();
setContentView(R.layout.activity_main);
Log.d("osu", "osu count:" + osuInfos.size());
View noOsuView = findViewById(R.id.no_osu);
if (osuInfos.size() > 0) {
noOsuView.setVisibility(View.GONE);
osuListAdapter = new OsuListAdapter2(this, osuInfos);
osuListView = (ListView) findViewById(R.id.profile_list);
osuListView.setAdapter(osuListAdapter);
osuListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
OSUInfo osuData = (OSUInfo) adapterView.getAdapter().getItem(position);
Log.d("osu", "launch osu:" + osuData.getName(LOCALE)
+ " id:" + osuData.getOsuID());
sOsuService.selectOsu(osuData.getOsuID());
finish();
}
});
} else {
noOsuView.setVisibility(View.VISIBLE);
}
} else if (intent.getAction().equals(AppBridge.ACTION_OSU_NOTIFICATION)) {
if (bundle.containsKey(AppBridge.OSU_COUNT)) {
showOsuCount(bundle.getInt("osu-count", 0), Collections.<OSUInfo>emptyList());
} else if (bundle.containsKey(AppBridge.PROV_SUCCESS)) {
showStatus(bundle.getBoolean(AppBridge.PROV_SUCCESS),
bundle.getString(AppBridge.SP_NAME),
bundle.getString(AppBridge.PROV_MESSAGE),
null);
} else if (bundle.containsKey(AppBridge.DEAUTH)) {
showDeauth(bundle.getString(AppBridge.SP_NAME),
bundle.getBoolean(AppBridge.DEAUTH),
bundle.getInt(AppBridge.DEAUTH_DELAY),
bundle.getString(AppBridge.DEAUTH_URL));
}
/*
else if (bundle.containsKey(AppBridge.OSU_INFO)) {
List<OsuData> osus = printOsuDataList(bundle.getParcelableArray(AppBridge.OSU_INFO));
showOsuList(osus);
}
*/
}
}
private void showOsuCount(int osuCount, List<OSUInfo> osus) {
if (osuCount > 0) {
printOsuDataList(osus);
sendNotification(osuCount);
} else {
cancelNotification();
}
finish();
}
private void showStatus(boolean provSuccess, String spName, String provMessage,
String remoteStatus) {
if (provSuccess) {
sendDialogMessage(
String.format("Credentials for %s was successfully installed", spName));
} else {
if (spName != null) {
if (remoteStatus != null) {
sendDialogMessage(
String.format("Failed to install credentials for %s: %s: %s",
spName, provMessage, remoteStatus));
} else {
sendDialogMessage(
String.format("Failed to install credentials for %s: %s",
spName, provMessage));
}
} else {
sendDialogMessage(
String.format("Failed to contact OSU: %s", provMessage));
}
}
}
private void showDeauth(String spName, boolean ess, int delay, String url) {
String delayReadable = getReadableTimeInSeconds(delay);
if (ess) {
if (delay > 60) {
sendDialogMessage(
String.format("There is an issue connecting to %s [for the next %s]. " +
"Please visit %s for details", spName, delayReadable, url));
} else {
sendDialogMessage(
String.format("There is an issue connecting to %s. " +
"Please visit %s for details", spName, url));
}
} else {
sendDialogMessage(
String.format("There is an issue with the closest Access Point for %s. " +
"You may wait %s or move to another Access Point to " +
"regain access. Please visit %s for details.",
spName, delayReadable, url));
}
}
private static final String ACTION_KEY = "action";
public static class WifiReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context c, Intent intent) {
Log.d(OSUManager.TAG, "OSU App got intent: " + intent.getAction());
Intent serviceIntent;
serviceIntent = new Intent(c, OSUService.class);
serviceIntent.putExtra(ACTION_KEY, intent.getAction());
serviceIntent.putExtras(intent);
c.startService(serviceIntent);
}
}
public static class OSUService extends IntentService {
private OSUManager mOsuManager;
private final IBinder mBinder = new Binder();
public OSUService() {
super("OSUService");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onHandleIntent(intent);
return START_STICKY;
}
@Override
public void onCreate() {
super.onCreate();
Log.d("YYY", String.format("Service %x running, OSU %x",
System.identityHashCode(this), System.identityHashCode(mOsuManager)));
if (mOsuManager == null) {
mOsuManager = new OSUManager(this);
}
sOsuService = this;
}
@Override
public void onDestroy() {
super.onDestroy();
Log.d("YYY", String.format("Service %x killed", System.identityHashCode(this)));
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
Log.d(OSUManager.TAG, "Null intent!");
return;
}
Bundle bundle = intent.getExtras();
WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
Log.d(OSUManager.TAG, "OSU Service got intent: " + intent.getStringExtra(ACTION_KEY));
switch (intent.getStringExtra(ACTION_KEY)) {
case WifiManager.SCAN_RESULTS_AVAILABLE_ACTION:
mOsuManager.pushScanResults(wifiManager.getScanResults());
break;
case WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION:
long bssid = bundle.getLong(WifiManager.EXTRA_PASSPOINT_WNM_BSSID);
String url = bundle.getString(WifiManager.EXTRA_PASSPOINT_WNM_URL);
try {
if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_METHOD)) {
int method = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_METHOD);
if (method != OSUProvider.OSUMethod.SoapXml.ordinal()) {
Log.w(OSUManager.TAG, "Unsupported remediation method: " + method);
}
PasspointMatch match = null;
if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH)) {
int ordinal =
bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH);
if (ordinal >= 0 && ordinal < PasspointMatch.values().length) {
match = PasspointMatch.values()[ordinal];
}
}
mOsuManager.wnmRemediate(bssid, url, match);
} else if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_ESS)) {
boolean ess = bundle.getBoolean(WifiManager.EXTRA_PASSPOINT_WNM_ESS);
int delay = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_DELAY);
mOsuManager.deauth(bssid, ess, delay, url);
} else {
Log.w(OSUManager.TAG, "Unknown WNM event");
}
} catch (IOException | SAXException e) {
Log.w(OSUManager.TAG, "Remediation event failed to parse: " + e);
}
break;
case WifiManager.PASSPOINT_ICON_RECEIVED_ACTION:
mOsuManager.notifyIconReceived(
bundle.getLong(WifiManager.EXTRA_PASSPOINT_ICON_BSSID),
bundle.getString(WifiManager.EXTRA_PASSPOINT_ICON_FILE),
bundle.getByteArray(WifiManager.EXTRA_PASSPOINT_ICON_DATA));
break;
case WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION:
boolean multiNetwork =
bundle.getBoolean(WifiManager.EXTRA_MULTIPLE_NETWORKS_CHANGED, false);
if (multiNetwork) {
mOsuManager.networkChanged(null);
} else {
WifiConfiguration configuration =
intent.getParcelableExtra(WifiManager.EXTRA_WIFI_CONFIGURATION);
switch (bundle.getInt(WifiManager.EXTRA_CHANGE_REASON,
WifiManager.CHANGE_REASON_CONFIG_CHANGE)) {
case WifiManager.CHANGE_REASON_ADDED:
break;
case WifiManager.CHANGE_REASON_REMOVED:
mOsuManager.networkDeleted(configuration);
break;
case WifiManager.CHANGE_REASON_CONFIG_CHANGE:
mOsuManager.networkChanged(configuration);
break;
}
}
mOsuManager.networkChanged((WifiConfiguration)
intent.getParcelableExtra(WifiManager.EXTRA_WIFI_CONFIGURATION));
break;
case WifiManager.WIFI_STATE_CHANGED_ACTION:
int state = bundle.getInt(WifiManager.EXTRA_WIFI_STATE);
if (state == WifiManager.WIFI_STATE_DISABLED) {
mOsuManager.wifiStateChange(false);
} else if (state == WifiManager.WIFI_STATE_ENABLED) {
mOsuManager.wifiStateChange(true);
}
break;
case WifiManager.NETWORK_STATE_CHANGED_ACTION:
mOsuManager.networkConnectEvent((WifiInfo)
intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO));
break;
}
}
public List<OSUInfo> getOsuInfos() {
return mOsuManager.getAvailableOSUs();
}
public void selectOsu(int id) {
mOsuManager.setOSUSelection(id);
}
}
private String getReadableTimeInSeconds(int timeSeconds) {
long hours = TimeUnit.SECONDS.toHours(timeSeconds);
long minutes = TimeUnit.SECONDS.toMinutes(timeSeconds) - TimeUnit.HOURS.toMinutes(hours);
long seconds =
timeSeconds - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minutes);
if (hours > 0) {
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
} else {
return String.format("%ds", seconds);
}
}
private void sendNotification(int count) {
Notification.Builder builder =
new Notification.Builder(this)
.setContentTitle(String.format("%s OSU available", count))
.setContentText("Choose one to connect")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setAutoCancel(false);
Intent resultIntent = new Intent(this, MainActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(resultPendingIntent);
NotificationManager notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, builder.build());
}
private void cancelNotification() {
NotificationManager notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.cancel(NOTIFICATION_ID);
}
private void sendDialogMessage(String message) {
// sendNotificationMessage(message);
this.message = message;
}
private void showDialog(String message) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(message)
.setTitle("OSU");
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
dialogInterface.cancel();
finish();
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
private void sendNotificationMessage(String title) {
Notification.Builder builder =
new Notification.Builder(this)
.setContentTitle(title)
.setContentText("Click to dismiss.")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setAutoCancel(true);
NotificationManager notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_MESSAGE_ID, builder.build());
}
private static class OsuListAdapter2 extends ArrayAdapter<OSUInfo> {
private Activity activity;
public OsuListAdapter2(Activity activity, List<OSUInfo> osuDataList) {
super(activity, R.layout.list_item, osuDataList);
this.activity = activity;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = LayoutInflater.from(getContext()).inflate(R.layout.list_item, parent, false);
}
OSUInfo osuData = getItem(position);
TextView osuName = (TextView) view.findViewById(R.id.profile_name);
osuName.setText(osuData.getName(LOCALE));
TextView osuDetail = (TextView) view.findViewById(R.id.profile_detail);
osuDetail.setText(osuData.getServiceDescription(LOCALE));
ImageView osuIcon = (ImageView) view.findViewById(R.id.profile_logo);
byte[] iconData = osuData.getIconFileElement().getIconData();
osuIcon.setImageDrawable(
new BitmapDrawable(activity.getResources(),
BitmapFactory.decodeByteArray(iconData, 0, iconData.length)));
return view;
}
}
private void printOsuDataList(List<OSUInfo> osuDataList) {
for (OSUInfo osuData : osuDataList) {
Log.d("osu", String.format("OSUData:[%s][%s][%d]",
osuData.getName(LOCALE), osuData.getServiceDescription(LOCALE),
osuData.getOsuID()));
}
}
}

View File

@@ -1,5 +1,9 @@
package com.android.anqp;
import android.os.Parcel;
import com.android.hotspot2.Utils;
import java.net.ProtocolException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@@ -71,4 +75,17 @@ public class HSIconFileElement extends ANQPElement {
", type='" + mType + '\'' +
", iconData=" + mIconData.length + " bytes }";
}
public HSIconFileElement(Parcel in) {
super(Constants.ANQPElementType.HSIconFile);
mStatusCode = Utils.mapEnum(in.readInt(), StatusCode.class);
mType = in.readString();
mIconData = in.readBlob();
}
public void writeParcel(Parcel out) {
out.writeInt(mStatusCode.ordinal());
out.writeString(mType);
out.writeBlob(mIconData);
}
}

View File

@@ -1,5 +1,7 @@
package com.android.anqp;
import android.os.Parcel;
import java.io.IOException;
import java.net.ProtocolException;
import java.nio.ByteBuffer;
@@ -77,4 +79,15 @@ public class I18Name {
public String toString() {
return mText + ':' + mLocale.getLanguage();
}
public I18Name(Parcel in) throws IOException {
mLanguage = in.readString();
mText = in.readString();
mLocale = Locale.forLanguageTag(mLanguage);
}
public void writeParcel(Parcel out) {
out.writeString(mLanguage);
out.writeString(mText);
}
}

View File

@@ -1,5 +1,7 @@
package com.android.anqp;
import android.os.Parcel;
import java.net.ProtocolException;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
@@ -88,4 +90,20 @@ public class IconInfo {
", FileName='" + mFileName + '\'' +
'}';
}
public IconInfo(Parcel in) {
mWidth = in.readInt();
mHeight = in.readInt();
mLanguage = in.readString();
mIconType = in.readString();
mFileName = in.readString();
}
public void writeParcel(Parcel out) {
out.writeInt(mWidth);
out.writeInt(mHeight);
out.writeString(mLanguage);
out.writeString(mIconType);
out.writeString(mFileName);
}
}

View File

@@ -1,5 +1,10 @@
package com.android.anqp;
import android.os.Parcel;
import com.android.hotspot2.Utils;
import java.io.IOException;
import java.net.ProtocolException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
@@ -155,4 +160,54 @@ public class OSUProvider {
", serviceDescriptions=" + mServiceDescriptions +
'}';
}
public OSUProvider(Parcel in) throws IOException {
mSSID = in.readString();
int nameCount = in.readInt();
mNames = new ArrayList<>(nameCount);
for (int n = 0; n < nameCount; n++) {
mNames.add(new I18Name(in));
}
mOSUServer = in.readString();
int methodCount = in.readInt();
mOSUMethods = new ArrayList<>(methodCount);
for (int n = 0; n < methodCount; n++) {
mOSUMethods.add(Utils.mapEnum(in.readInt(), OSUMethod.class));
}
int iconCount = in.readInt();
mIcons = new ArrayList<>(iconCount);
for (int n = 0; n < iconCount; n++) {
mIcons.add(new IconInfo(in));
}
mOsuNai = in.readString();
int serviceCount = in.readInt();
mServiceDescriptions = new ArrayList<>(serviceCount);
for (int n = 0; n < serviceCount; n++) {
mServiceDescriptions.add(new I18Name(in));
}
mHashCode = in.readInt();
}
public void writeParcel(Parcel out) {
out.writeString(mSSID);
out.writeInt(mNames.size());
for (I18Name name : mNames) {
name.writeParcel(out);
}
out.writeString(mOSUServer);
out.writeInt(mOSUMethods.size());
for (OSUMethod method : mOSUMethods) {
out.writeInt(method.ordinal());
}
out.writeInt(mIcons.size());
for (IconInfo iconInfo : mIcons) {
iconInfo.writeParcel(out);
}
out.writeString(mOsuNai);
out.writeInt(mServiceDescriptions.size());
for (I18Name serviceDescription : mServiceDescriptions) {
serviceDescription.writeParcel(out);
}
out.writeInt(mHashCode);
}
}

View File

@@ -2,13 +2,9 @@ package com.android.hotspot2;
import android.content.Context;
import android.content.Intent;
import android.os.UserHandle;
import com.android.hotspot2.osu.OSUInfo;
import com.android.hotspot2.osu.OSUOperationStatus;
import java.util.List;
public class AppBridge {
public static final String ACTION_OSU_NOTIFICATION = "com.android.hotspot2.OSU_NOTIFICATION";
public static final String OSU_COUNT = "osu-count";
@@ -28,7 +24,7 @@ public class AppBridge {
mContext = context;
}
public void showOsuCount(int osuCount, List<OSUInfo> osus) {
public void showOsuCount(int osuCount) {
Intent intent = new Intent(ACTION_OSU_NOTIFICATION);
intent.putExtra(OSU_COUNT, osuCount);
intent.setFlags(

View File

@@ -3,6 +3,9 @@ package com.android.hotspot2;
import com.android.anqp.Constants;
import java.nio.ByteBuffer;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
@@ -94,6 +97,14 @@ public abstract class Utils {
return prefix;
}
public static String toIpString(int leIp) {
return String.format("%d.%d.%d.%d",
leIp & BYTE_MASK,
(leIp >> 8) & BYTE_MASK,
(leIp >> 16) & BYTE_MASK,
(leIp >> 24) & BYTE_MASK);
}
public static String bssidsToString(Collection<Long> bssids) {
StringBuilder sb = new StringBuilder();
for (Long bssid : bssids) {
@@ -274,16 +285,107 @@ public abstract class Utils {
c.get(Calendar.SECOND));
}
public static String unquote(String s) {
if (s == null) {
return null;
} else if (s.length() > 1 && s.startsWith("\"") && s.endsWith("\"")) {
return s.substring(1, s.length() - 1);
} else {
return s;
/**
* Decode a wpa_supplicant SSID. wpa_supplicant uses double quotes around plain strings, or
* expects a hex-string if no quotes appear.
* For Ascii encoded string, any octet < 32 or > 127 is encoded as
* a "\x" followed by the hex representation of the octet.
* Exception chars are ", \, \e, \n, \r, \t which are escaped by a \
* See src/utils/common.c for the implementation in the supplicant.
*
* @param ssid The SSID from the config.
* @return The actual string content of the SSID
*/
public static String decodeSsid(String ssid) {
if (ssid.length() <= 1) {
return ssid;
} else if (ssid.startsWith("\"") && ssid.endsWith("\"")) {
return unescapeSsid(ssid.substring(1, ssid.length() - 1));
} else if ((ssid.length() & 1) == 1) {
return ssid;
}
byte[] codepoints;
try {
codepoints = new byte[ssid.length() / 2];
for (int n = 0; n < ssid.length(); n += 2) {
codepoints[n / 2] = (byte) decodeHexPair(ssid, n);
}
} catch (NumberFormatException nfe) {
return ssid;
}
try {
CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
return decoder.decode(ByteBuffer.wrap(codepoints)).toString();
} catch (CharacterCodingException cce) {
/* Do nothing, try LATIN-1 */
}
try {
CharsetDecoder decoder = StandardCharsets.ISO_8859_1.newDecoder();
return decoder.decode(ByteBuffer.wrap(codepoints)).toString();
} catch (CharacterCodingException cce) { // Should not be possible.
return ssid;
}
}
private static String unescapeSsid(String s) {
StringBuilder sb = new StringBuilder();
for (int n = 0; n < s.length(); n++) {
char ch = s.charAt(n);
if (ch != '\\' || n >= s.length() - 1) {
sb.append(ch);
} else {
n++;
ch = s.charAt(n);
switch (ch) {
case '"':
case '\\':
default:
sb.append(ch);
break;
case 'e':
sb.append((char) 27); // Escape char
break;
case 'n':
sb.append('\n');
break;
case 'r':
sb.append('\r');
break;
case 't':
sb.append('\t');
break;
case 'x':
if (s.length() - n < 3) {
sb.append('\\').append(ch);
} else {
n++;
sb.append((char) decodeHexPair(s, n));
n++;
}
break;
}
}
}
return sb.toString();
}
private static int decodeHexPair(String s, int position) {
return fromHex(s.charAt(position)) << 4 | fromHex(s.charAt(position + 1));
}
private static int fromHex(char ch) {
if (ch >= '0' && ch <= '9') {
return ch - '0';
} else if (ch >= 'A' && ch <= 'F') {
return ch - 'A' + 10;
} else if (ch >= 'a' && ch <= 'f') {
return ch - 'a' + 10;
} else {
throw new NumberFormatException(String.format("Not hex: '%c'", ch));
}
}
public static void delay(long ms) {
long until = System.currentTimeMillis() + ms;
@@ -297,4 +399,9 @@ public abstract class Utils {
} catch (InterruptedException ie) { /**/ }
}
}
public static <T extends Enum<T>> T mapEnum(int ordinal, Class<T> enumClass) {
T[] constants = enumClass.getEnumConstants();
return ordinal >= 0 && ordinal < constants.length ? constants[ordinal]: null;
}
}

View File

@@ -1,394 +0,0 @@
package com.android.hotspot2;
import android.content.Context;
import android.content.Intent;
import android.net.CaptivePortal;
import android.net.ConnectivityManager;
import android.net.ICaptivePortal;
import android.net.Network;
import android.net.wifi.PasspointManagementObjectDefinition;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiEnterpriseConfig;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.util.Log;
import com.android.configparse.ConfigBuilder;
import com.android.hotspot2.omadm.MOManager;
import com.android.hotspot2.omadm.MOTree;
import com.android.hotspot2.omadm.OMAConstants;
import com.android.hotspot2.omadm.OMAException;
import com.android.hotspot2.omadm.OMAParser;
import com.android.hotspot2.osu.OSUCertType;
import com.android.hotspot2.osu.OSUInfo;
import com.android.hotspot2.osu.OSUManager;
import com.android.hotspot2.osu.commands.MOData;
import com.android.hotspot2.pps.HomeSP;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class WifiNetworkAdapter {
private final Context mContext;
private final OSUManager mOSUManager;
private final Map<String, PasspointConfig> mPasspointConfigs = new HashMap<>();
private static class PasspointConfig {
private final WifiConfiguration mWifiConfiguration;
private final MOTree mMOTree;
private final HomeSP mHomeSP;
private PasspointConfig(WifiConfiguration config) throws IOException, SAXException {
mWifiConfiguration = config;
OMAParser omaParser = new OMAParser();
mMOTree = omaParser.parse(config.getMoTree(), OMAConstants.PPS_URN);
List<HomeSP> spList = MOManager.buildSPs(mMOTree);
if (spList.size() != 1) {
throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
}
mHomeSP = spList.iterator().next();
}
public WifiConfiguration getWifiConfiguration() {
return mWifiConfiguration;
}
public HomeSP getHomeSP() {
return mHomeSP;
}
public MOTree getmMOTree() {
return mMOTree;
}
}
public WifiNetworkAdapter(Context context, OSUManager osuManager) {
mOSUManager = osuManager;
mContext = context;
}
public void initialize() {
loadAllSps();
}
public void networkConfigChange(WifiConfiguration configuration) {
// !!! Watch out for changed r2 configs - remove the MO.
loadAllSps();
}
private void loadAllSps() {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
int count = 0;
for (WifiConfiguration config : wifiManager.getPrivilegedConfiguredNetworks()) {
String moTree = config.getMoTree();
if (moTree != null) {
try {
mPasspointConfigs.put(config.FQDN, new PasspointConfig(config));
count++;
} catch (IOException | SAXException e) {
Log.w(OSUManager.TAG, "Failed to parse MO: " + e);
}
}
}
Log.d(OSUManager.TAG, "Loaded " + count + " SPs");
// !!! Detect adds/deletes
}
public Collection<HomeSP> getLoadedSPs() {
List<HomeSP> homeSPs = new ArrayList<>();
for (PasspointConfig config : mPasspointConfigs.values()) {
homeSPs.add(config.getHomeSP());
}
return homeSPs;
}
public MOTree getMOTree(HomeSP homeSP) {
PasspointConfig config = mPasspointConfigs.get(homeSP.getFQDN());
return config != null ? config.getmMOTree() : null;
}
public void launchBrowser(URL target, Network network, URL endRedirect) {
Log.d(OSUManager.TAG, "Browser to " + target + ", land at " + endRedirect);
final Intent intent = new Intent(
ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
intent.putExtra(ConnectivityManager.EXTRA_NETWORK, network);
intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
new CaptivePortal(new ICaptivePortal.Stub() {
@Override
public void appResponse(int response) {
}
}));
//intent.setData(Uri.parse(target.toString())); !!! Doesn't work!
intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, target.toString());
intent.setFlags(
Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
public int addSP(String xml) throws IOException, SAXException {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
return wifiManager.addPasspointManagementObject(xml);
}
public int modifySP(HomeSP homeSP, Collection<MOData> mods) throws IOException {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
List<PasspointManagementObjectDefinition> defMods = new ArrayList<>(mods.size());
for (MOData mod : mods) {
defMods.add(new PasspointManagementObjectDefinition(mod.getBaseURI(),
mod.getURN(), mod.getMOTree().toXml()));
}
return wifiManager.modifyPasspointManagementObject(homeSP.getFQDN(), defMods);
}
public Network getCurrentNetwork() {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
return wifiManager.getCurrentNetwork();
}
public WifiConfiguration getActiveWifiConfig() {
WifiInfo wifiInfo = getConnectionInfo();
if (wifiInfo == null) {
return null;
}
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
for (WifiConfiguration config : wifiManager.getConfiguredNetworks()) {
if (config.networkId == wifiInfo.getNetworkId()) {
return config;
}
}
return null;
}
public WifiInfo getConnectionInfo() {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
return wifiManager.getConnectionInfo();
}
public PasspointMatch matchProviderWithCurrentNetwork(String fqdn) {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
int ordinal = wifiManager.matchProviderWithCurrentNetwork(fqdn);
return ordinal >= 0 && ordinal < PasspointMatch.values().length ?
PasspointMatch.values()[ordinal] : null;
}
public WifiConfiguration getWifiConfig(HomeSP homeSP) {
PasspointConfig passpointConfig = mPasspointConfigs.get(homeSP.getFQDN());
return passpointConfig != null ? passpointConfig.getWifiConfiguration() : null;
}
public HomeSP getHomeSP(WifiConfiguration configuration) {
if (configuration.isPasspoint()) {
PasspointConfig config = mPasspointConfigs.get(configuration.FQDN);
return config != null ? config.getHomeSP() : null;
}
return null;
}
public HomeSP getCurrentSP() {
PasspointConfig passpointConfig = getActivePasspointConfig();
return passpointConfig != null ? passpointConfig.getHomeSP() : null;
}
private PasspointConfig getActivePasspointConfig() {
WifiInfo wifiInfo = getConnectionInfo();
if (wifiInfo == null) {
return null;
}
for (PasspointConfig passpointConfig : mPasspointConfigs.values()) {
if (passpointConfig.getWifiConfiguration().networkId == wifiInfo.getNetworkId()) {
return passpointConfig;
}
}
return null;
}
public void doIconQuery(long bssid, String fileName) {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
Log.d("ZXZ", String.format("Icon query for %012x '%s'", bssid, fileName));
wifiManager.queryPasspointIcon(bssid, fileName);
}
public Integer addNetwork(HomeSP homeSP, Map<OSUCertType, List<X509Certificate>> certs,
PrivateKey privateKey, Network osuNetwork)
throws IOException, GeneralSecurityException {
List<X509Certificate> aaaTrust = certs.get(OSUCertType.AAA);
if (aaaTrust.isEmpty()) {
aaaTrust = certs.get(OSUCertType.CA); // Get the CAs from the EST flow.
}
WifiConfiguration config = ConfigBuilder.buildConfig(homeSP,
aaaTrust.iterator().next(),
certs.get(OSUCertType.Client), privateKey);
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
int nwkId = wifiManager.addNetwork(config);
boolean saved = false;
if (nwkId >= 0) {
saved = wifiManager.saveConfiguration();
}
Log.d(OSUManager.TAG, "Wifi configuration " + nwkId +
" " + (saved ? "saved" : "not saved"));
if (saved) {
reconnect(osuNetwork, nwkId);
return nwkId;
} else {
return null;
}
}
public void updateNetwork(HomeSP homeSP, X509Certificate caCert,
List<X509Certificate> clientCerts, PrivateKey privateKey)
throws IOException, GeneralSecurityException {
WifiConfiguration config = getWifiConfig(homeSP);
if (config == null) {
throw new IOException("Failed to find matching network config");
}
Log.d(OSUManager.TAG, "Found matching config " + config.networkId + ", updating");
WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
WifiConfiguration newConfig = ConfigBuilder.buildConfig(homeSP,
caCert != null ? caCert : enterpriseConfig.getCaCertificate(),
clientCerts, privateKey);
newConfig.networkId = config.networkId;
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
wifiManager.save(newConfig, null);
wifiManager.saveConfiguration();
}
/**
* Connect to an OSU provisioning network. The connection should not bring down other existing
* connection and the network should not be made the default network since the connection
* is solely for sign up and is neither intended for nor likely provides access to any
* generic resources.
*
* @param osuInfo The OSU info object that defines the parameters for the network. An OSU
* network is either an open network, or, if the OSU NAI is set, an "OSEN"
* network, which is an anonymous EAP-TLS network with special keys.
* @param info An opaque string that is passed on to any user notification. The string is used
* for the name of the service provider.
* @return an Integer holding the network-id of the just added network configuration, or null
* if the network existed prior to this call (was not added by the OSU infrastructure).
* The value will be used at the end of the OSU flow to delete the network as applicable.
* @throws IOException Issues:
* 1. The network id is not returned. addNetwork cannot be called from here since the method
* runs in the context of the app and doesn't have the appropriate permission.
* 2. The connection is not immediately usable if the network was not previously selected
* manually.
*/
public Integer connect(OSUInfo osuInfo, final String info) throws IOException {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
WifiConfiguration config = new WifiConfiguration();
config.SSID = '"' + osuInfo.getOsuSsid() + '"';
if (osuInfo.getOSUBssid() != 0) {
config.BSSID = Utils.macToString(osuInfo.getOSUBssid());
Log.d(OSUManager.TAG, String.format("Setting BSSID of '%s' to %012x",
osuInfo.getOsuSsid(), osuInfo.getOSUBssid()));
}
if (osuInfo.getOSUProvider().getOsuNai() == null) {
config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
} else {
config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.OSEN);
config.allowedProtocols.set(WifiConfiguration.Protocol.OSEN);
config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.GTK_NOT_USED);
config.enterpriseConfig = new WifiEnterpriseConfig();
config.enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.UNAUTH_TLS);
config.enterpriseConfig.setIdentity(osuInfo.getOSUProvider().getOsuNai());
// !!! OSEN CA Cert???
}
int networkId = wifiManager.addNetwork(config);
if (wifiManager.enableNetwork(networkId, true)) {
return networkId;
} else {
return null;
}
/* sequence of addNetwork(), enableNetwork(), saveConfiguration() and reconnect()
wifiManager.connect(config, new WifiManager.ActionListener() {
@Override
public void onSuccess() {
// Connection event comes from network change intent registered in initialize
}
@Override
public void onFailure(int reason) {
mOSUManager.notifyUser(OSUOperationStatus.ProvisioningFailure,
"Cannot connect to OSU network: " + reason, info);
}
});
return null;
/*
try {
int nwkID = wifiManager.addOrUpdateOSUNetwork(config);
if (nwkID == WifiConfiguration.INVALID_NETWORK_ID) {
throw new IOException("Failed to add OSU network");
}
wifiManager.enableNetwork(nwkID, false);
wifiManager.reconnect();
return nwkID;
}
catch (SecurityException se) {
Log.d("ZXZ", "Blah: " + se, se);
wifiManager.connect(config, new WifiManager.ActionListener() {
@Override
public void onSuccess() {
// Connection event comes from network change intent registered in initialize
}
@Override
public void onFailure(int reason) {
mOSUManager.notifyUser(OSUOperationStatus.ProvisioningFailure,
"Cannot connect to OSU network: " + reason, info);
}
});
return null;
}
*/
}
private void reconnect(Network osuNetwork, int newNwkId) {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
if (osuNetwork != null) {
wifiManager.disableNetwork(osuNetwork.netId);
}
if (newNwkId != WifiConfiguration.INVALID_NETWORK_ID) {
wifiManager.enableNetwork(newNwkId, true);
}
}
public void deleteNetwork(int id) {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
wifiManager.disableNetwork(id);
wifiManager.forget(id, null);
}
/**
* Set the re-authentication hold off time for the current network
*
* @param holdoff hold off time in milliseconds
* @param ess set if the hold off pertains to an ESS rather than a BSS
*/
public void setHoldoffTime(long holdoff, boolean ess) {
}
}

View File

@@ -0,0 +1,8 @@
package com.android.hotspot2.app;
import com.android.hotspot2.app.OSUData;
interface IOSUAccessor {
List<OSUData> getOsuData();
void selectOsu(int id);
}

View File

@@ -0,0 +1,15 @@
package com.android.hotspot2.app;
import android.os.Binder;
public class LocalServiceBinder extends Binder {
private final OSUService mDelegate;
public LocalServiceBinder(OSUService delegate) {
mDelegate = delegate;
}
public OSUService getService() {
return mDelegate;
}
}

View File

@@ -0,0 +1,303 @@
package com.android.hotspot2.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.TaskStackBuilder;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import com.android.hotspot2.AppBridge;
import com.android.hotspot2.R;
import com.android.hotspot2.osu.OSUManager;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* Main activity.
*/
public class MainActivity extends Activity {
private static final int NOTIFICATION_ID = 0; // Used for OSU count
private static final int NOTIFICATION_MESSAGE_ID = 1; // Used for other messages
private static final String ACTION_SVC_BOUND = "SVC_BOUND";
private volatile OSUService mLocalService;
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
LocalServiceBinder binder = (LocalServiceBinder) service;
mLocalService = binder.getService();
showOsuSelection(mLocalService);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mLocalService = null;
}
};
private ListView osuListView;
private OsuListAdapter osuListAdapter;
private String message;
public MainActivity() {
}
@Override
protected void onStop() {
super.onStop();
if (mLocalService != null) {
unbindService(mConnection);
mLocalService = null;
}
}
@Override
protected void onResume() {
super.onResume();
if (message != null) {
showDialog(message);
message = null;
}
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Intent intent = getIntent();
Bundle bundle = intent.getExtras();
if (intent.getAction() == null) {
if (mLocalService == null) {
bindService(new Intent(this, OSUService.class), mConnection, 0);
}
} else if (intent.getAction().equals(AppBridge.ACTION_OSU_NOTIFICATION)) {
if (bundle == null) {
Log.d(OSUManager.TAG, "No parameters for OSU notification");
return;
}
if (bundle.containsKey(AppBridge.OSU_COUNT)) {
showOsuCount(bundle.getInt("osu-count", 0), Collections.<OSUData>emptyList());
} else if (bundle.containsKey(AppBridge.PROV_SUCCESS)) {
showStatus(bundle.getBoolean(AppBridge.PROV_SUCCESS),
bundle.getString(AppBridge.SP_NAME),
bundle.getString(AppBridge.PROV_MESSAGE),
null);
} else if (bundle.containsKey(AppBridge.DEAUTH)) {
showDeauth(bundle.getString(AppBridge.SP_NAME),
bundle.getBoolean(AppBridge.DEAUTH),
bundle.getInt(AppBridge.DEAUTH_DELAY),
bundle.getString(AppBridge.DEAUTH_URL));
}
}
}
private void showOsuSelection(final OSUService osuService) {
List<OSUData> osuData = osuService.getOsuData();
setContentView(R.layout.activity_main);
Log.d("osu", "osu count:" + osuData.size());
View noOsuView = findViewById(R.id.no_osu);
if (osuData.size() > 0) {
noOsuView.setVisibility(View.GONE);
osuListAdapter = new OsuListAdapter(this, osuData);
osuListView = (ListView) findViewById(R.id.profile_list);
osuListView.setAdapter(osuListAdapter);
osuListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int position, long id) {
OSUData osuData = (OSUData) adapterView.getAdapter().getItem(position);
Log.d("osu", "launch osu:" + osuData.getName()
+ " id:" + osuData.getId());
osuService.selectOsu(osuData.getId());
finish();
}
});
} else {
noOsuView.setVisibility(View.VISIBLE);
}
}
private void showOsuCount(int osuCount, List<OSUData> osus) {
if (osuCount > 0) {
printOsuDataList(osus);
sendNotification(osuCount);
} else {
cancelNotification();
}
finish();
}
private void showStatus(boolean provSuccess, String spName, String provMessage,
String remoteStatus) {
if (provSuccess) {
sendDialogMessage(
String.format("Credentials for %s was successfully installed", spName));
} else {
if (spName != null) {
if (remoteStatus != null) {
sendDialogMessage(
String.format("Failed to install credentials for %s: %s: %s",
spName, provMessage, remoteStatus));
} else {
sendDialogMessage(
String.format("Failed to install credentials for %s: %s",
spName, provMessage));
}
} else {
sendDialogMessage(
String.format("Failed to contact OSU: %s", provMessage));
}
}
}
private void showDeauth(String spName, boolean ess, int delay, String url) {
String delayReadable = getReadableTimeInSeconds(delay);
if (ess) {
if (delay > 60) {
sendDialogMessage(
String.format("There is an issue connecting to %s [for the next %s]. " +
"Please visit %s for details", spName, delayReadable, url));
} else {
sendDialogMessage(
String.format("There is an issue connecting to %s. " +
"Please visit %s for details", spName, url));
}
} else {
sendDialogMessage(
String.format("There is an issue with the closest Access Point for %s. " +
"You may wait %s or move to another Access Point to " +
"regain access. Please visit %s for details.",
spName, delayReadable, url));
}
}
private String getReadableTimeInSeconds(int timeSeconds) {
long hours = TimeUnit.SECONDS.toHours(timeSeconds);
long minutes = TimeUnit.SECONDS.toMinutes(timeSeconds) - TimeUnit.HOURS.toMinutes(hours);
long seconds =
timeSeconds - TimeUnit.HOURS.toSeconds(hours) - TimeUnit.MINUTES.toSeconds(minutes);
if (hours > 0) {
return String.format("%02d:%02d:%02d", hours, minutes, seconds);
} else {
return String.format("%ds", seconds);
}
}
private void sendNotification(int count) {
Notification.Builder builder =
new Notification.Builder(this)
.setContentTitle(String.format("%s OSU available", count))
.setContentText("Choose one to connect")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setAutoCancel(false);
Intent resultIntent = new Intent(this, MainActivity.class);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addParentStack(MainActivity.class);
stackBuilder.addNextIntent(resultIntent);
PendingIntent resultPendingIntent =
stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(resultPendingIntent);
NotificationManager notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, builder.build());
}
private void cancelNotification() {
NotificationManager notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.cancel(NOTIFICATION_ID);
}
private void sendDialogMessage(String message) {
// sendNotificationMessage(message);
this.message = message;
}
private void showDialog(String message) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(message)
.setTitle("OSU");
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
dialogInterface.cancel();
finish();
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
private void sendNotificationMessage(String title) {
Notification.Builder builder =
new Notification.Builder(this)
.setContentTitle(title)
.setContentText("Click to dismiss.")
.setSmallIcon(android.R.drawable.ic_dialog_info)
.setAutoCancel(true);
NotificationManager notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_MESSAGE_ID, builder.build());
}
private static class OsuListAdapter extends ArrayAdapter<OSUData> {
private Activity activity;
public OsuListAdapter(Activity activity, List<OSUData> osuDataList) {
super(activity, R.layout.list_item, osuDataList);
this.activity = activity;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
view = LayoutInflater.from(getContext()).inflate(R.layout.list_item, parent, false);
}
OSUData osuData = getItem(position);
TextView osuName = (TextView) view.findViewById(R.id.profile_name);
osuName.setText(osuData.getName());
TextView osuDetail = (TextView) view.findViewById(R.id.profile_detail);
osuDetail.setText(osuData.getServiceDescription());
ImageView osuIcon = (ImageView) view.findViewById(R.id.profile_logo);
byte[] iconData = osuData.getIconData();
osuIcon.setImageDrawable(
new BitmapDrawable(activity.getResources(),
BitmapFactory.decodeByteArray(iconData, 0, iconData.length)));
return view;
}
}
private void printOsuDataList(List<OSUData> osuDataList) {
for (OSUData osuData : osuDataList) {
Log.d("osu", String.format("OSUData:[%s][%s][%d]",
osuData.getName(), osuData.getServiceDescription(),
osuData.getId()));
}
}
}

View File

@@ -0,0 +1,4 @@
package com.android.hotspot2.app;
parcelable OSUData;

View File

@@ -0,0 +1,69 @@
package com.android.hotspot2.app;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.hotspot2.flow.OSUInfo;
import com.android.hotspot2.osu.OSUManager;
public class OSUData implements Parcelable {
private final String mName;
private final String mServiceDescription;
private final byte[] mIconData;
private final int mId;
public OSUData(OSUInfo osuInfo) {
mName = osuInfo.getName(OSUManager.LOCALE);
mServiceDescription = osuInfo.getServiceDescription(OSUManager.LOCALE);
mIconData = osuInfo.getIconFileElement().getIconData();
mId = osuInfo.getOsuID();
}
public String getName() {
return mName;
}
public String getServiceDescription() {
return mServiceDescription;
}
public byte[] getIconData() {
return mIconData;
}
public int getId() {
return mId;
}
private OSUData(Parcel in) {
mName = in.readString();
mServiceDescription = in.readString();
int iconSize = in.readInt();
mIconData = new byte[iconSize];
in.readByteArray(mIconData);
mId = in.readInt();
}
public static final Parcelable.Creator<OSUData> CREATOR = new Parcelable.Creator<OSUData>() {
public OSUData createFromParcel(Parcel in) {
return new OSUData(in);
}
public OSUData[] newArray(int size) {
return new OSUData[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
dest.writeString(mServiceDescription);
dest.writeByteArray(mIconData);
dest.writeInt(mId);
}
}

View File

@@ -0,0 +1,202 @@
package com.android.hotspot2.app;
import android.app.IntentService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import com.android.anqp.OSUProvider;
import com.android.hotspot2.PasspointMatch;
import com.android.hotspot2.osu.OSUManager;
import java.io.IOException;
import java.util.List;
/**
* This is the Hotspot 2.0 release 2 OSU background service that is continuously running and caches
* OSU information.
*
* The OSU App is made up of two services; FlowService and OSUService.
*
* OSUService is a long running light weight service, kept alive throughout the lifetime of the
* operating system by being bound from the framework (in WifiManager in stage
* PHASE_THIRD_PARTY_APPS_CAN_START), and is responsible for continuously caching OSU information
* and notifying the UI when OSUs are available.
*
* FlowService is only started on demand from OSUService and is responsible for handling actual
* provisioning and remediation flows, and requires a fairly significant memory footprint.
*
* FlowService is defined to run in its own process through the definition
* <service android:name=".flow.FlowService" android:process=":osuflow">
* in the AndroidManifest.
* This is done as a means to keep total app memory footprint low (pss < 10M) and only start the
* FlowService on demand and make it available for "garbage collection" by the OS when not in use.
*/
public class OSUService extends IntentService {
public static final String REMEDIATION_DONE_ACTION = "com.android.hotspot2.REMEDIATION_DONE";
public static final String REMEDIATION_FQDN_EXTRA = "com.android.hotspot2.REMEDIATION_FQDN";
public static final String REMEDIATION_POLICY_EXTRA = "com.android.hotspot2.REMEDIATION_POLICY";
private static final String[] INTENTS = {
WifiManager.SCAN_RESULTS_AVAILABLE_ACTION,
WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION,
WifiManager.PASSPOINT_ICON_RECEIVED_ACTION,
WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION,
WifiManager.WIFI_STATE_CHANGED_ACTION,
WifiManager.NETWORK_STATE_CHANGED_ACTION,
REMEDIATION_DONE_ACTION
};
private OSUManager mOsuManager;
private final LocalServiceBinder mLocalServiceBinder;
public OSUService() {
super("OSUService");
mLocalServiceBinder = new LocalServiceBinder(this);
}
/*
public final class OSUAccessorImpl extends IOSUAccessor.Stub {
public List<OSUData> getOsuData() {
List<OSUInfo> infos = getOsuInfos();
List<OSUData> data = new ArrayList<>(infos.size());
for (OSUInfo osuInfo : infos) {
data.add(new OSUData(osuInfo));
}
return data;
}
public void selectOsu(int id) {
OSUService.this.selectOsu(id);
}
}
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
onHandleIntent(intent);
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleIntent(intent.getAction(), intent);
}
};
for (String intentString : INTENTS) {
registerReceiver(receiver, new IntentFilter(intentString));
}
return mLocalServiceBinder;
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent == null) {
Log.d(OSUManager.TAG, "Null intent!");
return;
}
//handleIntent(intent.getStringExtra(MainActivity.ACTION_KEY), intent);
}
private void handleIntent(String action, Intent intent) {
WifiManager wifiManager = (WifiManager) getSystemService(Context.WIFI_SERVICE);
Bundle bundle = intent.getExtras();
if (mOsuManager == null) {
mOsuManager = new OSUManager(this);
}
Log.d(OSUManager.TAG, "Got intent " + intent.getAction());
switch (action) {
case WifiManager.SCAN_RESULTS_AVAILABLE_ACTION:
mOsuManager.pushScanResults(wifiManager.getScanResults());
break;
case WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION:
long bssid = bundle.getLong(WifiManager.EXTRA_PASSPOINT_WNM_BSSID);
String url = bundle.getString(WifiManager.EXTRA_PASSPOINT_WNM_URL);
try {
if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_METHOD)) {
int method = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_METHOD);
if (method != OSUProvider.OSUMethod.SoapXml.ordinal()) {
Log.w(OSUManager.TAG, "Unsupported remediation method: " + method);
return;
}
PasspointMatch match = null;
if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH)) {
int ordinal =
bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_PPOINT_MATCH);
if (ordinal >= 0 && ordinal < PasspointMatch.values().length) {
match = PasspointMatch.values()[ordinal];
}
}
mOsuManager.wnmRemediate(bssid, url, match);
} else if (bundle.containsKey(WifiManager.EXTRA_PASSPOINT_WNM_ESS)) {
boolean ess = bundle.getBoolean(WifiManager.EXTRA_PASSPOINT_WNM_ESS);
int delay = bundle.getInt(WifiManager.EXTRA_PASSPOINT_WNM_DELAY);
mOsuManager.deauth(bssid, ess, delay, url);
} else {
Log.w(OSUManager.TAG, "Unknown WNM event");
}
} catch (IOException e) {
Log.w(OSUManager.TAG, "Remediation event failed to parse: " + e);
}
break;
case WifiManager.PASSPOINT_ICON_RECEIVED_ACTION:
mOsuManager.notifyIconReceived(
bundle.getLong(WifiManager.EXTRA_PASSPOINT_ICON_BSSID),
bundle.getString(WifiManager.EXTRA_PASSPOINT_ICON_FILE),
bundle.getByteArray(WifiManager.EXTRA_PASSPOINT_ICON_DATA));
break;
case WifiManager.NETWORK_STATE_CHANGED_ACTION:
mOsuManager.networkConnectChange(
(WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO));
break;
case WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION:
boolean multiNetwork =
bundle.getBoolean(WifiManager.EXTRA_MULTIPLE_NETWORKS_CHANGED, false);
if (multiNetwork) {
mOsuManager.networkConfigChanged();
} else if (bundle.getInt(WifiManager.EXTRA_CHANGE_REASON,
WifiManager.CHANGE_REASON_CONFIG_CHANGE)
== WifiManager.CHANGE_REASON_REMOVED) {
WifiConfiguration configuration =
intent.getParcelableExtra(WifiManager.EXTRA_WIFI_CONFIGURATION);
mOsuManager.networkDeleted(configuration);
} else {
mOsuManager.networkConfigChanged();
}
break;
case WifiManager.WIFI_STATE_CHANGED_ACTION:
int state = bundle.getInt(WifiManager.EXTRA_WIFI_STATE);
if (state == WifiManager.WIFI_STATE_DISABLED) {
mOsuManager.wifiStateChange(false);
} else if (state == WifiManager.WIFI_STATE_ENABLED) {
mOsuManager.wifiStateChange(true);
}
break;
case REMEDIATION_DONE_ACTION:
String fqdn = bundle.getString(REMEDIATION_FQDN_EXTRA);
boolean policy = bundle.getBoolean(REMEDIATION_POLICY_EXTRA);
mOsuManager.remediationDone(fqdn, policy);
break;
}
}
public List<OSUData> getOsuData() {
return mOsuManager.getAvailableOSUs();
}
public void selectOsu(int id) {
mOsuManager.setOSUSelection(id);
}
}

View File

@@ -14,7 +14,7 @@ import com.android.hotspot2.asn1.Asn1Object;
import com.android.hotspot2.asn1.Asn1Oid;
import com.android.hotspot2.asn1.OidMappings;
import com.android.hotspot2.osu.HTTPHandler;
import com.android.hotspot2.osu.OSUManager;
import com.android.hotspot2.osu.OSUFlowManager;
import com.android.hotspot2.osu.OSUSocketFactory;
import com.android.hotspot2.osu.commands.GetCertData;
import com.android.hotspot2.pps.HomeSP;
@@ -81,7 +81,7 @@ public class ESTHandler implements AutoCloseable {
private PrivateKey mClientKey;
public ESTHandler(GetCertData certData, Network network, OMADMAdapter omadmAdapter,
KeyManager km, KeyStore ks, HomeSP homeSP, OSUManager.FlowType flowType)
KeyManager km, KeyStore ks, HomeSP homeSP, OSUFlowManager.FlowType flowType)
throws IOException, GeneralSecurityException {
mURL = new URL(certData.getServer());
mUser = certData.getUserName();

View File

@@ -0,0 +1,168 @@
package com.android.hotspot2.flow;
import android.annotation.Nullable;
import android.app.IntentService;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.Network;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.IBinder;
import android.util.Log;
import com.android.hotspot2.osu.OSUFlowManager;
import com.android.hotspot2.osu.OSUManager;
import com.android.hotspot2.osu.OSUOperationStatus;
import com.android.hotspot2.pps.HomeSP;
import java.io.IOException;
/**
* This is the Hotspot 2.0 release 2 service that handles actual provisioning and remediation flows.
*
* The OSU App is made up of two services; FlowService and OSUService.
*
* OSUService is a long running light weight service, kept alive throughout the lifetime of the
* operating system by being bound from the framework (in WifiManager in stage
* PHASE_THIRD_PARTY_APPS_CAN_START), and is responsible for continuously caching OSU information
* and notifying the UI when OSUs are available.
*
* FlowService is only started on demand from OSUService and is responsible for handling actual
* provisioning and remediation flows, and requires a fairly significant memory footprint.
*
* FlowService is defined to run in its own process through the definition
* <service android:name=".flow.FlowService" android:process=":osuflow">
* in the AndroidManifest.
* This is done as a means to keep total app memory footprint low (pss < 10M) and only start the
* FlowService on demand and make it available for "garbage collection" by the OS when not in use.
*/
public class FlowService extends IntentService {
private static final String[] INTENTS = {
WifiManager.NETWORK_STATE_CHANGED_ACTION
};
private OSUFlowManager mOSUFlowManager;
private PlatformAdapter mPlatformAdapter;
private final FlowServiceImpl mOSUAccessor = new FlowServiceImpl();
/*
public FlowService(Context context) {
super("FlowService");
mOSUFlowManager = new OSUFlowManager();
mPlatformAdapter = new PlatformAdapter(context);
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleIntent(intent.getAction(), intent);
}
};
for (String intentString : INTENTS) {
context.registerReceiver(receiver, new IntentFilter(intentString));
}
}
*/
public FlowService() {
super("FlowService");
}
public final class FlowServiceImpl extends IFlowService.Stub {
public void provision(OSUInfo osuInfo) {
FlowService.this.provision(osuInfo);
}
public void remediate(String spFqdn, String url, boolean policy, Network network) {
FlowService.this.remediate(spFqdn, url, policy, network);
}
public void spDeleted(String fqdn) {
FlowService.this.serviceProviderDeleted(fqdn);
}
}
public void provision(OSUInfo osuInfo) {
try {
mOSUFlowManager.appendFlow(new OSUFlowManager.OSUFlow(osuInfo, mPlatformAdapter,
mPlatformAdapter.getKeyManager(null)));
} catch (IOException ioe) {
mPlatformAdapter.notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
osuInfo.getName(PlatformAdapter.LOCALE));
}
}
/**
* Initiate remediation
* @param spFqdn The FQDN of the current SP, not set for WNM based remediation
* @param url The URL of the remediation server
* @param policy Set if this is a policy update rather than a subscription update
* @param network The network to use for remediation
*/
public void remediate(String spFqdn, String url, boolean policy, Network network) {
Log.d(OSUManager.TAG, "Starting remediation for " + spFqdn + " to " + url);
if (spFqdn != null) {
HomeSP homeSP = mPlatformAdapter.getHomeSP(spFqdn);
if (homeSP == null) {
Log.e(OSUManager.TAG, "No HomeSP object matches '" + spFqdn + "'");
return;
}
try {
mOSUFlowManager.appendFlow(new OSUFlowManager.OSUFlow(network, url,
mPlatformAdapter, mPlatformAdapter.getKeyManager(homeSP),
homeSP, policy
? OSUFlowManager.FlowType.Policy : OSUFlowManager.FlowType.Remediation));
} catch (IOException ioe) {
Log.e(OSUManager.TAG, "Failed to remediate: " + ioe, ioe);
}
} else {
HomeSP homeSP = mPlatformAdapter.getCurrentSP();
if (homeSP == null) {
Log.e(OSUManager.TAG, "Remediation request on unidentified Passpoint network ");
return;
}
try {
mOSUFlowManager.appendFlow(new OSUFlowManager.OSUFlow(network, url,
mPlatformAdapter, mPlatformAdapter.getKeyManager(homeSP), homeSP,
OSUFlowManager.FlowType.Remediation));
} catch (IOException ioe) {
Log.e(OSUManager.TAG, "Failed to start remediation: " + ioe, ioe);
}
}
}
public void serviceProviderDeleted(String fqdn) {
mPlatformAdapter.serviceProviderDeleted(fqdn);
}
@Override
public IBinder onBind(Intent intent) {
mOSUFlowManager = new OSUFlowManager(this);
mPlatformAdapter = new PlatformAdapter(this);
BroadcastReceiver receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
handleIntent(intent.getAction(), intent);
}
};
for (String intentString : INTENTS) {
registerReceiver(receiver, new IntentFilter(intentString));
}
return mOSUAccessor;
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
}
private void handleIntent(String action, Intent intent) {
if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
WifiInfo wifiInfo = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO);
if (wifiInfo != null) {
mOSUFlowManager.networkChange();
}
}
}
}

View File

@@ -0,0 +1,10 @@
package com.android.hotspot2.flow;
import com.android.hotspot2.flow.OSUInfo;
import android.net.Network;
interface IFlowService {
void provision(in OSUInfo osuInfo);
void remediate(String spFqdn, String url, boolean policy, in Network network);
void spDeleted(String fqdn);
}

View File

@@ -0,0 +1,3 @@
package com.android.hotspot2.flow;
parcelable OSUInfo;

View File

@@ -1,6 +1,8 @@
package com.android.hotspot2.osu;
package com.android.hotspot2.flow;
import android.net.wifi.ScanResult;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import com.android.anqp.HSIconFileElement;
@@ -8,15 +10,18 @@ import com.android.anqp.I18Name;
import com.android.anqp.IconInfo;
import com.android.anqp.OSUProvider;
import com.android.hotspot2.Utils;
import com.android.hotspot2.osu.OSUManager;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Set;
public class OSUInfo {
public class OSUInfo implements Parcelable {
public static final String GenericLocale = "zxx";
public enum IconStatus {
@@ -29,7 +34,6 @@ public class OSUInfo {
private final long mBSSID;
private final long mHESSID;
private final int mAnqpDomID;
private final String mOsuSsid;
private final String mAdvertisingSSID;
private final OSUProvider mOSUProvider;
private final int mOsuID;
@@ -39,13 +43,12 @@ public class OSUInfo {
private String mIconFileName;
private IconInfo mIconInfo;
public OSUInfo(ScanResult scanResult, String ssid, OSUProvider osuProvider, int osuID) {
public OSUInfo(ScanResult scanResult, OSUProvider osuProvider, int osuID) {
mOsuID = osuID;
mBSSID = Utils.parseMac(scanResult.BSSID);
mHESSID = scanResult.hessid;
mAnqpDomID = scanResult.anqpDomainId;
mAdvertisingSSID = scanResult.SSID;
mOsuSsid = ssid;
mOSUProvider = osuProvider;
}
@@ -99,11 +102,11 @@ public class OSUInfo {
if (locale == null || name.getLocale().equals(locale)) {
return name.getText();
}
scoreList.add(new ScoreEntry<String>(name.getText(),
scoreList.add(new ScoreEntry<>(name.getText(),
languageScore(name.getLanguage(), locale)));
}
Collections.sort(scoreList);
return scoreList.isEmpty() ? null : scoreList.iterator().next().getData();
return scoreList.isEmpty() ? null : scoreList.get(scoreList.size() - 1).getData();
}
public String getServiceDescription(Locale locale) {
@@ -116,7 +119,7 @@ public class OSUInfo {
languageScore(service.getLanguage(), locale)));
}
Collections.sort(scoreList);
return scoreList.isEmpty() ? null : scoreList.iterator().next().getData();
return scoreList.isEmpty() ? null : scoreList.get(scoreList.size() - 1).getData();
}
public int getOsuID() {
@@ -182,6 +185,11 @@ public class OSUInfo {
public int compareTo(ScoreEntry other) {
return Integer.compare(mScore, other.mScore);
}
@Override
public String toString() {
return String.format("%d for '%s'", mScore, mData);
}
}
public List<IconInfo> getIconInfo(Locale locale, Set<String> types, int width, int height) {
@@ -219,8 +227,9 @@ public class OSUInfo {
}
Collections.sort(matches);
List<IconInfo> icons = new ArrayList<>(matches.size());
for (ScoreEntry<IconInfo> scoredIcon : matches) {
icons.add(scoredIcon.getData());
ListIterator<ScoreEntry<IconInfo>> matchIterator = matches.listIterator(matches.size());
while (matchIterator.hasPrevious()) {
icons.add(matchIterator.previous().getData());
}
return icons;
}
@@ -243,7 +252,7 @@ public class OSUInfo {
}
public String getOsuSsid() {
return mOsuSsid;
return mOSUProvider.getSSID();
}
public OSUProvider getOSUProvider() {
@@ -253,6 +262,60 @@ public class OSUInfo {
@Override
public String toString() {
return String.format("OSU Info '%s' %012x -> %s, icon %s",
mOsuSsid, mBSSID, getServiceDescription(null), mIconStatus);
getOsuSsid(), mBSSID, getServiceDescription(null), mIconStatus);
}
private OSUInfo(Parcel in) {
mBSSID = in.readLong();
mHESSID = in.readLong();
mAnqpDomID = in.readInt();
mAdvertisingSSID = in.readString();
mOsuID = in.readInt();
mOSUBssid = in.readLong();
mIconFileName = in.readString();
mIconStatus = Utils.mapEnum(in.readInt(), IconStatus.class);
OSUProvider osuProvider;
try {
osuProvider = new OSUProvider(in);
} catch (IOException ioe) {
osuProvider = null;
}
mOSUProvider = osuProvider;
if (osuProvider == null) {
return;
}
mIconFileElement = new HSIconFileElement(in);
int iconIndex = in.readInt();
mIconInfo = iconIndex >= 0 && iconIndex < mOSUProvider.getIcons().size()
? mOSUProvider.getIcons().get(iconIndex) : null;
}
public static final Parcelable.Creator<OSUInfo> CREATOR = new Parcelable.Creator<OSUInfo>() {
public OSUInfo createFromParcel(Parcel in) {
return new OSUInfo(in);
}
public OSUInfo[] newArray(int size) {
return new OSUInfo[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeLong(mBSSID);
dest.writeLong(mHESSID);
dest.writeInt(mAnqpDomID);
dest.writeString(mAdvertisingSSID);
dest.writeInt(mOsuID);
dest.writeLong(mOSUBssid);
dest.writeString(mIconFileName);
dest.writeInt(mIconStatus.ordinal());
mOSUProvider.writeParcel(dest);
mIconFileElement.writeParcel(dest);
}
}

View File

@@ -0,0 +1,611 @@
package com.android.hotspot2.flow;
import android.content.Context;
import android.content.Intent;
import android.net.Network;
import android.net.wifi.PasspointManagementObjectDefinition;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiEnterpriseConfig;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.util.Log;
import com.android.configparse.ConfigBuilder;
import com.android.hotspot2.AppBridge;
import com.android.hotspot2.Utils;
import com.android.hotspot2.app.OSUService;
import com.android.hotspot2.omadm.MOManager;
import com.android.hotspot2.omadm.MOTree;
import com.android.hotspot2.omadm.OMAConstants;
import com.android.hotspot2.omadm.OMAException;
import com.android.hotspot2.omadm.OMAParser;
import com.android.hotspot2.osu.ClientKeyManager;
import com.android.hotspot2.osu.OSUCertType;
import com.android.hotspot2.osu.OSUManager;
import com.android.hotspot2.osu.OSUOperationStatus;
import com.android.hotspot2.osu.OSUSocketFactory;
import com.android.hotspot2.osu.WiFiKeyManager;
import com.android.hotspot2.osu.commands.MOData;
import com.android.hotspot2.pps.HomeSP;
import org.xml.sax.SAXException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import javax.net.ssl.KeyManager;
public class PlatformAdapter {
private static final String TAG = "OSUFLOW";
public static final Locale LOCALE = Locale.getDefault();
public static final String CERT_WFA_ALIAS = "wfa-root-";
public static final String CERT_REM_ALIAS = "rem-";
public static final String CERT_POLICY_ALIAS = "pol-";
public static final String CERT_SHARED_ALIAS = "shr-";
public static final String CERT_CLT_CERT_ALIAS = "clt-";
public static final String CERT_CLT_KEY_ALIAS = "prv-";
public static final String CERT_CLT_CA_ALIAS = "aaa-";
private static final String KEYSTORE_FILE = "passpoint.ks";
private final Context mContext;
private final File mKeyStoreFile;
private final KeyStore mKeyStore;
private final AppBridge mAppBridge;
private final Map<String, PasspointConfig> mPasspointConfigs;
public PlatformAdapter(Context context) {
mContext = context;
mAppBridge = new AppBridge(context);
File appFolder = context.getFilesDir();
mKeyStoreFile = new File(appFolder, KEYSTORE_FILE);
Log.d(TAG, "KS file: " + mKeyStoreFile.getPath());
KeyStore ks = null;
try {
//ks = loadKeyStore(KEYSTORE_FILE, readCertsFromDisk(WFA_CA_LOC));
ks = loadKeyStore(mKeyStoreFile, OSUSocketFactory.buildCertSet());
} catch (IOException e) {
Log.e(TAG, "Failed to initialize Passpoint keystore, OSU disabled", e);
}
mKeyStore = ks;
mPasspointConfigs = loadAllSps(context);
}
private static KeyStore loadKeyStore(File ksFile, Set<X509Certificate> diskCerts)
throws IOException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
if (ksFile.exists()) {
try (FileInputStream in = new FileInputStream(ksFile)) {
keyStore.load(in, null);
}
// Note: comparing two sets of certs does not work.
boolean mismatch = false;
int loadCount = 0;
for (int n = 0; n < 1000; n++) {
String alias = String.format("%s%d", CERT_WFA_ALIAS, n);
Certificate cert = keyStore.getCertificate(alias);
if (cert == null) {
break;
}
loadCount++;
boolean matched = false;
Iterator<X509Certificate> iter = diskCerts.iterator();
while (iter.hasNext()) {
X509Certificate diskCert = iter.next();
if (cert.equals(diskCert)) {
iter.remove();
matched = true;
break;
}
}
if (!matched) {
mismatch = true;
break;
}
}
if (mismatch || !diskCerts.isEmpty()) {
Log.d(TAG, "Re-seeding Passpoint key store with " +
diskCerts.size() + " WFA certs");
for (int n = 0; n < 1000; n++) {
String alias = String.format("%s%d", CERT_WFA_ALIAS, n);
Certificate cert = keyStore.getCertificate(alias);
if (cert == null) {
break;
} else {
keyStore.deleteEntry(alias);
}
}
int index = 0;
for (X509Certificate caCert : diskCerts) {
keyStore.setCertificateEntry(
String.format("%s%d", CERT_WFA_ALIAS, index), caCert);
index++;
}
try (FileOutputStream out = new FileOutputStream(ksFile)) {
keyStore.store(out, null);
}
} else {
Log.d(TAG, "Loaded Passpoint key store with " + loadCount + " CA certs");
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
Log.d("ZXC", "KS Alias '" + aliases.nextElement() + "'");
}
}
} else {
keyStore.load(null, null);
int index = 0;
for (X509Certificate caCert : diskCerts) {
keyStore.setCertificateEntry(
String.format("%s%d", CERT_WFA_ALIAS, index), caCert);
index++;
}
try (FileOutputStream out = new FileOutputStream(ksFile)) {
keyStore.store(out, null);
}
Log.d(TAG, "Initialized Passpoint key store with " +
diskCerts.size() + " CA certs");
}
return keyStore;
} catch (GeneralSecurityException gse) {
throw new IOException(gse);
}
}
private static Map<String, PasspointConfig> loadAllSps(Context context) {
Map<String, PasspointConfig> passpointConfigs = new HashMap<>();
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
List<WifiConfiguration> configs = wifiManager.getPrivilegedConfiguredNetworks();
if (configs == null) {
return passpointConfigs;
}
int count = 0;
for (WifiConfiguration config : configs) {
String moTree = config.getMoTree();
if (moTree != null) {
try {
passpointConfigs.put(config.FQDN, new PasspointConfig(config));
count++;
} catch (IOException | SAXException e) {
Log.w(OSUManager.TAG, "Failed to parse MO: " + e);
}
}
}
Log.d(OSUManager.TAG, "Loaded " + count + " SPs");
return passpointConfigs;
}
public KeyStore getKeyStore() {
return mKeyStore;
}
public Context getContext() {
return mContext;
}
/**
* Connect to an OSU provisioning network. The connection should not bring down other existing
* connection and the network should not be made the default network since the connection
* is solely for sign up and is neither intended for nor likely provides access to any
* generic resources.
*
* @param osuInfo The OSU info object that defines the parameters for the network. An OSU
* network is either an open network, or, if the OSU NAI is set, an "OSEN"
* network, which is an anonymous EAP-TLS network with special keys.
* @return an Integer holding the network-id of the just added network configuration, or null
* if the network existed prior to this call (was not added by the OSU infrastructure).
* The value will be used at the end of the OSU flow to delete the network as applicable.
* @throws IOException Issues:
* 1. The network id is not returned. addNetwork cannot be called from here since the method
* runs in the context of the app and doesn't have the appropriate permission.
* 2. The connection is not immediately usable if the network was not previously selected
* manually.
*/
public Integer connect(OSUInfo osuInfo) throws IOException {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
WifiConfiguration config = new WifiConfiguration();
config.SSID = '"' + osuInfo.getOsuSsid() + '"';
if (osuInfo.getOSUBssid() != 0) {
config.BSSID = Utils.macToString(osuInfo.getOSUBssid());
Log.d(OSUManager.TAG, String.format("Setting BSSID of '%s' to %012x",
osuInfo.getOsuSsid(), osuInfo.getOSUBssid()));
}
if (osuInfo.getOSUProvider().getOsuNai() == null) {
config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
} else {
config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.OSEN);
config.allowedProtocols.set(WifiConfiguration.Protocol.OSEN);
config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP);
config.allowedGroupCiphers.set(WifiConfiguration.GroupCipher.GTK_NOT_USED);
config.enterpriseConfig = new WifiEnterpriseConfig();
config.enterpriseConfig.setEapMethod(WifiEnterpriseConfig.Eap.UNAUTH_TLS);
config.enterpriseConfig.setIdentity(osuInfo.getOSUProvider().getOsuNai());
Set<X509Certificate> cas = OSUSocketFactory.buildCertSet();
config.enterpriseConfig.setCaCertificates(cas.toArray(new X509Certificate[cas.size()]));
}
int networkId = wifiManager.addNetwork(config);
if (networkId < 0) {
throw new IOException("Failed to add OSU network");
}
if (wifiManager.enableNetwork(networkId, true)) {
return networkId;
} else {
throw new IOException("Failed to enable OSU network");
}
}
/**
* @param homeSP The Home SP associated with the keying material in question. Passing
* null returns a "system wide" KeyManager to support pre-provisioned certs based
* on names retrieved from the ClientCertInfo request.
* @return A key manager suitable for the given configuration (or pre-provisioned keys).
*/
public KeyManager getKeyManager(HomeSP homeSP) throws IOException {
return homeSP != null
? new ClientKeyManager(homeSP, mKeyStore) : new WiFiKeyManager(mKeyStore);
}
public void provisioningComplete(OSUInfo osuInfo,
MOData moData, Map<OSUCertType, List<X509Certificate>> certs,
PrivateKey privateKey, Network osuNetwork) {
try {
String xml = moData.getMOTree().toXml();
HomeSP homeSP = MOManager.buildSP(xml);
Integer spNwk = addNetwork(homeSP, certs, privateKey, osuNetwork);
if (spNwk == null) {
notifyUser(OSUOperationStatus.ProvisioningFailure,
"Failed to save network configuration", osuInfo.getName(LOCALE));
} else {
if (addSP(xml) < 0) {
deleteNetwork(spNwk);
Log.e(TAG, "Failed to provision: " + homeSP.getFQDN());
notifyUser(OSUOperationStatus.ProvisioningFailure, "Failed to add MO",
osuInfo.getName(LOCALE));
return;
}
Set<X509Certificate> rootCerts = OSUSocketFactory.getRootCerts(mKeyStore);
X509Certificate remCert = getCert(certs, OSUCertType.Remediation);
X509Certificate polCert = getCert(certs, OSUCertType.Policy);
int newCerts = 0;
if (privateKey != null) {
X509Certificate cltCert = getCert(certs, OSUCertType.Client);
mKeyStore.setKeyEntry(CERT_CLT_KEY_ALIAS + homeSP.getFQDN(),
privateKey, null, new X509Certificate[]{cltCert});
mKeyStore.setCertificateEntry(CERT_CLT_CERT_ALIAS + homeSP.getFQDN(), cltCert);
newCerts++;
}
boolean usingShared = false;
if (remCert != null) {
if (!rootCerts.contains(remCert)) {
if (remCert.equals(polCert)) {
mKeyStore.setCertificateEntry(CERT_SHARED_ALIAS + homeSP.getFQDN(),
remCert);
usingShared = true;
newCerts++;
} else {
mKeyStore.setCertificateEntry(CERT_REM_ALIAS + homeSP.getFQDN(),
remCert);
newCerts++;
}
}
}
if (!usingShared && polCert != null) {
if (!rootCerts.contains(polCert)) {
mKeyStore.setCertificateEntry(CERT_POLICY_ALIAS + homeSP.getFQDN(),
remCert);
newCerts++;
}
}
if (newCerts > 0) {
try (FileOutputStream out = new FileOutputStream(mKeyStoreFile)) {
mKeyStore.store(out, null);
}
}
notifyUser(OSUOperationStatus.ProvisioningSuccess, null, osuInfo.getName(LOCALE));
Log.d(TAG, "Provisioning complete.");
}
} catch (IOException | GeneralSecurityException | SAXException e) {
Log.e(TAG, "Failed to provision: " + e, e);
notifyUser(OSUOperationStatus.ProvisioningFailure, e.toString(),
osuInfo.getName(LOCALE));
}
}
public void remediationComplete(HomeSP homeSP, Collection<MOData> mods,
Map<OSUCertType, List<X509Certificate>> certs,
PrivateKey privateKey, boolean policy)
throws IOException, GeneralSecurityException {
HomeSP altSP = null;
if (modifySP(homeSP, mods) > 0) {
altSP = MOManager.modifySP(homeSP, getMOTree(homeSP), mods);
}
X509Certificate caCert = null;
List<X509Certificate> clientCerts = null;
if (certs != null) {
List<X509Certificate> certList = certs.get(OSUCertType.AAA);
caCert = certList != null && !certList.isEmpty() ? certList.iterator().next() : null;
clientCerts = certs.get(OSUCertType.Client);
}
if (altSP != null || certs != null) {
if (altSP == null) {
altSP = homeSP;
}
updateNetwork(altSP, caCert, clientCerts, privateKey);
}
Intent intent = new Intent(OSUService.REMEDIATION_DONE_ACTION);
intent.putExtra(OSUService.REMEDIATION_FQDN_EXTRA, homeSP.getFQDN());
intent.putExtra(OSUService.REMEDIATION_POLICY_EXTRA, policy);
mContext.sendBroadcast(intent);
notifyUser(OSUOperationStatus.ProvisioningSuccess, null, homeSP.getFriendlyName());
}
public void serviceProviderDeleted(String fqdn) {
int count = deleteCerts(mKeyStore, fqdn,
CERT_REM_ALIAS, CERT_POLICY_ALIAS, CERT_SHARED_ALIAS, CERT_CLT_CERT_ALIAS);
Log.d(TAG, "Passpoint network deleted, removing " + count + " key store entries");
try {
if (mKeyStore.getKey(CERT_CLT_KEY_ALIAS + fqdn, null) != null) {
mKeyStore.deleteEntry(CERT_CLT_KEY_ALIAS + fqdn);
}
} catch (GeneralSecurityException e) {
/**/
}
if (count > 0) {
try (FileOutputStream out = new FileOutputStream(mKeyStoreFile)) {
mKeyStore.store(out, null);
} catch (IOException | GeneralSecurityException e) {
Log.w(TAG, "Failed to remove certs from key store: " + e);
}
}
}
private static int deleteCerts(KeyStore keyStore, String fqdn, String... prefixes) {
int count = 0;
for (String prefix : prefixes) {
try {
String alias = prefix + fqdn;
Certificate cert = keyStore.getCertificate(alias);
if (cert != null) {
keyStore.deleteEntry(alias);
count++;
}
} catch (KeyStoreException kse) {
/**/
}
}
return count;
}
private static X509Certificate getCert(Map<OSUCertType, List<X509Certificate>> certMap,
OSUCertType certType) {
List<X509Certificate> certs = certMap.get(certType);
if (certs == null || certs.isEmpty()) {
return null;
}
return certs.iterator().next();
}
public String notifyUser(OSUOperationStatus status, String message, String spName) {
if (status == OSUOperationStatus.UserInputComplete) {
return null;
}
mAppBridge.showStatus(status, spName, message, null);
return null;
}
public void provisioningFailed(String spName, String message) {
notifyUser(OSUOperationStatus.ProvisioningFailure, message, spName);
}
private Integer addNetwork(HomeSP homeSP, Map<OSUCertType, List<X509Certificate>> certs,
PrivateKey privateKey, Network osuNetwork)
throws IOException, GeneralSecurityException {
List<X509Certificate> aaaTrust = certs.get(OSUCertType.AAA);
if (aaaTrust.isEmpty()) {
aaaTrust = certs.get(OSUCertType.CA); // Get the CAs from the EST flow.
}
WifiConfiguration config = ConfigBuilder.buildConfig(homeSP,
aaaTrust.iterator().next(),
certs.get(OSUCertType.Client), privateKey);
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
int nwkId = wifiManager.addNetwork(config);
boolean saved = false;
if (nwkId >= 0) {
saved = wifiManager.saveConfiguration();
}
Log.d(OSUManager.TAG, "Wifi configuration " + nwkId +
" " + (saved ? "saved" : "not saved"));
if (saved) {
reconnect(osuNetwork, nwkId);
return nwkId;
} else {
return null;
}
}
private void updateNetwork(HomeSP homeSP, X509Certificate caCert,
List<X509Certificate> clientCerts, PrivateKey privateKey)
throws IOException, GeneralSecurityException {
WifiConfiguration config = getWifiConfig(homeSP);
if (config == null) {
throw new IOException("Failed to find matching network config");
}
Log.d(OSUManager.TAG, "Found matching config " + config.networkId + ", updating");
WifiEnterpriseConfig enterpriseConfig = config.enterpriseConfig;
WifiConfiguration newConfig = ConfigBuilder.buildConfig(homeSP,
caCert != null ? caCert : enterpriseConfig.getCaCertificate(),
clientCerts, privateKey);
newConfig.networkId = config.networkId;
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
wifiManager.save(newConfig, null);
wifiManager.saveConfiguration();
}
private WifiConfiguration getWifiConfig(HomeSP homeSP) {
PasspointConfig passpointConfig = mPasspointConfigs.get(homeSP.getFQDN());
return passpointConfig != null ? passpointConfig.getWifiConfiguration() : null;
}
public MOTree getMOTree(HomeSP homeSP) {
PasspointConfig config = mPasspointConfigs.get(homeSP.getFQDN());
return config != null ? config.getmMOTree() : null;
}
public HomeSP getHomeSP(String fqdn) {
PasspointConfig passpointConfig = mPasspointConfigs.get(fqdn);
return passpointConfig != null ? passpointConfig.getHomeSP() : null;
}
public HomeSP getCurrentSP() {
PasspointConfig passpointConfig = getActivePasspointConfig();
return passpointConfig != null ? passpointConfig.getHomeSP() : null;
}
private PasspointConfig getActivePasspointConfig() {
WifiInfo wifiInfo = getConnectionInfo();
if (wifiInfo == null) {
return null;
}
for (PasspointConfig passpointConfig : mPasspointConfigs.values()) {
if (passpointConfig.getWifiConfiguration().networkId == wifiInfo.getNetworkId()) {
return passpointConfig;
}
}
return null;
}
private int addSP(String xml) throws IOException, SAXException {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
return wifiManager.addPasspointManagementObject(xml);
}
private int modifySP(HomeSP homeSP, Collection<MOData> mods) throws IOException {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
List<PasspointManagementObjectDefinition> defMods = new ArrayList<>(mods.size());
for (MOData mod : mods) {
defMods.add(new PasspointManagementObjectDefinition(mod.getBaseURI(),
mod.getURN(), mod.getMOTree().toXml()));
}
return wifiManager.modifyPasspointManagementObject(homeSP.getFQDN(), defMods);
}
private void reconnect(Network osuNetwork, int newNwkId) {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
if (osuNetwork != null) {
wifiManager.disableNetwork(osuNetwork.netId);
}
if (newNwkId != WifiConfiguration.INVALID_NETWORK_ID) {
wifiManager.enableNetwork(newNwkId, true);
}
}
public void deleteNetwork(int id) {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
wifiManager.disableNetwork(id);
wifiManager.forget(id, null);
}
public WifiInfo getConnectionInfo() {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
return wifiManager.getConnectionInfo();
}
public Network getCurrentNetwork() {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
return wifiManager.getCurrentNetwork();
}
public WifiConfiguration getActiveWifiConfig() {
WifiInfo wifiInfo = getConnectionInfo();
if (wifiInfo == null) {
return null;
}
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
List<WifiConfiguration> configs = wifiManager.getConfiguredNetworks();
if (configs == null) {
return null;
}
for (WifiConfiguration config : configs) {
if (config.networkId == wifiInfo.getNetworkId()) {
return config;
}
}
return null;
}
private static class PasspointConfig {
private final WifiConfiguration mWifiConfiguration;
private final MOTree mMOTree;
private final HomeSP mHomeSP;
private PasspointConfig(WifiConfiguration config) throws IOException, SAXException {
mWifiConfiguration = config;
OMAParser omaParser = new OMAParser();
mMOTree = omaParser.parse(config.getMoTree(), OMAConstants.PPS_URN);
List<HomeSP> spList = MOManager.buildSPs(mMOTree);
if (spList.size() != 1) {
throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
}
mHomeSP = spList.iterator().next();
}
public WifiConfiguration getWifiConfiguration() {
return mWifiConfiguration;
}
public HomeSP getHomeSP() {
return mHomeSP;
}
public MOTree getmMOTree() {
return mMOTree;
}
}
}

View File

@@ -2,6 +2,7 @@ package com.android.hotspot2.osu;
import android.util.Log;
import com.android.hotspot2.flow.PlatformAdapter;
import com.android.hotspot2.pps.HomeSP;
import java.io.IOException;
@@ -30,9 +31,9 @@ public class ClientKeyManager implements X509KeyManager {
public ClientKeyManager(HomeSP homeSP, KeyStore keyStore) throws IOException {
mKeyStore = keyStore;
mAliasMap = new HashMap<>();
mAliasMap.put(OSUCertType.AAA, OSUManager.CERT_CLT_CA_ALIAS + homeSP.getFQDN());
mAliasMap.put(OSUCertType.Client, OSUManager.CERT_CLT_CERT_ALIAS + homeSP.getFQDN());
mAliasMap.put(OSUCertType.PrivateKey, OSUManager.CERT_CLT_KEY_ALIAS + homeSP.getFQDN());
mAliasMap.put(OSUCertType.AAA, PlatformAdapter.CERT_CLT_CA_ALIAS + homeSP.getFQDN());
mAliasMap.put(OSUCertType.Client, PlatformAdapter.CERT_CLT_CERT_ALIAS + homeSP.getFQDN());
mAliasMap.put(OSUCertType.PrivateKey, PlatformAdapter.CERT_CLT_KEY_ALIAS + homeSP.getFQDN());
mTempKeys = new HashMap<>();
}

View File

@@ -171,8 +171,10 @@ public class HTTPHandler implements AutoCloseable {
}
public void close() throws IOException {
mSocket.shutdownInput();
mSocket.shutdownOutput();
mSocket.close();
mIn.close();
mOut.close();
mSocket.close();
}
}

View File

@@ -5,6 +5,7 @@ import android.util.Log;
import com.android.anqp.HSIconFileElement;
import com.android.anqp.IconInfo;
import com.android.hotspot2.Utils;
import com.android.hotspot2.flow.OSUInfo;
import java.net.ProtocolException;
import java.nio.BufferUnderflowException;
@@ -132,7 +133,7 @@ public class IconCache extends Thread {
if (!mBssids.contains(bssid)) {
return 0;
}
Log.d("ZXZ", "Updating icon on " + mQueued.size() + " osus");
Log.d(OSUManager.TAG, "Updating icon on " + mQueued.size() + " osus");
for (OSUInfo osuInfo : mQueued) {
osuInfo.setIconFileElement(iconFileElement, mFileName);
}
@@ -188,29 +189,28 @@ public class IconCache extends Thread {
HSIconFileElement iconFileElement = get(key, fileName);
if (iconFileElement != null) {
osuInfo.setIconFileElement(iconFileElement, fileName);
Log.d("ZXZ", "Icon cache hit for " + osuInfo + "/" + fileName);
Log.d(OSUManager.TAG, "Icon cache hit for " + osuInfo + "/" + fileName);
modCount++;
} else {
FileEntry fileEntry = enqueue(key, fileName, osuInfo);
if (fileEntry != null) {
Log.d("ZXZ", "Initiating icon query for " + osuInfo + "/" + fileName);
Log.d(OSUManager.TAG, "Initiating icon query for "
+ osuInfo + "/" + fileName);
mOsuManager.doIconQuery(osuInfo.getBSSID(), fileName);
} else {
Log.d("ZXZ", "Piggybacking icon query for " + osuInfo + "/" + fileName);
Log.d(OSUManager.TAG, "Piggybacking icon query for "
+ osuInfo + "/" + fileName);
}
}
}
}
Log.d("ZXZ", "Current keys " + current + " from " + osuInfos);
// Drop all non-current ESS's
Iterator<EssKey> pendingKeys = mPending.keySet().iterator();
while (pendingKeys.hasNext()) {
EssKey key = pendingKeys.next();
if (!current.contains(key)) {
pendingKeys.remove();
Log.d("ZXZ", "Removing pending " + key);
}
}
Iterator<EssKey> cacheKeys = mCache.keySet().iterator();
@@ -218,7 +218,6 @@ public class IconCache extends Thread {
EssKey key = cacheKeys.next();
if (!current.contains(key)) {
cacheKeys.remove();
Log.d("ZXZ", "Removing cache " + key);
}
}
return modCount;
@@ -235,7 +234,7 @@ public class IconCache extends Thread {
}
public int notifyIconReceived(long bssid, String fileName, byte[] iconData) {
Log.d("ZXZ", String.format("Icon '%s':%d received from %012x",
Log.d(OSUManager.TAG, String.format("Icon '%s':%d received from %012x",
fileName, iconData != null ? iconData.length : -1, bssid));
if (fileName == null || iconData == null) {
return 0;
@@ -273,7 +272,6 @@ public class IconCache extends Thread {
}
public void tick(boolean wifiOff) {
Log.d("ZXZ", "Ticking icon cache: " + wifiOff);
if (wifiOff) {
mPending.clear();
mCache.clear();
@@ -291,10 +289,8 @@ public class IconCache extends Thread {
FileEntry fileEntry = fileEntries.next().getValue();
long age = now - fileEntry.getTimestamp();
if (age > REQUERY_TIMEOUT || fileEntry.getAndIncrementRetry() > MAX_RETRY) {
Log.d("ZXZ", "Timing out " + fileEntry);
fileEntries.remove();
} else if (age > REQUERY_TIME) {
Log.d("ZXZ", "Retrying " + fileEntry);
mOsuManager.doIconQuery(fileEntry.getLastBssid(), fileEntry.getFileName());
}
}

View File

@@ -81,6 +81,8 @@ public class OSUCache {
for (ScanResult scanResult : scanResults) {
AnqpInformationElement[] osuInfo = scanResult.anqpElements;
if (osuInfo != null && osuInfo.length > 0) {
Log.d(OSUManager.TAG, scanResult.SSID +
" has " + osuInfo.length + " ANQP elements");
putResult(scanResult, osuInfo);
}
}
@@ -89,6 +91,8 @@ public class OSUCache {
private void putResult(ScanResult scanResult, AnqpInformationElement[] elements) {
for (AnqpInformationElement ie : elements) {
Log.d(OSUManager.TAG, String.format("ANQP IE %d vid %x size %d", ie.getElementId(),
ie.getVendorId(), ie.getPayload().length));
if (ie.getElementId() == AnqpInformationElement.HS_OSU_PROVIDERS
&& ie.getVendorId() == AnqpInformationElement.HOTSPOT20_VENDOR_ID) {
try {
@@ -106,6 +110,7 @@ public class OSUCache {
}
private void putProviders(ScanResult scanResult, HSOsuProvidersElement osuProviders) {
Log.d(OSUManager.TAG, osuProviders.getProviders().size() + " OSU providers in element");
for (OSUProvider provider : osuProviders.getProviders()) {
// Make a predictive put
ScanResult existing = mBatchedOSUs.put(provider, scanResult);
@@ -126,13 +131,14 @@ public class OSUCache {
mCache.put(entry.getKey(), new ScanInstance(entry.getValue(), mInstant));
changes++;
if (current == null) {
Log.d("ZXZ", "Add OSU " + entry.getKey() + " from " + entry.getValue().SSID);
Log.d(OSUManager.TAG,
"Add OSU " + entry.getKey() + " from " + entry.getValue().SSID);
} else {
Log.d("ZXZ", "Update OSU " + entry.getKey() + " with " +
Log.d(OSUManager.TAG, "Update OSU " + entry.getKey() + " with " +
entry.getValue().SSID + " to " + current);
}
} else {
Log.d("ZXZ", "Existing OSU " + entry.getKey() + ", "
Log.d(OSUManager.TAG, "Existing OSU " + entry.getKey() + ", "
+ current.getInstant() + " -> " + mInstant);
current.updateInstant(mInstant);
}
@@ -140,7 +146,7 @@ public class OSUCache {
for (Map.Entry<OSUProvider, ScanInstance> entry : aged.entrySet()) {
if (mInstant - entry.getValue().getInstant() > SCAN_BATCH_HISTORY_SIZE) {
Log.d("ZXZ", "Remove OSU " + entry.getKey() + ", "
Log.d(OSUManager.TAG, "Remove OSU " + entry.getKey() + ", "
+ entry.getValue().getInstant() + " @ " + mInstant);
mCache.remove(entry.getKey());
changes++;

View File

@@ -11,17 +11,22 @@ package com.android.hotspot2.osu;
* subscription-server.r2-testbed-rks IN A 10.123.107.107
*/
import android.content.Context;
import android.content.Intent;
import android.net.Network;
import android.util.Log;
import com.android.hotspot2.OMADMAdapter;
import com.android.hotspot2.est.ESTHandler;
import com.android.hotspot2.flow.OSUInfo;
import com.android.hotspot2.flow.PlatformAdapter;
import com.android.hotspot2.omadm.OMAConstants;
import com.android.hotspot2.omadm.OMANode;
import com.android.hotspot2.osu.commands.BrowserURI;
import com.android.hotspot2.osu.commands.ClientCertInfo;
import com.android.hotspot2.osu.commands.GetCertData;
import com.android.hotspot2.osu.commands.MOData;
import com.android.hotspot2.osu.service.RedirectListener;
import com.android.hotspot2.pps.Credential;
import com.android.hotspot2.pps.HomeSP;
import com.android.hotspot2.pps.UpdateInfo;
@@ -38,6 +43,7 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -48,38 +54,46 @@ import javax.net.ssl.KeyManager;
public class OSUClient {
private static final String TAG = "OSUCLT";
private static final String TTLS_OSU =
"https://osu-server.r2-testbed-rks.wi-fi.org:9447/OnlineSignup/services/newUser/digest";
private static final String TLS_OSU =
"https://osu-server.r2-testbed-rks.wi-fi.org:9446/OnlineSignup/services/newUser/certificate";
private final OSUInfo mOSUInfo;
private final URL mURL;
private final KeyStore mKeyStore;
private final Context mContext;
private volatile HTTPHandler mHTTPHandler;
private volatile RedirectListener mRedirectListener;
public OSUClient(OSUInfo osuInfo, KeyStore ks) throws MalformedURLException {
public OSUClient(OSUInfo osuInfo, KeyStore ks, Context context) throws MalformedURLException {
mOSUInfo = osuInfo;
mURL = new URL(osuInfo.getOSUProvider().getOSUServer());
mKeyStore = ks;
mContext = context;
}
public OSUClient(String osu, KeyStore ks) throws MalformedURLException {
public OSUClient(String osu, KeyStore ks, Context context) throws MalformedURLException {
mOSUInfo = null;
mURL = new URL(osu);
mKeyStore = ks;
mContext = context;
}
public void provision(OSUManager osuManager, Network network, KeyManager km)
public OSUInfo getOSUInfo() {
return mOSUInfo;
}
public void provision(PlatformAdapter platformAdapter, Network network, KeyManager km)
throws IOException, GeneralSecurityException {
try (HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8,
OSUSocketFactory.getSocketFactory(mKeyStore, null, OSUManager.FlowType.Provisioning,
network, mURL, km, true))) {
OSUSocketFactory.getSocketFactory(mKeyStore, null,
OSUFlowManager.FlowType.Provisioning, network, mURL, km, true))) {
mHTTPHandler = httpHandler;
SPVerifier spVerifier = new SPVerifier(mOSUInfo);
spVerifier.verify(httpHandler.getOSUCertificate(mURL));
URL redirectURL = osuManager.prepareUserInput(mOSUInfo.getName(Locale.getDefault()));
OMADMAdapter omadmAdapter = osuManager.getOMADMAdapter();
URL redirectURL = prepareUserInput(platformAdapter,
mOSUInfo.getName(Locale.getDefault()));
OMADMAdapter omadmAdapter = getOMADMAdapter();
String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRegistration,
null,
@@ -135,7 +149,7 @@ public class OSUClient {
throw new IOException("Bad or missing session ID in webURL");
}
if (!osuManager.startUserInput(new URL(webURL), network)) {
if (!startUserInput(new URL(webURL), network)) {
throw new IOException("User session failed");
}
@@ -159,8 +173,8 @@ public class OSUClient {
moData = (MOData) provResponse.getCommandData();
} else {
try (ESTHandler estHandler = new ESTHandler((GetCertData) provResponse.
getCommandData(), network, osuManager.getOMADMAdapter(),
km, mKeyStore, null, OSUManager.FlowType.Provisioning)) {
getCommandData(), network, getOMADMAdapter(),
km, mKeyStore, null, OSUFlowManager.FlowType.Provisioning)) {
estHandler.execute(false);
certs.put(OSUCertType.CA, estHandler.getCACerts());
certs.put(OSUCertType.Client, estHandler.getClientCerts());
@@ -197,16 +211,19 @@ public class OSUClient {
}
retrieveCerts(moData.getMOTree().getRoot(), certs, network, km, mKeyStore);
osuManager.provisioningComplete(mOSUInfo, moData, certs, clientKey, network);
platformAdapter.provisioningComplete(mOSUInfo, moData, certs, clientKey, network);
}
}
public void remediate(OSUManager osuManager, Network network, KeyManager km, HomeSP homeSP,
OSUManager.FlowType flowType)
public void remediate(PlatformAdapter platformAdapter, Network network, KeyManager km,
HomeSP homeSP, OSUFlowManager.FlowType flowType)
throws IOException, GeneralSecurityException {
try (HTTPHandler httpHandler = createHandler(network, homeSP, km, flowType)) {
URL redirectURL = osuManager.prepareUserInput(homeSP.getFriendlyName());
OMADMAdapter omadmAdapter = osuManager.getOMADMAdapter();
mHTTPHandler = httpHandler;
URL redirectURL = prepareUserInput(platformAdapter, homeSP.getFriendlyName());
OMADMAdapter omadmAdapter = getOMADMAdapter();
String regRequest = SOAPBuilder.buildPostDevDataResponse(RequestReason.SubRemediation,
null,
@@ -233,7 +250,7 @@ public class OSUClient {
redirectURL.toString(),
omadmAdapter.getMO(OMAConstants.DevInfoURN),
omadmAdapter.getMO(OMAConstants.DevDetailURN),
osuManager.getMOTree(homeSP));
platformAdapter.getMOTree(homeSP));
Log.d(TAG, "Upload MO: " + ulMessage);
@@ -245,7 +262,7 @@ public class OSUClient {
}
if (pddResponse.getExecCommand() == ExecCommand.Browser) {
if (flowType == OSUManager.FlowType.Policy) {
if (flowType == OSUFlowManager.FlowType.Policy) {
throw new IOException("Browser launch requested in policy flow");
}
String webURL = ((BrowserURI) pddResponse.getCommandData()).getURI();
@@ -256,7 +273,7 @@ public class OSUClient {
throw new IOException("Bad or missing session ID in webURL");
}
if (!osuManager.startUserInput(new URL(webURL), network)) {
if (!startUserInput(new URL(webURL), network)) {
throw new IOException("User session failed");
}
@@ -275,7 +292,7 @@ public class OSUClient {
} else if (pddResponse.getExecCommand() == ExecCommand.GetCert) {
certs = new HashMap<>();
try (ESTHandler estHandler = new ESTHandler((GetCertData) pddResponse.
getCommandData(), network, osuManager.getOMADMAdapter(),
getCommandData(), network, getOMADMAdapter(),
km, mKeyStore, homeSP, flowType)) {
estHandler.execute(true);
certs.put(OSUCertType.CA, estHandler.getCACerts());
@@ -347,17 +364,54 @@ public class OSUClient {
// There's a chicken and egg here: If the config is saved before sending update complete
// the network is lost and the remediation flow fails.
try {
osuManager.remediationComplete(homeSP, mods, certs, clientKey);
platformAdapter.remediationComplete(homeSP, mods, certs, clientKey,
flowType == OSUFlowManager.FlowType.Policy);
} catch (IOException | GeneralSecurityException e) {
osuManager.provisioningFailed(homeSP.getFriendlyName(), e.getMessage(), homeSP,
OSUManager.FlowType.Remediation);
platformAdapter.provisioningFailed(homeSP.getFriendlyName(), e.getMessage());
error = OSUError.CommandFailed;
}
}
}
private OMADMAdapter getOMADMAdapter() {
return OMADMAdapter.getInstance(mContext);
}
private URL prepareUserInput(PlatformAdapter platformAdapter, String spName)
throws IOException {
mRedirectListener = new RedirectListener(platformAdapter, spName);
return mRedirectListener.getURL();
}
private boolean startUserInput(URL target, Network network)
throws IOException {
mRedirectListener.startService();
Intent intent = new Intent(mContext, OSUWebView.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(OSUWebView.OSU_NETWORK, network);
intent.putExtra(OSUWebView.OSU_URL, target.toString());
mContext.startActivity(intent);
return mRedirectListener.waitForUser();
}
public void close(boolean abort) {
if (mRedirectListener != null) {
mRedirectListener.abort();
mRedirectListener = null;
}
if (abort) {
try {
mHTTPHandler.close();
} catch (IOException ioe) {
/**/
}
}
}
private HTTPHandler createHandler(Network network, HomeSP homeSP, KeyManager km,
OSUManager.FlowType flowType)
OSUFlowManager.FlowType flowType)
throws GeneralSecurityException, IOException {
Credential credential = homeSP.getCredential();
@@ -367,7 +421,7 @@ public class OSUClient {
String user;
byte[] password;
UpdateInfo subscriptionUpdate;
if (flowType == OSUManager.FlowType.Policy) {
if (flowType == OSUFlowManager.FlowType.Policy) {
subscriptionUpdate = homeSP.getPolicy() != null ?
homeSP.getPolicy().getPolicyUpdate() : null;
} else {
@@ -439,8 +493,8 @@ public class OSUClient {
for (String urlString : urls) {
URL url = new URL(urlString);
HTTPHandler httpHandler = new HTTPHandler(StandardCharsets.UTF_8,
OSUSocketFactory.getSocketFactory(ks, null, OSUManager.FlowType.Provisioning,
network, url, km, false));
OSUSocketFactory.getSocketFactory(ks, null,
OSUFlowManager.FlowType.Provisioning, network, url, km, false));
certs.add((X509Certificate) certFactory.generateCertificate(httpHandler.doGet(url)));
}
@@ -457,7 +511,7 @@ public class OSUClient {
case "?":
for (OMANode node : root.getChildren()) {
if (!node.isLeaf()) {
nodes = Arrays.asList(node);
nodes = Collections.singletonList(node);
break;
}
}
@@ -466,7 +520,7 @@ public class OSUClient {
nodes = root.getChildren();
break;
default:
nodes = Arrays.asList(root.getChild(name));
nodes = Collections.singletonList(root.getChild(name));
break;
}

View File

@@ -0,0 +1,392 @@
package com.android.hotspot2.osu;
import android.content.Context;
import android.content.Intent;
import android.net.Network;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.os.SystemClock;
import android.util.Log;
import com.android.hotspot2.Utils;
import com.android.hotspot2.flow.FlowService;
import com.android.hotspot2.flow.OSUInfo;
import com.android.hotspot2.flow.PlatformAdapter;
import com.android.hotspot2.pps.HomeSP;
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Iterator;
import java.util.LinkedList;
import javax.net.ssl.KeyManager;
public class OSUFlowManager {
private static final boolean MATCH_BSSID = false;
private static final long WAIT_QUANTA = 10000L;
private static final long WAIT_TIMEOUT = 1800000L;
private final Context mContext;
private final LinkedList<OSUFlow> mQueue;
private FlowWorker mWorker;
private OSUFlow mCurrent;
public OSUFlowManager(Context context) {
mContext = context;
mQueue = new LinkedList<>();
}
public enum FlowType {Provisioning, Remediation, Policy}
public static class OSUFlow implements Runnable {
private final OSUClient mOSUClient;
private final PlatformAdapter mPlatformAdapter;
private final HomeSP mHomeSP;
private final String mSpName;
private final FlowType mFlowType;
private final KeyManager mKeyManager;
private final Object mNetworkLock = new Object();
private final Network mNetwork;
private Network mResultNetwork;
private boolean mNetworkCreated;
private int mWifiNetworkId;
private volatile long mLaunchTime;
private volatile boolean mAborted;
/**
* A policy flow.
* @param osuInfo The OSU information for the flow (SSID, BSSID, URL)
* @param platformAdapter the platform adapter
* @param km A key manager for TLS
* @throws MalformedURLException
*/
public OSUFlow(OSUInfo osuInfo, PlatformAdapter platformAdapter, KeyManager km)
throws MalformedURLException {
mWifiNetworkId = -1;
mNetwork = null;
mOSUClient = new OSUClient(osuInfo,
platformAdapter.getKeyStore(), platformAdapter.getContext());
mPlatformAdapter = platformAdapter;
mHomeSP = null;
mSpName = osuInfo.getName(OSUManager.LOCALE);
mFlowType = FlowType.Provisioning;
mKeyManager = km;
}
/**
* A Remediation flow for credential or policy provisioning.
* @param network The network to use, only set for timed provisioning
* @param osuURL The URL to connect to.
* @param platformAdapter the platform adapter
* @param km A key manager for TLS
* @param homeSP The Home SP to which this remediation flow pertains.
* @param flowType Remediation or Policy
* @throws MalformedURLException
*/
public OSUFlow(Network network, String osuURL,
PlatformAdapter platformAdapter, KeyManager km, HomeSP homeSP,
FlowType flowType) throws MalformedURLException {
mNetwork = network;
mWifiNetworkId = network.netId;
mOSUClient = new OSUClient(osuURL,
platformAdapter.getKeyStore(), platformAdapter.getContext());
mPlatformAdapter = platformAdapter;
mHomeSP = homeSP;
mSpName = homeSP.getFriendlyName();
mFlowType = flowType;
mKeyManager = km;
}
private boolean deleteNetwork(OSUFlow next) {
synchronized (mNetworkLock) {
if (!mNetworkCreated) {
return false;
} else if (next.getFlowType() != FlowType.Provisioning) {
return true;
}
OSUInfo thisInfo = mOSUClient.getOSUInfo();
OSUInfo thatInfo = next.mOSUClient.getOSUInfo();
if (thisInfo.getOsuSsid().equals(thatInfo.getOsuSsid())
&& thisInfo.getOSUBssid() == thatInfo.getOSUBssid()) {
// Reuse the OSU network from previous and carry forward the creation fact.
mNetworkCreated = true;
return false;
} else {
return true;
}
}
}
private Network connect() throws IOException {
Network network = networkMatch();
synchronized (mNetworkLock) {
mResultNetwork = network;
if (mResultNetwork != null) {
return mResultNetwork;
}
}
Log.d(OSUManager.TAG, "No network match for " + toString());
int osuNetworkId = -1;
boolean created = false;
if (mFlowType == FlowType.Provisioning) {
osuNetworkId = mPlatformAdapter.connect(mOSUClient.getOSUInfo());
created = true;
}
synchronized (mNetworkLock) {
mNetworkCreated = created;
if (created) {
mWifiNetworkId = osuNetworkId;
}
Log.d(OSUManager.TAG, String.format("%s waiting for %snet ID %d",
toString(), created ? "created " : "existing ", osuNetworkId));
while (mResultNetwork == null && !mAborted) {
try {
mNetworkLock.wait();
} catch (InterruptedException ie) {
throw new IOException("Interrupted");
}
}
if (mAborted) {
throw new IOException("Aborted");
}
Utils.delay(500L);
}
return mResultNetwork;
}
private Network networkMatch() {
if (mFlowType == FlowType.Provisioning) {
OSUInfo match = mOSUClient.getOSUInfo();
WifiConfiguration config = mPlatformAdapter.getActiveWifiConfig();
if (config != null && bssidMatch(match, mPlatformAdapter)
&& Utils.decodeSsid(config.SSID).equals(match.getOsuSsid())) {
synchronized (mNetworkLock) {
mWifiNetworkId = config.networkId;
}
return mPlatformAdapter.getCurrentNetwork();
}
} else {
WifiConfiguration config = mPlatformAdapter.getActiveWifiConfig();
synchronized (mNetworkLock) {
mWifiNetworkId = config != null ? config.networkId : -1;
}
return mNetwork;
}
return null;
}
private void networkChange() {
WifiInfo connectionInfo = mPlatformAdapter.getConnectionInfo();
if (connectionInfo == null) {
return;
}
Network network = mPlatformAdapter.getCurrentNetwork();
Log.d(OSUManager.TAG, "New network " + network
+ ", current OSU " + mOSUClient.getOSUInfo() +
", addr " + Utils.toIpString(connectionInfo.getIpAddress()));
synchronized (mNetworkLock) {
if (mResultNetwork == null && network != null && connectionInfo.getIpAddress() != 0
&& connectionInfo.getNetworkId() == mWifiNetworkId) {
mResultNetwork = network;
mNetworkLock.notifyAll();
}
}
}
public boolean createdNetwork() {
synchronized (mNetworkLock) {
return mNetworkCreated;
}
}
public FlowType getFlowType() {
return mFlowType;
}
public PlatformAdapter getPlatformAdapter() {
return mPlatformAdapter;
}
private void setLaunchTime() {
mLaunchTime = SystemClock.currentThreadTimeMillis();
}
public long getLaunchTime() {
return mLaunchTime;
}
private int getWifiNetworkId() {
synchronized (mNetworkLock) {
return mWifiNetworkId;
}
}
@Override
public void run() {
try {
Network network = connect();
Log.d(OSUManager.TAG, "OSU SSID Associated at " + network);
if (mFlowType == FlowType.Provisioning) {
mOSUClient.provision(mPlatformAdapter, network, mKeyManager);
} else {
mOSUClient.remediate(mPlatformAdapter, network,
mKeyManager, mHomeSP, mFlowType);
}
} catch (Throwable t) {
if (mAborted) {
Log.d(OSUManager.TAG, "OSU flow aborted: " + t, t);
} else {
Log.w(OSUManager.TAG, "OSU flow failed: " + t, t);
mPlatformAdapter.provisioningFailed(mSpName, t.getMessage());
}
} finally {
if (!mAborted) {
mOSUClient.close(false);
}
}
}
public void abort() {
synchronized (mNetworkLock) {
mAborted = true;
mNetworkLock.notifyAll();
}
// Sockets cannot be closed on the main thread...
// TODO: Might want to change this to a handler.
new Thread() {
@Override
public void run() {
try {
mOSUClient.close(true);
} catch (Throwable t) {
Log.d(OSUManager.TAG, "Exception aborting " + toString());
}
}
}.start();
}
@Override
public String toString() {
return mFlowType + " for " + mSpName;
}
}
private class FlowWorker extends Thread {
private final PlatformAdapter mPlatformAdapter;
private FlowWorker(PlatformAdapter platformAdapter) {
mPlatformAdapter = platformAdapter;
}
@Override
public void run() {
for (; ; ) {
synchronized (mQueue) {
if (mCurrent != null && mCurrent.createdNetwork()
&& (mQueue.isEmpty() || mCurrent.deleteNetwork(mQueue.getLast()))) {
mPlatformAdapter.deleteNetwork(mCurrent.getWifiNetworkId());
}
mCurrent = null;
while (mQueue.isEmpty()) {
try {
mQueue.wait(WAIT_QUANTA);
} catch (InterruptedException ie) {
return;
}
if (mQueue.isEmpty()) {
// Bail out on time out
Log.d(OSUManager.TAG, "Flow worker terminating.");
mWorker = null;
mContext.stopService(new Intent(mContext, FlowService.class));
return;
}
}
mCurrent = mQueue.removeLast();
mCurrent.setLaunchTime();
}
Log.d(OSUManager.TAG, "Starting " + mCurrent);
mCurrent.run();
Log.d(OSUManager.TAG, "Exiting " + mCurrent);
}
}
}
/*
* Provisioning: Wait until there is an active WiFi info and the active WiFi config
* matches SSID and optionally BSSID.
* WNM Remediation: Wait until the active WiFi info matches BSSID.
* Timed remediation: The network is given (may be cellular).
*/
public void appendFlow(OSUFlow flow) {
synchronized (mQueue) {
if (mCurrent != null &&
SystemClock.currentThreadTimeMillis()
- mCurrent.getLaunchTime() >= WAIT_TIMEOUT) {
Log.d(OSUManager.TAG, "Aborting stale OSU flow " + mCurrent);
mCurrent.abort();
mCurrent = null;
}
if (flow.getFlowType() == FlowType.Provisioning) {
// Kill any outstanding provisioning flows.
Iterator<OSUFlow> flows = mQueue.iterator();
while (flows.hasNext()) {
if (flows.next().getFlowType() == FlowType.Provisioning) {
flows.remove();
}
}
if (mCurrent != null
&& mCurrent.getFlowType() == FlowType.Provisioning) {
Log.d(OSUManager.TAG, "Aborting current provisioning flow " + mCurrent);
mCurrent.abort();
mCurrent = null;
}
mQueue.addLast(flow);
} else {
mQueue.addFirst(flow);
}
if (mWorker == null) {
// TODO: Might want to change this to a handler.
mWorker = new FlowWorker(flow.getPlatformAdapter());
mWorker.start();
}
mQueue.notifyAll();
}
}
public void networkChange() {
OSUFlow pending;
synchronized (mQueue) {
pending = mCurrent;
}
Log.d(OSUManager.TAG, "Network change, current flow: " + pending);
if (pending != null) {
pending.networkChange();
}
}
private static boolean bssidMatch(OSUInfo osuInfo, PlatformAdapter platformAdapter) {
if (MATCH_BSSID) {
WifiInfo wifiInfo = platformAdapter.getConnectionInfo();
return wifiInfo != null && Utils.parseMac(wifiInfo.getBSSID()) == osuInfo.getOSUBssid();
} else {
return true;
}
}
}

View File

@@ -1,111 +1,55 @@
package com.android.hotspot2.osu;
import android.content.ComponentName;
import android.content.Context;
import android.net.Network;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.android.anqp.Constants;
import com.android.anqp.HSIconFileElement;
import com.android.anqp.OSUProvider;
import com.android.hotspot2.AppBridge;
import com.android.hotspot2.OMADMAdapter;
import com.android.hotspot2.PasspointMatch;
import com.android.hotspot2.Utils;
import com.android.hotspot2.WifiNetworkAdapter;
import com.android.hotspot2.omadm.MOManager;
import com.android.hotspot2.omadm.MOTree;
import com.android.hotspot2.osu.commands.MOData;
import com.android.hotspot2.osu.service.RedirectListener;
import com.android.hotspot2.osu.service.SubscriptionTimer;
import com.android.hotspot2.pps.HomeSP;
import com.android.hotspot2.pps.UpdateInfo;
import org.xml.sax.SAXException;
import com.android.hotspot2.app.OSUData;
import com.android.hotspot2.flow.FlowService;
import com.android.hotspot2.flow.OSUInfo;
import com.android.hotspot2.osu.service.RemediationHandler;
import com.android.hotspot2.flow.IFlowService;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.KeyManager;
public class OSUManager {
public static final String TAG = "OSUMGR";
public static final boolean R2_ENABLED = true;
public static final boolean R2_MOCK = true;
private static final boolean MATCH_BSSID = false;
private static final String KEYSTORE_FILE = "passpoint.ks";
private static final String OSU_COUNT = "osu-count";
private static final String SP_NAME = "sp-name";
private static final String PROV_SUCCESS = "prov-success";
private static final String DEAUTH = "deauth";
private static final String DEAUTH_DELAY = "deauth-delay";
private static final String DEAUTH_URL = "deauth-url";
private static final String PROV_MESSAGE = "prov-message";
private static final long REMEDIATION_TIMEOUT = 120000L;
// How many scan result batches to hang on to
public enum FlowType {Provisioning, Remediation, Policy}
public static final String CERT_WFA_ALIAS = "wfa-root-";
public static final String CERT_REM_ALIAS = "rem-";
public static final String CERT_POLICY_ALIAS = "pol-";
public static final String CERT_SHARED_ALIAS = "shr-";
public static final String CERT_CLT_CERT_ALIAS = "clt-";
public static final String CERT_CLT_KEY_ALIAS = "prv-";
public static final String CERT_CLT_CA_ALIAS = "aaa-";
private static final long THREAD_TIMEOUT = 10; // Seconds
private static final String REMEDIATION_FILE = "remediation.state";
// Preferred icon parameters
public static final Locale LOCALE = java.util.Locale.getDefault();
private final Object mFlowLock = new Object();
private final LinkedBlockingQueue<FlowWorker> mWorkQueue = new LinkedBlockingQueue<>();
private FlowRunner mFlowThread;
private final WifiNetworkAdapter mWifiNetworkAdapter;
private final AppBridge mAppBridge;
private final Context mContext;
private final IconCache mIconCache;
private final SubscriptionTimer mSubscriptionTimer;
private final RemediationHandler mRemediationHandler;
private final Set<String> mOSUSSIDs = new HashSet<>();
private final Map<OSUProvider, OSUInfo> mOSUMap = new HashMap<>();
private final File mKeyStoreFile;
private final KeyStore mKeyStore;
private volatile RedirectListener mRedirectListener;
private final AtomicInteger mOSUSequence = new AtomicInteger();
private volatile Network mActiveNetwork;
private volatile FlowWorker mRemediationFlow;
private volatile OSUInfo mPendingOSU;
private volatile Integer mOSUNwkID;
private final OSUCache mOSUCache;
@@ -113,315 +57,31 @@ public class OSUManager {
mContext = context;
mAppBridge = new AppBridge(context);
mIconCache = new IconCache(this);
mWifiNetworkAdapter = new WifiNetworkAdapter(context, this);
mSubscriptionTimer = new SubscriptionTimer(this, mWifiNetworkAdapter, context);
File appFolder = context.getFilesDir();
mRemediationHandler =
new RemediationHandler(context, new File(appFolder, REMEDIATION_FILE));
mOSUCache = new OSUCache();
mKeyStoreFile = new File(context.getFilesDir(), KEYSTORE_FILE);
Log.d(TAG, "KS file: " + mKeyStoreFile.getPath());
KeyStore ks = null;
try {
//ks = loadKeyStore(KEYSTORE_FILE, readCertsFromDisk(WFA_CA_LOC));
ks = loadKeyStore(mKeyStoreFile, OSUSocketFactory.buildCertSet());
} catch (IOException e) {
Log.e(TAG, "Failed to initialize Passpoint keystore, OSU disabled", e);
}
mKeyStore = ks;
}
private static KeyStore loadKeyStore(File ksFile, Set<X509Certificate> diskCerts)
throws IOException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
if (ksFile.exists()) {
try (FileInputStream in = new FileInputStream(ksFile)) {
keyStore.load(in, null);
}
// Note: comparing two sets of certs does not work.
boolean mismatch = false;
int loadCount = 0;
for (int n = 0; n < 1000; n++) {
String alias = String.format("%s%d", CERT_WFA_ALIAS, n);
Certificate cert = keyStore.getCertificate(alias);
if (cert == null) {
break;
}
loadCount++;
boolean matched = false;
Iterator<X509Certificate> iter = diskCerts.iterator();
while (iter.hasNext()) {
X509Certificate diskCert = iter.next();
if (cert.equals(diskCert)) {
iter.remove();
matched = true;
break;
}
}
if (!matched) {
mismatch = true;
break;
}
}
if (mismatch || !diskCerts.isEmpty()) {
Log.d(TAG, "Re-seeding Passpoint key store with " +
diskCerts.size() + " WFA certs");
for (int n = 0; n < 1000; n++) {
String alias = String.format("%s%d", CERT_WFA_ALIAS, n);
Certificate cert = keyStore.getCertificate(alias);
if (cert == null) {
break;
} else {
keyStore.deleteEntry(alias);
}
}
int index = 0;
for (X509Certificate caCert : diskCerts) {
keyStore.setCertificateEntry(
String.format("%s%d", CERT_WFA_ALIAS, index), caCert);
index++;
}
try (FileOutputStream out = new FileOutputStream(ksFile)) {
keyStore.store(out, null);
}
} else {
Log.d(TAG, "Loaded Passpoint key store with " + loadCount + " CA certs");
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
Log.d("ZXC", "KS Alias '" + aliases.nextElement() + "'");
}
}
} else {
keyStore.load(null, null);
int index = 0;
for (X509Certificate caCert : diskCerts) {
keyStore.setCertificateEntry(
String.format("%s%d", CERT_WFA_ALIAS, index), caCert);
index++;
}
try (FileOutputStream out = new FileOutputStream(ksFile)) {
keyStore.store(out, null);
}
Log.d(TAG, "Initialized Passpoint key store with " +
diskCerts.size() + " CA certs");
}
return keyStore;
} catch (GeneralSecurityException gse) {
throw new IOException(gse);
}
public Context getContext() {
return mContext;
}
public KeyStore getKeyStore() {
return mKeyStore;
}
private static class FlowWorker implements Runnable {
private final OSUClient mOSUClient;
private final OSUManager mOSUManager;
private final HomeSP mHomeSP;
private final String mSpName;
private final FlowType mFlowType;
private final KeyManager mKeyManager;
private final long mLaunchTime;
private final Network mNetwork;
private FlowWorker(Network network, OSUInfo osuInfo, OSUManager osuManager, KeyManager km)
throws MalformedURLException {
mNetwork = network;
mOSUClient = new OSUClient(osuInfo, osuManager.getKeyStore());
mOSUManager = osuManager;
mHomeSP = null;
mSpName = osuInfo.getName(LOCALE);
mFlowType = FlowType.Provisioning;
mKeyManager = km;
mLaunchTime = System.currentTimeMillis();
}
private FlowWorker(Network network, String osuURL, OSUManager osuManager, KeyManager km,
HomeSP homeSP, FlowType flowType) throws MalformedURLException {
mNetwork = network;
mOSUClient = new OSUClient(osuURL, osuManager.getKeyStore());
mOSUManager = osuManager;
mHomeSP = homeSP;
mSpName = homeSP.getFriendlyName();
mFlowType = flowType;
mKeyManager = km;
mLaunchTime = System.currentTimeMillis();
}
public long getLaunchTime() {
return mLaunchTime;
}
private Network getNetwork() {
return mNetwork;
}
@Override
public void run() {
Log.d(TAG, "OSU SSID Associated at " + mNetwork);
try {
if (mFlowType == FlowType.Provisioning) {
mOSUClient.provision(mOSUManager, mNetwork, mKeyManager);
} else {
mOSUClient.remediate(mOSUManager, mNetwork, mKeyManager, mHomeSP, mFlowType);
}
} catch (Throwable t) {
Log.w(TAG, "OSU flow failed: " + t, t);
mOSUManager.provisioningFailed(mSpName, t.getMessage(), mHomeSP, mFlowType);
}
}
@Override
public String toString() {
return mFlowType + " for " + mSpName;
}
}
private static class FlowRunner extends Thread {
private final LinkedBlockingQueue<FlowWorker> mWorkQueue;
private final OSUManager mOSUManager;
private FlowRunner(LinkedBlockingQueue<FlowWorker> workQueue, OSUManager osuManager) {
mWorkQueue = workQueue;
mOSUManager = osuManager;
setDaemon(true);
setName("OSU Client Thread");
}
@Override
public void run() {
for (;;) {
FlowWorker flowWorker;
try {
flowWorker = mWorkQueue.poll(THREAD_TIMEOUT, TimeUnit.SECONDS);
} catch (InterruptedException ie) {
flowWorker = null;
}
if (flowWorker == null) {
if (mOSUManager.serviceThreadExit()) {
return;
} else {
continue;
}
}
Log.d(TAG, "Starting " + flowWorker);
flowWorker.run();
}
}
}
private void startOsuFlow(FlowWorker flowWorker) {
synchronized (mFlowLock) {
mWorkQueue.offer(flowWorker);
if (mFlowThread == null) {
mFlowThread = new FlowRunner(mWorkQueue, this);
mFlowThread.start();
}
}
}
private boolean serviceThreadExit() {
synchronized (mFlowLock) {
if (mWorkQueue.isEmpty()) {
mFlowThread = null;
return true;
} else {
return false;
}
}
}
/*
public void startOSU() {
registerUserInputListener(new UserInputListener() {
@Override
public void requestUserInput(URL target, Network network, URL endRedirect) {
Log.d(TAG, "Browser to " + target + ", land at " + endRedirect);
final Intent intent = new Intent(
ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN);
intent.putExtra(ConnectivityManager.EXTRA_NETWORK, network);
intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL,
new CaptivePortal(new ICaptivePortal.Stub() {
@Override
public void appResponse(int response) {
}
}));
//intent.setData(Uri.parse(target.toString())); !!! Doesn't work!
intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, target.toString());
intent.setFlags(
Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(intent, UserHandle.CURRENT);
}
@Override
public String operationStatus(String spIdentity, OSUOperationStatus status,
String message) {
Log.d(TAG, "OSU OP Status: " + status + ", message " + message);
Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION);
intent.putExtra(SP_NAME, spIdentity);
intent.putExtra(PROV_SUCCESS, status == OSUOperationStatus.ProvisioningSuccess);
if (message != null) {
intent.putExtra(PROV_MESSAGE, message);
}
intent.setFlags(
Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(intent, UserHandle.CURRENT);
return null;
}
@Override
public void deAuthNotification(String spIdentity, boolean ess, int delay, URL url) {
Log.i(TAG, "De-authentication imminent for " + (ess ? "ess" : "bss") +
", redirect to " + url);
Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION);
intent.putExtra(SP_NAME, spIdentity);
intent.putExtra(DEAUTH, ess);
intent.putExtra(DEAUTH_DELAY, delay);
intent.putExtra(DEAUTH_URL, url.toString());
intent.setFlags(
Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(intent, UserHandle.CURRENT);
}
});
addOSUListener(new OSUListener() {
@Override
public void osuNotification(int count) {
Intent intent = new Intent(Intent.ACTION_OSU_NOTIFICATION);
intent.putExtra(OSU_COUNT, count);
intent.setFlags(
Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivityAsUser(intent, UserHandle.CURRENT);
}
});
mWifiNetworkAdapter.initialize();
mSubscriptionTimer.checkUpdates();
}
*/
public List<OSUInfo> getAvailableOSUs() {
public List<OSUData> getAvailableOSUs() {
synchronized (mOSUMap) {
List<OSUInfo> completeOSUs = new ArrayList<>();
List<OSUData> completeOSUs = new ArrayList<>();
for (OSUInfo osuInfo : mOSUMap.values()) {
if (osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
completeOSUs.add(osuInfo);
completeOSUs.add(new OSUData(osuInfo));
}
}
return completeOSUs;
}
}
public void recheckTimers() {
mSubscriptionTimer.checkUpdates();
}
public void setOSUSelection(int osuID) {
OSUInfo selection = null;
for (OSUInfo osuInfo : mOSUMap.values()) {
Log.d("ZXZ", "In select: " + osuInfo + ", id " + osuInfo.getOsuID());
if (osuInfo.getOsuID() == osuID &&
osuInfo.getIconStatus() == OSUInfo.IconStatus.Available) {
selection = osuInfo;
@@ -429,137 +89,84 @@ public class OSUManager {
}
}
Log.d(TAG, "Selected OSU ID " + osuID + ", matches " + selection);
Log.d(TAG, "Selected OSU ID " + osuID + ": " + selection);
if (selection == null) {
mPendingOSU = null;
return;
}
mPendingOSU = selection;
WifiConfiguration config = mWifiNetworkAdapter.getActiveWifiConfig();
final OSUInfo osu = selection;
if (config != null &&
bssidMatch(selection) &&
Utils.unquote(config.SSID).equals(selection.getOsuSsid())) {
try {
// Go straight to provisioning if the network is already selected.
// Also note that mOSUNwkID is left unset to leave the network around after
// flow completion since it was not added by the OSU flow.
initiateProvisioning(mPendingOSU, mWifiNetworkAdapter.getCurrentNetwork());
} catch (IOException ioe) {
notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
mPendingOSU.getName(LOCALE));
} finally {
mPendingOSU = null;
mContext.bindService(new Intent(mContext, FlowService.class), new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
try {
IFlowService fs = IFlowService.Stub.asInterface(service);
fs.provision(osu);
} catch (RemoteException re) {
Log.e(OSUManager.TAG, "Caught re: " + re);
}
}
} else {
try {
mOSUNwkID = mWifiNetworkAdapter.connect(selection, mPendingOSU.getName(LOCALE));
} catch (IOException ioe) {
notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
selection.getName(LOCALE));
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(OSUManager.TAG, "Service disconnect: " + name);
}
}
}, Context.BIND_AUTO_CREATE);
}
public void networkDeleted(WifiConfiguration configuration) {
Log.d("ZXZ", "Network deleted: " + configuration.FQDN);
HomeSP homeSP = mWifiNetworkAdapter.getHomeSP(configuration);
if (homeSP != null) {
spDeleted(homeSP.getFQDN());
public void networkDeleted(final WifiConfiguration configuration) {
if (configuration.FQDN == null) {
return;
}
mRemediationHandler.networkConfigChange();
mContext.bindService(new Intent(mContext, FlowService.class), new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
try {
IFlowService fs = IFlowService.Stub.asInterface(service);
fs.spDeleted(configuration.FQDN);
} catch (RemoteException re) {
Log.e(OSUManager.TAG, "Caught re: " + re);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}, Context.BIND_AUTO_CREATE);
}
public void networkChanged(WifiConfiguration configuration) {
mWifiNetworkAdapter.networkConfigChange(configuration);
public void networkConnectChange(WifiInfo newNetwork) {
mRemediationHandler.newConnection(newNetwork);
}
public void networkConnectEvent(WifiInfo wifiInfo) {
if (wifiInfo != null) {
setActiveNetwork(mWifiNetworkAdapter.getActiveWifiConfig(),
mWifiNetworkAdapter.getCurrentNetwork());
}
public void networkConfigChanged() {
mRemediationHandler.networkConfigChange();
}
public void wifiStateChange(boolean on) {
if (!on) {
int current = mOSUMap.size();
mOSUMap.clear();
mOSUCache.clearAll();
mIconCache.tick(true);
if (current > 0) {
notifyOSUCount();
}
}
}
private boolean bssidMatch(OSUInfo osuInfo) {
if (MATCH_BSSID) {
WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo();
return wifiInfo != null && Utils.parseMac(wifiInfo.getBSSID()) == osuInfo.getBSSID();
} else {
return true;
}
}
public void setActiveNetwork(WifiConfiguration wifiConfiguration, Network network) {
Log.d(TAG, "Network change: " + network + ", cfg " +
(wifiConfiguration != null ? wifiConfiguration.SSID : "-") + ", osu " + mPendingOSU);
mActiveNetwork = network;
if (mPendingOSU != null &&
wifiConfiguration != null &&
network != null &&
bssidMatch(mPendingOSU) &&
Utils.unquote(wifiConfiguration.SSID).equals(mPendingOSU.getOsuSsid())) {
try {
Log.d(TAG, "New network " + network + ", current OSU " + mPendingOSU);
initiateProvisioning(mPendingOSU, network);
} catch (IOException ioe) {
notifyUser(OSUOperationStatus.ProvisioningFailure, ioe.getMessage(),
mPendingOSU.getName(LOCALE));
} finally {
mPendingOSU = null;
}
if (on) {
return;
}
if (mRemediationFlow != null && network != null &&
mRemediationFlow.getNetwork().netId == network.netId) {
startOsuFlow(mRemediationFlow);
mRemediationFlow = null;
// Notify the remediation handler that there are no WiFi networks available.
// Do NOT turn it off though as remediation, per at least this implementation, can take
// place over cellular. The subject of remediation over cellular (when restriction is
// "unrestricted") is not addresses by the WFA spec and direct ask to authors gives no
// distinct answer one way or the other.
mRemediationHandler.newConnection(null);
int current = mOSUMap.size();
mOSUMap.clear();
mOSUCache.clearAll();
mIconCache.tick(true);
if (current > 0) {
notifyOSUCount();
}
}
/**
* Called when an OSU has been selected and the associated network is fully connected.
*
* @param osuInfo The selected OSUInfo or null if the current OSU flow is cancelled externally,
* e.g. WiFi is turned off or the OSU network is otherwise detected as
* unreachable.
* @param network The currently associated network (for the OSU SSID).
* @throws IOException
* @throws GeneralSecurityException
*/
private void initiateProvisioning(OSUInfo osuInfo, Network network)
throws IOException {
startOsuFlow(new FlowWorker(network, osuInfo, this, getKeyManager(null, mKeyStore)));
}
/**
* @param homeSP The Home SP associated with the keying material in question. Passing
* null returns a "system wide" KeyManager to support pre-provisioned certs based
* on names retrieved from the ClientCertInfo request.
* @return A key manager suitable for the given configuration (or pre-provisioned keys).
*/
private static KeyManager getKeyManager(HomeSP homeSP, KeyStore keyStore)
throws IOException {
return homeSP != null ? new ClientKeyManager(homeSP, keyStore) :
new WiFiKeyManager(keyStore);
}
public boolean isOSU(String ssid) {
synchronized (mOSUMap) {
return mOSUSSIDs.contains(ssid);
@@ -581,17 +188,17 @@ public class OSUManager {
long bssid = Utils.parseMac(entry.getValue().BSSID);
if (existing == null) {
osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey().getSSID(),
entry.getKey(), mOSUSequence.getAndIncrement()));
osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey(),
mOSUSequence.getAndIncrement()));
} else if (existing.getBSSID() != bssid) {
HSIconFileElement icon = mIconCache.getIcon(existing);
if (icon != null && icon.equals(existing.getIconFileElement())) {
OSUInfo osuInfo = new OSUInfo(entry.getValue(), entry.getKey().getSSID(),
entry.getKey(), existing.getOsuID());
OSUInfo osuInfo = new OSUInfo(entry.getValue(), entry.getKey(),
existing.getOsuID());
osuInfo.setIconFileElement(icon, existing.getIconFileName());
osus.put(entry.getKey(), osuInfo);
} else {
osus.put(entry.getKey(), new OSUInfo(entry.getValue(), entry.getKey().getSSID(),
osus.put(entry.getKey(), new OSUInfo(entry.getValue(),
entry.getKey(), mOSUSequence.getAndIncrement()));
}
} else {
@@ -622,7 +229,8 @@ public class OSUManager {
}
public void doIconQuery(long bssid, String fileName) {
mWifiNetworkAdapter.doIconQuery(bssid, fileName);
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
wifiManager.queryPasspointIcon(bssid, fileName);
}
private void notifyOSUCount() {
@@ -633,292 +241,24 @@ public class OSUManager {
}
}
Log.d(TAG, "Latest OSU info: " + count + " with icons, map " + mOSUMap);
mAppBridge.showOsuCount(count, getAvailableOSUs());
mAppBridge.showOsuCount(count);
}
public void deauth(long bssid, boolean ess, int delay, String url) throws MalformedURLException {
public void deauth(long bssid, boolean ess, int delay, String url)
throws MalformedURLException {
Log.d(TAG, String.format("De-auth imminent on %s, delay %ss to '%s'",
ess ? "ess" : "bss",
delay,
url));
mWifiNetworkAdapter.setHoldoffTime(delay * Constants.MILLIS_IN_A_SEC, ess);
HomeSP homeSP = mWifiNetworkAdapter.getCurrentSP();
String spName = homeSP != null ? homeSP.getFriendlyName() : "unknown";
ess ? "ess" : "bss", delay, url));
// TODO: Missing framework functionality:
// mWifiNetworkAdapter.setHoldoffTime(delay * Constants.MILLIS_IN_A_SEC, ess);
String spName = mRemediationHandler.getCurrentSpName();
mAppBridge.showDeauth(spName, ess, delay, url);
}
// !!! Consistently check passpoint match.
public void wnmRemediate(long bssid, String url, PasspointMatch match)
throws IOException, SAXException {
HomeSP homeSP = mWifiNetworkAdapter.getCurrentSP();
if (homeSP == null) {
throw new IOException("Remediation request on unidentified Passpoint network ");
}
Network network = mWifiNetworkAdapter.getCurrentNetwork();
if (network == null) {
throw new IOException("Failed to determine current network");
}
WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo();
if (wifiInfo == null || Utils.parseMac(wifiInfo.getBSSID()) != bssid) {
throw new IOException("Mismatching BSSID");
}
Log.d(TAG, "WNM Remediation on " + network.netId + " FQDN " + homeSP.getFQDN());
FlowWorker flowWorker = new FlowWorker(network, url, this,
getKeyManager(homeSP, mKeyStore), homeSP, FlowType.Remediation);
if (mActiveNetwork != null && wifiInfo.getNetworkId() == mActiveNetwork.netId) {
startOsuFlow(flowWorker);
} else {
mRemediationFlow = flowWorker;
}
public void wnmRemediate(final long bssid, final String url, PasspointMatch match) {
mRemediationHandler.wnmReceived(bssid, url);
}
public void remediate(HomeSP homeSP, boolean policy) throws IOException, SAXException {
UpdateInfo updateInfo;
if (policy) {
if (homeSP.getPolicy() == null) {
throw new IOException("No policy object");
}
updateInfo = homeSP.getPolicy().getPolicyUpdate();
} else {
updateInfo = homeSP.getSubscriptionUpdate();
}
switch (updateInfo.getUpdateRestriction()) {
case HomeSP: {
Network network = mWifiNetworkAdapter.getCurrentNetwork();
if (network == null) {
throw new IOException("Failed to determine current network");
}
HomeSP activeSP = mWifiNetworkAdapter.getCurrentSP();
if (activeSP == null || !activeSP.getFQDN().equals(homeSP.getFQDN())) {
throw new IOException("Remediation restricted to HomeSP");
}
doRemediate(updateInfo.getURI(), network, homeSP, policy);
break;
}
case RoamingPartner: {
Network network = mWifiNetworkAdapter.getCurrentNetwork();
if (network == null) {
throw new IOException("Failed to determine current network");
}
WifiInfo wifiInfo = mWifiNetworkAdapter.getConnectionInfo();
if (wifiInfo == null) {
throw new IOException("Unable to determine WiFi info");
}
PasspointMatch match = mWifiNetworkAdapter.
matchProviderWithCurrentNetwork(homeSP.getFQDN());
if (match == PasspointMatch.HomeProvider ||
match == PasspointMatch.RoamingProvider) {
doRemediate(updateInfo.getURI(), network, homeSP, policy);
} else {
throw new IOException("No roaming network match: " + match);
}
break;
}
case Unrestricted: {
Network network = mWifiNetworkAdapter.getCurrentNetwork();
doRemediate(updateInfo.getURI(), network, homeSP, policy);
break;
}
}
}
private void doRemediate(String url, Network network, HomeSP homeSP, boolean policy)
throws IOException {
startOsuFlow(new FlowWorker(network, url, this,
getKeyManager(homeSP, mKeyStore),
homeSP, policy ? FlowType.Policy : FlowType.Remediation));
}
public MOTree getMOTree(HomeSP homeSP) throws IOException {
return mWifiNetworkAdapter.getMOTree(homeSP);
}
protected URL prepareUserInput(String spName) throws IOException {
mRedirectListener = new RedirectListener(this, spName);
return mRedirectListener.getURL();
}
protected boolean startUserInput(URL target, Network network) throws IOException {
mRedirectListener.startService();
mWifiNetworkAdapter.launchBrowser(target, network, mRedirectListener.getURL());
return mRedirectListener.waitForUser();
}
public String notifyUser(OSUOperationStatus status, String message, String spName) {
if (status == OSUOperationStatus.UserInputComplete) {
return null;
}
if (mOSUNwkID != null) {
// Delete the OSU network if it was added by the OSU flow
mWifiNetworkAdapter.deleteNetwork(mOSUNwkID);
mOSUNwkID = null;
}
mAppBridge.showStatus(status, spName, message, null);
return null;
}
public void provisioningFailed(String spName, String message,
HomeSP homeSP, FlowType flowType) {
if (mRedirectListener != null) {
mRedirectListener.abort();
mRedirectListener = null;
}
notifyUser(OSUOperationStatus.ProvisioningFailure, message, spName);
}
public void provisioningComplete(OSUInfo osuInfo,
MOData moData, Map<OSUCertType, List<X509Certificate>> certs,
PrivateKey privateKey, Network osuNetwork) {
try {
String xml = moData.getMOTree().toXml();
HomeSP homeSP = MOManager.buildSP(xml);
Integer spNwk = mWifiNetworkAdapter.addNetwork(homeSP, certs, privateKey, osuNetwork);
if (spNwk == null) {
notifyUser(OSUOperationStatus.ProvisioningFailure,
"Failed to save network configuration", osuInfo.getName(LOCALE));
} else {
if (mWifiNetworkAdapter.addSP(xml) < 0) {
mWifiNetworkAdapter.deleteNetwork(spNwk);
Log.e(TAG, "Failed to provision: " + homeSP.getFQDN());
notifyUser(OSUOperationStatus.ProvisioningFailure, "Failed to add MO",
osuInfo.getName(LOCALE));
return;
}
Set<X509Certificate> rootCerts = OSUSocketFactory.getRootCerts(mKeyStore);
X509Certificate remCert = getCert(certs, OSUCertType.Remediation);
X509Certificate polCert = getCert(certs, OSUCertType.Policy);
int newCerts = 0;
if (privateKey != null) {
X509Certificate cltCert = getCert(certs, OSUCertType.Client);
mKeyStore.setKeyEntry(CERT_CLT_KEY_ALIAS + homeSP.getFQDN(),
privateKey, null, new X509Certificate[]{cltCert});
mKeyStore.setCertificateEntry(CERT_CLT_CERT_ALIAS + homeSP.getFQDN(), cltCert);
newCerts++;
}
boolean usingShared = false;
if (remCert != null) {
if (!rootCerts.contains(remCert)) {
if (remCert.equals(polCert)) {
mKeyStore.setCertificateEntry(CERT_SHARED_ALIAS + homeSP.getFQDN(),
remCert);
usingShared = true;
newCerts++;
} else {
mKeyStore.setCertificateEntry(CERT_REM_ALIAS + homeSP.getFQDN(),
remCert);
newCerts++;
}
}
}
if (!usingShared && polCert != null) {
if (!rootCerts.contains(polCert)) {
mKeyStore.setCertificateEntry(CERT_POLICY_ALIAS + homeSP.getFQDN(),
remCert);
newCerts++;
}
}
Log.d("ZXZ", "Got " + newCerts + " new certs.");
if (newCerts > 0) {
try (FileOutputStream out = new FileOutputStream(mKeyStoreFile)) {
mKeyStore.store(out, null);
}
}
notifyUser(OSUOperationStatus.ProvisioningSuccess, null, osuInfo.getName(LOCALE));
Log.d(TAG, "Provisioning complete.");
}
} catch (IOException | GeneralSecurityException | SAXException e) {
Log.e(TAG, "Failed to provision: " + e, e);
notifyUser(OSUOperationStatus.ProvisioningFailure, e.toString(),
osuInfo.getName(LOCALE));
}
}
private static X509Certificate getCert(Map<OSUCertType, List<X509Certificate>> certMap,
OSUCertType certType) {
List<X509Certificate> certs = certMap.get(certType);
if (certs == null || certs.isEmpty()) {
return null;
}
return certs.iterator().next();
}
public void spDeleted(String fqdn) {
int count = deleteCerts(mKeyStore, fqdn,
CERT_REM_ALIAS, CERT_POLICY_ALIAS, CERT_SHARED_ALIAS, CERT_CLT_CERT_ALIAS);
Log.d(TAG, "Passpoint network deleted, removing " + count + " key store entries");
try {
if (mKeyStore.getKey(CERT_CLT_KEY_ALIAS + fqdn, null) != null) {
mKeyStore.deleteEntry(CERT_CLT_KEY_ALIAS + fqdn);
}
} catch (GeneralSecurityException e) {
/**/
}
if (count > 0) {
try (FileOutputStream out = new FileOutputStream(mKeyStoreFile)) {
mKeyStore.store(out, null);
} catch (IOException | GeneralSecurityException e) {
Log.w(TAG, "Failed to remove certs from key store: " + e);
}
}
}
private static int deleteCerts(KeyStore keyStore, String fqdn, String... prefixes) {
int count = 0;
for (String prefix : prefixes) {
try {
String alias = prefix + fqdn;
Certificate cert = keyStore.getCertificate(alias);
if (cert != null) {
keyStore.deleteEntry(alias);
count++;
}
} catch (KeyStoreException kse) {
/**/
}
}
return count;
}
public void remediationComplete(HomeSP homeSP, Collection<MOData> mods,
Map<OSUCertType, List<X509Certificate>> certs,
PrivateKey privateKey)
throws IOException, GeneralSecurityException {
HomeSP altSP = null;
if (mWifiNetworkAdapter.modifySP(homeSP, mods) > 0) {
altSP = MOManager.modifySP(homeSP, mWifiNetworkAdapter.getMOTree(homeSP), mods);
}
X509Certificate caCert = null;
List<X509Certificate> clientCerts = null;
if (certs != null) {
List<X509Certificate> certList = certs.get(OSUCertType.AAA);
caCert = certList != null && !certList.isEmpty() ? certList.iterator().next() : null;
clientCerts = certs.get(OSUCertType.Client);
}
if (altSP != null || certs != null) {
if (altSP == null) {
altSP = homeSP;
}
mWifiNetworkAdapter.updateNetwork(altSP, caCert, clientCerts, privateKey);
}
notifyUser(OSUOperationStatus.ProvisioningSuccess, null, homeSP.getFriendlyName());
}
protected OMADMAdapter getOMADMAdapter() {
return OMADMAdapter.getInstance(mContext);
public void remediationDone(String fqdn, boolean policy) {
mRemediationHandler.remediationDone(fqdn, policy);
}
}

View File

@@ -5,6 +5,7 @@ import android.util.Base64;
import android.util.Log;
import com.android.hotspot2.Utils;
import com.android.hotspot2.flow.PlatformAdapter;
import com.android.hotspot2.pps.HomeSP;
import java.io.ByteArrayInputStream;
@@ -76,7 +77,7 @@ public class OSUSocketFactory {
}
public static OSUSocketFactory getSocketFactory(KeyStore ks, HomeSP homeSP,
OSUManager.FlowType flowType,
OSUFlowManager.FlowType flowType,
Network network, URL url, KeyManager km,
boolean enforceSecurity)
throws GeneralSecurityException, IOException {
@@ -87,7 +88,7 @@ public class OSUSocketFactory {
return new OSUSocketFactory(ks, homeSP, flowType, network, url, km);
}
private OSUSocketFactory(KeyStore ks, HomeSP homeSP, OSUManager.FlowType flowType,
private OSUSocketFactory(KeyStore ks, HomeSP homeSP, OSUFlowManager.FlowType flowType,
Network network,
URL url, KeyManager km) throws GeneralSecurityException, IOException {
mNetwork = network;
@@ -217,10 +218,10 @@ public class OSUSocketFactory {
private static class WFATrustManager implements X509TrustManager {
private final KeyStore mKeyStore;
private final HomeSP mHomeSP;
private final OSUManager.FlowType mFlowType;
private final OSUFlowManager.FlowType mFlowType;
private X509Certificate[] mTrustChain;
private WFATrustManager(KeyStore ks, HomeSP homeSP, OSUManager.FlowType flowType)
private WFATrustManager(KeyStore ks, HomeSP homeSP, OSUFlowManager.FlowType flowType)
throws CertificateException {
mKeyStore = ks;
mHomeSP = homeSP;
@@ -250,12 +251,13 @@ public class OSUSocketFactory {
trustAnchors.add(new TrustAnchor(cert, null));
}
} else {
String prefix = mFlowType == OSUManager.FlowType.Remediation ?
OSUManager.CERT_REM_ALIAS : OSUManager.CERT_POLICY_ALIAS;
String prefix = mFlowType == OSUFlowManager.FlowType.Remediation ?
PlatformAdapter.CERT_REM_ALIAS : PlatformAdapter.CERT_POLICY_ALIAS;
X509Certificate cert = getCert(mKeyStore, prefix + mHomeSP.getFQDN());
if (cert == null) {
cert = getCert(mKeyStore, OSUManager.CERT_SHARED_ALIAS + mHomeSP.getFQDN());
cert = getCert(mKeyStore,
PlatformAdapter.CERT_SHARED_ALIAS + mHomeSP.getFQDN());
}
if (cert == null) {
for (X509Certificate root : getRootCerts(mKeyStore)) {
@@ -300,7 +302,7 @@ public class OSUSocketFactory {
int index = 0;
for (int n = 0; n < 1000; n++) {
Certificate cert = keyStore.getCertificate(
String.format("%s%d", OSUManager.CERT_WFA_ALIAS, index));
String.format("%s%d", PlatformAdapter.CERT_WFA_ALIAS, index));
if (cert == null) {
break;
} else if (cert instanceof X509Certificate) {

View File

@@ -0,0 +1,91 @@
package com.android.hotspot2.osu;
import android.annotation.Nullable;
import android.app.Activity;
import android.graphics.Bitmap;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.http.SslError;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.webkit.SslErrorHandler;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.android.hotspot2.R;
public class OSUWebView extends Activity {
public static final String OSU_URL = "com.android.hotspot2.osu.URL";
public static final String OSU_NETWORK = "com.android.hotspot2.osu.NETWORK";
private String mUrl;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(OSUManager.TAG, "Opening OSU Web View");
ConnectivityManager connectivityManager = ConnectivityManager.from(this);
mUrl = getIntent().getStringExtra(OSU_URL);
Network network = getIntent().getParcelableExtra(OSU_NETWORK);
connectivityManager.bindProcessToNetwork(network);
getActionBar().setDisplayShowHomeEnabled(false);
setContentView(R.layout.osu_web_view);
getActionBar().setDisplayShowHomeEnabled(false);
final WebView myWebView = (WebView) findViewById(R.id.webview);
myWebView.clearCache(true);
WebSettings webSettings = myWebView.getSettings();
webSettings.setJavaScriptEnabled(true);
MyWebViewClient mWebViewClient = new MyWebViewClient();
myWebView.setWebViewClient(mWebViewClient);
Log.d(OSUManager.TAG, "OSU Web View to " + mUrl);
myWebView.loadUrl(mUrl);
Log.d(OSUManager.TAG, "OSU Web View loading");
//myWebView.setWebChromeClient(new MyWebChromeClient());
// Start initial page load so WebView finishes loading proxy settings.
// Actual load of mUrl is initiated by MyWebViewClient.
//myWebView.loadData("", "text/html", null);
}
private class MyWebViewClient extends WebViewClient {
private static final String INTERNAL_ASSETS = "file:///android_asset/";
// How many Android device-independent-pixels per scaled-pixel
// dp/sp = (px/sp) / (px/dp) = (1/sp) / (1/dp)
private final float mDpPerSp = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 1,
getResources().getDisplayMetrics()) /
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1,
getResources().getDisplayMetrics());
private int mPagesLoaded;
// If we haven't finished cleaning up the history, don't allow going back.
public boolean allowBack() {
return mPagesLoaded > 1;
}
// Convert Android device-independent-pixels (dp) to HTML size.
private String dp(int dp) {
// HTML px's are scaled just like dp's, so just add "px" suffix.
return Integer.toString(dp) + "px";
}
// Convert Android scaled-pixels (sp) to HTML size.
private String sp(int sp) {
// Convert sp to dp's.
float dp = sp * mDpPerSp;
// Apply a scale factor to make things look right.
dp *= 1.3;
// Convert dp's to HTML size.
return dp((int)dp);
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
Log.d(OSUManager.TAG, "TLS error in Web View: " + error);
}
}
}

View File

@@ -15,6 +15,7 @@ import com.android.hotspot2.asn1.Asn1Octets;
import com.android.hotspot2.asn1.Asn1Oid;
import com.android.hotspot2.asn1.Asn1String;
import com.android.hotspot2.asn1.OidMappings;
import com.android.hotspot2.flow.OSUInfo;
import java.io.IOException;
import java.nio.ByteBuffer;

View File

@@ -2,7 +2,7 @@ package com.android.hotspot2.osu.service;
import android.util.Log;
import com.android.hotspot2.osu.OSUManager;
import com.android.hotspot2.flow.PlatformAdapter;
import com.android.hotspot2.osu.OSUOperationStatus;
import java.io.BufferedReader;
@@ -36,7 +36,7 @@ public class RedirectListener extends Thread {
"</body>" +
"</html>\r\n";
private final OSUManager mOSUManager;
private final PlatformAdapter mPlatformAdapter;
private final String mSpName;
private final ServerSocket mServerSocket;
private final String mPath;
@@ -47,8 +47,8 @@ public class RedirectListener extends Thread {
private OSUOperationStatus mUserStatus;
private volatile boolean mAborted;
public RedirectListener(OSUManager osuManager, String spName) throws IOException {
mOSUManager = osuManager;
public RedirectListener(PlatformAdapter platformAdapter, String spName) throws IOException {
mPlatformAdapter = platformAdapter;
mSpName = spName;
mServerSocket = new ServerSocket(0, 5, InetAddress.getLocalHost());
Random rnd = new Random(System.currentTimeMillis());
@@ -109,6 +109,10 @@ public class RedirectListener extends Thread {
public void abort() {
try {
synchronized (mLock) {
mUserStatus = OSUOperationStatus.UserInputAborted;
mLock.notifyAll();
}
mAborted = true;
mServerSocket.close();
} catch (IOException ioe) {
@@ -197,6 +201,6 @@ public class RedirectListener extends Thread {
String message = (status == OSUOperationStatus.UserInputAborted) ?
"Browser closed" : null;
return mOSUManager.notifyUser(status, message, mSpName);
return mPlatformAdapter.notifyUser(status, message, mSpName);
}
}

View File

@@ -0,0 +1,581 @@
package com.android.hotspot2.osu.service;
import android.app.AlarmManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.net.Network;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.android.hotspot2.PasspointMatch;
import com.android.hotspot2.Utils;
import com.android.hotspot2.flow.FlowService;
import com.android.hotspot2.omadm.MOManager;
import com.android.hotspot2.omadm.MOTree;
import com.android.hotspot2.omadm.OMAConstants;
import com.android.hotspot2.omadm.OMAException;
import com.android.hotspot2.omadm.OMAParser;
import com.android.hotspot2.osu.OSUManager;
import com.android.hotspot2.pps.HomeSP;
import com.android.hotspot2.pps.UpdateInfo;
import com.android.hotspot2.flow.IFlowService;
import org.xml.sax.SAXException;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import static com.android.hotspot2.pps.UpdateInfo.UpdateRestriction;
public class RemediationHandler implements AlarmManager.OnAlarmListener {
private final Context mContext;
private final File mStateFile;
private final Map<String, PasspointConfig> mPasspointConfigs = new HashMap<>();
private final Map<String, List<RemediationEvent>> mUpdates = new HashMap<>();
private final LinkedList<PendingUpdate> mOutstanding = new LinkedList<>();
private WifiInfo mActiveWifiInfo;
private PasspointConfig mActivePasspointConfig;
public RemediationHandler(Context context, File stateFile) {
mContext = context;
mStateFile = stateFile;
Log.d(OSUManager.TAG, "State file: " + stateFile);
reloadAll(context, mPasspointConfigs, stateFile, mUpdates);
mActivePasspointConfig = getActivePasspointConfig();
calculateTimeout();
}
/**
* Network configs change: Re-evaluate set of HomeSPs and recalculate next time-out.
*/
public void networkConfigChange() {
Log.d(OSUManager.TAG, "Networks changed");
mPasspointConfigs.clear();
mUpdates.clear();
Iterator<PendingUpdate> updates = mOutstanding.iterator();
while (updates.hasNext()) {
PendingUpdate update = updates.next();
if (!update.isWnmBased()) {
updates.remove();
}
}
reloadAll(mContext, mPasspointConfigs, mStateFile, mUpdates);
calculateTimeout();
}
/**
* Connected to new network: Try to rematch any outstanding remediation entries to the new
* config.
*/
public void newConnection(WifiInfo newNetwork) {
mActivePasspointConfig = newNetwork != null ? getActivePasspointConfig() : null;
if (mActivePasspointConfig != null) {
Log.d(OSUManager.TAG, "New connection to "
+ mActivePasspointConfig.getHomeSP().getFQDN());
} else {
Log.d(OSUManager.TAG, "No passpoint connection");
return;
}
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
Network network = wifiManager.getCurrentNetwork();
Iterator<PendingUpdate> updates = mOutstanding.iterator();
while (updates.hasNext()) {
PendingUpdate update = updates.next();
try {
if (update.matches(wifiInfo, mActivePasspointConfig.getHomeSP())) {
update.remediate(network);
updates.remove();
} else if (update.isWnmBased()) {
Log.d(OSUManager.TAG, "WNM sender mismatches with BSS, cancelling remediation");
// Drop WNM update if it doesn't match the connected network
updates.remove();
}
} catch (IOException ioe) {
updates.remove();
}
}
}
/**
* Remediation timer fired: Iterate HomeSP and either pass on to remediation if there is a
* policy match or put on hold-off queue until a new network connection is made.
*/
@Override
public void onAlarm() {
Log.d(OSUManager.TAG, "Remediation timer");
calculateTimeout();
}
/**
* Remediation frame received, either pass on to pre-remediation check right away or await
* network connection.
*/
public void wnmReceived(long bssid, String url) {
PendingUpdate update = new PendingUpdate(bssid, url);
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
try {
if (mActivePasspointConfig != null
&& update.matches(wifiInfo, mActivePasspointConfig.getHomeSP())) {
Log.d(OSUManager.TAG, "WNM frame received, remediating now");
update.remediate(wifiManager.getCurrentNetwork());
} else {
Log.d(OSUManager.TAG, "WNM frame received, adding to outstanding remediations");
mOutstanding.addFirst(new PendingUpdate(bssid, url));
}
} catch (IOException ioe) {
Log.w(OSUManager.TAG, "Failed to remediate from WNM: " + ioe);
}
}
/**
* Callback to indicate that remediation has succeeded.
* @param fqdn The SPs FQDN
* @param policy set if this update was a policy update rather than a subscription update.
*/
public void remediationDone(String fqdn, boolean policy) {
Log.d(OSUManager.TAG, "Remediation complete for " + fqdn);
long now = System.currentTimeMillis();
List<RemediationEvent> events = mUpdates.get(fqdn);
if (events == null) {
events = new ArrayList<>();
events.add(new RemediationEvent(fqdn, policy, now));
mUpdates.put(fqdn, events);
} else {
Iterator<RemediationEvent> eventsIterator = events.iterator();
while (eventsIterator.hasNext()) {
RemediationEvent event = eventsIterator.next();
if (event.isPolicy() == policy) {
eventsIterator.remove();
}
}
events.add(new RemediationEvent(fqdn, policy, now));
}
saveUpdates(mStateFile, mUpdates);
}
public String getCurrentSpName() {
PasspointConfig config = getActivePasspointConfig();
return config != null ? config.getHomeSP().getFriendlyName() : "unknown";
}
private PasspointConfig getActivePasspointConfig() {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
mActiveWifiInfo = wifiManager.getConnectionInfo();
if (mActiveWifiInfo == null) {
return null;
}
for (PasspointConfig passpointConfig : mPasspointConfigs.values()) {
if (passpointConfig.getWifiConfiguration().networkId
== mActiveWifiInfo.getNetworkId()) {
return passpointConfig;
}
}
return null;
}
private void calculateTimeout() {
long now = System.currentTimeMillis();
long next = Long.MAX_VALUE;
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
Network network = wifiManager.getCurrentNetwork();
boolean newBaseTimes = false;
for (PasspointConfig passpointConfig : mPasspointConfigs.values()) {
HomeSP homeSP = passpointConfig.getHomeSP();
for (boolean policy : new boolean[] {false, true}) {
Long expiry = getNextUpdate(homeSP, policy, now);
Log.d(OSUManager.TAG, "Next remediation for " + homeSP.getFQDN()
+ (policy ? "/policy" : "/subscription")
+ " is " + toExpiry(expiry));
if (expiry == null || inProgress(homeSP, policy)) {
continue;
} else if (expiry < 0) {
next = now - expiry;
newBaseTimes = true;
continue;
}
if (expiry <= now) {
String uri = policy ? homeSP.getPolicy().getPolicyUpdate().getURI()
: homeSP.getSubscriptionUpdate().getURI();
PendingUpdate update = new PendingUpdate(homeSP, uri, policy);
try {
if (update.matches(mActiveWifiInfo, homeSP)) {
update.remediate(network);
} else {
Log.d(OSUManager.TAG, "Remediation for "
+ homeSP.getFQDN() + " pending");
mOutstanding.addLast(update);
}
} catch (IOException ioe) {
Log.w(OSUManager.TAG, "Failed to remediate "
+ homeSP.getFQDN() + ": " + ioe);
}
} else {
next = Math.min(next, expiry);
}
}
}
if (newBaseTimes) {
saveUpdates(mStateFile, mUpdates);
}
Log.d(OSUManager.TAG, "Next time-out at " + toExpiry(next));
AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
alarmManager.set(AlarmManager.RTC, next, "osu-remediation", this, null);
}
private static String toExpiry(Long time) {
if (time == null) {
return "n/a";
} else if (time < 0) {
return Utils.toHMS(-time) + " from now";
} else if (time > 0xffffffffffffL) {
return "infinity";
} else {
return Utils.toUTCString(time);
}
}
/**
* Get the next update time for the homeSP subscription or policy entry. Automatically add a
* wall time reference if it is missing.
* @param homeSP The HomeSP to check
* @param policy policy or subscription object.
* @return -interval if no wall time ref, null if n/a, otherwise wall time of next update.
*/
private Long getNextUpdate(HomeSP homeSP, boolean policy, long now) {
long interval;
if (policy) {
interval = homeSP.getPolicy().getPolicyUpdate().getInterval();
} else if (homeSP.getSubscriptionUpdate() != null) {
interval = homeSP.getSubscriptionUpdate().getInterval();
} else {
return null;
}
if (interval < 0) {
return null;
}
RemediationEvent event = getMatchingEvent(mUpdates.get(homeSP.getFQDN()), policy);
if (event == null) {
List<RemediationEvent> events = mUpdates.get(homeSP.getFQDN());
if (events == null) {
events = new ArrayList<>();
mUpdates.put(homeSP.getFQDN(), events);
}
events.add(new RemediationEvent(homeSP.getFQDN(), policy, now));
return -interval;
}
return event.getLastUpdate() + interval;
}
private boolean inProgress(HomeSP homeSP, boolean policy) {
Iterator<PendingUpdate> updates = mOutstanding.iterator();
while (updates.hasNext()) {
PendingUpdate update = updates.next();
if (update.getHomeSP() != null
&& update.getHomeSP().getFQDN().equals(homeSP.getFQDN())) {
if (update.isPolicy() && !policy) {
// Subscription updates takes precedence over policy updates
updates.remove();
return false;
} else {
return true;
}
}
}
return false;
}
private static RemediationEvent getMatchingEvent(
List<RemediationEvent> events, boolean policy) {
if (events == null) {
return null;
}
for (RemediationEvent event : events) {
if (event.isPolicy() == policy) {
return event;
}
}
return null;
}
private static void reloadAll(Context context, Map<String, PasspointConfig> passpointConfigs,
File stateFile, Map<String, List<RemediationEvent>> updates) {
loadAllSps(context, passpointConfigs);
try {
loadUpdates(stateFile, updates);
} catch (IOException ioe) {
Log.w(OSUManager.TAG, "Failed to load updates file: " + ioe);
}
boolean change = false;
Iterator<Map.Entry<String, List<RemediationEvent>>> events = updates.entrySet().iterator();
while (events.hasNext()) {
Map.Entry<String, List<RemediationEvent>> event = events.next();
if (!passpointConfigs.containsKey(event.getKey())) {
events.remove();
change = true;
}
}
Log.d(OSUManager.TAG, "Updates: " + updates);
if (change) {
saveUpdates(stateFile, updates);
}
}
private static void loadAllSps(Context context, Map<String, PasspointConfig> passpointConfigs) {
WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
List<WifiConfiguration> configs = wifiManager.getPrivilegedConfiguredNetworks();
if (configs == null) {
return;
}
int count = 0;
for (WifiConfiguration config : configs) {
String moTree = config.getMoTree();
if (moTree != null) {
try {
passpointConfigs.put(config.FQDN, new PasspointConfig(config));
count++;
} catch (IOException | SAXException e) {
Log.w(OSUManager.TAG, "Failed to parse MO: " + e);
}
}
}
Log.d(OSUManager.TAG, "Loaded " + count + " SPs");
}
private static void loadUpdates(File file, Map<String, List<RemediationEvent>> updates)
throws IOException {
try (BufferedReader in = new BufferedReader(new FileReader(file))) {
String line;
while ((line = in.readLine()) != null) {
try {
RemediationEvent event = new RemediationEvent(line);
List<RemediationEvent> events = updates.get(event.getFqdn());
if (events == null) {
events = new ArrayList<>();
updates.put(event.getFqdn(), events);
}
events.add(event);
} catch (IOException | NumberFormatException e) {
Log.w(OSUManager.TAG, "Bad line in " + file + ": '" + line + "': " + e);
}
}
}
}
private static void saveUpdates(File file, Map<String, List<RemediationEvent>> updates) {
try (BufferedWriter out = new BufferedWriter(new FileWriter(file, false))) {
for (List<RemediationEvent> events : updates.values()) {
for (RemediationEvent event : events) {
Log.d(OSUManager.TAG, "Writing wall time ref for " + event);
out.write(event.toString());
out.newLine();
}
}
} catch (IOException ioe) {
Log.w(OSUManager.TAG, "Failed to save update state: " + ioe);
}
}
private static class PasspointConfig {
private final WifiConfiguration mWifiConfiguration;
private final MOTree mMOTree;
private final HomeSP mHomeSP;
private PasspointConfig(WifiConfiguration config) throws IOException, SAXException {
mWifiConfiguration = config;
OMAParser omaParser = new OMAParser();
mMOTree = omaParser.parse(config.getMoTree(), OMAConstants.PPS_URN);
List<HomeSP> spList = MOManager.buildSPs(mMOTree);
if (spList.size() != 1) {
throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
}
mHomeSP = spList.iterator().next();
}
public WifiConfiguration getWifiConfiguration() {
return mWifiConfiguration;
}
public HomeSP getHomeSP() {
return mHomeSP;
}
public MOTree getMOTree() {
return mMOTree;
}
}
private static class RemediationEvent {
private final String mFqdn;
private final boolean mPolicy;
private final long mLastUpdate;
private RemediationEvent(String value) throws IOException {
String[] segments = value.split(" ");
if (segments.length != 3) {
throw new IOException("Bad line: '" + value + "'");
}
mFqdn = segments[0];
mPolicy = segments[1].equals("1");
mLastUpdate = Long.parseLong(segments[2]);
}
private RemediationEvent(String fqdn, boolean policy, long now) {
mFqdn = fqdn;
mPolicy = policy;
mLastUpdate = now;
}
public String getFqdn() {
return mFqdn;
}
public boolean isPolicy() {
return mPolicy;
}
public long getLastUpdate() {
return mLastUpdate;
}
@Override
public String toString() {
return String.format("%s %c %d", mFqdn, mPolicy ? '1' : '0', mLastUpdate);
}
}
private class PendingUpdate {
private final HomeSP mHomeSP; // For time based updates
private final long mBssid; // WNM based
private final String mUrl; // WNM based
private final boolean mPolicy;
private PendingUpdate(HomeSP homeSP, String url, boolean policy) {
mHomeSP = homeSP;
mPolicy = policy;
mBssid = 0L;
mUrl = url;
}
private PendingUpdate(long bssid, String url) {
mBssid = bssid;
mUrl = url;
mHomeSP = null;
mPolicy = false;
}
private boolean matches(WifiInfo wifiInfo, HomeSP activeSP) throws IOException {
if (mHomeSP == null) {
// WNM initiated remediation, HomeSP restriction
Log.d(OSUManager.TAG, String.format("Checking applicability of %s to %012x\n",
wifiInfo != null ? wifiInfo.getBSSID() : "-", mBssid));
return wifiInfo != null
&& Utils.parseMac(wifiInfo.getBSSID()) == mBssid;
//&& passesRestriction(activeSP); // !!! b/28600780
} else {
return passesRestriction(mHomeSP);
}
}
private boolean passesRestriction(HomeSP restrictingSP)
throws IOException {
UpdateInfo updateInfo;
if (mPolicy) {
if (restrictingSP.getPolicy() == null) {
throw new IOException("No policy object");
}
updateInfo = restrictingSP.getPolicy().getPolicyUpdate();
} else {
updateInfo = restrictingSP.getSubscriptionUpdate();
}
if (updateInfo.getUpdateRestriction() == UpdateRestriction.Unrestricted) {
return true;
}
PasspointMatch match = matchProviderWithCurrentNetwork(restrictingSP.getFQDN());
Log.d(OSUManager.TAG, "Current match for '" + restrictingSP.getFQDN()
+ "' is " + match + ", restriction " + updateInfo.getUpdateRestriction());
return match == PasspointMatch.HomeProvider
|| (match == PasspointMatch.RoamingProvider
&& updateInfo.getUpdateRestriction() == UpdateRestriction.RoamingPartner);
}
private void remediate(Network network) {
RemediationHandler.this.remediate(mHomeSP != null ? mHomeSP.getFQDN() : null,
mUrl, mPolicy, network);
}
private HomeSP getHomeSP() {
return mHomeSP;
}
private boolean isPolicy() {
return mPolicy;
}
private boolean isWnmBased() {
return mHomeSP == null;
}
private PasspointMatch matchProviderWithCurrentNetwork(String fqdn) {
WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
return Utils.mapEnum(wifiManager.matchProviderWithCurrentNetwork(fqdn),
PasspointMatch.class);
}
}
/**
* Initiate remediation
* @param spFqdn The FQDN of the current SP, not set for WNM based remediation
* @param url The URL of the remediation server
* @param policy Set if this is a policy update rather than a subscription update
* @param network The network to use for remediation
*/
private void remediate(final String spFqdn, final String url,
final boolean policy, final Network network) {
mContext.bindService(new Intent(mContext, FlowService.class), new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
try {
IFlowService fs = IFlowService.Stub.asInterface(service);
fs.remediate(spFqdn, url, policy, network);
} catch (RemoteException re) {
Log.e(OSUManager.TAG, "Caught re: " + re);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}, Context.BIND_AUTO_CREATE);
}
}

View File

@@ -1,108 +0,0 @@
package com.android.hotspot2.osu.service;
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import com.android.hotspot2.Utils;
import com.android.hotspot2.WifiNetworkAdapter;
import com.android.hotspot2.osu.OSUManager;
import com.android.hotspot2.pps.HomeSP;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class SubscriptionTimer implements Runnable {
private final Handler mHandler;
private final OSUManager mOSUManager;
private final WifiNetworkAdapter mWifiNetworkAdapter;
private final Map<HomeSP, UpdateAction> mOutstanding = new HashMap<>();
private static class UpdateAction {
private final long mRemediation;
private final long mPolicy;
private UpdateAction(HomeSP homeSP, long now) {
mRemediation = homeSP.getSubscriptionUpdate() != null ?
now + homeSP.getSubscriptionUpdate().getInterval() : -1;
mPolicy = homeSP.getPolicy() != null ?
now + homeSP.getPolicy().getPolicyUpdate().getInterval() : -1;
Log.d(OSUManager.TAG, "Timer set for " + homeSP.getFQDN() +
", remediation: " + Utils.toUTCString(mRemediation) +
", policy: " + Utils.toUTCString(mPolicy));
}
private boolean remediate(long now) {
return mRemediation > 0 && now >= mRemediation;
}
private boolean policyUpdate(long now) {
return mPolicy > 0 && now >= mPolicy;
}
private long nextExpiry(long now) {
long min = Long.MAX_VALUE;
if (mRemediation > now) {
min = mRemediation;
}
if (mPolicy > now) {
min = Math.min(min, mPolicy);
}
return min;
}
}
private static final String ACTION_TIMER =
"com.android.hotspot2.osu.service.SubscriptionTimer.action.TICK";
public SubscriptionTimer(OSUManager osuManager,
WifiNetworkAdapter wifiNetworkAdapter, Context context) {
mOSUManager = osuManager;
mWifiNetworkAdapter = wifiNetworkAdapter;
mHandler = new Handler();
}
@Override
public void run() {
checkUpdates();
}
public void checkUpdates() {
mHandler.removeCallbacks(this);
long now = System.currentTimeMillis();
long next = Long.MAX_VALUE;
Collection<HomeSP> homeSPs = mWifiNetworkAdapter.getLoadedSPs();
if (homeSPs.isEmpty()) {
return;
}
for (HomeSP homeSP : homeSPs) {
UpdateAction updateAction = mOutstanding.get(homeSP);
try {
if (updateAction == null) {
updateAction = new UpdateAction(homeSP, now);
mOutstanding.put(homeSP, updateAction);
} else if (updateAction.remediate(now)) {
mOSUManager.remediate(homeSP, false);
mOutstanding.put(homeSP, new UpdateAction(homeSP, now));
} else if (updateAction.policyUpdate(now)) {
mOSUManager.remediate(homeSP, true);
mOutstanding.put(homeSP, new UpdateAction(homeSP, now));
}
next = Math.min(next, updateAction.nextExpiry(now));
} catch (IOException | SAXException e) {
Log.d(OSUManager.TAG, "Failed subscription update: " + e.getMessage());
}
}
setAlarm(next);
}
private void setAlarm(long tod) {
long delay = tod - System.currentTimeMillis();
mHandler.postAtTime(this, Math.max(1, delay));
}
}

View File

@@ -23,6 +23,8 @@ import static com.android.hotspot2.omadm.MOManager.TAG_UsernamePassword;
public class UpdateInfo {
public enum UpdateRestriction {HomeSP, RoamingPartner, Unrestricted}
public static final long NO_UPDATE = 0xffffffffL;
private final long mInterval;
private final boolean mSPPClientInitiated;
private final UpdateRestriction mUpdateRestriction;
@@ -33,8 +35,8 @@ public class UpdateInfo {
private final String mCertFP;
public UpdateInfo(OMANode policyUpdate) throws OMAException {
mInterval = MOManager.getLong(policyUpdate, TAG_UpdateInterval, null) *
MOManager.IntervalFactor;
long minutes = MOManager.getLong(policyUpdate, TAG_UpdateInterval, null);
mInterval = minutes == NO_UPDATE ? -1 : minutes * MOManager.IntervalFactor;
mSPPClientInitiated = MOManager.getSelection(policyUpdate, TAG_UpdateMethod);
mUpdateRestriction = MOManager.getSelection(policyUpdate, TAG_Restriction);
mURI = MOManager.getString(policyUpdate, TAG_URI);

View File

@@ -36,13 +36,9 @@ public class HTTPResponse implements HTTPMessage {
while (offset < expected) {
int amount = in.read(input, offset, input.length - offset);
Log.d(OSUManager.TAG, String.format("Reading into %d from %d, amount %d -> %d",
input.length, offset, input.length - offset, amount));
if (amount < 0) {
throw new EOFException();
}
//Log.d("ZXZ", "HTTP response: '"
// + new String(input, 0, offset + amount, StandardCharsets.ISO_8859_1));
if (body < 0) {
for (int n = offset; n < offset + amount; n++) {