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