blob: d130e012dbc14046b036ba52a312b43a56519f99 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.wifi.hotspot2;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_ICON_BSSID;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_ICON_DATA;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_ICON_FILE;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_BSSID;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_DELAY;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_ESS;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_METHOD;
import static android.net.wifi.WifiManager.EXTRA_PASSPOINT_WNM_URL;
import static android.net.wifi.WifiManager.PASSPOINT_ICON_RECEIVED_ACTION;
import static android.net.wifi.WifiManager.PASSPOINT_WNM_FRAME_RECEIVED_ACTION;
import android.content.Context;
import android.content.Intent;
import android.net.wifi.hotspot2.PasspointConfiguration;
import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
import com.android.server.wifi.Clock;
import com.android.server.wifi.IMSIParameter;
import com.android.server.wifi.SIMAccessor;
import com.android.server.wifi.WifiKeyStore;
import com.android.server.wifi.WifiNative;
import com.android.server.wifi.anqp.ANQPElement;
import com.android.server.wifi.anqp.Constants;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Responsible for managing passpoint networks.
*/
public class PasspointManager {
private static final String TAG = "PasspointManager";
private final PasspointEventHandler mHandler;
private final SIMAccessor mSimAccessor;
private final WifiKeyStore mKeyStore;
private final Clock mClock;
private final PasspointObjectFactory mObjectFactory;
private final Map<String, PasspointProvider> mProviders;
private class CallbackHandler implements PasspointEventHandler.Callbacks {
private final Context mContext;
CallbackHandler(Context context) {
mContext = context;
}
@Override
public void onANQPResponse(long bssid,
Map<Constants.ANQPElementType, ANQPElement> anqpElements) {
// TO BE IMPLEMENTED.
}
@Override
public void onIconResponse(long bssid, String fileName, byte[] data) {
Intent intent = new Intent(PASSPOINT_ICON_RECEIVED_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.putExtra(EXTRA_PASSPOINT_ICON_BSSID, bssid);
intent.putExtra(EXTRA_PASSPOINT_ICON_FILE, fileName);
if (data != null) {
intent.putExtra(EXTRA_PASSPOINT_ICON_DATA, data);
}
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
@Override
public void onWnmFrameReceived(WnmData event) {
// %012x HS20-SUBSCRIPTION-REMEDIATION "%u %s", osu_method, url
// %012x HS20-DEAUTH-IMMINENT-NOTICE "%u %u %s", code, reauth_delay, url
Intent intent = new Intent(PASSPOINT_WNM_FRAME_RECEIVED_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
intent.putExtra(EXTRA_PASSPOINT_WNM_BSSID, event.getBssid());
intent.putExtra(EXTRA_PASSPOINT_WNM_URL, event.getUrl());
if (event.isDeauthEvent()) {
intent.putExtra(EXTRA_PASSPOINT_WNM_ESS, event.isEss());
intent.putExtra(EXTRA_PASSPOINT_WNM_DELAY, event.getDelay());
} else {
intent.putExtra(EXTRA_PASSPOINT_WNM_METHOD, event.getMethod());
// TODO(zqiu): set the passpoint matching status with the respect to the
// current connected network (e.g. HomeProvider, RoamingProvider, None,
// Declined).
}
mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
}
}
public PasspointManager(Context context, WifiNative wifiNative, WifiKeyStore keyStore,
Clock clock, SIMAccessor simAccessor, PasspointObjectFactory objectFactory) {
mHandler = objectFactory.makePasspointEventHandler(wifiNative,
new CallbackHandler(context));
mKeyStore = keyStore;
mClock = clock;
mSimAccessor = simAccessor;
mObjectFactory = objectFactory;
mProviders = new HashMap<>();
// TODO(zqiu): load providers from the persistent storage.
}
/**
* Add or install a Passpoint provider with the given configuration.
*
* Each provider is uniquely identified by its FQDN (Fully Qualified Domain Name).
* In the case when there is an existing configuration with the same base
* domain, a provider with the new configuration will replace the existing provider.
*
* @param config Configuration of the Passpoint provider to be added
* @return true if provider is added, false otherwise
*/
public boolean addProvider(PasspointConfiguration config) {
if (config == null) {
Log.e(TAG, "Configuration not provided");
return false;
}
if (!config.validate()) {
Log.e(TAG, "Invalid configuration");
return false;
}
// Verify IMSI against the IMSI of the installed SIM cards for SIM credential.
if (config.credential.simCredential != null) {
try {
if (mSimAccessor.getMatchingImsis(
new IMSIParameter(config.credential.simCredential.imsi)) == null) {
Log.e(TAG, "IMSI does not match any SIM card");
return false;
}
} catch (IOException e) {
return false;
}
}
// Create a provider and install the necessary certificates and keys.
PasspointProvider newProvider = mObjectFactory.makePasspointProvider(
config, mKeyStore, mClock.getWallClockMillis());
if (!newProvider.installCertsAndKeys()) {
Log.e(TAG, "Failed to install certificates and keys to keystore");
return false;
}
// Detect existing configuration in the same base domain.
PasspointProvider existingProvider = findProviderInSameBaseDomain(config.homeSp.fqdn);
if (existingProvider != null) {
Log.d(TAG, "Replacing configuration for " + existingProvider.getConfig().homeSp.fqdn
+ " with " + config.homeSp.fqdn);
existingProvider.uninstallCertsAndKeys();
mProviders.remove(existingProvider.getConfig().homeSp.fqdn);
}
mProviders.put(config.homeSp.fqdn, newProvider);
// TODO(b/31065385): Persist updated providers configuration to the persistent storage.
return true;
}
/**
* Remove a Passpoint provider identified by the given FQDN.
*
* @param fqdn The FQDN of the provider to remove
* @return true if a provider is removed, false otherwise
*/
public boolean removeProvider(String fqdn) {
if (!mProviders.containsKey(fqdn)) {
Log.e(TAG, "Config doesn't exist");
return false;
}
mProviders.get(fqdn).uninstallCertsAndKeys();
mProviders.remove(fqdn);
return true;
}
/**
* Return the installed Passpoint provider configurations.
*
* @return A list of {@link PasspointConfiguration} or null if none is installed
*/
public List<PasspointConfiguration> getProviderConfigs() {
if (mProviders.size() == 0) {
return null;
}
List<PasspointConfiguration> configs = new ArrayList<>();
for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
configs.add(entry.getValue().getConfig());
}
return configs;
}
/**
* Notify the completion of an ANQP request.
* TODO(zqiu): currently the notification is done through WifiMonitor,
* will no longer be the case once we switch over to use wificond.
*/
public void notifyANQPDone(long bssid, boolean success) {
mHandler.notifyANQPDone(bssid, success);
}
/**
* Notify the completion of an icon request.
* TODO(zqiu): currently the notification is done through WifiMonitor,
* will no longer be the case once we switch over to use wificond.
*/
public void notifyIconDone(long bssid, IconEvent iconEvent) {
mHandler.notifyIconDone(bssid, iconEvent);
}
/**
* Notify the reception of a Wireless Network Management (WNM) frame.
* TODO(zqiu): currently the notification is done through WifiMonitor,
* will no longer be the case once we switch over to use wificond.
*/
public void receivedWnmFrame(WnmData data) {
mHandler.notifyWnmFrameReceived(data);
}
/**
* Request the specified icon file |fileName| from the specified AP |bssid|.
* @return true if the request is sent successfully, false otherwise
*/
public boolean queryPasspointIcon(long bssid, String fileName) {
return mHandler.requestIcon(bssid, fileName);
}
/**
* Find a provider that have FQDN in the same base domain as the given domain.
*
* @param domain The domain to be compared
* @return {@link PasspointProvider} if a match is found, null otherwise
*/
private PasspointProvider findProviderInSameBaseDomain(String domain) {
for (Map.Entry<String, PasspointProvider> entry : mProviders.entrySet()) {
if (isSameBaseDomain(entry.getKey(), domain)) {
return entry.getValue();
}
}
return null;
}
/**
* Check if one domain is the base domain for the other. For example, "test1.test.com"
* and "test.com" should return true.
*
* @param domain1 First domain to be compared
* @param domain2 Second domain to be compared
* @return true if one domain is a base domain for the other, false otherwise.
*/
private static boolean isSameBaseDomain(String domain1, String domain2) {
if (domain1 == null || domain2 == null) {
return false;
}
List<String> labelList1 = Utils.splitDomain(domain1);
List<String> labelList2 = Utils.splitDomain(domain2);
Iterator<String> l1 = labelList1.iterator();
Iterator<String> l2 = labelList2.iterator();
while (l1.hasNext() && l2.hasNext()) {
if (!TextUtils.equals(l1.next(), l2.next())) {
return false;
}
}
return true;
}
}