• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
RemediationHandler(Context context, File stateFile)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      */
networkConfigChange()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      */
newConnection(WifiInfo newNetwork)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
onAlarm()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      */
wnmReceived(long bssid, String url)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      */
remediationDone(String fqdn, boolean policy)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 
getCurrentSpName()181     public String getCurrentSpName() {
182         PasspointConfig config = getActivePasspointConfig();
183         return config != null ? config.getHomeSP().getFriendlyName() : "unknown";
184     }
185 
getActivePasspointConfig()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 
calculateTimeout()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 
toExpiry(Long time)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      */
getNextUpdate(HomeSP homeSP, boolean policy, long now)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 
inProgress(HomeSP homeSP, boolean policy)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 
getMatchingEvent( List<RemediationEvent> events, boolean policy)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 
reloadAll(Context context, Map<String, PasspointConfig> passpointConfigs, File stateFile, Map<String, List<RemediationEvent>> updates)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 
loadAllSps(Context context, Map<String, PasspointConfig> passpointConfigs)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 
loadUpdates(File file, Map<String, List<RemediationEvent>> updates)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 
saveUpdates(File file, Map<String, List<RemediationEvent>> updates)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 
PasspointConfig(WifiConfiguration config)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 
getWifiConfiguration()426         public WifiConfiguration getWifiConfiguration() {
427             return mWifiConfiguration;
428         }
429 
getHomeSP()430         public HomeSP getHomeSP() {
431             return mHomeSP;
432         }
433 
getMOTree()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 
RemediationEvent(String value)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 
RemediationEvent(String fqdn, boolean policy, long now)454         private RemediationEvent(String fqdn, boolean policy, long now) {
455             mFqdn = fqdn;
456             mPolicy = policy;
457             mLastUpdate = now;
458         }
459 
getFqdn()460         public String getFqdn() {
461             return mFqdn;
462         }
463 
isPolicy()464         public boolean isPolicy() {
465             return mPolicy;
466         }
467 
getLastUpdate()468         public long getLastUpdate() {
469             return mLastUpdate;
470         }
471 
472         @Override
toString()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 
PendingUpdate(HomeSP homeSP, String url, boolean policy)484         private PendingUpdate(HomeSP homeSP, String url, boolean policy) {
485             mHomeSP = homeSP;
486             mPolicy = policy;
487             mBssid = 0L;
488             mUrl = url;
489         }
490 
PendingUpdate(long bssid, String url)491         private PendingUpdate(long bssid, String url) {
492             mBssid = bssid;
493             mUrl = url;
494             mHomeSP = null;
495             mPolicy = false;
496         }
497 
matches(WifiInfo wifiInfo, HomeSP activeSP)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 
passesRestriction(HomeSP restrictingSP)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 
remediate(Network network)535         private void remediate(Network network) {
536             RemediationHandler.this.remediate(mHomeSP != null ? mHomeSP.getFQDN() : null,
537                     mUrl, mPolicy, network);
538         }
539 
getHomeSP()540         private HomeSP getHomeSP() {
541             return mHomeSP;
542         }
543 
isPolicy()544         private boolean isPolicy() {
545             return mPolicy;
546         }
547 
isWnmBased()548         private boolean isWnmBased() {
549             return mHomeSP == null;
550         }
551 
matchProviderWithCurrentNetwork(String fqdn)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      */
remediate(final String spFqdn, final String url, final boolean policy, final Network network)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