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