CaptivePortalProbeSpec.java revision 8255c2d6c27363daa8bd92f7fcb3302682b9950a
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
17package android.net.captiveportal;
18
19import static android.net.captiveportal.CaptivePortalProbeResult.PORTAL_CODE;
20import static android.net.captiveportal.CaptivePortalProbeResult.SUCCESS_CODE;
21
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.text.TextUtils;
25import android.util.Log;
26
27import java.net.MalformedURLException;
28import java.net.URL;
29import java.text.ParseException;
30import java.util.ArrayList;
31import java.util.List;
32import java.util.regex.Pattern;
33import java.util.regex.PatternSyntaxException;
34
35/** @hide */
36public abstract class CaptivePortalProbeSpec {
37    public static final String HTTP_LOCATION_HEADER_NAME = "Location";
38
39    private static final String TAG = CaptivePortalProbeSpec.class.getSimpleName();
40    private static final String REGEX_SEPARATOR = "@@/@@";
41    private static final String SPEC_SEPARATOR = "@@,@@";
42
43    private final String mEncodedSpec;
44    private final URL mUrl;
45
46    CaptivePortalProbeSpec(String encodedSpec, URL url) {
47        mEncodedSpec = encodedSpec;
48        mUrl = url;
49    }
50
51    /**
52     * Parse a {@link CaptivePortalProbeSpec} from a {@link String}.
53     *
54     * <p>The valid format is a URL followed by two regular expressions, each separated by "@@/@@".
55     * @throws MalformedURLException The URL has invalid format for {@link URL#URL(String)}.
56     * @throws ParseException The string is empty, does not match the above format, or a regular
57     * expression is invalid for {@link Pattern#compile(String)}.
58     */
59    @NonNull
60    public static CaptivePortalProbeSpec parseSpec(String spec) throws ParseException,
61            MalformedURLException {
62        if (TextUtils.isEmpty(spec)) {
63            throw new ParseException("Empty probe spec", 0 /* errorOffset */);
64        }
65
66        String[] splits = TextUtils.split(spec, REGEX_SEPARATOR);
67        if (splits.length != 3) {
68            throw new ParseException("Probe spec does not have 3 parts", 0 /* errorOffset */);
69        }
70
71        final int statusRegexPos = splits[0].length() + REGEX_SEPARATOR.length();
72        final int locationRegexPos = statusRegexPos + splits[1].length() + REGEX_SEPARATOR.length();
73        final Pattern statusRegex = parsePatternIfNonEmpty(splits[1], statusRegexPos);
74        final Pattern locationRegex = parsePatternIfNonEmpty(splits[2], locationRegexPos);
75
76        return new RegexMatchProbeSpec(spec, new URL(splits[0]), statusRegex, locationRegex);
77    }
78
79    @Nullable
80    private static Pattern parsePatternIfNonEmpty(String pattern, int pos) throws ParseException {
81        if (TextUtils.isEmpty(pattern)) {
82            return null;
83        }
84        try {
85            return Pattern.compile(pattern);
86        } catch (PatternSyntaxException e) {
87            throw new ParseException(
88                    String.format("Invalid status pattern [%s]: %s", pattern, e),
89                    pos /* errorOffset */);
90        }
91    }
92
93    /**
94     * Parse a {@link CaptivePortalProbeSpec} from a {@link String}, or return a fallback spec
95     * based on the status code of the provided URL if the spec cannot be parsed.
96     */
97    @Nullable
98    public static CaptivePortalProbeSpec parseSpecOrNull(@Nullable String spec) {
99        if (spec != null) {
100            try {
101                return parseSpec(spec);
102            } catch (ParseException | MalformedURLException e) {
103                Log.e(TAG, "Invalid probe spec: " + spec, e);
104                // Fall through
105            }
106        }
107        return null;
108    }
109
110    /**
111     * Parse a config String to build an array of {@link CaptivePortalProbeSpec}.
112     *
113     * <p>Each spec is separated by @@,@@ and follows the format for {@link #parseSpec(String)}.
114     * <p>This method does not throw but ignores any entry that could not be parsed.
115     */
116    public static CaptivePortalProbeSpec[] parseCaptivePortalProbeSpecs(String settingsVal) {
117        List<CaptivePortalProbeSpec> specs = new ArrayList<>();
118        if (settingsVal != null) {
119            for (String spec : TextUtils.split(settingsVal, SPEC_SEPARATOR)) {
120                try {
121                    specs.add(parseSpec(spec));
122                } catch (ParseException | MalformedURLException e) {
123                    Log.e(TAG, "Invalid probe spec: " + spec, e);
124                }
125            }
126        }
127
128        if (specs.isEmpty()) {
129            Log.e(TAG, String.format("could not create any validation spec from %s", settingsVal));
130        }
131        return specs.toArray(new CaptivePortalProbeSpec[specs.size()]);
132    }
133
134    /**
135     * Get the probe result from HTTP status and location header.
136     */
137    public abstract CaptivePortalProbeResult getResult(int status, @Nullable String locationHeader);
138
139    public String getEncodedSpec() {
140        return mEncodedSpec;
141    }
142
143    public URL getUrl() {
144        return mUrl;
145    }
146
147    /**
148     * Implementation of {@link CaptivePortalProbeSpec} that is based on configurable regular
149     * expressions for the HTTP status code and location header (if any). Matches indicate that
150     * the page is not a portal.
151     * This probe cannot fail: it always returns SUCCESS_CODE or PORTAL_CODE
152     */
153    private static class RegexMatchProbeSpec extends CaptivePortalProbeSpec {
154        @Nullable
155        final Pattern mStatusRegex;
156        @Nullable
157        final Pattern mLocationHeaderRegex;
158
159        RegexMatchProbeSpec(
160                String spec, URL url, Pattern statusRegex, Pattern locationHeaderRegex) {
161            super(spec, url);
162            mStatusRegex = statusRegex;
163            mLocationHeaderRegex = locationHeaderRegex;
164        }
165
166        @Override
167        public CaptivePortalProbeResult getResult(int status, String locationHeader) {
168            final boolean statusMatch = safeMatch(String.valueOf(status), mStatusRegex);
169            final boolean locationMatch = safeMatch(locationHeader, mLocationHeaderRegex);
170            final int returnCode = statusMatch && locationMatch ? SUCCESS_CODE : PORTAL_CODE;
171            return new CaptivePortalProbeResult(
172                    returnCode, locationHeader, getUrl().toString(), this);
173        }
174    }
175
176    private static boolean safeMatch(@Nullable String value, @Nullable Pattern pattern) {
177        // No value is a match ("no location header" passes the location rule for non-redirects)
178        return pattern == null || TextUtils.isEmpty(value) || pattern.matcher(value).matches();
179    }
180}
181