• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package android.net.captiveportal;
18 
19 import static android.net.captiveportal.CaptivePortalProbeResult.PORTAL_CODE;
20 import static android.net.captiveportal.CaptivePortalProbeResult.SUCCESS_CODE;
21 
22 import android.text.TextUtils;
23 import android.util.Log;
24 
25 import androidx.annotation.NonNull;
26 import androidx.annotation.Nullable;
27 import androidx.annotation.VisibleForTesting;
28 
29 import java.net.MalformedURLException;
30 import java.net.URL;
31 import java.text.ParseException;
32 import java.util.ArrayList;
33 import java.util.Collection;
34 import java.util.List;
35 import java.util.regex.Pattern;
36 import java.util.regex.PatternSyntaxException;
37 
38 /** @hide */
39 public 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 
CaptivePortalProbeSpec(@onNull String encodedSpec, @NonNull URL url)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
parseSpec(@onNull String spec)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
parsePatternIfNonEmpty(@ullable String pattern, int pos)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
parseSpecOrNull(@ullable String spec)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
parseCaptivePortalProbeSpecs( @onNull String settingsVal)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
getResult(int status, @Nullable String locationHeader)144     public abstract CaptivePortalProbeResult getResult(int status, @Nullable String locationHeader);
145 
146     @NonNull
getEncodedSpec()147     public String getEncodedSpec() {
148         return mEncodedSpec;
149     }
150 
151     @NonNull
getUrl()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 
RegexMatchProbeSpec( String spec, URL url, Pattern statusRegex, Pattern locationHeaderRegex)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
getResult(int status, String locationHeader)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 
safeMatch(@ullable String value, @Nullable Pattern pattern)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.
checkNotNull(T object)191     private static <T> T checkNotNull(T object) {
192         if (object == null) throw new NullPointerException();
193         return object;
194     }
195 }
196