Loading apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java +6 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.networkstack.apishim.api29; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.networkstack.apishim.CaptivePortalDataShim; import com.android.networkstack.apishim.UnsupportedApiLevelException; Loading Loading @@ -46,4 +47,9 @@ public abstract class CaptivePortalDataShimImpl implements CaptivePortalDataShim // Data class not supported in API 29 throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29"); } @VisibleForTesting public static boolean isSupported() { return false; } } apishim/30/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java +7 −1 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import android.os.Build; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import org.json.JSONException; import org.json.JSONObject; Loading @@ -47,7 +48,7 @@ public class CaptivePortalDataShimImpl @NonNull public static CaptivePortalDataShim fromJson(JSONObject obj) throws JSONException, UnsupportedApiLevelException { if (!ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)) { if (!isSupported()) { return com.android.networkstack.apishim.api29.CaptivePortalDataShimImpl.fromJson(obj); } Loading @@ -69,6 +70,11 @@ public class CaptivePortalDataShimImpl .build()); } @VisibleForTesting public static boolean isSupported() { return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q); } private static long getLongOrDefault(JSONObject o, String key, long def) throws JSONException { if (!o.has(key)) return def; return o.getLong(key); Loading common/moduleutils/src/android/net/shared/IpConfigurationParcelableUtil.java +2 −0 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ public final class IpConfigurationParcelableUtil { p.serverAddress = parcelAddress(results.serverAddress); p.vendorInfo = results.vendorInfo; p.serverHostName = results.serverHostName; p.captivePortalApiUrl = results.captivePortalApiUrl; return p; } Loading @@ -56,6 +57,7 @@ public final class IpConfigurationParcelableUtil { results.serverAddress = (Inet4Address) unparcelAddress(p.serverAddress); results.vendorInfo = p.vendorInfo; results.serverHostName = p.serverHostName; results.captivePortalApiUrl = p.captivePortalApiUrl; return results; } Loading common/networkstackclient/src/android/net/DhcpResultsParcelable.aidl +1 −0 Original line number Diff line number Diff line Loading @@ -25,4 +25,5 @@ parcelable DhcpResultsParcelable { String serverAddress; String vendorInfo; String serverHostName; String captivePortalApiUrl; } src/com/android/server/connectivity/NetworkMonitor.java +200 −37 Original line number Diff line number Diff line Loading @@ -139,19 +139,26 @@ import androidx.annotation.BoolRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.RingBufferIndices; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.TrafficStatsConstants; import com.android.networkstack.R; import com.android.networkstack.apishim.CaptivePortalDataShim; import com.android.networkstack.apishim.CaptivePortalDataShimImpl; import com.android.networkstack.apishim.NetworkInformationShimImpl; import com.android.networkstack.apishim.ShimUtils; import com.android.networkstack.apishim.UnsupportedApiLevelException; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; import com.android.networkstack.netlink.TcpSocketTracker; import com.android.networkstack.util.DnsUtils; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; Loading @@ -169,6 +176,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Random; import java.util.StringJoiner; import java.util.UUID; Loading Loading @@ -197,6 +205,11 @@ public class NetworkMonitor extends StateMachine { private static final int SOCKET_TIMEOUT_MS = 10000; private static final int PROBE_TIMEOUT_MS = 3000; private static final int CAPPORT_API_MAX_JSON_LENGTH = 4096; private static final String ACCEPT_HEADER = "Accept"; private static final String CONTENT_TYPE_HEADER = "Content-Type"; private static final String CAPPORT_API_CONTENT_TYPE = "application/captive+json"; enum EvaluationResult { VALIDATED(true), CAPTIVE_PORTAL(false); Loading Loading @@ -760,7 +773,12 @@ public class NetworkMonitor extends StateMachine { maybeDisableHttpsProbing(true /* acceptPartial */); break; case EVENT_LINK_PROPERTIES_CHANGED: final Uri oldCapportUrl = getCaptivePortalApiUrl(mLinkProperties); mLinkProperties = (LinkProperties) message.obj; final Uri newCapportUrl = getCaptivePortalApiUrl(mLinkProperties); if (!Objects.equals(oldCapportUrl, newCapportUrl)) { sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0); } break; case EVENT_NETWORK_CAPABILITIES_CHANGED: mNetworkCapabilities = (NetworkCapabilities) message.obj; Loading Loading @@ -964,6 +982,8 @@ public class NetworkMonitor extends StateMachine { // Being in the EvaluatingState State indicates the Network is being evaluated for internet // connectivity, or that the user has indicated that this network is unwanted. private class EvaluatingState extends State { private Uri mEvaluatingCapportUrl; @Override public void enter() { // If we have already started to track time spent in EvaluatingState Loading @@ -979,6 +999,7 @@ public class NetworkMonitor extends StateMachine { } mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS; mEvaluateAttempts = 0; mEvaluatingCapportUrl = getCaptivePortalApiUrl(mLinkProperties); // Reset all current probe results to zero, but retain current validation state until // validation succeeds or fails. mEvaluationState.clearProbeResults(); Loading Loading @@ -1029,10 +1050,8 @@ public class NetworkMonitor extends StateMachine { transitionTo(mProbingState); return HANDLED; case CMD_FORCE_REEVALUATION: // Before IGNORE_REEVALUATE_ATTEMPTS attempts are made, // ignore any re-evaluation requests. After, restart the // evaluation process via EvaluatingState#enter. return (mEvaluateAttempts < IGNORE_REEVALUATE_ATTEMPTS) ? HANDLED : NOT_HANDLED; // The evaluation process restarts via EvaluatingState#enter. return shouldAcceptForceRevalidation() ? NOT_HANDLED : HANDLED; // Disable HTTPS probe and transition to EvaluatingPrivateDnsState because: // 1. Network is connected and finish the network validation. // 2. NetworkMonitor detects network is partial connectivity and user accepts it. Loading @@ -1045,6 +1064,15 @@ public class NetworkMonitor extends StateMachine { } } private boolean shouldAcceptForceRevalidation() { // If the captive portal URL has changed since the last evaluation attempt, always // revalidate. Otherwise, ignore any re-evaluation requests before // IGNORE_REEVALUATE_ATTEMPTS are made. return mEvaluateAttempts >= IGNORE_REEVALUATE_ATTEMPTS || !Objects.equals( mEvaluatingCapportUrl, getCaptivePortalApiUrl(mLinkProperties)); } @Override public void exit() { TrafficStats.clearThreadStatsUid(); Loading Loading @@ -1942,45 +1970,161 @@ public class NetworkMonitor extends StateMachine { } } private CaptivePortalProbeResult sendParallelHttpProbes( ProxyInfo proxy, URL httpsUrl, URL httpUrl) { // Number of probes to wait for. If a probe completes with a conclusive answer // it shortcuts the latch immediately by forcing the count to 0. final CountDownLatch latch = new CountDownLatch(2); private abstract static class ProbeThread extends Thread { private final CountDownLatch mLatch; private final ProxyInfo mProxy; private final URL mUrl; protected final Uri mCaptivePortalApiUrl; final class ProbeThread extends Thread { private final boolean mIsHttps; private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED; ProbeThread(boolean isHttps) { mIsHttps = isHttps; protected ProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { mLatch = latch; mProxy = proxy; mUrl = url; mCaptivePortalApiUrl = captivePortalApiUrl; } private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED; public CaptivePortalProbeResult result() { return mResult; } protected abstract CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url); public abstract boolean isConclusiveResult(CaptivePortalProbeResult result); @Override public void run() { if (mIsHttps) { mResult = sendDnsAndHttpProbes(proxy, httpsUrl, ValidationProbeEvent.PROBE_HTTPS); } else { mResult = sendDnsAndHttpProbes(proxy, httpUrl, ValidationProbeEvent.PROBE_HTTP); } if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) { // Stop waiting immediately if https succeeds or if http finds a portal. while (latch.getCount() > 0) { latch.countDown(); mResult = sendProbe(mProxy, mUrl); if (isConclusiveResult(mResult)) { // Stop waiting immediately if any probe is conclusive. while (mLatch.getCount() > 0) { mLatch.countDown(); } } // Signal this probe has completed. latch.countDown(); mLatch.countDown(); } } final ProbeThread httpsProbe = new ProbeThread(true); final ProbeThread httpProbe = new ProbeThread(false); final class HttpsProbeThread extends ProbeThread { HttpsProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { super(latch, proxy, url, captivePortalApiUrl); } @Override protected CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url) { return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTPS); } @Override public boolean isConclusiveResult(CaptivePortalProbeResult result) { // isPortal() is not expected on the HTTPS probe, but check it nonetheless. // In case the capport API is available, the API is authoritative on whether there is // a portal, so the HTTPS probe is not enough to conclude there is connectivity, // and a determination will be made once the capport API probe returns. Note that the // API can only force the system to detect a portal even if the HTTPS probe succeeds. // It cannot force the system to detect no portal if the HTTPS probe fails. return (result.isPortal() || result.isSuccessful()) && mCaptivePortalApiUrl == null; } } final class HttpProbeThread extends ProbeThread { private volatile CaptivePortalDataShim mCapportData; HttpProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { super(latch, proxy, url, captivePortalApiUrl); } CaptivePortalDataShim getCaptivePortalData() { return mCapportData; } private CaptivePortalDataShim tryCapportApiProbe() { if (mCaptivePortalApiUrl == null) return null; validationLog("Fetching captive portal data from " + mCaptivePortalApiUrl); final String apiContent; try { final URL url = new URL(mCaptivePortalApiUrl.toString()); if (!"https".equals(url.getProtocol())) { validationLog("Invalid captive portal API protocol: " + url.getProtocol()); return null; } final HttpURLConnection conn = makeProbeConnection( url, true /* followRedirects */); conn.setRequestProperty(ACCEPT_HEADER, CAPPORT_API_CONTENT_TYPE); final int responseCode = conn.getResponseCode(); if (responseCode != 200) { validationLog("Non-200 API response code: " + conn.getResponseCode()); return null; } final Charset charset = extractCharset(conn.getHeaderField(CONTENT_TYPE_HEADER)); if (charset != StandardCharsets.UTF_8) { validationLog("Invalid charset for capport API: " + charset); return null; } apiContent = readAsString(conn.getInputStream(), CAPPORT_API_MAX_JSON_LENGTH, charset); } catch (IOException e) { validationLog("I/O error reading capport data: " + e.getMessage()); return null; } try { final JSONObject info = new JSONObject(apiContent); return CaptivePortalDataShimImpl.fromJson(info); } catch (JSONException e) { validationLog("Could not parse capport API JSON: " + e.getMessage()); return null; } catch (UnsupportedApiLevelException e) { validationLog("Platform API too low to support capport API"); return null; } } @Override protected CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url) { mCapportData = tryCapportApiProbe(); if (mCapportData != null && mCapportData.isCaptive()) { if (mCapportData.getUserPortalUrl() == null) { validationLog("Missing user-portal-url from capport response"); return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTP); } final String loginUrlString = mCapportData.getUserPortalUrl().toString(); // Starting from R (where CaptivePortalData was introduced), the captive portal app // delegates to NetworkMonitor for verifying when the network validates instead of // probing the detectUrl. So pass the detectUrl to have the portal open on that, // page; CaptivePortalLogin will not use it for probing. return new CaptivePortalProbeResult( CaptivePortalProbeResult.PORTAL_CODE, loginUrlString /* redirectUrl */, loginUrlString /* detectUrl */); } // If the API says it's not captive, still check for HTTP connectivity. This helps // with partial connectivity detection, and a broken API saying that there is no // redirect when there is one. return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTP); } @Override public boolean isConclusiveResult(CaptivePortalProbeResult result) { return result.isPortal(); } } private CaptivePortalProbeResult sendParallelHttpProbes( ProxyInfo proxy, URL httpsUrl, URL httpUrl) { // Number of probes to wait for. If a probe completes with a conclusive answer // it shortcuts the latch immediately by forcing the count to 0. final CountDownLatch latch = new CountDownLatch(2); final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties); final HttpsProbeThread httpsProbe = new HttpsProbeThread(latch, proxy, httpsUrl, capportApiUrl); final HttpProbeThread httpProbe = new HttpProbeThread(latch, proxy, httpUrl, capportApiUrl); try { httpsProbe.start(); Loading @@ -1995,12 +2139,14 @@ public class NetworkMonitor extends StateMachine { final CaptivePortalProbeResult httpResult = httpProbe.result(); // Look for a conclusive probe result first. if (httpResult.isPortal()) { if (httpProbe.isConclusiveResult(httpResult)) { maybeReportCaptivePortalData(httpProbe.getCaptivePortalData()); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, httpResult); return httpResult; } // httpsResult.isPortal() is not expected, but check it nonetheless. if (httpsResult.isPortal() || httpsResult.isSuccessful()) { if (httpsProbe.isConclusiveResult(httpsResult)) { maybeReportCaptivePortalData(httpProbe.getCaptivePortalData()); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsResult); return httpsResult; } Loading @@ -2019,6 +2165,7 @@ public class NetworkMonitor extends StateMachine { // Otherwise wait until http and https probes completes and use their results. try { httpProbe.join(); maybeReportCaptivePortalData(httpProbe.getCaptivePortalData()); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, httpProbe.result()); if (httpProbe.result().isPortal()) { Loading Loading @@ -2519,6 +2666,18 @@ public class NetworkMonitor extends StateMachine { mEvaluationState.noteProbeResult(probeResult, succeeded); } private void maybeReportCaptivePortalData(@Nullable CaptivePortalDataShim data) { // Do not clear data even if it is null: access points should not stop serving the API, so // if the API disappears this is treated as a temporary failure, and previous data should // remain valid. if (data == null) return; try { data.notifyChanged(mCallback); } catch (RemoteException e) { Log.e(TAG, "Error notifying ConnectivityService of new capport data", e); } } /** * Interface for logging dns results. */ Loading Loading @@ -2546,4 +2705,8 @@ public class NetworkMonitor extends StateMachine { return ((type & DATA_STALL_EVALUATION_TYPE_DNS) != 0) ? new DnsStallDetector(threshold) : null; } private static Uri getCaptivePortalApiUrl(LinkProperties lp) { return NetworkInformationShimImpl.newInstance().getCaptivePortalApiUrl(lp); } } Loading
apishim/29/com/android/networkstack/apishim/api29/CaptivePortalDataShimImpl.java +6 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.networkstack.apishim.api29; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.networkstack.apishim.CaptivePortalDataShim; import com.android.networkstack.apishim.UnsupportedApiLevelException; Loading Loading @@ -46,4 +47,9 @@ public abstract class CaptivePortalDataShimImpl implements CaptivePortalDataShim // Data class not supported in API 29 throw new UnsupportedApiLevelException("CaptivePortalData not supported on API 29"); } @VisibleForTesting public static boolean isSupported() { return false; } }
apishim/30/com/android/networkstack/apishim/CaptivePortalDataShimImpl.java +7 −1 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ import android.os.Build; import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import org.json.JSONException; import org.json.JSONObject; Loading @@ -47,7 +48,7 @@ public class CaptivePortalDataShimImpl @NonNull public static CaptivePortalDataShim fromJson(JSONObject obj) throws JSONException, UnsupportedApiLevelException { if (!ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q)) { if (!isSupported()) { return com.android.networkstack.apishim.api29.CaptivePortalDataShimImpl.fromJson(obj); } Loading @@ -69,6 +70,11 @@ public class CaptivePortalDataShimImpl .build()); } @VisibleForTesting public static boolean isSupported() { return ShimUtils.isReleaseOrDevelopmentApiAbove(Build.VERSION_CODES.Q); } private static long getLongOrDefault(JSONObject o, String key, long def) throws JSONException { if (!o.has(key)) return def; return o.getLong(key); Loading
common/moduleutils/src/android/net/shared/IpConfigurationParcelableUtil.java +2 −0 Original line number Diff line number Diff line Loading @@ -42,6 +42,7 @@ public final class IpConfigurationParcelableUtil { p.serverAddress = parcelAddress(results.serverAddress); p.vendorInfo = results.vendorInfo; p.serverHostName = results.serverHostName; p.captivePortalApiUrl = results.captivePortalApiUrl; return p; } Loading @@ -56,6 +57,7 @@ public final class IpConfigurationParcelableUtil { results.serverAddress = (Inet4Address) unparcelAddress(p.serverAddress); results.vendorInfo = p.vendorInfo; results.serverHostName = p.serverHostName; results.captivePortalApiUrl = p.captivePortalApiUrl; return results; } Loading
common/networkstackclient/src/android/net/DhcpResultsParcelable.aidl +1 −0 Original line number Diff line number Diff line Loading @@ -25,4 +25,5 @@ parcelable DhcpResultsParcelable { String serverAddress; String vendorInfo; String serverHostName; String captivePortalApiUrl; }
src/com/android/server/connectivity/NetworkMonitor.java +200 −37 Original line number Diff line number Diff line Loading @@ -139,19 +139,26 @@ import androidx.annotation.BoolRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.annotation.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.RingBufferIndices; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import com.android.internal.util.TrafficStatsConstants; import com.android.networkstack.R; import com.android.networkstack.apishim.CaptivePortalDataShim; import com.android.networkstack.apishim.CaptivePortalDataShimImpl; import com.android.networkstack.apishim.NetworkInformationShimImpl; import com.android.networkstack.apishim.ShimUtils; import com.android.networkstack.apishim.UnsupportedApiLevelException; import com.android.networkstack.metrics.DataStallDetectionStats; import com.android.networkstack.metrics.DataStallStatsUtils; import com.android.networkstack.netlink.TcpSocketTracker; import com.android.networkstack.util.DnsUtils; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; Loading @@ -169,6 +176,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Random; import java.util.StringJoiner; import java.util.UUID; Loading Loading @@ -197,6 +205,11 @@ public class NetworkMonitor extends StateMachine { private static final int SOCKET_TIMEOUT_MS = 10000; private static final int PROBE_TIMEOUT_MS = 3000; private static final int CAPPORT_API_MAX_JSON_LENGTH = 4096; private static final String ACCEPT_HEADER = "Accept"; private static final String CONTENT_TYPE_HEADER = "Content-Type"; private static final String CAPPORT_API_CONTENT_TYPE = "application/captive+json"; enum EvaluationResult { VALIDATED(true), CAPTIVE_PORTAL(false); Loading Loading @@ -760,7 +773,12 @@ public class NetworkMonitor extends StateMachine { maybeDisableHttpsProbing(true /* acceptPartial */); break; case EVENT_LINK_PROPERTIES_CHANGED: final Uri oldCapportUrl = getCaptivePortalApiUrl(mLinkProperties); mLinkProperties = (LinkProperties) message.obj; final Uri newCapportUrl = getCaptivePortalApiUrl(mLinkProperties); if (!Objects.equals(oldCapportUrl, newCapportUrl)) { sendMessage(CMD_FORCE_REEVALUATION, NO_UID, 0); } break; case EVENT_NETWORK_CAPABILITIES_CHANGED: mNetworkCapabilities = (NetworkCapabilities) message.obj; Loading Loading @@ -964,6 +982,8 @@ public class NetworkMonitor extends StateMachine { // Being in the EvaluatingState State indicates the Network is being evaluated for internet // connectivity, or that the user has indicated that this network is unwanted. private class EvaluatingState extends State { private Uri mEvaluatingCapportUrl; @Override public void enter() { // If we have already started to track time spent in EvaluatingState Loading @@ -979,6 +999,7 @@ public class NetworkMonitor extends StateMachine { } mReevaluateDelayMs = INITIAL_REEVALUATE_DELAY_MS; mEvaluateAttempts = 0; mEvaluatingCapportUrl = getCaptivePortalApiUrl(mLinkProperties); // Reset all current probe results to zero, but retain current validation state until // validation succeeds or fails. mEvaluationState.clearProbeResults(); Loading Loading @@ -1029,10 +1050,8 @@ public class NetworkMonitor extends StateMachine { transitionTo(mProbingState); return HANDLED; case CMD_FORCE_REEVALUATION: // Before IGNORE_REEVALUATE_ATTEMPTS attempts are made, // ignore any re-evaluation requests. After, restart the // evaluation process via EvaluatingState#enter. return (mEvaluateAttempts < IGNORE_REEVALUATE_ATTEMPTS) ? HANDLED : NOT_HANDLED; // The evaluation process restarts via EvaluatingState#enter. return shouldAcceptForceRevalidation() ? NOT_HANDLED : HANDLED; // Disable HTTPS probe and transition to EvaluatingPrivateDnsState because: // 1. Network is connected and finish the network validation. // 2. NetworkMonitor detects network is partial connectivity and user accepts it. Loading @@ -1045,6 +1064,15 @@ public class NetworkMonitor extends StateMachine { } } private boolean shouldAcceptForceRevalidation() { // If the captive portal URL has changed since the last evaluation attempt, always // revalidate. Otherwise, ignore any re-evaluation requests before // IGNORE_REEVALUATE_ATTEMPTS are made. return mEvaluateAttempts >= IGNORE_REEVALUATE_ATTEMPTS || !Objects.equals( mEvaluatingCapportUrl, getCaptivePortalApiUrl(mLinkProperties)); } @Override public void exit() { TrafficStats.clearThreadStatsUid(); Loading Loading @@ -1942,45 +1970,161 @@ public class NetworkMonitor extends StateMachine { } } private CaptivePortalProbeResult sendParallelHttpProbes( ProxyInfo proxy, URL httpsUrl, URL httpUrl) { // Number of probes to wait for. If a probe completes with a conclusive answer // it shortcuts the latch immediately by forcing the count to 0. final CountDownLatch latch = new CountDownLatch(2); private abstract static class ProbeThread extends Thread { private final CountDownLatch mLatch; private final ProxyInfo mProxy; private final URL mUrl; protected final Uri mCaptivePortalApiUrl; final class ProbeThread extends Thread { private final boolean mIsHttps; private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED; ProbeThread(boolean isHttps) { mIsHttps = isHttps; protected ProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { mLatch = latch; mProxy = proxy; mUrl = url; mCaptivePortalApiUrl = captivePortalApiUrl; } private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED; public CaptivePortalProbeResult result() { return mResult; } protected abstract CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url); public abstract boolean isConclusiveResult(CaptivePortalProbeResult result); @Override public void run() { if (mIsHttps) { mResult = sendDnsAndHttpProbes(proxy, httpsUrl, ValidationProbeEvent.PROBE_HTTPS); } else { mResult = sendDnsAndHttpProbes(proxy, httpUrl, ValidationProbeEvent.PROBE_HTTP); } if ((mIsHttps && mResult.isSuccessful()) || (!mIsHttps && mResult.isPortal())) { // Stop waiting immediately if https succeeds or if http finds a portal. while (latch.getCount() > 0) { latch.countDown(); mResult = sendProbe(mProxy, mUrl); if (isConclusiveResult(mResult)) { // Stop waiting immediately if any probe is conclusive. while (mLatch.getCount() > 0) { mLatch.countDown(); } } // Signal this probe has completed. latch.countDown(); mLatch.countDown(); } } final ProbeThread httpsProbe = new ProbeThread(true); final ProbeThread httpProbe = new ProbeThread(false); final class HttpsProbeThread extends ProbeThread { HttpsProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { super(latch, proxy, url, captivePortalApiUrl); } @Override protected CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url) { return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTPS); } @Override public boolean isConclusiveResult(CaptivePortalProbeResult result) { // isPortal() is not expected on the HTTPS probe, but check it nonetheless. // In case the capport API is available, the API is authoritative on whether there is // a portal, so the HTTPS probe is not enough to conclude there is connectivity, // and a determination will be made once the capport API probe returns. Note that the // API can only force the system to detect a portal even if the HTTPS probe succeeds. // It cannot force the system to detect no portal if the HTTPS probe fails. return (result.isPortal() || result.isSuccessful()) && mCaptivePortalApiUrl == null; } } final class HttpProbeThread extends ProbeThread { private volatile CaptivePortalDataShim mCapportData; HttpProbeThread(CountDownLatch latch, ProxyInfo proxy, URL url, Uri captivePortalApiUrl) { super(latch, proxy, url, captivePortalApiUrl); } CaptivePortalDataShim getCaptivePortalData() { return mCapportData; } private CaptivePortalDataShim tryCapportApiProbe() { if (mCaptivePortalApiUrl == null) return null; validationLog("Fetching captive portal data from " + mCaptivePortalApiUrl); final String apiContent; try { final URL url = new URL(mCaptivePortalApiUrl.toString()); if (!"https".equals(url.getProtocol())) { validationLog("Invalid captive portal API protocol: " + url.getProtocol()); return null; } final HttpURLConnection conn = makeProbeConnection( url, true /* followRedirects */); conn.setRequestProperty(ACCEPT_HEADER, CAPPORT_API_CONTENT_TYPE); final int responseCode = conn.getResponseCode(); if (responseCode != 200) { validationLog("Non-200 API response code: " + conn.getResponseCode()); return null; } final Charset charset = extractCharset(conn.getHeaderField(CONTENT_TYPE_HEADER)); if (charset != StandardCharsets.UTF_8) { validationLog("Invalid charset for capport API: " + charset); return null; } apiContent = readAsString(conn.getInputStream(), CAPPORT_API_MAX_JSON_LENGTH, charset); } catch (IOException e) { validationLog("I/O error reading capport data: " + e.getMessage()); return null; } try { final JSONObject info = new JSONObject(apiContent); return CaptivePortalDataShimImpl.fromJson(info); } catch (JSONException e) { validationLog("Could not parse capport API JSON: " + e.getMessage()); return null; } catch (UnsupportedApiLevelException e) { validationLog("Platform API too low to support capport API"); return null; } } @Override protected CaptivePortalProbeResult sendProbe(ProxyInfo proxy, URL url) { mCapportData = tryCapportApiProbe(); if (mCapportData != null && mCapportData.isCaptive()) { if (mCapportData.getUserPortalUrl() == null) { validationLog("Missing user-portal-url from capport response"); return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTP); } final String loginUrlString = mCapportData.getUserPortalUrl().toString(); // Starting from R (where CaptivePortalData was introduced), the captive portal app // delegates to NetworkMonitor for verifying when the network validates instead of // probing the detectUrl. So pass the detectUrl to have the portal open on that, // page; CaptivePortalLogin will not use it for probing. return new CaptivePortalProbeResult( CaptivePortalProbeResult.PORTAL_CODE, loginUrlString /* redirectUrl */, loginUrlString /* detectUrl */); } // If the API says it's not captive, still check for HTTP connectivity. This helps // with partial connectivity detection, and a broken API saying that there is no // redirect when there is one. return sendDnsAndHttpProbes(proxy, url, ValidationProbeEvent.PROBE_HTTP); } @Override public boolean isConclusiveResult(CaptivePortalProbeResult result) { return result.isPortal(); } } private CaptivePortalProbeResult sendParallelHttpProbes( ProxyInfo proxy, URL httpsUrl, URL httpUrl) { // Number of probes to wait for. If a probe completes with a conclusive answer // it shortcuts the latch immediately by forcing the count to 0. final CountDownLatch latch = new CountDownLatch(2); final Uri capportApiUrl = getCaptivePortalApiUrl(mLinkProperties); final HttpsProbeThread httpsProbe = new HttpsProbeThread(latch, proxy, httpsUrl, capportApiUrl); final HttpProbeThread httpProbe = new HttpProbeThread(latch, proxy, httpUrl, capportApiUrl); try { httpsProbe.start(); Loading @@ -1995,12 +2139,14 @@ public class NetworkMonitor extends StateMachine { final CaptivePortalProbeResult httpResult = httpProbe.result(); // Look for a conclusive probe result first. if (httpResult.isPortal()) { if (httpProbe.isConclusiveResult(httpResult)) { maybeReportCaptivePortalData(httpProbe.getCaptivePortalData()); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, httpResult); return httpResult; } // httpsResult.isPortal() is not expected, but check it nonetheless. if (httpsResult.isPortal() || httpsResult.isSuccessful()) { if (httpsProbe.isConclusiveResult(httpsResult)) { maybeReportCaptivePortalData(httpProbe.getCaptivePortalData()); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTPS, httpsResult); return httpsResult; } Loading @@ -2019,6 +2165,7 @@ public class NetworkMonitor extends StateMachine { // Otherwise wait until http and https probes completes and use their results. try { httpProbe.join(); maybeReportCaptivePortalData(httpProbe.getCaptivePortalData()); reportHttpProbeResult(NETWORK_VALIDATION_PROBE_HTTP, httpProbe.result()); if (httpProbe.result().isPortal()) { Loading Loading @@ -2519,6 +2666,18 @@ public class NetworkMonitor extends StateMachine { mEvaluationState.noteProbeResult(probeResult, succeeded); } private void maybeReportCaptivePortalData(@Nullable CaptivePortalDataShim data) { // Do not clear data even if it is null: access points should not stop serving the API, so // if the API disappears this is treated as a temporary failure, and previous data should // remain valid. if (data == null) return; try { data.notifyChanged(mCallback); } catch (RemoteException e) { Log.e(TAG, "Error notifying ConnectivityService of new capport data", e); } } /** * Interface for logging dns results. */ Loading Loading @@ -2546,4 +2705,8 @@ public class NetworkMonitor extends StateMachine { return ((type & DATA_STALL_EVALUATION_TYPE_DNS) != 0) ? new DnsStallDetector(threshold) : null; } private static Uri getCaptivePortalApiUrl(LinkProperties lp) { return NetworkInformationShimImpl.newInstance().getCaptivePortalApiUrl(lp); } }