1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.connectivity; 18 19 import static android.net.CaptivePortal.APP_RETURN_DISMISSED; 20 import static android.net.CaptivePortal.APP_RETURN_UNWANTED; 21 import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS; 22 23 import android.app.AlarmManager; 24 import android.app.PendingIntent; 25 import android.content.BroadcastReceiver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.net.CaptivePortal; 30 import android.net.ConnectivityManager; 31 import android.net.ICaptivePortal; 32 import android.net.NetworkRequest; 33 import android.net.ProxyInfo; 34 import android.net.TrafficStats; 35 import android.net.Uri; 36 import android.net.metrics.IpConnectivityLog; 37 import android.net.metrics.NetworkEvent; 38 import android.net.metrics.ValidationProbeEvent; 39 import android.net.util.Stopwatch; 40 import android.net.wifi.WifiInfo; 41 import android.net.wifi.WifiManager; 42 import android.os.Handler; 43 import android.os.Message; 44 import android.os.SystemClock; 45 import android.os.UserHandle; 46 import android.provider.Settings; 47 import android.telephony.CellIdentityCdma; 48 import android.telephony.CellIdentityGsm; 49 import android.telephony.CellIdentityLte; 50 import android.telephony.CellIdentityWcdma; 51 import android.telephony.CellInfo; 52 import android.telephony.CellInfoCdma; 53 import android.telephony.CellInfoGsm; 54 import android.telephony.CellInfoLte; 55 import android.telephony.CellInfoWcdma; 56 import android.telephony.TelephonyManager; 57 import android.text.TextUtils; 58 import android.util.LocalLog; 59 import android.util.LocalLog.ReadOnlyLocalLog; 60 import android.util.Log; 61 62 import com.android.internal.annotations.VisibleForTesting; 63 import com.android.internal.util.Protocol; 64 import com.android.internal.util.State; 65 import com.android.internal.util.StateMachine; 66 67 import java.io.IOException; 68 import java.net.HttpURLConnection; 69 import java.net.InetAddress; 70 import java.net.MalformedURLException; 71 import java.net.URL; 72 import java.net.UnknownHostException; 73 import java.util.List; 74 import java.util.Random; 75 import java.util.concurrent.CountDownLatch; 76 import java.util.concurrent.TimeUnit; 77 78 /** 79 * {@hide} 80 */ 81 public class NetworkMonitor extends StateMachine { 82 private static final String TAG = NetworkMonitor.class.getSimpleName(); 83 private static final boolean DBG = false; 84 85 // Default configuration values for captive portal detection probes. 86 // TODO: append a random length parameter to the default HTTPS url. 87 // TODO: randomize browser version ids in the default User-Agent String. 88 private static final String DEFAULT_HTTPS_URL = "https://www.google.com/generate_204"; 89 private static final String DEFAULT_HTTP_URL = 90 "http://connectivitycheck.gstatic.com/generate_204"; 91 private static final String DEFAULT_FALLBACK_URL = "http://www.google.com/gen_204"; 92 private static final String DEFAULT_USER_AGENT = "Mozilla/5.0 (X11; Linux x86_64) " 93 + "AppleWebKit/537.36 (KHTML, like Gecko) " 94 + "Chrome/52.0.2743.82 Safari/537.36"; 95 96 private static final int SOCKET_TIMEOUT_MS = 10000; 97 private static final int PROBE_TIMEOUT_MS = 3000; 98 99 public static final String ACTION_NETWORK_CONDITIONS_MEASURED = 100 "android.net.conn.NETWORK_CONDITIONS_MEASURED"; 101 public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type"; 102 public static final String EXTRA_NETWORK_TYPE = "extra_network_type"; 103 public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received"; 104 public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal"; 105 public static final String EXTRA_CELL_ID = "extra_cellid"; 106 public static final String EXTRA_SSID = "extra_ssid"; 107 public static final String EXTRA_BSSID = "extra_bssid"; 108 /** real time since boot */ 109 public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms"; 110 public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms"; 111 112 private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS = 113 "android.permission.ACCESS_NETWORK_CONDITIONS"; 114 115 // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED. 116 // The network should be used as a default internet connection. It was found to be: 117 // 1. a functioning network providing internet access, or 118 // 2. a captive portal and the user decided to use it as is. 119 public static final int NETWORK_TEST_RESULT_VALID = 0; 120 // After a network has been tested this result can be sent with EVENT_NETWORK_TESTED. 121 // The network should not be used as a default internet connection. It was found to be: 122 // 1. a captive portal and the user is prompted to sign-in, or 123 // 2. a captive portal and the user did not want to use it, or 124 // 3. a broken network (e.g. DNS failed, connect failed, HTTP request failed). 125 public static final int NETWORK_TEST_RESULT_INVALID = 1; 126 127 private static final int BASE = Protocol.BASE_NETWORK_MONITOR; 128 129 /** 130 * Inform NetworkMonitor that their network is connected. 131 * Initiates Network Validation. 132 */ 133 public static final int CMD_NETWORK_CONNECTED = BASE + 1; 134 135 /** 136 * Inform ConnectivityService that the network has been tested. 137 * obj = String representing URL that Internet probe was redirect to, if it was redirected. 138 * arg1 = One of the NETWORK_TESTED_RESULT_* constants. 139 * arg2 = NetID. 140 */ 141 public static final int EVENT_NETWORK_TESTED = BASE + 2; 142 143 /** 144 * Message to self indicating it's time to evaluate a network's connectivity. 145 * arg1 = Token to ignore old messages. 146 */ 147 private static final int CMD_REEVALUATE = BASE + 6; 148 149 /** 150 * Inform NetworkMonitor that the network has disconnected. 151 */ 152 public static final int CMD_NETWORK_DISCONNECTED = BASE + 7; 153 154 /** 155 * Force evaluation even if it has succeeded in the past. 156 * arg1 = UID responsible for requesting this reeval. Will be billed for data. 157 */ 158 public static final int CMD_FORCE_REEVALUATION = BASE + 8; 159 160 /** 161 * Message to self indicating captive portal app finished. 162 * arg1 = one of: APP_RETURN_DISMISSED, 163 * APP_RETURN_UNWANTED, 164 * APP_RETURN_WANTED_AS_IS 165 * obj = mCaptivePortalLoggedInResponseToken as String 166 */ 167 private static final int CMD_CAPTIVE_PORTAL_APP_FINISHED = BASE + 9; 168 169 /** 170 * Request ConnectivityService display provisioning notification. 171 * arg1 = Whether to make the notification visible. 172 * arg2 = NetID. 173 * obj = Intent to be launched when notification selected by user, null if !arg1. 174 */ 175 public static final int EVENT_PROVISIONING_NOTIFICATION = BASE + 10; 176 177 /** 178 * Message to self indicating sign-in app should be launched. 179 * Sent by mLaunchCaptivePortalAppBroadcastReceiver when the 180 * user touches the sign in notification. 181 */ 182 private static final int CMD_LAUNCH_CAPTIVE_PORTAL_APP = BASE + 11; 183 184 /** 185 * Retest network to see if captive portal is still in place. 186 * arg1 = UID responsible for requesting this reeval. Will be billed for data. 187 * 0 indicates self-initiated, so nobody to blame. 188 */ 189 private static final int CMD_CAPTIVE_PORTAL_RECHECK = BASE + 12; 190 191 // Start mReevaluateDelayMs at this value and double. 192 private static final int INITIAL_REEVALUATE_DELAY_MS = 1000; 193 private static final int MAX_REEVALUATE_DELAY_MS = 10*60*1000; 194 // Before network has been evaluated this many times, ignore repeated reevaluate requests. 195 private static final int IGNORE_REEVALUATE_ATTEMPTS = 5; 196 private int mReevaluateToken = 0; 197 private static final int INVALID_UID = -1; 198 private int mUidResponsibleForReeval = INVALID_UID; 199 // Stop blaming UID that requested re-evaluation after this many attempts. 200 private static final int BLAME_FOR_EVALUATION_ATTEMPTS = 5; 201 // Delay between reevaluations once a captive portal has been found. 202 private static final int CAPTIVE_PORTAL_REEVALUATE_DELAY_MS = 10*60*1000; 203 204 private final Context mContext; 205 private final Handler mConnectivityServiceHandler; 206 private final NetworkAgentInfo mNetworkAgentInfo; 207 private final int mNetId; 208 private final TelephonyManager mTelephonyManager; 209 private final WifiManager mWifiManager; 210 private final AlarmManager mAlarmManager; 211 private final NetworkRequest mDefaultRequest; 212 private final IpConnectivityLog mMetricsLog; 213 214 private boolean mIsCaptivePortalCheckEnabled; 215 private boolean mUseHttps; 216 217 // Set if the user explicitly selected "Do not use this network" in captive portal sign-in app. 218 private boolean mUserDoesNotWant = false; 219 // Avoids surfacing "Sign in to network" notification. 220 private boolean mDontDisplaySigninNotification = false; 221 222 public boolean systemReady = false; 223 224 private final State mDefaultState = new DefaultState(); 225 private final State mValidatedState = new ValidatedState(); 226 private final State mMaybeNotifyState = new MaybeNotifyState(); 227 private final State mEvaluatingState = new EvaluatingState(); 228 private final State mCaptivePortalState = new CaptivePortalState(); 229 230 private CustomIntentReceiver mLaunchCaptivePortalAppBroadcastReceiver = null; 231 232 private final LocalLog validationLogs = new LocalLog(20); // 20 lines 233 234 private final Stopwatch mEvaluationTimer = new Stopwatch(); 235 236 // This variable is set before transitioning to the mCaptivePortalState. 237 private CaptivePortalProbeResult mLastPortalProbeResult = CaptivePortalProbeResult.FAILED; 238 239 public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo, 240 NetworkRequest defaultRequest) { 241 this(context, handler, networkAgentInfo, defaultRequest, new IpConnectivityLog()); 242 } 243 244 @VisibleForTesting 245 protected NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo, 246 NetworkRequest defaultRequest, IpConnectivityLog logger) { 247 // Add suffix indicating which NetworkMonitor we're talking about. 248 super(TAG + networkAgentInfo.name()); 249 250 mContext = context; 251 mMetricsLog = logger; 252 mConnectivityServiceHandler = handler; 253 mNetworkAgentInfo = networkAgentInfo; 254 mNetId = mNetworkAgentInfo.network.netId; 255 mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); 256 mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE); 257 mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); 258 mDefaultRequest = defaultRequest; 259 260 addState(mDefaultState); 261 addState(mValidatedState, mDefaultState); 262 addState(mMaybeNotifyState, mDefaultState); 263 addState(mEvaluatingState, mMaybeNotifyState); 264 addState(mCaptivePortalState, mMaybeNotifyState); 265 setInitialState(mDefaultState); 266 267 mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(), 268 Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; 269 mUseHttps = Settings.Global.getInt(mContext.getContentResolver(), 270 Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1; 271 272 start(); 273 } 274 275 @Override 276 protected void log(String s) { 277 if (DBG) Log.d(TAG + "/" + mNetworkAgentInfo.name(), s); 278 } 279 280 private void validationLog(String s) { 281 if (DBG) log(s); 282 validationLogs.log(s); 283 } 284 285 public ReadOnlyLocalLog getValidationLogs() { 286 return validationLogs.readOnlyLocalLog(); 287 } 288 289 // DefaultState is the parent of all States. It exists only to handle CMD_* messages but 290 // does not entail any real state (hence no enter() or exit() routines). 291 private class DefaultState extends State { 292 @Override 293 public boolean processMessage(Message message) { 294 switch (message.what) { 295 case CMD_NETWORK_CONNECTED: 296 logNetworkEvent(NetworkEvent.NETWORK_CONNECTED); 297 transitionTo(mEvaluatingState); 298 return HANDLED; 299 case CMD_NETWORK_DISCONNECTED: 300 logNetworkEvent(NetworkEvent.NETWORK_DISCONNECTED); 301 if (mLaunchCaptivePortalAppBroadcastReceiver != null) { 302 mContext.unregisterReceiver(mLaunchCaptivePortalAppBroadcastReceiver); 303 mLaunchCaptivePortalAppBroadcastReceiver = null; 304 } 305 quit(); 306 return HANDLED; 307 case CMD_FORCE_REEVALUATION: 308 case CMD_CAPTIVE_PORTAL_RECHECK: 309 log("Forcing reevaluation for UID " + message.arg1); 310 mUidResponsibleForReeval = message.arg1; 311 transitionTo(mEvaluatingState); 312 return HANDLED; 313 case CMD_CAPTIVE_PORTAL_APP_FINISHED: 314 log("CaptivePortal App responded with " + message.arg1); 315 316 // If the user has seen and acted on a captive portal notification, and the 317 // captive portal app is now closed, disable HTTPS probes. This avoids the 318 // following pathological situation: 319 // 320 // 1. HTTP probe returns a captive portal, HTTPS probe fails or times out. 321 // 2. User opens the app and logs into the captive portal. 322 // 3. HTTP starts working, but HTTPS still doesn't work for some other reason - 323 // perhaps due to the network blocking HTTPS? 324 // 325 // In this case, we'll fail to validate the network even after the app is 326 // dismissed. There is now no way to use this network, because the app is now 327 // gone, so the user cannot select "Use this network as is". 328 mUseHttps = false; 329 330 switch (message.arg1) { 331 case APP_RETURN_DISMISSED: 332 sendMessage(CMD_FORCE_REEVALUATION, 0 /* no UID */, 0); 333 break; 334 case APP_RETURN_WANTED_AS_IS: 335 mDontDisplaySigninNotification = true; 336 // TODO: Distinguish this from a network that actually validates. 337 // Displaying the "!" on the system UI icon may still be a good idea. 338 transitionTo(mValidatedState); 339 break; 340 case APP_RETURN_UNWANTED: 341 mDontDisplaySigninNotification = true; 342 mUserDoesNotWant = true; 343 mConnectivityServiceHandler.sendMessage(obtainMessage( 344 EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, 345 mNetId, null)); 346 // TODO: Should teardown network. 347 mUidResponsibleForReeval = 0; 348 transitionTo(mEvaluatingState); 349 break; 350 } 351 return HANDLED; 352 default: 353 return HANDLED; 354 } 355 } 356 } 357 358 // Being in the ValidatedState State indicates a Network is: 359 // - Successfully validated, or 360 // - Wanted "as is" by the user, or 361 // - Does not satisfy the default NetworkRequest and so validation has been skipped. 362 private class ValidatedState extends State { 363 @Override 364 public void enter() { 365 maybeLogEvaluationResult(NetworkEvent.NETWORK_VALIDATED); 366 mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED, 367 NETWORK_TEST_RESULT_VALID, mNetworkAgentInfo.network.netId, null)); 368 } 369 370 @Override 371 public boolean processMessage(Message message) { 372 switch (message.what) { 373 case CMD_NETWORK_CONNECTED: 374 transitionTo(mValidatedState); 375 return HANDLED; 376 default: 377 return NOT_HANDLED; 378 } 379 } 380 } 381 382 // Being in the MaybeNotifyState State indicates the user may have been notified that sign-in 383 // is required. This State takes care to clear the notification upon exit from the State. 384 private class MaybeNotifyState extends State { 385 @Override 386 public boolean processMessage(Message message) { 387 switch (message.what) { 388 case CMD_LAUNCH_CAPTIVE_PORTAL_APP: 389 final Intent intent = new Intent( 390 ConnectivityManager.ACTION_CAPTIVE_PORTAL_SIGN_IN); 391 intent.putExtra(ConnectivityManager.EXTRA_NETWORK, mNetworkAgentInfo.network); 392 intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL, 393 new CaptivePortal(new ICaptivePortal.Stub() { 394 @Override 395 public void appResponse(int response) { 396 if (response == APP_RETURN_WANTED_AS_IS) { 397 mContext.enforceCallingPermission( 398 android.Manifest.permission.CONNECTIVITY_INTERNAL, 399 "CaptivePortal"); 400 } 401 sendMessage(CMD_CAPTIVE_PORTAL_APP_FINISHED, response); 402 } 403 })); 404 intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL, 405 mLastPortalProbeResult.detectUrl); 406 intent.setFlags( 407 Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK); 408 mContext.startActivityAsUser(intent, UserHandle.CURRENT); 409 return HANDLED; 410 default: 411 return NOT_HANDLED; 412 } 413 } 414 415 @Override 416 public void exit() { 417 Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 0, 418 mNetworkAgentInfo.network.netId, null); 419 mConnectivityServiceHandler.sendMessage(message); 420 } 421 } 422 423 /** 424 * Result of calling isCaptivePortal(). 425 * @hide 426 */ 427 @VisibleForTesting 428 public static final class CaptivePortalProbeResult { 429 static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599); 430 431 private final int mHttpResponseCode; // HTTP response code returned from Internet probe. 432 final String redirectUrl; // Redirect destination returned from Internet probe. 433 final String detectUrl; // URL where a 204 response code indicates 434 // captive portal has been appeased. 435 436 public CaptivePortalProbeResult( 437 int httpResponseCode, String redirectUrl, String detectUrl) { 438 mHttpResponseCode = httpResponseCode; 439 this.redirectUrl = redirectUrl; 440 this.detectUrl = detectUrl; 441 } 442 443 public CaptivePortalProbeResult(int httpResponseCode) { 444 this(httpResponseCode, null, null); 445 } 446 447 boolean isSuccessful() { return mHttpResponseCode == 204; } 448 boolean isPortal() { 449 return !isSuccessful() && mHttpResponseCode >= 200 && mHttpResponseCode <= 399; 450 } 451 } 452 453 // Being in the EvaluatingState State indicates the Network is being evaluated for internet 454 // connectivity, or that the user has indicated that this network is unwanted. 455 private class EvaluatingState extends State { 456 private int mReevaluateDelayMs; 457 private int mAttempts; 458 459 @Override 460 public void enter() { 461 // If we have already started to track time spent in EvaluatingState 462 // don't reset the timer due simply to, say, commands or events that 463 // cause us to exit and re-enter EvaluatingState. 464 if (!mEvaluationTimer.isStarted()) { 465 mEvaluationTimer.start(); 466 } 467 sendMessage(CMD_REEVALUATE, ++mReevaluateToken, 0); 468 if (mUidResponsibleForReeval != INVALID_UID) { 469 TrafficStats.setThreadStatsUid(mUidResponsibleForReeval); 470 mUidResponsibleForReeval = INVALID_UID; 471 } 472 mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS; 473 mAttempts = 0; 474 } 475 476 @Override 477 public boolean processMessage(Message message) { 478 switch (message.what) { 479 case CMD_REEVALUATE: 480 if (message.arg1 != mReevaluateToken || mUserDoesNotWant) 481 return HANDLED; 482 // Don't bother validating networks that don't satisify the default request. 483 // This includes: 484 // - VPNs which can be considered explicitly desired by the user and the 485 // user's desire trumps whether the network validates. 486 // - Networks that don't provide internet access. It's unclear how to 487 // validate such networks. 488 // - Untrusted networks. It's unsafe to prompt the user to sign-in to 489 // such networks and the user didn't express interest in connecting to 490 // such networks (an app did) so the user may be unhappily surprised when 491 // asked to sign-in to a network they didn't want to connect to in the 492 // first place. Validation could be done to adjust the network scores 493 // however these networks are app-requested and may not be intended for 494 // general usage, in which case general validation may not be an accurate 495 // measure of the network's quality. Only the app knows how to evaluate 496 // the network so don't bother validating here. Furthermore sending HTTP 497 // packets over the network may be undesirable, for example an extremely 498 // expensive metered network, or unwanted leaking of the User Agent string. 499 if (!mDefaultRequest.networkCapabilities.satisfiedByNetworkCapabilities( 500 mNetworkAgentInfo.networkCapabilities)) { 501 validationLog("Network would not satisfy default request, not validating"); 502 transitionTo(mValidatedState); 503 return HANDLED; 504 } 505 mAttempts++; 506 // Note: This call to isCaptivePortal() could take up to a minute. Resolving the 507 // server's IP addresses could hit the DNS timeout, and attempting connections 508 // to each of the server's several IP addresses (currently one IPv4 and one 509 // IPv6) could each take SOCKET_TIMEOUT_MS. During this time this StateMachine 510 // will be unresponsive. isCaptivePortal() could be executed on another Thread 511 // if this is found to cause problems. 512 CaptivePortalProbeResult probeResult = isCaptivePortal(); 513 if (probeResult.isSuccessful()) { 514 transitionTo(mValidatedState); 515 } else if (probeResult.isPortal()) { 516 mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED, 517 NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.redirectUrl)); 518 mLastPortalProbeResult = probeResult; 519 transitionTo(mCaptivePortalState); 520 } else { 521 final Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0); 522 sendMessageDelayed(msg, mReevaluateDelayMs); 523 logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED); 524 mConnectivityServiceHandler.sendMessage(obtainMessage( 525 EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId, 526 probeResult.redirectUrl)); 527 if (mAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) { 528 // Don't continue to blame UID forever. 529 TrafficStats.clearThreadStatsUid(); 530 } 531 mReevaluateDelayMs *= 2; 532 if (mReevaluateDelayMs > MAX_REEVALUATE_DELAY_MS) { 533 mReevaluateDelayMs = MAX_REEVALUATE_DELAY_MS; 534 } 535 } 536 return HANDLED; 537 case CMD_FORCE_REEVALUATION: 538 // Before IGNORE_REEVALUATE_ATTEMPTS attempts are made, 539 // ignore any re-evaluation requests. After, restart the 540 // evaluation process via EvaluatingState#enter. 541 return (mAttempts < IGNORE_REEVALUATE_ATTEMPTS) ? HANDLED : NOT_HANDLED; 542 default: 543 return NOT_HANDLED; 544 } 545 } 546 547 @Override 548 public void exit() { 549 TrafficStats.clearThreadStatsUid(); 550 } 551 } 552 553 // BroadcastReceiver that waits for a particular Intent and then posts a message. 554 private class CustomIntentReceiver extends BroadcastReceiver { 555 private final int mToken; 556 private final int mWhat; 557 private final String mAction; 558 CustomIntentReceiver(String action, int token, int what) { 559 mToken = token; 560 mWhat = what; 561 mAction = action + "_" + mNetworkAgentInfo.network.netId + "_" + token; 562 mContext.registerReceiver(this, new IntentFilter(mAction)); 563 } 564 public PendingIntent getPendingIntent() { 565 final Intent intent = new Intent(mAction); 566 intent.setPackage(mContext.getPackageName()); 567 return PendingIntent.getBroadcast(mContext, 0, intent, 0); 568 } 569 @Override 570 public void onReceive(Context context, Intent intent) { 571 if (intent.getAction().equals(mAction)) sendMessage(obtainMessage(mWhat, mToken)); 572 } 573 } 574 575 // Being in the CaptivePortalState State indicates a captive portal was detected and the user 576 // has been shown a notification to sign-in. 577 private class CaptivePortalState extends State { 578 private static final String ACTION_LAUNCH_CAPTIVE_PORTAL_APP = 579 "android.net.netmon.launchCaptivePortalApp"; 580 581 @Override 582 public void enter() { 583 maybeLogEvaluationResult(NetworkEvent.NETWORK_CAPTIVE_PORTAL_FOUND); 584 // Don't annoy user with sign-in notifications. 585 if (mDontDisplaySigninNotification) return; 586 // Create a CustomIntentReceiver that sends us a 587 // CMD_LAUNCH_CAPTIVE_PORTAL_APP message when the user 588 // touches the notification. 589 if (mLaunchCaptivePortalAppBroadcastReceiver == null) { 590 // Wait for result. 591 mLaunchCaptivePortalAppBroadcastReceiver = new CustomIntentReceiver( 592 ACTION_LAUNCH_CAPTIVE_PORTAL_APP, new Random().nextInt(), 593 CMD_LAUNCH_CAPTIVE_PORTAL_APP); 594 } 595 // Display the sign in notification. 596 Message message = obtainMessage(EVENT_PROVISIONING_NOTIFICATION, 1, 597 mNetworkAgentInfo.network.netId, 598 mLaunchCaptivePortalAppBroadcastReceiver.getPendingIntent()); 599 mConnectivityServiceHandler.sendMessage(message); 600 // Retest for captive portal occasionally. 601 sendMessageDelayed(CMD_CAPTIVE_PORTAL_RECHECK, 0 /* no UID */, 602 CAPTIVE_PORTAL_REEVALUATE_DELAY_MS); 603 } 604 605 @Override 606 public void exit() { 607 removeMessages(CMD_CAPTIVE_PORTAL_RECHECK); 608 } 609 } 610 611 private static String getCaptivePortalServerHttpsUrl(Context context) { 612 return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTPS_URL, DEFAULT_HTTPS_URL); 613 } 614 615 public static String getCaptivePortalServerHttpUrl(Context context) { 616 return getSetting(context, Settings.Global.CAPTIVE_PORTAL_HTTP_URL, DEFAULT_HTTP_URL); 617 } 618 619 private static String getCaptivePortalFallbackUrl(Context context) { 620 return getSetting(context, 621 Settings.Global.CAPTIVE_PORTAL_FALLBACK_URL, DEFAULT_FALLBACK_URL); 622 } 623 624 private static String getCaptivePortalUserAgent(Context context) { 625 return getSetting(context, Settings.Global.CAPTIVE_PORTAL_USER_AGENT, DEFAULT_USER_AGENT); 626 } 627 628 private static String getSetting(Context context, String symbol, String defaultValue) { 629 final String value = Settings.Global.getString(context.getContentResolver(), symbol); 630 return value != null ? value : defaultValue; 631 } 632 633 @VisibleForTesting 634 protected CaptivePortalProbeResult isCaptivePortal() { 635 if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204); 636 637 URL pacUrl = null, httpsUrl = null, httpUrl = null, fallbackUrl = null; 638 639 // On networks with a PAC instead of fetching a URL that should result in a 204 640 // response, we instead simply fetch the PAC script. This is done for a few reasons: 641 // 1. At present our PAC code does not yet handle multiple PACs on multiple networks 642 // until something like https://android-review.googlesource.com/#/c/115180/ lands. 643 // Network.openConnection() will ignore network-specific PACs and instead fetch 644 // using NO_PROXY. If a PAC is in place, the only fetch we know will succeed with 645 // NO_PROXY is the fetch of the PAC itself. 646 // 2. To proxy the generate_204 fetch through a PAC would require a number of things 647 // happen before the fetch can commence, namely: 648 // a) the PAC script be fetched 649 // b) a PAC script resolver service be fired up and resolve the captive portal 650 // server. 651 // Network validation could be delayed until these prerequisities are satisifed or 652 // could simply be left to race them. Neither is an optimal solution. 653 // 3. PAC scripts are sometimes used to block or restrict Internet access and may in 654 // fact block fetching of the generate_204 URL which would lead to false negative 655 // results for network validation. 656 final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy(); 657 if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) { 658 pacUrl = makeURL(proxyInfo.getPacFileUrl().toString()); 659 if (pacUrl == null) { 660 return CaptivePortalProbeResult.FAILED; 661 } 662 } 663 664 if (pacUrl == null) { 665 httpsUrl = makeURL(getCaptivePortalServerHttpsUrl(mContext)); 666 httpUrl = makeURL(getCaptivePortalServerHttpUrl(mContext)); 667 fallbackUrl = makeURL(getCaptivePortalFallbackUrl(mContext)); 668 if (httpUrl == null || httpsUrl == null) { 669 return CaptivePortalProbeResult.FAILED; 670 } 671 } 672 673 long startTime = SystemClock.elapsedRealtime(); 674 675 // Pre-resolve the captive portal server host so we can log it. 676 // Only do this if HttpURLConnection is about to, to avoid any potentially 677 // unnecessary resolution. 678 String hostToResolve = null; 679 if (pacUrl != null) { 680 hostToResolve = pacUrl.getHost(); 681 } else if (proxyInfo != null) { 682 hostToResolve = proxyInfo.getHost(); 683 } else { 684 hostToResolve = httpUrl.getHost(); 685 } 686 687 if (!TextUtils.isEmpty(hostToResolve)) { 688 String probeName = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS); 689 final Stopwatch dnsTimer = new Stopwatch().start(); 690 int dnsResult; 691 long dnsLatency; 692 try { 693 InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(hostToResolve); 694 dnsResult = ValidationProbeEvent.DNS_SUCCESS; 695 dnsLatency = dnsTimer.stop(); 696 final StringBuffer connectInfo = new StringBuffer(", " + hostToResolve + "="); 697 for (InetAddress address : addresses) { 698 connectInfo.append(address.getHostAddress()); 699 if (address != addresses[addresses.length-1]) connectInfo.append(","); 700 } 701 validationLog(probeName + " OK " + dnsLatency + "ms" + connectInfo); 702 } catch (UnknownHostException e) { 703 dnsResult = ValidationProbeEvent.DNS_FAILURE; 704 dnsLatency = dnsTimer.stop(); 705 validationLog(probeName + " FAIL " + dnsLatency + "ms, " + hostToResolve); 706 } 707 logValidationProbe(dnsLatency, ValidationProbeEvent.PROBE_DNS, dnsResult); 708 } 709 710 CaptivePortalProbeResult result; 711 if (pacUrl != null) { 712 result = sendHttpProbe(pacUrl, ValidationProbeEvent.PROBE_PAC); 713 } else if (mUseHttps) { 714 result = sendParallelHttpProbes(httpsUrl, httpUrl, fallbackUrl); 715 } else { 716 result = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP); 717 } 718 719 long endTime = SystemClock.elapsedRealtime(); 720 721 sendNetworkConditionsBroadcast(true /* response received */, 722 result.isPortal() /* isCaptivePortal */, 723 startTime, endTime); 724 725 return result; 726 } 727 728 /** 729 * Do a URL fetch on a known server to see if we get the data we expect. 730 * Returns HTTP response code. 731 */ 732 @VisibleForTesting 733 protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType) { 734 HttpURLConnection urlConnection = null; 735 int httpResponseCode = 599; 736 String redirectUrl = null; 737 final Stopwatch probeTimer = new Stopwatch().start(); 738 try { 739 urlConnection = (HttpURLConnection) mNetworkAgentInfo.network.openConnection(url); 740 urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC); 741 urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); 742 urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); 743 urlConnection.setUseCaches(false); 744 final String userAgent = getCaptivePortalUserAgent(mContext); 745 if (userAgent != null) { 746 urlConnection.setRequestProperty("User-Agent", userAgent); 747 } 748 749 // Time how long it takes to get a response to our request 750 long requestTimestamp = SystemClock.elapsedRealtime(); 751 752 httpResponseCode = urlConnection.getResponseCode(); 753 redirectUrl = urlConnection.getHeaderField("location"); 754 755 // Time how long it takes to get a response to our request 756 long responseTimestamp = SystemClock.elapsedRealtime(); 757 758 validationLog(ValidationProbeEvent.getProbeName(probeType) + " " + url + 759 " time=" + (responseTimestamp - requestTimestamp) + "ms" + 760 " ret=" + httpResponseCode + 761 " headers=" + urlConnection.getHeaderFields()); 762 // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive 763 // portal. The only example of this seen so far was a captive portal. For 764 // the time being go with prior behavior of assuming it's not a captive 765 // portal. If it is considered a captive portal, a different sign-in URL 766 // is needed (i.e. can't browse a 204). This could be the result of an HTTP 767 // proxy server. 768 769 // Consider 200 response with "Content-length=0" to not be a captive portal. 770 // There's no point in considering this a captive portal as the user cannot 771 // sign-in to an empty page. Probably the result of a broken transparent proxy. 772 // See http://b/9972012. 773 if (httpResponseCode == 200 && urlConnection.getContentLength() == 0) { 774 validationLog("Empty 200 response interpreted as 204 response."); 775 httpResponseCode = 204; 776 } 777 778 if (httpResponseCode == 200 && probeType == ValidationProbeEvent.PROBE_PAC) { 779 validationLog("PAC fetch 200 response interpreted as 204 response."); 780 httpResponseCode = 204; 781 } 782 } catch (IOException e) { 783 validationLog("Probably not a portal: exception " + e); 784 if (httpResponseCode == 599) { 785 // TODO: Ping gateway and DNS server and log results. 786 } 787 } finally { 788 if (urlConnection != null) { 789 urlConnection.disconnect(); 790 } 791 } 792 logValidationProbe(probeTimer.stop(), probeType, httpResponseCode); 793 return new CaptivePortalProbeResult(httpResponseCode, redirectUrl, url.toString()); 794 } 795 796 private CaptivePortalProbeResult sendParallelHttpProbes( 797 URL httpsUrl, URL httpUrl, URL fallbackUrl) { 798 // Number of probes to wait for. If a probe completes with a conclusive answer 799 // it shortcuts the latch immediately by forcing the count to 0. 800 final CountDownLatch latch = new CountDownLatch(2); 801 802 final class ProbeThread extends Thread { 803 private final boolean mIsHttps; 804 private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED; 805 806 public ProbeThread(boolean isHttps) { 807 mIsHttps = isHttps; 808 } 809 810 public CaptivePortalProbeResult result() { 811 return mResult; 812 } 813 814 @Override 815 public void run() { 816 if (mIsHttps) { 817 mResult = sendHttpProbe(httpsUrl, ValidationProbeEvent.PROBE_HTTPS); 818 } else { 819 mResult = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP); 820 } 821 if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) { 822 // Stop waiting immediately if https succeeds or if http finds a portal. 823 while (latch.getCount() > 0) { 824 latch.countDown(); 825 } 826 } 827 // Signal this probe has completed. 828 latch.countDown(); 829 } 830 } 831 832 final ProbeThread httpsProbe = new ProbeThread(true); 833 final ProbeThread httpProbe = new ProbeThread(false); 834 835 try { 836 httpsProbe.start(); 837 httpProbe.start(); 838 latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS); 839 } catch (InterruptedException e) { 840 validationLog("Error: probes wait interrupted!"); 841 return CaptivePortalProbeResult.FAILED; 842 } 843 844 final CaptivePortalProbeResult httpsResult = httpsProbe.result(); 845 final CaptivePortalProbeResult httpResult = httpProbe.result(); 846 847 // Look for a conclusive probe result first. 848 if (httpResult.isPortal()) { 849 return httpResult; 850 } 851 // httpsResult.isPortal() is not expected, but check it nonetheless. 852 if (httpsResult.isPortal() || httpsResult.isSuccessful()) { 853 return httpsResult; 854 } 855 // If a fallback url is specified, use a fallback probe to try again portal detection. 856 if (fallbackUrl != null) { 857 CaptivePortalProbeResult result = 858 sendHttpProbe(fallbackUrl, ValidationProbeEvent.PROBE_FALLBACK); 859 if (result.isPortal()) { 860 return result; 861 } 862 } 863 // Otherwise wait until https probe completes and use its result. 864 try { 865 httpsProbe.join(); 866 } catch (InterruptedException e) { 867 validationLog("Error: https probe wait interrupted!"); 868 return CaptivePortalProbeResult.FAILED; 869 } 870 return httpsProbe.result(); 871 } 872 873 private URL makeURL(String url) { 874 if (url != null) { 875 try { 876 return new URL(url); 877 } catch (MalformedURLException e) { 878 validationLog("Bad URL: " + url); 879 } 880 } 881 return null; 882 } 883 884 /** 885 * @param responseReceived - whether or not we received a valid HTTP response to our request. 886 * If false, isCaptivePortal and responseTimestampMs are ignored 887 * TODO: This should be moved to the transports. The latency could be passed to the transports 888 * along with the captive portal result. Currently the TYPE_MOBILE broadcasts appear unused so 889 * perhaps this could just be added to the WiFi transport only. 890 */ 891 private void sendNetworkConditionsBroadcast(boolean responseReceived, boolean isCaptivePortal, 892 long requestTimestampMs, long responseTimestampMs) { 893 if (Settings.Global.getInt(mContext.getContentResolver(), 894 Settings.Global.WIFI_SCAN_ALWAYS_AVAILABLE, 0) == 0) { 895 return; 896 } 897 898 if (systemReady == false) return; 899 900 Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED); 901 switch (mNetworkAgentInfo.networkInfo.getType()) { 902 case ConnectivityManager.TYPE_WIFI: 903 WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo(); 904 if (currentWifiInfo != null) { 905 // NOTE: getSSID()'s behavior changed in API 17; before that, SSIDs were not 906 // surrounded by double quotation marks (thus violating the Javadoc), but this 907 // was changed to match the Javadoc in API 17. Since clients may have started 908 // sanitizing the output of this method since API 17 was released, we should 909 // not change it here as it would become impossible to tell whether the SSID is 910 // simply being surrounded by quotes due to the API, or whether those quotes 911 // are actually part of the SSID. 912 latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID()); 913 latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID()); 914 } else { 915 if (DBG) logw("network info is TYPE_WIFI but no ConnectionInfo found"); 916 return; 917 } 918 break; 919 case ConnectivityManager.TYPE_MOBILE: 920 latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType()); 921 List<CellInfo> info = mTelephonyManager.getAllCellInfo(); 922 if (info == null) return; 923 int numRegisteredCellInfo = 0; 924 for (CellInfo cellInfo : info) { 925 if (cellInfo.isRegistered()) { 926 numRegisteredCellInfo++; 927 if (numRegisteredCellInfo > 1) { 928 log("more than one registered CellInfo. Can't " + 929 "tell which is active. Bailing."); 930 return; 931 } 932 if (cellInfo instanceof CellInfoCdma) { 933 CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity(); 934 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 935 } else if (cellInfo instanceof CellInfoGsm) { 936 CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity(); 937 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 938 } else if (cellInfo instanceof CellInfoLte) { 939 CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity(); 940 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 941 } else if (cellInfo instanceof CellInfoWcdma) { 942 CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity(); 943 latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId); 944 } else { 945 if (DBG) logw("Registered cellinfo is unrecognized"); 946 return; 947 } 948 } 949 } 950 break; 951 default: 952 return; 953 } 954 latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkAgentInfo.networkInfo.getType()); 955 latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived); 956 latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs); 957 958 if (responseReceived) { 959 latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal); 960 latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs); 961 } 962 mContext.sendBroadcastAsUser(latencyBroadcast, UserHandle.CURRENT, 963 PERMISSION_ACCESS_NETWORK_CONDITIONS); 964 } 965 966 private void logNetworkEvent(int evtype) { 967 mMetricsLog.log(new NetworkEvent(mNetId, evtype)); 968 } 969 970 private void maybeLogEvaluationResult(int evtype) { 971 if (mEvaluationTimer.isRunning()) { 972 mMetricsLog.log(new NetworkEvent(mNetId, evtype, mEvaluationTimer.stop())); 973 mEvaluationTimer.reset(); 974 } 975 } 976 977 private void logValidationProbe(long durationMs, int probeType, int probeResult) { 978 mMetricsLog.log(new ValidationProbeEvent(mNetId, durationMs, probeType, probeResult)); 979 } 980 } 981