Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b77631da authored by Remi NGUYEN VAN's avatar Remi NGUYEN VAN
Browse files

Add CaptivePortalData fetching to NetworkMonitor am: 2fa48c28

Change-Id: I51e96f74b6e76944a0ec78c2256755303b0b859a
parents 4b8f48e6 2fa48c28
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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;
    }
}
+7 −1
Original line number Diff line number Diff line
@@ -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;
@@ -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);
        }

@@ -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);
+2 −0
Original line number Diff line number Diff line
@@ -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;
    }

@@ -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;
    }

+1 −0
Original line number Diff line number Diff line
@@ -25,4 +25,5 @@ parcelable DhcpResultsParcelable {
    String serverAddress;
    String vendorInfo;
    String serverHostName;
    String captivePortalApiUrl;
}
+200 −37
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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);
@@ -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;
@@ -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
@@ -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();
@@ -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.
@@ -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();
@@ -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();
@@ -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;
        }
@@ -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()) {
@@ -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.
     */
@@ -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