Loading core/java/android/net/ConnectivityManager.java +8 −0 Original line number Diff line number Diff line Loading @@ -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} Loading core/java/android/net/captiveportal/CaptivePortalProbeResult.java +11 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package android.net.captiveportal; import android.annotation.Nullable; /** * Result of calling isCaptivePortal(). * @hide Loading @@ -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 = Loading @@ -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() { Loading core/java/android/net/captiveportal/CaptivePortalProbeSpec.java 0 → 100644 +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(); } } core/java/android/provider/Settings.java +9 −0 Original line number Diff line number Diff line Loading @@ -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 Loading core/tests/coretests/src/android/provider/SettingsBackupTest.java +1 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
core/java/android/net/ConnectivityManager.java +8 −0 Original line number Diff line number Diff line Loading @@ -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} Loading
core/java/android/net/captiveportal/CaptivePortalProbeResult.java +11 −0 Original line number Diff line number Diff line Loading @@ -16,6 +16,8 @@ package android.net.captiveportal; import android.annotation.Nullable; /** * Result of calling isCaptivePortal(). * @hide Loading @@ -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 = Loading @@ -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() { Loading
core/java/android/net/captiveportal/CaptivePortalProbeSpec.java 0 → 100644 +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(); } }
core/java/android/provider/Settings.java +9 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
core/tests/coretests/src/android/provider/SettingsBackupTest.java +1 −0 Original line number Diff line number Diff line Loading @@ -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