Home | History | Annotate | Download | only in captiveportal
      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.annotation.NonNull;
     23 import android.annotation.Nullable;
     24 import android.text.TextUtils;
     25 import android.util.Log;
     26 
     27 import java.net.MalformedURLException;
     28 import java.net.URL;
     29 import java.text.ParseException;
     30 import java.util.ArrayList;
     31 import java.util.List;
     32 import java.util.regex.Pattern;
     33 import java.util.regex.PatternSyntaxException;
     34 
     35 /** @hide */
     36 public 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