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

Commit d953bf85 authored by Hugo Benichi's avatar Hugo Benichi
Browse files

Captive portal detection uses 3rd fallback probe

This patch adds the possitibility to send a 3rd fallback validation
probe in sendParallelHttpProbes when neither the 1st http probe nor the
https probe came back with a conclusive answer.

This 3rd probe is only used for trying again captive portal detection
and does not return success, so that network validation always fails if
the https probe fails.

In addition, the url reveals a captive portal is now sent to the
CaptivePortalLoginActivity so that all three probes can use different
urls.

Bug: 29367974

Change-Id: I7385fde1aa1316d94aac350af0e956cb193aa4ee
parent e83639f6
Loading
Loading
Loading
Loading
+7 −5
Original line number Diff line number Diff line
@@ -38,6 +38,8 @@ public final class ValidationProbeEvent implements Parcelable {
    public static final int PROBE_HTTP      = 1;
    public static final int PROBE_HTTPS     = 2;
    public static final int PROBE_PAC       = 3;
    /** {@hide} */
    public static final int PROBE_FALLBACK  = 4;

    public static final int DNS_FAILURE = 0;
    public static final int DNS_SUCCESS = 1;
@@ -57,7 +59,7 @@ public final class ValidationProbeEvent implements Parcelable {
    public final @ProbeType int probeType;
    public final @ReturnCode int returnCode;

    /** @hide */
    /** {@hide} */
    public ValidationProbeEvent(
            int netId, long durationMs, @ProbeType int probeType, @ReturnCode int returnCode) {
        this.netId = netId;
+72 −42
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import static android.net.CaptivePortal.APP_RETURN_WANTED_AS_IS;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -37,14 +36,12 @@ import android.net.Uri;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.NetworkEvent;
import android.net.metrics.ValidationProbeEvent;
import android.net.util.Stopwatch;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.util.Stopwatch;
import android.os.Handler;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings;
import android.telephony.CellIdentityCdma;
@@ -66,18 +63,17 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Protocol;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.internal.util.WakeupMessage;

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.net.UnknownHostException;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

/**
 * {@hide}
@@ -87,6 +83,7 @@ public class NetworkMonitor extends StateMachine {
    private static final String TAG = NetworkMonitor.class.getSimpleName();
    private static final String DEFAULT_SERVER = "connectivitycheck.gstatic.com";
    private static final int SOCKET_TIMEOUT_MS = 10000;
    private static final int PROBE_TIMEOUT_MS = 3000;
    public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
            "android.net.conn.NETWORK_CONDITIONS_MEASURED";
    public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
@@ -224,6 +221,9 @@ public class NetworkMonitor extends StateMachine {

    private final Stopwatch mEvaluationTimer = new Stopwatch();

    // This variable is set before transitioning to the mCaptivePortalState.
    private CaptivePortalProbeResult mLastPortalProbeResult = CaptivePortalProbeResult.FAILED;

    public NetworkMonitor(Context context, Handler handler, NetworkAgentInfo networkAgentInfo,
            NetworkRequest defaultRequest) {
        this(context, handler, networkAgentInfo, defaultRequest, new IpConnectivityLog());
@@ -389,6 +389,8 @@ public class NetworkMonitor extends StateMachine {
                                    sendMessage(CMD_CAPTIVE_PORTAL_APP_FINISHED, response);
                                }
                            }));
                    intent.putExtra(ConnectivityManager.EXTRA_CAPTIVE_PORTAL_URL,
                            mLastPortalProbeResult.detectUrl);
                    intent.setFlags(
                            Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT | Intent.FLAG_ACTIVITY_NEW_TASK);
                    mContext.startActivityAsUser(intent, UserHandle.CURRENT);
@@ -412,14 +414,22 @@ public class NetworkMonitor extends StateMachine {
     */
    @VisibleForTesting
    public static final class CaptivePortalProbeResult {
        static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599, null);
        static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(599);

        final int mHttpResponseCode; // HTTP response code returned from Internet probe.
        final String mRedirectUrl;   // Redirect destination returned from Internet probe.
        private final int mHttpResponseCode;  // HTTP response code returned from Internet probe.
        final String redirectUrl;             // Redirect destination returned from Internet probe.
        final String detectUrl;               // URL where a 204 response code indicates
                                              // captive portal has been appeased.

        public CaptivePortalProbeResult(int httpResponseCode, String redirectUrl) {
        public CaptivePortalProbeResult(
                int httpResponseCode, String redirectUrl, String detectUrl) {
            mHttpResponseCode = httpResponseCode;
            mRedirectUrl = redirectUrl;
            this.redirectUrl = redirectUrl;
            this.detectUrl = detectUrl;
        }

        public CaptivePortalProbeResult(int httpResponseCode) {
            this(httpResponseCode, null, null);
        }

        boolean isSuccessful() { return mHttpResponseCode == 204; }
@@ -492,7 +502,8 @@ public class NetworkMonitor extends StateMachine {
                        transitionTo(mValidatedState);
                    } else if (probeResult.isPortal()) {
                        mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
                                NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.mRedirectUrl));
                                NETWORK_TEST_RESULT_INVALID, mNetId, probeResult.redirectUrl));
                        mLastPortalProbeResult = probeResult;
                        transitionTo(mCaptivePortalState);
                    } else {
                        final Message msg = obtainMessage(CMD_REEVALUATE, ++mReevaluateToken, 0);
@@ -500,7 +511,7 @@ public class NetworkMonitor extends StateMachine {
                        logNetworkEvent(NetworkEvent.NETWORK_VALIDATION_FAILED);
                        mConnectivityServiceHandler.sendMessage(obtainMessage(
                                EVENT_NETWORK_TESTED, NETWORK_TEST_RESULT_INVALID, mNetId,
                                probeResult.mRedirectUrl));
                                probeResult.redirectUrl));
                        if (mAttempts >= BLAME_FOR_EVALUATION_ATTEMPTS) {
                            // Don't continue to blame UID forever.
                            TrafficStats.clearThreadStatsUid();
@@ -598,7 +609,7 @@ public class NetworkMonitor extends StateMachine {

    @VisibleForTesting
    protected CaptivePortalProbeResult isCaptivePortal() {
        if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204, null);
        if (!mIsCaptivePortalCheckEnabled) return new CaptivePortalProbeResult(204);

        URL pacUrl = null, httpUrl = null, httpsUrl = null;

@@ -680,7 +691,7 @@ public class NetworkMonitor extends StateMachine {
        if (pacUrl != null) {
            result = sendHttpProbe(pacUrl, ValidationProbeEvent.PROBE_PAC);
        } else if (mUseHttps) {
            result = sendParallelHttpProbes(httpsUrl, httpUrl);
            result = sendParallelHttpProbes(httpsUrl, httpUrl, null);
        } else {
            result = sendHttpProbe(httpUrl, ValidationProbeEvent.PROBE_HTTP);
        }
@@ -755,28 +766,24 @@ public class NetworkMonitor extends StateMachine {
            }
        }
        logValidationProbe(probeTimer.stop(), probeType, httpResponseCode);
        return new CaptivePortalProbeResult(httpResponseCode, redirectUrl);
        return new CaptivePortalProbeResult(httpResponseCode, redirectUrl, url.toString());
    }

    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.
    private CaptivePortalProbeResult sendParallelHttpProbes(
            URL httpsUrl, URL httpUrl, URL fallbackUrl) {
        // 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);

        // 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;
            private volatile CaptivePortalProbeResult mResult = CaptivePortalProbeResult.FAILED;

            public ProbeThread(boolean isHttps) {
                mIsHttps = isHttps;
            }

            public CaptivePortalProbeResult getResult() {
            public CaptivePortalProbeResult result() {
                return mResult;
            }

@@ -788,32 +795,55 @@ public class NetworkMonitor extends StateMachine {
                    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);
                    // Stop waiting immediately if https succeeds or if http finds a portal.
                    while (latch.getCount() > 0) {
                        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.
                }
                // Signal this probe has completed.
                latch.countDown();
            }
        }

        ProbeThread httpsProbe = new ProbeThread(true);
        ProbeThread httpProbe = new ProbeThread(false);
        httpsProbe.start();
        httpProbe.start();
        final ProbeThread httpsProbe = new ProbeThread(true);
        final ProbeThread httpProbe = new ProbeThread(false);

        try {
            latch.await();
            httpsProbe.start();
            httpProbe.start();
            latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            validationLog("Error: probe wait interrupted!");
            validationLog("Error: probes wait interrupted!");
            return CaptivePortalProbeResult.FAILED;
        }

        // If there was no deciding probe, that means that both probes completed. Return HTTPS.
        finalResult.compareAndSet(null, httpsProbe.getResult());
        final CaptivePortalProbeResult httpsResult = httpsProbe.result();
        final CaptivePortalProbeResult httpResult = httpProbe.result();

        return finalResult.get();
        // Look for a conclusive probe result first.
        if (httpResult.isPortal()) {
            return httpResult;
        }
        // httpsResult.isPortal() is not expected, but check it nonetheless.
        if (httpsResult.isPortal() || httpsResult.isSuccessful()) {
            return httpsResult;
        }
        // If a fallback url is specified, use a fallback probe to try again portal detection.
        if (fallbackUrl != null) {
            CaptivePortalProbeResult result =
                    sendHttpProbe(fallbackUrl, ValidationProbeEvent.PROBE_FALLBACK);
            if (result.isPortal()) {
                return result;
            }
        }
        // Otherwise wait until https probe completes and use its result.
        try {
            httpsProbe.join();
        } catch (InterruptedException e) {
            validationLog("Error: https probe wait interrupted!");
            return CaptivePortalProbeResult.FAILED;
        }
        return httpsProbe.result();
    }

    /**
+1 −1
Original line number Diff line number Diff line
@@ -596,7 +596,7 @@ public class ConnectivityServiceTest extends AndroidTestCase {

        @Override
        protected CaptivePortalProbeResult isCaptivePortal() {
            return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl);
            return new CaptivePortalProbeResult(gen204ProbeResult, gen204ProbeRedirectUrl, null);
        }
    }