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