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

Commit 1f8f21af authored by Remi NGUYEN VAN's avatar Remi NGUYEN VAN Committed by Android (Google) Code Review
Browse files

Merge "Add configurable captive portal probes" into pi-dev

parents c8494ab0 8255c2d6
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -237,6 +237,14 @@ public class ConnectivityManager {
     */
    public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL";

    /**
     * Key for passing a {@link android.net.captiveportal.CaptivePortalProbeSpec} to the captive
     * portal login activity.
     * {@hide}
     */
    public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC =
            "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC";

    /**
     * Key for passing a user agent string to the captive portal login activity.
     * {@hide}
+11 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.net.captiveportal;

import android.annotation.Nullable;

/**
 * Result of calling isCaptivePortal().
 * @hide
@@ -23,6 +25,7 @@ package android.net.captiveportal;
public final class CaptivePortalProbeResult {
    public static final int SUCCESS_CODE = 204;
    public static final int FAILED_CODE = 599;
    public static final int PORTAL_CODE = 302;

    public static final CaptivePortalProbeResult FAILED = new CaptivePortalProbeResult(FAILED_CODE);
    public static final CaptivePortalProbeResult SUCCESS =
@@ -32,15 +35,23 @@ public final class CaptivePortalProbeResult {
    public final String redirectUrl;      // Redirect destination returned from Internet probe.
    public final String detectUrl;        // URL where a 204 response code indicates
                                          // captive portal has been appeased.
    @Nullable
    public final CaptivePortalProbeSpec probeSpec;

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

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

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

    public boolean isSuccessful() {
+180 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net.captiveportal;

import static android.net.captiveportal.CaptivePortalProbeResult.PORTAL_CODE;
import static android.net.captiveportal.CaptivePortalProbeResult.SUCCESS_CODE;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

/** @hide */
public abstract class CaptivePortalProbeSpec {
    public static final String HTTP_LOCATION_HEADER_NAME = "Location";

    private static final String TAG = CaptivePortalProbeSpec.class.getSimpleName();
    private static final String REGEX_SEPARATOR = "@@/@@";
    private static final String SPEC_SEPARATOR = "@@,@@";

    private final String mEncodedSpec;
    private final URL mUrl;

    CaptivePortalProbeSpec(String encodedSpec, URL url) {
        mEncodedSpec = encodedSpec;
        mUrl = url;
    }

    /**
     * Parse a {@link CaptivePortalProbeSpec} from a {@link String}.
     *
     * <p>The valid format is a URL followed by two regular expressions, each separated by "@@/@@".
     * @throws MalformedURLException The URL has invalid format for {@link URL#URL(String)}.
     * @throws ParseException The string is empty, does not match the above format, or a regular
     * expression is invalid for {@link Pattern#compile(String)}.
     */
    @NonNull
    public static CaptivePortalProbeSpec parseSpec(String spec) throws ParseException,
            MalformedURLException {
        if (TextUtils.isEmpty(spec)) {
            throw new ParseException("Empty probe spec", 0 /* errorOffset */);
        }

        String[] splits = TextUtils.split(spec, REGEX_SEPARATOR);
        if (splits.length != 3) {
            throw new ParseException("Probe spec does not have 3 parts", 0 /* errorOffset */);
        }

        final int statusRegexPos = splits[0].length() + REGEX_SEPARATOR.length();
        final int locationRegexPos = statusRegexPos + splits[1].length() + REGEX_SEPARATOR.length();
        final Pattern statusRegex = parsePatternIfNonEmpty(splits[1], statusRegexPos);
        final Pattern locationRegex = parsePatternIfNonEmpty(splits[2], locationRegexPos);

        return new RegexMatchProbeSpec(spec, new URL(splits[0]), statusRegex, locationRegex);
    }

    @Nullable
    private static Pattern parsePatternIfNonEmpty(String pattern, int pos) throws ParseException {
        if (TextUtils.isEmpty(pattern)) {
            return null;
        }
        try {
            return Pattern.compile(pattern);
        } catch (PatternSyntaxException e) {
            throw new ParseException(
                    String.format("Invalid status pattern [%s]: %s", pattern, e),
                    pos /* errorOffset */);
        }
    }

    /**
     * Parse a {@link CaptivePortalProbeSpec} from a {@link String}, or return a fallback spec
     * based on the status code of the provided URL if the spec cannot be parsed.
     */
    @Nullable
    public static CaptivePortalProbeSpec parseSpecOrNull(@Nullable String spec) {
        if (spec != null) {
            try {
                return parseSpec(spec);
            } catch (ParseException | MalformedURLException e) {
                Log.e(TAG, "Invalid probe spec: " + spec, e);
                // Fall through
            }
        }
        return null;
    }

    /**
     * Parse a config String to build an array of {@link CaptivePortalProbeSpec}.
     *
     * <p>Each spec is separated by @@,@@ and follows the format for {@link #parseSpec(String)}.
     * <p>This method does not throw but ignores any entry that could not be parsed.
     */
    public static CaptivePortalProbeSpec[] parseCaptivePortalProbeSpecs(String settingsVal) {
        List<CaptivePortalProbeSpec> specs = new ArrayList<>();
        if (settingsVal != null) {
            for (String spec : TextUtils.split(settingsVal, SPEC_SEPARATOR)) {
                try {
                    specs.add(parseSpec(spec));
                } catch (ParseException | MalformedURLException e) {
                    Log.e(TAG, "Invalid probe spec: " + spec, e);
                }
            }
        }

        if (specs.isEmpty()) {
            Log.e(TAG, String.format("could not create any validation spec from %s", settingsVal));
        }
        return specs.toArray(new CaptivePortalProbeSpec[specs.size()]);
    }

    /**
     * Get the probe result from HTTP status and location header.
     */
    public abstract CaptivePortalProbeResult getResult(int status, @Nullable String locationHeader);

    public String getEncodedSpec() {
        return mEncodedSpec;
    }

    public URL getUrl() {
        return mUrl;
    }

    /**
     * Implementation of {@link CaptivePortalProbeSpec} that is based on configurable regular
     * expressions for the HTTP status code and location header (if any). Matches indicate that
     * the page is not a portal.
     * This probe cannot fail: it always returns SUCCESS_CODE or PORTAL_CODE
     */
    private static class RegexMatchProbeSpec extends CaptivePortalProbeSpec {
        @Nullable
        final Pattern mStatusRegex;
        @Nullable
        final Pattern mLocationHeaderRegex;

        RegexMatchProbeSpec(
                String spec, URL url, Pattern statusRegex, Pattern locationHeaderRegex) {
            super(spec, url);
            mStatusRegex = statusRegex;
            mLocationHeaderRegex = locationHeaderRegex;
        }

        @Override
        public CaptivePortalProbeResult getResult(int status, String locationHeader) {
            final boolean statusMatch = safeMatch(String.valueOf(status), mStatusRegex);
            final boolean locationMatch = safeMatch(locationHeader, mLocationHeaderRegex);
            final int returnCode = statusMatch && locationMatch ? SUCCESS_CODE : PORTAL_CODE;
            return new CaptivePortalProbeResult(
                    returnCode, locationHeader, getUrl().toString(), this);
        }
    }

    private static boolean safeMatch(@Nullable String value, @Nullable Pattern pattern) {
        // No value is a match ("no location header" passes the location rule for non-redirects)
        return pattern == null || TextUtils.isEmpty(value) || pattern.matcher(value).matches();
    }
}
+9 −0
Original line number Diff line number Diff line
@@ -10281,6 +10281,15 @@ public final class Settings {
        public static final String CAPTIVE_PORTAL_OTHER_FALLBACK_URLS =
                "captive_portal_other_fallback_urls";
        /**
         * A list of captive portal detection specifications used in addition to the fallback URLs.
         * Each spec has the format url@@/@@statusCodeRegex@@/@@contentRegex. Specs are separated
         * by "@@,@@".
         * @hide
         */
        public static final String CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS =
                "captive_portal_fallback_probe_specs";
        /**
         * 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
+1 −0
Original line number Diff line number Diff line
@@ -156,6 +156,7 @@ public class SettingsBackupTest {
                    Settings.Global.CAPTIVE_PORTAL_HTTP_URL,
                    Settings.Global.CAPTIVE_PORTAL_MODE,
                    Settings.Global.CAPTIVE_PORTAL_OTHER_FALLBACK_URLS,
                    Settings.Global.CAPTIVE_PORTAL_FALLBACK_PROBE_SPECS,
                    Settings.Global.CAPTIVE_PORTAL_SERVER,
                    Settings.Global.CAPTIVE_PORTAL_USE_HTTPS,
                    Settings.Global.CAPTIVE_PORTAL_USER_AGENT,
Loading