Loading api/system-current.txt +6 −2 Original line number Diff line number Diff line Loading @@ -26170,8 +26170,12 @@ package android.net.metrics { method public static void logEvent(int, long, int, int); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.net.metrics.ValidationProbeEvent> CREATOR; field public static final int PROBE_HTTP = 0; // 0x0 field public static final int PROBE_HTTPS = 1; // 0x1 field public static final int DNS_FAILURE = 0; // 0x0 field public static final int DNS_SUCCESS = 1; // 0x1 field public static final int PROBE_DNS = 0; // 0x0 field public static final int PROBE_HTTP = 1; // 0x1 field public static final int PROBE_HTTPS = 2; // 0x2 field public static final int PROBE_PAC = 3; // 0x3 field public final long durationMs; field public final int netId; field public final int probeType; core/java/android/net/metrics/ValidationProbeEvent.java +13 −3 Original line number Diff line number Diff line Loading @@ -29,8 +29,13 @@ import com.android.internal.util.MessageUtils; @SystemApi public final class ValidationProbeEvent extends IpConnectivityEvent implements Parcelable { public static final int PROBE_HTTP = 0; public static final int PROBE_HTTPS = 1; public static final int PROBE_DNS = 0; public static final int PROBE_HTTP = 1; public static final int PROBE_HTTPS = 2; public static final int PROBE_PAC = 3; public static final int DNS_FAILURE = 0; public static final int DNS_SUCCESS = 1; public final int netId; public final long durationMs; Loading Loading @@ -73,6 +78,11 @@ public final class ValidationProbeEvent extends IpConnectivityEvent implements P } }; /** @hide */ public static String getProbeName(int probeType) { return Decoder.constants.get(probeType, "PROBE_???"); } public static void logEvent(int netId, long durationMs, int probeType, int returnCode) { logEvent(new ValidationProbeEvent(netId, durationMs, probeType, returnCode)); } Loading @@ -80,7 +90,7 @@ public final class ValidationProbeEvent extends IpConnectivityEvent implements P @Override public String toString() { return String.format("ValidationProbeEvent(%d, %s:%d, %dms)", netId, Decoder.constants.get(probeType), returnCode, durationMs); netId, getProbeName(probeType), returnCode, durationMs); } final static class Decoder { Loading core/java/android/provider/Settings.java +9 −0 Original line number Diff line number Diff line Loading @@ -7705,6 +7705,15 @@ public final class Settings { */ public static final String CAPTIVE_PORTAL_SERVER = "captive_portal_server"; /** * Whether to use HTTPS for network validation. This is enabled by default and the setting * needs to be set to 0 to disable it. This setting is a misnomer because captive portals * don't actually use HTTPS, but it's consistent with the other settings. * * @hide */ public static final String CAPTIVE_PORTAL_USE_HTTPS = "captive_portal_use_https"; /** * Whether network service discovery is enabled. * Loading services/core/java/com/android/server/connectivity/NetworkMonitor.java +199 −60 Original line number Diff line number Diff line Loading @@ -71,7 +71,11 @@ import com.android.server.connectivity.NetworkAgentInfo; import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.UnknownHostException; import java.net.URL; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import java.util.List; import java.util.Random; Loading Loading @@ -228,7 +232,8 @@ public class NetworkMonitor extends StateMachine { private final AlarmManager mAlarmManager; private final NetworkRequest mDefaultRequest; private boolean mIsCaptivePortalCheckEnabled = false; private boolean mIsCaptivePortalCheckEnabled; private boolean mUseHttps; // Set if the user explicitly selected "Do not use this network" in captive portal sign-in app. private boolean mUserDoesNotWant = false; Loading Loading @@ -276,6 +281,8 @@ public class NetworkMonitor extends StateMachine { mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; mUseHttps = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1; start(); } Loading Loading @@ -324,6 +331,21 @@ public class NetworkMonitor extends StateMachine { return HANDLED; case CMD_CAPTIVE_PORTAL_APP_FINISHED: log("CaptivePortal App responded with " + message.arg1); // If the user has seen and acted on a captive portal notification, and the // captive portal app is now closed, disable HTTPS probes. This avoids the // following pathological situation: // // 1. HTTP probe returns a captive portal, HTTPS probe fails or times out. // 2. User opens the app and logs into the captive portal. // 3. HTTP starts working, but HTTPS still doesn't work for some other reason - // perhaps due to the network blocking HTTPS? // // In this case, we'll fail to validate the network even after the app is // dismissed. There is now no way to use this network, because the app is now // gone, so the user cannot select "Use this network as is". mUseHttps = false; switch (message.arg1) { case APP_RETURN_DISMISSED: sendMessage(CMD_FORCE_REEVALUATION, 0 /* no UID */, 0); Loading Loading @@ -424,6 +446,8 @@ public class NetworkMonitor extends StateMachine { */ @VisibleForTesting public static final class CaptivePortalProbeResult { static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599, null); final int mHttpResponseCode; // HTTP response code returned from Internet probe. final String mRedirectUrl; // Redirect destination returned from Internet probe. Loading @@ -431,6 +455,11 @@ public class NetworkMonitor extends StateMachine { mHttpResponseCode = httpResponseCode; mRedirectUrl = redirectUrl; } boolean isSuccessful() { return mHttpResponseCode == 204; } boolean isPortal() { return !isSuccessful() && mHttpResponseCode >= 200 && mHttpResponseCode <= 399; } } // Being in the EvaluatingState State indicates the Network is being evaluated for internet Loading Loading @@ -481,6 +510,7 @@ public class NetworkMonitor extends StateMachine { // expensive metered network, or unwanted leaking of the User Agent string. if (!mDefaultRequest.networkCapabilities.satisfiedByNetworkCapabilities( mNetworkAgentInfo.networkCapabilities)) { validationLog("Network would not satisfy default request, not validating"); transitionTo(mValidatedState); return HANDLED; } Loading @@ -492,10 +522,9 @@ public class NetworkMonitor extends StateMachine { // will be unresponsive. isCaptivePortal() could be executed on another Thread // if this is found to cause problems. CaptivePortalProbeResult probeResult = isCaptivePortal(); if (probeResult.mHttpResponseCode == 204) { if (probeResult.isSuccessful()) { transitionTo(mValidatedState); } else if (probeResult.mHttpResponseCode >= 200 && probeResult.mHttpResponseCode <= 399) { } else if (probeResult.isPortal()) { mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.mRedirectUrl)); transitionTo(mCaptivePortalState); Loading Loading @@ -659,27 +688,23 @@ public class NetworkMonitor extends StateMachine { } } public static String getCaptivePortalServerUrl(Context context) { private static String getCaptivePortalServerUrl(Context context, boolean isHttps) { String server = Settings.Global.getString(context.getContentResolver(), Settings.Global.CAPTIVE_PORTAL_SERVER); if (server == null) server = DEFAULT_SERVER; return "http://" + server + "/generate_204"; return (isHttps ? "https" : "http") + "://" + server + "/generate_204"; } public static String getCaptivePortalServerUrl(Context context) { return getCaptivePortalServerUrl(context, false); } /** * Do a URL fetch on a known server to see if we get the data we expect. * Returns HTTP response code. */ @VisibleForTesting protected CaptivePortalProbeResult isCaptivePortal() { if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204, null); HttpURLConnection urlConnection = null; int httpResponseCode = 599; String redirectUrl = null; final Stopwatch probeTimer = new Stopwatch().start(); try { URL url = new URL(getCaptivePortalServerUrl(mContext)); URL pacUrl = null, httpUrl = null, httpsUrl = null; // On networks with a PAC instead of fetching a URL that should result in a 204 // response, we instead simply fetch the PAC script. This is done for a few reasons: // 1. At present our PAC code does not yet handle multiple PACs on multiple networks Loading @@ -697,34 +722,93 @@ public class NetworkMonitor extends StateMachine { // 3. PAC scripts are sometimes used to block or restrict Internet access and may in // fact block fetching of the generate_204 URL which would lead to false negative // results for network validation. boolean fetchPac = false; final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy(); if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) { url = new URL(proxyInfo.getPacFileUrl().toString()); fetchPac = true; try { pacUrl = new URL(proxyInfo.getPacFileUrl().toString()); } catch (MalformedURLException e) { validationLog("Invalid PAC URL: " + proxyInfo.getPacFileUrl().toString()); return CaptivePortalProbeResult.FAILED; } final StringBuffer connectInfo = new StringBuffer(); String hostToResolve = null; // Only resolve a host if HttpURLConnection is about to, to avoid any potentially } if (pacUrl == null) { try { httpUrl = new URL(getCaptivePortalServerUrl(mContext, false)); httpsUrl = new URL(getCaptivePortalServerUrl(mContext, true)); } catch (MalformedURLException e) { validationLog("Bad validation URL: " + getCaptivePortalServerUrl(mContext, false)); return CaptivePortalProbeResult.FAILED; } } long startTime = SystemClock.elapsedRealtime(); // Pre-resolve the captive portal server host so we can log it. // Only do this if HttpURLConnection is about to, to avoid any potentially // unnecessary resolution. if (proxyInfo == null || fetchPac) { hostToResolve = url.getHost(); String hostToResolve = null; if (pacUrl != null) { hostToResolve = pacUrl.getHost(); } else if (proxyInfo != null) { hostToResolve = proxyInfo.getHost(); } else { hostToResolve = httpUrl.getHost(); } if (!TextUtils.isEmpty(hostToResolve)) { connectInfo.append(", " + hostToResolve + "="); final InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(hostToResolve); String probeName = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS); final Stopwatch dnsTimer = new Stopwatch().start(); try { InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(hostToResolve); long dnsLatency = dnsTimer.stop(); ValidationProbeEvent.logEvent(mNetId, dnsLatency, ValidationProbeEvent.PROBE_DNS, ValidationProbeEvent.DNS_SUCCESS); final StringBuffer connectInfo = new StringBuffer(", " + hostToResolve + "="); for (InetAddress address : addresses) { connectInfo.append(address.getHostAddress()); if (address != addresses[addresses.length-1]) connectInfo.append(","); } validationLog(probeName + " OK " + dnsLatency + "ms" + connectInfo); } catch (UnknownHostException e) { long dnsLatency = dnsTimer.stop(); ValidationProbeEvent.logEvent(mNetId, dnsLatency, ValidationProbeEvent.PROBE_DNS, ValidationProbeEvent.DNS_FAILURE); validationLog(probeName + " FAIL " + dnsLatency + "ms, " + hostToResolve); } validationLog("Checking " + url.toString() + " on " + mNetworkAgentInfo.networkInfo.getExtraInfo() + connectInfo); } CaptivePortalProbeResult result; if (pacUrl != null) { result = sendHttpProbe(pacUrl, ValidationProbeEvent.PROBE_PAC); } else if (mUseHttps) { result = sendParallelHttpProbes(httpsUrl, httpUrl); } else { result = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP); } long endTime = SystemClock.elapsedRealtime(); sendNetworkConditionsBroadcast(true /* response received */, result.isPortal() /* isCaptivePortal */, startTime, endTime); return result; } /** * Do a URL fetch on a known server to see if we get the data we expect. * Returns HTTP response code. */ @VisibleForTesting protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType) { HttpURLConnection urlConnection = null; int httpResponseCode = 599; String redirectUrl = null; final Stopwatch probeTimer = new Stopwatch().start(); try { urlConnection = (HttpURLConnection) mNetworkAgentInfo.network.openConnection(url); urlConnection.setInstanceFollowRedirects(fetchPac); urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC); urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); urlConnection.setUseCaches(false); Loading @@ -738,7 +822,9 @@ public class NetworkMonitor extends StateMachine { // Time how long it takes to get a response to our request long responseTimestamp = SystemClock.elapsedRealtime(); validationLog("isCaptivePortal: ret=" + httpResponseCode + validationLog(ValidationProbeEvent.getProbeName(probeType) + " " + url + " time=" + (responseTimestamp - requestTimestamp) + "ms" + " ret=" + httpResponseCode + " headers=" + urlConnection.getHeaderFields()); // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive // portal. The only example of this seen so far was a captive portal. For Loading @@ -756,14 +842,10 @@ public class NetworkMonitor extends StateMachine { httpResponseCode = 204; } if (httpResponseCode == 200 && fetchPac) { if (httpResponseCode == 200 && probeType == ValidationProbeEvent.PROBE_PAC) { validationLog("PAC fetch 200 response interpreted as 204 response."); httpResponseCode = 204; } sendNetworkConditionsBroadcast(true /* response received */, httpResponseCode != 204 /* isCaptivePortal */, requestTimestamp, responseTimestamp); } catch (IOException e) { validationLog("Probably not a portal: exception " + e); if (httpResponseCode == 599) { Loading @@ -774,11 +856,68 @@ public class NetworkMonitor extends StateMachine { urlConnection.disconnect(); } } final int probeType = ValidationProbeEvent.PROBE_HTTP; ValidationProbeEvent.logEvent(mNetId, probeTimer.stop(), probeType, httpResponseCode); return new CaptivePortalProbeResult(httpResponseCode, redirectUrl); } private CaptivePortalProbeResult sendParallelHttpProbes(URL httpsUrl, URL httpUrl) { // Number of probes to wait for. We might wait for all of them, but we might also return if // only one of them has replied. For example, we immediately return if the HTTP probe finds // a captive portal, even if the HTTPS probe is timing out. final CountDownLatch latch = new CountDownLatch(2); // Which probe result we're going to use. This doesn't need to be atomic, but it does need // to be final because otherwise we can't set it from the ProbeThreads. final AtomicReference<CaptivePortalProbeResult> finalResult = new AtomicReference<>(); final class ProbeThread extends Thread { private final boolean mIsHttps; private volatile CaptivePortalProbeResult mResult; public ProbeThread(boolean isHttps) { mIsHttps = isHttps; } public CaptivePortalProbeResult getResult() { return mResult; } @Override public void run() { if (mIsHttps) { mResult = sendHttpProbe(httpsUrl, ValidationProbeEvent.PROBE_HTTPS); } else { mResult = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP); } if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) { // HTTPS succeeded, or HTTP found a portal. Don't wait for the other probe. finalResult.compareAndSet(null, mResult); latch.countDown(); } // Signal that one probe has completed. If we've already made a decision, or if this // is the second probe, the latch will be at zero and we'll return a result. latch.countDown(); } } ProbeThread httpsProbe = new ProbeThread(true); ProbeThread httpProbe = new ProbeThread(false); httpsProbe.start(); httpProbe.start(); try { latch.await(); } catch (InterruptedException e) { validationLog("Error: probe wait interrupted!"); return CaptivePortalProbeResult.FAILED; } // If there was no deciding probe, that means that both probes completed. Return HTTPS. finalResult.compareAndSet(null, httpsProbe.getResult()); return finalResult.get(); } /** * @param responseReceived - whether or not we received a valid HTTP response to our request. * If false, isCaptivePortal and responseTimestampMs are ignored Loading Loading
api/system-current.txt +6 −2 Original line number Diff line number Diff line Loading @@ -26170,8 +26170,12 @@ package android.net.metrics { method public static void logEvent(int, long, int, int); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.net.metrics.ValidationProbeEvent> CREATOR; field public static final int PROBE_HTTP = 0; // 0x0 field public static final int PROBE_HTTPS = 1; // 0x1 field public static final int DNS_FAILURE = 0; // 0x0 field public static final int DNS_SUCCESS = 1; // 0x1 field public static final int PROBE_DNS = 0; // 0x0 field public static final int PROBE_HTTP = 1; // 0x1 field public static final int PROBE_HTTPS = 2; // 0x2 field public static final int PROBE_PAC = 3; // 0x3 field public final long durationMs; field public final int netId; field public final int probeType;
core/java/android/net/metrics/ValidationProbeEvent.java +13 −3 Original line number Diff line number Diff line Loading @@ -29,8 +29,13 @@ import com.android.internal.util.MessageUtils; @SystemApi public final class ValidationProbeEvent extends IpConnectivityEvent implements Parcelable { public static final int PROBE_HTTP = 0; public static final int PROBE_HTTPS = 1; public static final int PROBE_DNS = 0; public static final int PROBE_HTTP = 1; public static final int PROBE_HTTPS = 2; public static final int PROBE_PAC = 3; public static final int DNS_FAILURE = 0; public static final int DNS_SUCCESS = 1; public final int netId; public final long durationMs; Loading Loading @@ -73,6 +78,11 @@ public final class ValidationProbeEvent extends IpConnectivityEvent implements P } }; /** @hide */ public static String getProbeName(int probeType) { return Decoder.constants.get(probeType, "PROBE_???"); } public static void logEvent(int netId, long durationMs, int probeType, int returnCode) { logEvent(new ValidationProbeEvent(netId, durationMs, probeType, returnCode)); } Loading @@ -80,7 +90,7 @@ public final class ValidationProbeEvent extends IpConnectivityEvent implements P @Override public String toString() { return String.format("ValidationProbeEvent(%d, %s:%d, %dms)", netId, Decoder.constants.get(probeType), returnCode, durationMs); netId, getProbeName(probeType), returnCode, durationMs); } final static class Decoder { Loading
core/java/android/provider/Settings.java +9 −0 Original line number Diff line number Diff line Loading @@ -7705,6 +7705,15 @@ public final class Settings { */ public static final String CAPTIVE_PORTAL_SERVER = "captive_portal_server"; /** * Whether to use HTTPS for network validation. This is enabled by default and the setting * needs to be set to 0 to disable it. This setting is a misnomer because captive portals * don't actually use HTTPS, but it's consistent with the other settings. * * @hide */ public static final String CAPTIVE_PORTAL_USE_HTTPS = "captive_portal_use_https"; /** * Whether network service discovery is enabled. * Loading
services/core/java/com/android/server/connectivity/NetworkMonitor.java +199 −60 Original line number Diff line number Diff line Loading @@ -71,7 +71,11 @@ import com.android.server.connectivity.NetworkAgentInfo; import java.io.IOException; import java.net.HttpURLConnection; import java.net.InetAddress; import java.net.MalformedURLException; import java.net.UnknownHostException; import java.net.URL; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicReference; import java.util.List; import java.util.Random; Loading Loading @@ -228,7 +232,8 @@ public class NetworkMonitor extends StateMachine { private final AlarmManager mAlarmManager; private final NetworkRequest mDefaultRequest; private boolean mIsCaptivePortalCheckEnabled = false; private boolean mIsCaptivePortalCheckEnabled; private boolean mUseHttps; // Set if the user explicitly selected "Do not use this network" in captive portal sign-in app. private boolean mUserDoesNotWant = false; Loading Loading @@ -276,6 +281,8 @@ public class NetworkMonitor extends StateMachine { mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1; mUseHttps = Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1; start(); } Loading Loading @@ -324,6 +331,21 @@ public class NetworkMonitor extends StateMachine { return HANDLED; case CMD_CAPTIVE_PORTAL_APP_FINISHED: log("CaptivePortal App responded with " + message.arg1); // If the user has seen and acted on a captive portal notification, and the // captive portal app is now closed, disable HTTPS probes. This avoids the // following pathological situation: // // 1. HTTP probe returns a captive portal, HTTPS probe fails or times out. // 2. User opens the app and logs into the captive portal. // 3. HTTP starts working, but HTTPS still doesn't work for some other reason - // perhaps due to the network blocking HTTPS? // // In this case, we'll fail to validate the network even after the app is // dismissed. There is now no way to use this network, because the app is now // gone, so the user cannot select "Use this network as is". mUseHttps = false; switch (message.arg1) { case APP_RETURN_DISMISSED: sendMessage(CMD_FORCE_REEVALUATION, 0 /* no UID */, 0); Loading Loading @@ -424,6 +446,8 @@ public class NetworkMonitor extends StateMachine { */ @VisibleForTesting public static final class CaptivePortalProbeResult { static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599, null); final int mHttpResponseCode; // HTTP response code returned from Internet probe. final String mRedirectUrl; // Redirect destination returned from Internet probe. Loading @@ -431,6 +455,11 @@ public class NetworkMonitor extends StateMachine { mHttpResponseCode = httpResponseCode; mRedirectUrl = redirectUrl; } boolean isSuccessful() { return mHttpResponseCode == 204; } boolean isPortal() { return !isSuccessful() && mHttpResponseCode >= 200 && mHttpResponseCode <= 399; } } // Being in the EvaluatingState State indicates the Network is being evaluated for internet Loading Loading @@ -481,6 +510,7 @@ public class NetworkMonitor extends StateMachine { // expensive metered network, or unwanted leaking of the User Agent string. if (!mDefaultRequest.networkCapabilities.satisfiedByNetworkCapabilities( mNetworkAgentInfo.networkCapabilities)) { validationLog("Network would not satisfy default request, not validating"); transitionTo(mValidatedState); return HANDLED; } Loading @@ -492,10 +522,9 @@ public class NetworkMonitor extends StateMachine { // will be unresponsive. isCaptivePortal() could be executed on another Thread // if this is found to cause problems. CaptivePortalProbeResult probeResult = isCaptivePortal(); if (probeResult.mHttpResponseCode == 204) { if (probeResult.isSuccessful()) { transitionTo(mValidatedState); } else if (probeResult.mHttpResponseCode >= 200 && probeResult.mHttpResponseCode <= 399) { } else if (probeResult.isPortal()) { mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.mRedirectUrl)); transitionTo(mCaptivePortalState); Loading Loading @@ -659,27 +688,23 @@ public class NetworkMonitor extends StateMachine { } } public static String getCaptivePortalServerUrl(Context context) { private static String getCaptivePortalServerUrl(Context context, boolean isHttps) { String server = Settings.Global.getString(context.getContentResolver(), Settings.Global.CAPTIVE_PORTAL_SERVER); if (server == null) server = DEFAULT_SERVER; return "http://" + server + "/generate_204"; return (isHttps ? "https" : "http") + "://" + server + "/generate_204"; } public static String getCaptivePortalServerUrl(Context context) { return getCaptivePortalServerUrl(context, false); } /** * Do a URL fetch on a known server to see if we get the data we expect. * Returns HTTP response code. */ @VisibleForTesting protected CaptivePortalProbeResult isCaptivePortal() { if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204, null); HttpURLConnection urlConnection = null; int httpResponseCode = 599; String redirectUrl = null; final Stopwatch probeTimer = new Stopwatch().start(); try { URL url = new URL(getCaptivePortalServerUrl(mContext)); URL pacUrl = null, httpUrl = null, httpsUrl = null; // On networks with a PAC instead of fetching a URL that should result in a 204 // response, we instead simply fetch the PAC script. This is done for a few reasons: // 1. At present our PAC code does not yet handle multiple PACs on multiple networks Loading @@ -697,34 +722,93 @@ public class NetworkMonitor extends StateMachine { // 3. PAC scripts are sometimes used to block or restrict Internet access and may in // fact block fetching of the generate_204 URL which would lead to false negative // results for network validation. boolean fetchPac = false; final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy(); if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) { url = new URL(proxyInfo.getPacFileUrl().toString()); fetchPac = true; try { pacUrl = new URL(proxyInfo.getPacFileUrl().toString()); } catch (MalformedURLException e) { validationLog("Invalid PAC URL: " + proxyInfo.getPacFileUrl().toString()); return CaptivePortalProbeResult.FAILED; } final StringBuffer connectInfo = new StringBuffer(); String hostToResolve = null; // Only resolve a host if HttpURLConnection is about to, to avoid any potentially } if (pacUrl == null) { try { httpUrl = new URL(getCaptivePortalServerUrl(mContext, false)); httpsUrl = new URL(getCaptivePortalServerUrl(mContext, true)); } catch (MalformedURLException e) { validationLog("Bad validation URL: " + getCaptivePortalServerUrl(mContext, false)); return CaptivePortalProbeResult.FAILED; } } long startTime = SystemClock.elapsedRealtime(); // Pre-resolve the captive portal server host so we can log it. // Only do this if HttpURLConnection is about to, to avoid any potentially // unnecessary resolution. if (proxyInfo == null || fetchPac) { hostToResolve = url.getHost(); String hostToResolve = null; if (pacUrl != null) { hostToResolve = pacUrl.getHost(); } else if (proxyInfo != null) { hostToResolve = proxyInfo.getHost(); } else { hostToResolve = httpUrl.getHost(); } if (!TextUtils.isEmpty(hostToResolve)) { connectInfo.append(", " + hostToResolve + "="); final InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(hostToResolve); String probeName = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS); final Stopwatch dnsTimer = new Stopwatch().start(); try { InetAddress[] addresses = mNetworkAgentInfo.network.getAllByName(hostToResolve); long dnsLatency = dnsTimer.stop(); ValidationProbeEvent.logEvent(mNetId, dnsLatency, ValidationProbeEvent.PROBE_DNS, ValidationProbeEvent.DNS_SUCCESS); final StringBuffer connectInfo = new StringBuffer(", " + hostToResolve + "="); for (InetAddress address : addresses) { connectInfo.append(address.getHostAddress()); if (address != addresses[addresses.length-1]) connectInfo.append(","); } validationLog(probeName + " OK " + dnsLatency + "ms" + connectInfo); } catch (UnknownHostException e) { long dnsLatency = dnsTimer.stop(); ValidationProbeEvent.logEvent(mNetId, dnsLatency, ValidationProbeEvent.PROBE_DNS, ValidationProbeEvent.DNS_FAILURE); validationLog(probeName + " FAIL " + dnsLatency + "ms, " + hostToResolve); } validationLog("Checking " + url.toString() + " on " + mNetworkAgentInfo.networkInfo.getExtraInfo() + connectInfo); } CaptivePortalProbeResult result; if (pacUrl != null) { result = sendHttpProbe(pacUrl, ValidationProbeEvent.PROBE_PAC); } else if (mUseHttps) { result = sendParallelHttpProbes(httpsUrl, httpUrl); } else { result = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP); } long endTime = SystemClock.elapsedRealtime(); sendNetworkConditionsBroadcast(true /* response received */, result.isPortal() /* isCaptivePortal */, startTime, endTime); return result; } /** * Do a URL fetch on a known server to see if we get the data we expect. * Returns HTTP response code. */ @VisibleForTesting protected CaptivePortalProbeResult sendHttpProbe(URL url, int probeType) { HttpURLConnection urlConnection = null; int httpResponseCode = 599; String redirectUrl = null; final Stopwatch probeTimer = new Stopwatch().start(); try { urlConnection = (HttpURLConnection) mNetworkAgentInfo.network.openConnection(url); urlConnection.setInstanceFollowRedirects(fetchPac); urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC); urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS); urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS); urlConnection.setUseCaches(false); Loading @@ -738,7 +822,9 @@ public class NetworkMonitor extends StateMachine { // Time how long it takes to get a response to our request long responseTimestamp = SystemClock.elapsedRealtime(); validationLog("isCaptivePortal: ret=" + httpResponseCode + validationLog(ValidationProbeEvent.getProbeName(probeType) + " " + url + " time=" + (responseTimestamp - requestTimestamp) + "ms" + " ret=" + httpResponseCode + " headers=" + urlConnection.getHeaderFields()); // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive // portal. The only example of this seen so far was a captive portal. For Loading @@ -756,14 +842,10 @@ public class NetworkMonitor extends StateMachine { httpResponseCode = 204; } if (httpResponseCode == 200 && fetchPac) { if (httpResponseCode == 200 && probeType == ValidationProbeEvent.PROBE_PAC) { validationLog("PAC fetch 200 response interpreted as 204 response."); httpResponseCode = 204; } sendNetworkConditionsBroadcast(true /* response received */, httpResponseCode != 204 /* isCaptivePortal */, requestTimestamp, responseTimestamp); } catch (IOException e) { validationLog("Probably not a portal: exception " + e); if (httpResponseCode == 599) { Loading @@ -774,11 +856,68 @@ public class NetworkMonitor extends StateMachine { urlConnection.disconnect(); } } final int probeType = ValidationProbeEvent.PROBE_HTTP; ValidationProbeEvent.logEvent(mNetId, probeTimer.stop(), probeType, httpResponseCode); return new CaptivePortalProbeResult(httpResponseCode, redirectUrl); } private CaptivePortalProbeResult sendParallelHttpProbes(URL httpsUrl, URL httpUrl) { // Number of probes to wait for. We might wait for all of them, but we might also return if // only one of them has replied. For example, we immediately return if the HTTP probe finds // a captive portal, even if the HTTPS probe is timing out. final CountDownLatch latch = new CountDownLatch(2); // Which probe result we're going to use. This doesn't need to be atomic, but it does need // to be final because otherwise we can't set it from the ProbeThreads. final AtomicReference<CaptivePortalProbeResult> finalResult = new AtomicReference<>(); final class ProbeThread extends Thread { private final boolean mIsHttps; private volatile CaptivePortalProbeResult mResult; public ProbeThread(boolean isHttps) { mIsHttps = isHttps; } public CaptivePortalProbeResult getResult() { return mResult; } @Override public void run() { if (mIsHttps) { mResult = sendHttpProbe(httpsUrl, ValidationProbeEvent.PROBE_HTTPS); } else { mResult = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP); } if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) { // HTTPS succeeded, or HTTP found a portal. Don't wait for the other probe. finalResult.compareAndSet(null, mResult); latch.countDown(); } // Signal that one probe has completed. If we've already made a decision, or if this // is the second probe, the latch will be at zero and we'll return a result. latch.countDown(); } } ProbeThread httpsProbe = new ProbeThread(true); ProbeThread httpProbe = new ProbeThread(false); httpsProbe.start(); httpProbe.start(); try { latch.await(); } catch (InterruptedException e) { validationLog("Error: probe wait interrupted!"); return CaptivePortalProbeResult.FAILED; } // If there was no deciding probe, that means that both probes completed. Return HTTPS. finalResult.compareAndSet(null, httpsProbe.getResult()); return finalResult.get(); } /** * @param responseReceived - whether or not we received a valid HTTP response to our request. * If false, isCaptivePortal and responseTimestampMs are ignored Loading