Home | History | Annotate | Download | only in service
      1 package com.android.hotspot2.osu.service;
      2 
      3 import android.app.AlarmManager;
      4 import android.content.ComponentName;
      5 import android.content.Context;
      6 import android.content.Intent;
      7 import android.content.ServiceConnection;
      8 import android.net.Network;
      9 import android.net.wifi.WifiConfiguration;
     10 import android.net.wifi.WifiInfo;
     11 import android.net.wifi.WifiManager;
     12 import android.os.IBinder;
     13 import android.os.RemoteException;
     14 import android.util.Log;
     15 
     16 import com.android.hotspot2.PasspointMatch;
     17 import com.android.hotspot2.Utils;
     18 import com.android.hotspot2.flow.FlowService;
     19 import com.android.hotspot2.omadm.MOManager;
     20 import com.android.hotspot2.omadm.MOTree;
     21 import com.android.hotspot2.omadm.OMAConstants;
     22 import com.android.hotspot2.omadm.OMAException;
     23 import com.android.hotspot2.omadm.OMAParser;
     24 import com.android.hotspot2.osu.OSUManager;
     25 import com.android.hotspot2.pps.HomeSP;
     26 import com.android.hotspot2.pps.UpdateInfo;
     27 import com.android.hotspot2.flow.IFlowService;
     28 
     29 import org.xml.sax.SAXException;
     30 
     31 import java.io.BufferedReader;
     32 import java.io.BufferedWriter;
     33 import java.io.File;
     34 import java.io.FileReader;
     35 import java.io.FileWriter;
     36 import java.io.IOException;
     37 import java.util.ArrayList;
     38 import java.util.HashMap;
     39 import java.util.Iterator;
     40 import java.util.LinkedList;
     41 import java.util.List;
     42 import java.util.Map;
     43 
     44 import static com.android.hotspot2.pps.UpdateInfo.UpdateRestriction;
     45 
     46 public class RemediationHandler implements AlarmManager.OnAlarmListener {
     47     private final Context mContext;
     48     private final File mStateFile;
     49 
     50     private final Map<String, PasspointConfig> mPasspointConfigs = new HashMap<>();
     51     private final Map<String, List<RemediationEvent>> mUpdates = new HashMap<>();
     52     private final LinkedList<PendingUpdate> mOutstanding = new LinkedList<>();
     53 
     54     private WifiInfo mActiveWifiInfo;
     55     private PasspointConfig mActivePasspointConfig;
     56 
     57     public RemediationHandler(Context context, File stateFile) {
     58         mContext = context;
     59         mStateFile = stateFile;
     60         Log.d(OSUManager.TAG, "State file: " + stateFile);
     61         reloadAll(context, mPasspointConfigs, stateFile, mUpdates);
     62         mActivePasspointConfig = getActivePasspointConfig();
     63         calculateTimeout();
     64     }
     65 
     66     /**
     67      * Network configs change: Re-evaluate set of HomeSPs and recalculate next time-out.
     68      */
     69     public void networkConfigChange() {
     70         Log.d(OSUManager.TAG, "Networks changed");
     71         mPasspointConfigs.clear();
     72         mUpdates.clear();
     73         Iterator<PendingUpdate> updates = mOutstanding.iterator();
     74         while (updates.hasNext()) {
     75             PendingUpdate update = updates.next();
     76             if (!update.isWnmBased()) {
     77                 updates.remove();
     78             }
     79         }
     80         reloadAll(mContext, mPasspointConfigs, mStateFile, mUpdates);
     81         calculateTimeout();
     82     }
     83 
     84     /**
     85      * Connected to new network: Try to rematch any outstanding remediation entries to the new
     86      * config.
     87      */
     88     public void newConnection(WifiInfo newNetwork) {
     89         mActivePasspointConfig = newNetwork != null ? getActivePasspointConfig() : null;
     90         if (mActivePasspointConfig != null) {
     91             Log.d(OSUManager.TAG, "New connection to "
     92                     + mActivePasspointConfig.getHomeSP().getFQDN());
     93         } else {
     94             Log.d(OSUManager.TAG, "No passpoint connection");
     95             return;
     96         }
     97         WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
     98         WifiInfo wifiInfo = wifiManager.getConnectionInfo();
     99         Network network = wifiManager.getCurrentNetwork();
    100 
    101         Iterator<PendingUpdate> updates = mOutstanding.iterator();
    102         while (updates.hasNext()) {
    103             PendingUpdate update = updates.next();
    104             try {
    105                 if (update.matches(wifiInfo, mActivePasspointConfig.getHomeSP())) {
    106                     update.remediate(network);
    107                     updates.remove();
    108                 } else if (update.isWnmBased()) {
    109                     Log.d(OSUManager.TAG, "WNM sender mismatches with BSS, cancelling remediation");
    110                     // Drop WNM update if it doesn't match the connected network
    111                     updates.remove();
    112                 }
    113             } catch (IOException ioe) {
    114                 updates.remove();
    115             }
    116         }
    117     }
    118 
    119     /**
    120      * Remediation timer fired: Iterate HomeSP and either pass on to remediation if there is a
    121      * policy match or put on hold-off queue until a new network connection is made.
    122      */
    123     @Override
    124     public void onAlarm() {
    125         Log.d(OSUManager.TAG, "Remediation timer");
    126         calculateTimeout();
    127     }
    128 
    129     /**
    130      * Remediation frame received, either pass on to pre-remediation check right away or await
    131      * network connection.
    132      */
    133     public void wnmReceived(long bssid, String url) {
    134         PendingUpdate update = new PendingUpdate(bssid, url);
    135         WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
    136         WifiInfo wifiInfo = wifiManager.getConnectionInfo();
    137         try {
    138             if (mActivePasspointConfig == null) {
    139                 Log.d(OSUManager.TAG, String.format("WNM remediation frame '%s' through %012x " +
    140                         "received, adding to outstanding remediations", url, bssid));
    141                 mOutstanding.addFirst(new PendingUpdate(bssid, url));
    142             } else if (update.matches(wifiInfo, mActivePasspointConfig.getHomeSP())) {
    143                 Log.d(OSUManager.TAG, String.format("WNM remediation frame '%s' through %012x " +
    144                         "received, remediating now", url, bssid));
    145                 update.remediate(wifiManager.getCurrentNetwork());
    146             } else {
    147                 Log.w(OSUManager.TAG, String.format("WNM remediation frame '%s' through %012x " +
    148                         "does not meet restriction", url, bssid));
    149             }
    150         } catch (IOException ioe) {
    151             Log.w(OSUManager.TAG, "Failed to remediate from WNM: " + ioe);
    152         }
    153     }
    154 
    155     /**
    156      * Callback to indicate that remediation has succeeded.
    157      * @param fqdn The SPs FQDN
    158      * @param policy set if this update was a policy update rather than a subscription update.
    159      */
    160     public void remediationDone(String fqdn, boolean policy) {
    161         Log.d(OSUManager.TAG, "Remediation complete for " + fqdn);
    162         long now = System.currentTimeMillis();
    163         List<RemediationEvent> events = mUpdates.get(fqdn);
    164         if (events == null) {
    165             events = new ArrayList<>();
    166             events.add(new RemediationEvent(fqdn, policy, now));
    167             mUpdates.put(fqdn, events);
    168         } else {
    169             Iterator<RemediationEvent> eventsIterator = events.iterator();
    170             while (eventsIterator.hasNext()) {
    171                 RemediationEvent event = eventsIterator.next();
    172                 if (event.isPolicy() == policy) {
    173                     eventsIterator.remove();
    174                 }
    175             }
    176             events.add(new RemediationEvent(fqdn, policy, now));
    177         }
    178         saveUpdates(mStateFile, mUpdates);
    179     }
    180 
    181     public String getCurrentSpName() {
    182         PasspointConfig config = getActivePasspointConfig();
    183         return config != null ? config.getHomeSP().getFriendlyName() : "unknown";
    184     }
    185 
    186     private PasspointConfig getActivePasspointConfig() {
    187         WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
    188         mActiveWifiInfo = wifiManager.getConnectionInfo();
    189         if (mActiveWifiInfo == null) {
    190             return null;
    191         }
    192 
    193         for (PasspointConfig passpointConfig : mPasspointConfigs.values()) {
    194             if (passpointConfig.getWifiConfiguration().networkId
    195                     == mActiveWifiInfo.getNetworkId()) {
    196                 return passpointConfig;
    197             }
    198         }
    199         return null;
    200     }
    201 
    202     private void calculateTimeout() {
    203         long now = System.currentTimeMillis();
    204         long next = Long.MAX_VALUE;
    205         WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
    206         Network network = wifiManager.getCurrentNetwork();
    207 
    208         boolean newBaseTimes = false;
    209         for (PasspointConfig passpointConfig : mPasspointConfigs.values()) {
    210             HomeSP homeSP = passpointConfig.getHomeSP();
    211 
    212             for (boolean policy : new boolean[] {false, true}) {
    213                 Long expiry = getNextUpdate(homeSP, policy, now);
    214                 Log.d(OSUManager.TAG, "Next remediation for " + homeSP.getFQDN()
    215                         + (policy ? "/policy" : "/subscription")
    216                         + " is " + toExpiry(expiry));
    217                 if (expiry == null || inProgress(homeSP, policy)) {
    218                     continue;
    219                 } else if (expiry < 0) {
    220                     next = now - expiry;
    221                     newBaseTimes = true;
    222                     continue;
    223                 }
    224 
    225                 if (expiry <= now) {
    226                     String uri = policy ? homeSP.getPolicy().getPolicyUpdate().getURI()
    227                             : homeSP.getSubscriptionUpdate().getURI();
    228                     PendingUpdate update = new PendingUpdate(homeSP, uri, policy);
    229                     try {
    230                         if (update.matches(mActiveWifiInfo, homeSP)) {
    231                             update.remediate(network);
    232                         } else {
    233                             Log.d(OSUManager.TAG, "Remediation for "
    234                                     + homeSP.getFQDN() + " pending");
    235                             mOutstanding.addLast(update);
    236                         }
    237                     } catch (IOException ioe) {
    238                         Log.w(OSUManager.TAG, "Failed to remediate "
    239                                 + homeSP.getFQDN() + ": " + ioe);
    240                     }
    241                 } else {
    242                     next = Math.min(next, expiry);
    243                 }
    244             }
    245         }
    246         if (newBaseTimes) {
    247             saveUpdates(mStateFile, mUpdates);
    248         }
    249         Log.d(OSUManager.TAG, "Next time-out at " + toExpiry(next));
    250         AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
    251         alarmManager.set(AlarmManager.RTC, next, "osu-remediation", this, null);
    252     }
    253 
    254     private static String toExpiry(Long time) {
    255         if (time == null) {
    256             return "n/a";
    257         } else if (time < 0) {
    258             return Utils.toHMS(-time) + " from now";
    259         } else if (time > 0xffffffffffffL) {
    260             return "infinity";
    261         } else {
    262             return Utils.toUTCString(time);
    263         }
    264     }
    265 
    266     /**
    267      * Get the next update time for the homeSP subscription or policy entry. Automatically add a
    268      * wall time reference if it is missing.
    269      * @param homeSP The HomeSP to check
    270      * @param policy policy or subscription object.
    271      * @return -interval if no wall time ref, null if n/a, otherwise wall time of next update.
    272      */
    273     private Long getNextUpdate(HomeSP homeSP, boolean policy, long now) {
    274         long interval;
    275         if (policy) {
    276             interval = homeSP.getPolicy().getPolicyUpdate().getInterval();
    277         } else if (homeSP.getSubscriptionUpdate() != null) {
    278             interval = homeSP.getSubscriptionUpdate().getInterval();
    279         } else {
    280             return null;
    281         }
    282         if (interval < 0) {
    283             return null;
    284         }
    285 
    286         RemediationEvent event = getMatchingEvent(mUpdates.get(homeSP.getFQDN()), policy);
    287         if (event == null) {
    288             List<RemediationEvent> events = mUpdates.get(homeSP.getFQDN());
    289             if (events == null) {
    290                 events = new ArrayList<>();
    291                 mUpdates.put(homeSP.getFQDN(), events);
    292             }
    293             events.add(new RemediationEvent(homeSP.getFQDN(), policy, now));
    294             return -interval;
    295         }
    296         return event.getLastUpdate() + interval;
    297     }
    298 
    299     private boolean inProgress(HomeSP homeSP, boolean policy) {
    300         Iterator<PendingUpdate> updates = mOutstanding.iterator();
    301         while (updates.hasNext()) {
    302             PendingUpdate update = updates.next();
    303             if (update.getHomeSP() != null
    304                     && update.getHomeSP().getFQDN().equals(homeSP.getFQDN())) {
    305                 if (update.isPolicy() && !policy) {
    306                     // Subscription updates takes precedence over policy updates
    307                     updates.remove();
    308                     return false;
    309                 } else {
    310                     return true;
    311                 }
    312             }
    313         }
    314         return false;
    315     }
    316 
    317     private static RemediationEvent getMatchingEvent(
    318             List<RemediationEvent> events, boolean policy) {
    319         if (events == null) {
    320             return null;
    321         }
    322         for (RemediationEvent event : events) {
    323             if (event.isPolicy() == policy) {
    324                 return event;
    325             }
    326         }
    327         return null;
    328     }
    329 
    330     private static void reloadAll(Context context, Map<String, PasspointConfig> passpointConfigs,
    331                                   File stateFile, Map<String, List<RemediationEvent>> updates) {
    332 
    333         loadAllSps(context, passpointConfigs);
    334         try {
    335             loadUpdates(stateFile, updates);
    336         } catch (IOException ioe) {
    337             Log.w(OSUManager.TAG, "Failed to load updates file: " + ioe);
    338         }
    339 
    340         boolean change = false;
    341         Iterator<Map.Entry<String, List<RemediationEvent>>> events = updates.entrySet().iterator();
    342         while (events.hasNext()) {
    343             Map.Entry<String, List<RemediationEvent>> event = events.next();
    344             if (!passpointConfigs.containsKey(event.getKey())) {
    345                 events.remove();
    346                 change = true;
    347             }
    348         }
    349         Log.d(OSUManager.TAG, "Updates: " + updates);
    350         if (change) {
    351             saveUpdates(stateFile, updates);
    352         }
    353     }
    354 
    355     private static void loadAllSps(Context context, Map<String, PasspointConfig> passpointConfigs) {
    356         WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
    357         List<WifiConfiguration> configs = wifiManager.getPrivilegedConfiguredNetworks();
    358         if (configs == null) {
    359             return;
    360         }
    361         int count = 0;
    362         for (WifiConfiguration config : configs) {
    363             String moTree = config.getMoTree();
    364             if (moTree != null) {
    365                 try {
    366                     passpointConfigs.put(config.FQDN, new PasspointConfig(config));
    367                     count++;
    368                 } catch (IOException | SAXException e) {
    369                     Log.w(OSUManager.TAG, "Failed to parse MO: " + e);
    370                 }
    371             }
    372         }
    373         Log.d(OSUManager.TAG, "Loaded " + count + " SPs");
    374     }
    375 
    376     private static void loadUpdates(File file, Map<String, List<RemediationEvent>> updates)
    377             throws IOException {
    378         try (BufferedReader in = new BufferedReader(new FileReader(file))) {
    379             String line;
    380             while ((line = in.readLine()) != null) {
    381                 try {
    382                     RemediationEvent event = new RemediationEvent(line);
    383                     List<RemediationEvent> events = updates.get(event.getFqdn());
    384                     if (events == null) {
    385                         events = new ArrayList<>();
    386                         updates.put(event.getFqdn(), events);
    387                     }
    388                     events.add(event);
    389                 } catch (IOException | NumberFormatException e) {
    390                     Log.w(OSUManager.TAG, "Bad line in " + file + ": '" + line + "': " + e);
    391                 }
    392             }
    393         }
    394     }
    395 
    396     private static void saveUpdates(File file, Map<String, List<RemediationEvent>> updates) {
    397         try (BufferedWriter out = new BufferedWriter(new FileWriter(file, false))) {
    398             for (List<RemediationEvent> events : updates.values()) {
    399                 for (RemediationEvent event : events) {
    400                     Log.d(OSUManager.TAG, "Writing wall time ref for " + event);
    401                     out.write(event.toString());
    402                     out.newLine();
    403                 }
    404             }
    405         } catch (IOException ioe) {
    406             Log.w(OSUManager.TAG, "Failed to save update state: " + ioe);
    407         }
    408     }
    409 
    410     private static class PasspointConfig {
    411         private final WifiConfiguration mWifiConfiguration;
    412         private final MOTree mMOTree;
    413         private final HomeSP mHomeSP;
    414 
    415         private PasspointConfig(WifiConfiguration config) throws IOException, SAXException {
    416             mWifiConfiguration = config;
    417             OMAParser omaParser = new OMAParser();
    418             mMOTree = omaParser.parse(config.getMoTree(), OMAConstants.PPS_URN);
    419             List<HomeSP> spList = MOManager.buildSPs(mMOTree);
    420             if (spList.size() != 1) {
    421                 throw new OMAException("Expected exactly one HomeSP, got " + spList.size());
    422             }
    423             mHomeSP = spList.iterator().next();
    424         }
    425 
    426         public WifiConfiguration getWifiConfiguration() {
    427             return mWifiConfiguration;
    428         }
    429 
    430         public HomeSP getHomeSP() {
    431             return mHomeSP;
    432         }
    433 
    434         public MOTree getMOTree() {
    435             return mMOTree;
    436         }
    437     }
    438 
    439     private static class RemediationEvent {
    440         private final String mFqdn;
    441         private final boolean mPolicy;
    442         private final long mLastUpdate;
    443 
    444         private RemediationEvent(String value) throws IOException {
    445             String[] segments = value.split(" ");
    446             if (segments.length != 3) {
    447                 throw new IOException("Bad line: '" + value + "'");
    448             }
    449             mFqdn = segments[0];
    450             mPolicy = segments[1].equals("1");
    451             mLastUpdate = Long.parseLong(segments[2]);
    452         }
    453 
    454         private RemediationEvent(String fqdn, boolean policy, long now) {
    455             mFqdn = fqdn;
    456             mPolicy = policy;
    457             mLastUpdate = now;
    458         }
    459 
    460         public String getFqdn() {
    461             return mFqdn;
    462         }
    463 
    464         public boolean isPolicy() {
    465             return mPolicy;
    466         }
    467 
    468         public long getLastUpdate() {
    469             return mLastUpdate;
    470         }
    471 
    472         @Override
    473         public String toString() {
    474             return String.format("%s %c %d", mFqdn, mPolicy ? '1' : '0', mLastUpdate);
    475         }
    476     }
    477 
    478     private class PendingUpdate {
    479         private final HomeSP mHomeSP;       // For time based updates
    480         private final long mBssid;          // WNM based
    481         private final String mUrl;          // WNM based
    482         private final boolean mPolicy;
    483 
    484         private PendingUpdate(HomeSP homeSP, String url, boolean policy) {
    485             mHomeSP = homeSP;
    486             mPolicy = policy;
    487             mBssid = 0L;
    488             mUrl = url;
    489         }
    490 
    491         private PendingUpdate(long bssid, String url) {
    492             mBssid = bssid;
    493             mUrl = url;
    494             mHomeSP = null;
    495             mPolicy = false;
    496         }
    497 
    498         private boolean matches(WifiInfo wifiInfo, HomeSP activeSP) throws IOException {
    499             if (mHomeSP == null) {
    500                 // WNM initiated remediation, HomeSP restriction
    501                 Log.d(OSUManager.TAG, String.format("Checking applicability of %s to %012x\n",
    502                         wifiInfo != null ? wifiInfo.getBSSID() : "-", mBssid));
    503                 return wifiInfo != null
    504                         && Utils.parseMac(wifiInfo.getBSSID()) == mBssid
    505                         && passesRestriction(activeSP);   // !!! b/28600780
    506             } else {
    507                 return passesRestriction(mHomeSP);
    508             }
    509         }
    510 
    511         private boolean passesRestriction(HomeSP restrictingSP)
    512                 throws IOException {
    513             UpdateInfo updateInfo;
    514             if (mPolicy) {
    515                 if (restrictingSP.getPolicy() == null) {
    516                     throw new IOException("No policy object");
    517                 }
    518                 updateInfo = restrictingSP.getPolicy().getPolicyUpdate();
    519             } else {
    520                 updateInfo = restrictingSP.getSubscriptionUpdate();
    521             }
    522 
    523             if (updateInfo.getUpdateRestriction() == UpdateRestriction.Unrestricted) {
    524                 return true;
    525             }
    526 
    527             PasspointMatch match = matchProviderWithCurrentNetwork(restrictingSP.getFQDN());
    528             Log.d(OSUManager.TAG, "Current match for '" + restrictingSP.getFQDN()
    529                     + "' is " + match + ", restriction " + updateInfo.getUpdateRestriction());
    530             return match == PasspointMatch.HomeProvider
    531                     || (match == PasspointMatch.RoamingProvider
    532                     && updateInfo.getUpdateRestriction() == UpdateRestriction.RoamingPartner);
    533         }
    534 
    535         private void remediate(Network network) {
    536             RemediationHandler.this.remediate(mHomeSP != null ? mHomeSP.getFQDN() : null,
    537                     mUrl, mPolicy, network);
    538         }
    539 
    540         private HomeSP getHomeSP() {
    541             return mHomeSP;
    542         }
    543 
    544         private boolean isPolicy() {
    545             return mPolicy;
    546         }
    547 
    548         private boolean isWnmBased() {
    549             return mHomeSP == null;
    550         }
    551 
    552         private PasspointMatch matchProviderWithCurrentNetwork(String fqdn) {
    553             WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
    554             return Utils.mapEnum(wifiManager.matchProviderWithCurrentNetwork(fqdn),
    555                     PasspointMatch.class);
    556         }
    557     }
    558 
    559     /**
    560      * Initiate remediation
    561      * @param spFqdn The FQDN of the current SP, not set for WNM based remediation
    562      * @param url The URL of the remediation server
    563      * @param policy Set if this is a policy update rather than a subscription update
    564      * @param network The network to use for remediation
    565      */
    566     private void remediate(final String spFqdn, final String url,
    567                            final boolean policy, final Network network) {
    568         mContext.bindService(new Intent(mContext, FlowService.class), new ServiceConnection() {
    569             @Override
    570             public void onServiceConnected(ComponentName name, IBinder service) {
    571                 try {
    572                     IFlowService fs = IFlowService.Stub.asInterface(service);
    573                     fs.remediate(spFqdn, url, policy, network);
    574                 } catch (RemoteException re) {
    575                     Log.e(OSUManager.TAG, "Caught re: " + re);
    576                 }
    577             }
    578 
    579             @Override
    580             public void onServiceDisconnected(ComponentName name) {
    581 
    582             }
    583         }, Context.BIND_AUTO_CREATE);
    584     }
    585 }
    586