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

Commit 1d3c5945 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Make isCaptivePortal perform both HTTP and HTTPS probes." into nyc-dev

parents 831ecc81 c5be12e7
Loading
Loading
Loading
Loading
+6 −2
Original line number Original line Diff line number Diff line
@@ -26170,8 +26170,12 @@ package android.net.metrics {
    method public static void logEvent(int, long, int, int);
    method public static void logEvent(int, long, int, int);
    method public void writeToParcel(android.os.Parcel, 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 android.os.Parcelable.Creator<android.net.metrics.ValidationProbeEvent> CREATOR;
    field public static final int PROBE_HTTP = 0; // 0x0
    field public static final int DNS_FAILURE = 0; // 0x0
    field public static final int PROBE_HTTPS = 1; // 0x1
    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 long durationMs;
    field public final int netId;
    field public final int netId;
    field public final int probeType;
    field public final int probeType;
+13 −3
Original line number Original line Diff line number Diff line
@@ -29,8 +29,13 @@ import com.android.internal.util.MessageUtils;
@SystemApi
@SystemApi
public final class ValidationProbeEvent extends IpConnectivityEvent implements Parcelable {
public final class ValidationProbeEvent extends IpConnectivityEvent implements Parcelable {


    public static final int PROBE_HTTP  = 0;
    public static final int PROBE_DNS   = 0;
    public static final int PROBE_HTTPS = 1;
    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 int netId;
    public final long durationMs;
    public final long durationMs;
@@ -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) {
    public static void logEvent(int netId, long durationMs, int probeType, int returnCode) {
        logEvent(new ValidationProbeEvent(netId, durationMs, probeType, returnCode));
        logEvent(new ValidationProbeEvent(netId, durationMs, probeType, returnCode));
    }
    }
@@ -80,7 +90,7 @@ public final class ValidationProbeEvent extends IpConnectivityEvent implements P
    @Override
    @Override
    public String toString() {
    public String toString() {
        return String.format("ValidationProbeEvent(%d, %s:%d, %dms)",
        return String.format("ValidationProbeEvent(%d, %s:%d, %dms)",
                netId, Decoder.constants.get(probeType), returnCode, durationMs);
                netId, getProbeName(probeType), returnCode, durationMs);
    }
    }


    final static class Decoder {
    final static class Decoder {
+9 −0
Original line number Original line Diff line number Diff line
@@ -7705,6 +7705,15 @@ public final class Settings {
         */
         */
        public static final String CAPTIVE_PORTAL_SERVER = "captive_portal_server";
        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.
         * Whether network service discovery is enabled.
         *
         *
+199 −60
Original line number Original line Diff line number Diff line
@@ -71,7 +71,11 @@ import com.android.server.connectivity.NetworkAgentInfo;
import java.io.IOException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.net.URL;
import java.net.URL;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.List;
import java.util.List;
import java.util.Random;
import java.util.Random;


@@ -228,7 +232,8 @@ public class NetworkMonitor extends StateMachine {
    private final AlarmManager mAlarmManager;
    private final AlarmManager mAlarmManager;
    private final NetworkRequest mDefaultRequest;
    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.
    // Set if the user explicitly selected "Do not use this network" in captive portal sign-in app.
    private boolean mUserDoesNotWant = false;
    private boolean mUserDoesNotWant = false;
@@ -276,6 +281,8 @@ public class NetworkMonitor extends StateMachine {


        mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
        mIsCaptivePortalCheckEnabled = Settings.Global.getInt(mContext.getContentResolver(),
                Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
                Settings.Global.CAPTIVE_PORTAL_DETECTION_ENABLED, 1) == 1;
        mUseHttps = Settings.Global.getInt(mContext.getContentResolver(),
                Settings.Global.CAPTIVE_PORTAL_USE_HTTPS, 1) == 1;


        start();
        start();
    }
    }
@@ -324,6 +331,21 @@ public class NetworkMonitor extends StateMachine {
                    return HANDLED;
                    return HANDLED;
                case CMD_CAPTIVE_PORTAL_APP_FINISHED:
                case CMD_CAPTIVE_PORTAL_APP_FINISHED:
                    log("CaptivePortal App responded with " + message.arg1);
                    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) {
                    switch (message.arg1) {
                        case APP_RETURN_DISMISSED:
                        case APP_RETURN_DISMISSED:
                            sendMessage(CMD_FORCE_REEVALUATION, 0 /* no UID */, 0);
                            sendMessage(CMD_FORCE_REEVALUATION, 0 /* no UID */, 0);
@@ -424,6 +446,8 @@ public class NetworkMonitor extends StateMachine {
     */
     */
    @VisibleForTesting
    @VisibleForTesting
    public static final class CaptivePortalProbeResult {
    public static final class CaptivePortalProbeResult {
        static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599, null);

        final int mHttpResponseCode; // HTTP response code returned from Internet probe.
        final int mHttpResponseCode; // HTTP response code returned from Internet probe.
        final String mRedirectUrl;   // Redirect destination returned from Internet probe.
        final String mRedirectUrl;   // Redirect destination returned from Internet probe.


@@ -431,6 +455,11 @@ public class NetworkMonitor extends StateMachine {
            mHttpResponseCode = httpResponseCode;
            mHttpResponseCode = httpResponseCode;
            mRedirectUrl = redirectUrl;
            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
    // Being in the EvaluatingState State indicates the Network is being evaluated for internet
@@ -481,6 +510,7 @@ public class NetworkMonitor extends StateMachine {
                    //    expensive metered network, or unwanted leaking of the User Agent string.
                    //    expensive metered network, or unwanted leaking of the User Agent string.
                    if (!mDefaultRequest.networkCapabilities.satisfiedByNetworkCapabilities(
                    if (!mDefaultRequest.networkCapabilities.satisfiedByNetworkCapabilities(
                            mNetworkAgentInfo.networkCapabilities)) {
                            mNetworkAgentInfo.networkCapabilities)) {
                        validationLog("Network would not satisfy default request, not validating");
                        transitionTo(mValidatedState);
                        transitionTo(mValidatedState);
                        return HANDLED;
                        return HANDLED;
                    }
                    }
@@ -492,10 +522,9 @@ public class NetworkMonitor extends StateMachine {
                    // will be unresponsive. isCaptivePortal() could be executed on another Thread
                    // will be unresponsive. isCaptivePortal() could be executed on another Thread
                    // if this is found to cause problems.
                    // if this is found to cause problems.
                    CaptivePortalProbeResult probeResult = isCaptivePortal();
                    CaptivePortalProbeResult probeResult = isCaptivePortal();
                    if (probeResult.mHttpResponseCode == 204) {
                    if (probeResult.isSuccessful()) {
                        transitionTo(mValidatedState);
                        transitionTo(mValidatedState);
                    } else if (probeResult.mHttpResponseCode >= 200 &&
                    } else if (probeResult.isPortal()) {
                            probeResult.mHttpResponseCode <= 399) {
                        mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
                        mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
                                NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.mRedirectUrl));
                                NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.mRedirectUrl));
                        transitionTo(mCaptivePortalState);
                        transitionTo(mCaptivePortalState);
@@ -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(),
        String server = Settings.Global.getString(context.getContentResolver(),
                Settings.Global.CAPTIVE_PORTAL_SERVER);
                Settings.Global.CAPTIVE_PORTAL_SERVER);
        if (server == null) server = DEFAULT_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
    @VisibleForTesting
    protected CaptivePortalProbeResult isCaptivePortal() {
    protected CaptivePortalProbeResult isCaptivePortal() {
        if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204, null);
        if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204, null);


        HttpURLConnection urlConnection = null;
        URL pacUrl = null, httpUrl = null, httpsUrl = null;
        int httpResponseCode = 599;

        String redirectUrl = null;
        final Stopwatch probeTimer = new Stopwatch().start();
        try {
            URL url = new URL(getCaptivePortalServerUrl(mContext));
        // On networks with a PAC instead of fetching a URL that should result in a 204
        // 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:
        // 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
        // 1. At present our PAC code does not yet handle multiple PACs on multiple networks
@@ -697,34 +722,93 @@ public class NetworkMonitor extends StateMachine {
        // 3. PAC scripts are sometimes used to block or restrict Internet access and may in
        // 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
        //    fact block fetching of the generate_204 URL which would lead to false negative
        //    results for network validation.
        //    results for network validation.
            boolean fetchPac = false;
        final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy();
        final ProxyInfo proxyInfo = mNetworkAgentInfo.linkProperties.getHttpProxy();
        if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
        if (proxyInfo != null && !Uri.EMPTY.equals(proxyInfo.getPacFileUrl())) {
                url = new URL(proxyInfo.getPacFileUrl().toString());
            try {
                fetchPac = true;
                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.
        // unnecessary resolution.
            if (proxyInfo == null || fetchPac) {
        String hostToResolve = null;
                hostToResolve = url.getHost();
        if (pacUrl != null) {
            hostToResolve = pacUrl.getHost();
        } else if (proxyInfo != null) {
        } else if (proxyInfo != null) {
            hostToResolve = proxyInfo.getHost();
            hostToResolve = proxyInfo.getHost();
        } else {
            hostToResolve = httpUrl.getHost();
        }
        }

        if (!TextUtils.isEmpty(hostToResolve)) {
        if (!TextUtils.isEmpty(hostToResolve)) {
                connectInfo.append(", " + hostToResolve + "=");
            String probeName = ValidationProbeEvent.getProbeName(ValidationProbeEvent.PROBE_DNS);
                final InetAddress[] addresses =
            final Stopwatch dnsTimer = new Stopwatch().start();
                        mNetworkAgentInfo.network.getAllByName(hostToResolve);
            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) {
                for (InetAddress address : addresses) {
                    connectInfo.append(address.getHostAddress());
                    connectInfo.append(address.getHostAddress());
                    if (address != addresses[addresses.length-1]) connectInfo.append(",");
                    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 = (HttpURLConnection) mNetworkAgentInfo.network.openConnection(url);
            urlConnection.setInstanceFollowRedirects(fetchPac);
            urlConnection.setInstanceFollowRedirects(probeType == ValidationProbeEvent.PROBE_PAC);
            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
            urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
            urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
            urlConnection.setUseCaches(false);
            urlConnection.setUseCaches(false);
@@ -738,7 +822,9 @@ public class NetworkMonitor extends StateMachine {
            // Time how long it takes to get a response to our request
            // Time how long it takes to get a response to our request
            long responseTimestamp = SystemClock.elapsedRealtime();
            long responseTimestamp = SystemClock.elapsedRealtime();


            validationLog("isCaptivePortal: ret=" + httpResponseCode +
            validationLog(ValidationProbeEvent.getProbeName(probeType) + " " + url +
                    " time=" + (responseTimestamp - requestTimestamp) + "ms" +
                    " ret=" + httpResponseCode +
                    " headers=" + urlConnection.getHeaderFields());
                    " headers=" + urlConnection.getHeaderFields());
            // NOTE: We may want to consider an "HTTP/1.0 204" response to be a captive
            // 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
            // portal.  The only example of this seen so far was a captive portal.  For
@@ -756,14 +842,10 @@ public class NetworkMonitor extends StateMachine {
                httpResponseCode = 204;
                httpResponseCode = 204;
            }
            }


            if (httpResponseCode == 200 && fetchPac) {
            if (httpResponseCode == 200 && probeType == ValidationProbeEvent.PROBE_PAC) {
                validationLog("PAC fetch 200 response interpreted as 204 response.");
                validationLog("PAC fetch 200 response interpreted as 204 response.");
                httpResponseCode = 204;
                httpResponseCode = 204;
            }
            }

            sendNetworkConditionsBroadcast(true /* response received */,
                    httpResponseCode != 204 /* isCaptivePortal */,
                    requestTimestamp, responseTimestamp);
        } catch (IOException e) {
        } catch (IOException e) {
            validationLog("Probably not a portal: exception " + e);
            validationLog("Probably not a portal: exception " + e);
            if (httpResponseCode == 599) {
            if (httpResponseCode == 599) {
@@ -774,11 +856,68 @@ public class NetworkMonitor extends StateMachine {
                urlConnection.disconnect();
                urlConnection.disconnect();
            }
            }
        }
        }
        final int probeType = ValidationProbeEvent.PROBE_HTTP;
        ValidationProbeEvent.logEvent(mNetId, probeTimer.stop(), probeType, httpResponseCode);
        ValidationProbeEvent.logEvent(mNetId, probeTimer.stop(), probeType, httpResponseCode);
        return new CaptivePortalProbeResult(httpResponseCode, redirectUrl);
        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.
     * @param responseReceived - whether or not we received a valid HTTP response to our request.
     * If false, isCaptivePortal and responseTimestampMs are ignored
     * If false, isCaptivePortal and responseTimestampMs are ignored