blob: bf983a50ab512bbfdea29de32fbe3b06b6ac97b3 [file] [log] [blame]
paulhu5982eff2019-03-29 19:21:30 +08001/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.net.captiveportal;
18
19import static android.net.captiveportal.CaptivePortalProbeResult.PORTAL_CODE;
20import static android.net.captiveportal.CaptivePortalProbeResult.SUCCESS_CODE;
21
22import android.text.TextUtils;
23import android.util.Log;
24
25import androidx.annotation.NonNull;
26import androidx.annotation.Nullable;
27import androidx.annotation.VisibleForTesting;
28
29import java.net.MalformedURLException;
30import java.net.URL;
31import java.text.ParseException;
32import java.util.ArrayList;
33import java.util.Collection;
34import java.util.List;
35import java.util.regex.Pattern;
36import java.util.regex.PatternSyntaxException;
37
38/** @hide */
39public abstract class CaptivePortalProbeSpec {
40 private static final String TAG = CaptivePortalProbeSpec.class.getSimpleName();
41 private static final String REGEX_SEPARATOR = "@@/@@";
42 private static final String SPEC_SEPARATOR = "@@,@@";
43
44 private final String mEncodedSpec;
45 private final URL mUrl;
46
47 CaptivePortalProbeSpec(@NonNull String encodedSpec, @NonNull URL url) {
48 mEncodedSpec = checkNotNull(encodedSpec);
49 mUrl = checkNotNull(url);
50 }
51
52 /**
53 * Parse a {@link CaptivePortalProbeSpec} from a {@link String}.
54 *
55 * <p>The valid format is a URL followed by two regular expressions, each separated by "@@/@@".
56 * @throws MalformedURLException The URL has invalid format for {@link URL#URL(String)}.
57 * @throws ParseException The string is empty, does not match the above format, or a regular
58 * expression is invalid for {@link Pattern#compile(String)}.
59 * @hide
60 */
61 @VisibleForTesting
62 @NonNull
63 public static CaptivePortalProbeSpec parseSpec(@NonNull String spec) throws ParseException,
64 MalformedURLException {
65 if (TextUtils.isEmpty(spec)) {
66 throw new ParseException("Empty probe spec", 0 /* errorOffset */);
67 }
68
69 String[] splits = TextUtils.split(spec, REGEX_SEPARATOR);
70 if (splits.length != 3) {
71 throw new ParseException("Probe spec does not have 3 parts", 0 /* errorOffset */);
72 }
73
74 final int statusRegexPos = splits[0].length() + REGEX_SEPARATOR.length();
75 final int locationRegexPos = statusRegexPos + splits[1].length() + REGEX_SEPARATOR.length();
76 final Pattern statusRegex = parsePatternIfNonEmpty(splits[1], statusRegexPos);
77 final Pattern locationRegex = parsePatternIfNonEmpty(splits[2], locationRegexPos);
78
79 return new RegexMatchProbeSpec(spec, new URL(splits[0]), statusRegex, locationRegex);
80 }
81
82 @Nullable
83 private static Pattern parsePatternIfNonEmpty(@Nullable String pattern, int pos)
84 throws ParseException {
85 if (TextUtils.isEmpty(pattern)) {
86 return null;
87 }
88 try {
89 return Pattern.compile(pattern);
90 } catch (PatternSyntaxException e) {
91 throw new ParseException(
92 String.format("Invalid status pattern [%s]: %s", pattern, e),
93 pos /* errorOffset */);
94 }
95 }
96
97 /**
98 * Parse a {@link CaptivePortalProbeSpec} from a {@link String}, or return a fallback spec
99 * based on the status code of the provided URL if the spec cannot be parsed.
100 */
101 @Nullable
102 public static CaptivePortalProbeSpec parseSpecOrNull(@Nullable String spec) {
103 if (spec != null) {
104 try {
105 return parseSpec(spec);
106 } catch (ParseException | MalformedURLException e) {
107 Log.e(TAG, "Invalid probe spec: " + spec, e);
108 // Fall through
109 }
110 }
111 return null;
112 }
113
114 /**
115 * Parse a config String to build an array of {@link CaptivePortalProbeSpec}.
116 *
117 * <p>Each spec is separated by @@,@@ and follows the format for {@link #parseSpec(String)}.
118 * <p>This method does not throw but ignores any entry that could not be parsed.
119 */
120 @NonNull
121 public static Collection<CaptivePortalProbeSpec> parseCaptivePortalProbeSpecs(
122 @NonNull String settingsVal) {
123 List<CaptivePortalProbeSpec> specs = new ArrayList<>();
124 if (settingsVal != null) {
125 for (String spec : TextUtils.split(settingsVal, SPEC_SEPARATOR)) {
126 try {
127 specs.add(parseSpec(spec));
128 } catch (ParseException | MalformedURLException e) {
129 Log.e(TAG, "Invalid probe spec: " + spec, e);
130 }
131 }
132 }
133
134 if (specs.isEmpty()) {
135 Log.e(TAG, String.format("could not create any validation spec from %s", settingsVal));
136 }
137 return specs;
138 }
139
140 /**
141 * Get the probe result from HTTP status and location header.
142 */
143 @NonNull
144 public abstract CaptivePortalProbeResult getResult(int status, @Nullable String locationHeader);
145
146 @NonNull
147 public String getEncodedSpec() {
148 return mEncodedSpec;
149 }
150
151 @NonNull
152 public URL getUrl() {
153 return mUrl;
154 }
155
156 /**
157 * Implementation of {@link CaptivePortalProbeSpec} that is based on configurable regular
158 * expressions for the HTTP status code and location header (if any). Matches indicate that
159 * the page is not a portal.
160 * This probe cannot fail: it always returns SUCCESS_CODE or PORTAL_CODE
161 */
162 private static class RegexMatchProbeSpec extends CaptivePortalProbeSpec {
163 @Nullable
164 final Pattern mStatusRegex;
165 @Nullable
166 final Pattern mLocationHeaderRegex;
167
168 RegexMatchProbeSpec(
169 String spec, URL url, Pattern statusRegex, Pattern locationHeaderRegex) {
170 super(spec, url);
171 mStatusRegex = statusRegex;
172 mLocationHeaderRegex = locationHeaderRegex;
173 }
174
175 @Override
176 public CaptivePortalProbeResult getResult(int status, String locationHeader) {
177 final boolean statusMatch = safeMatch(String.valueOf(status), mStatusRegex);
178 final boolean locationMatch = safeMatch(locationHeader, mLocationHeaderRegex);
179 final int returnCode = statusMatch && locationMatch ? SUCCESS_CODE : PORTAL_CODE;
180 return new CaptivePortalProbeResult(
181 returnCode, locationHeader, getUrl().toString(), this);
182 }
183 }
184
185 private static boolean safeMatch(@Nullable String value, @Nullable Pattern pattern) {
186 // No value is a match ("no location header" passes the location rule for non-redirects)
187 return pattern == null || TextUtils.isEmpty(value) || pattern.matcher(value).matches();
188 }
189
190 // Throws NullPointerException if the input is null.
191 private static <T> T checkNotNull(T object) {
192 if (object == null) throw new NullPointerException();
193 return object;
194 }
195}