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 CaptivePortalProbeSpec(String encodedSpec, URL url)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 parseSpec(String spec)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 parsePatternIfNonEmpty(String pattern, int pos)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 parseSpecOrNull(@ullable String spec)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 */ parseCaptivePortalProbeSpecs(String settingsVal)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 */ getResult(int status, @Nullable String locationHeader)137 public abstract CaptivePortalProbeResult getResult(int status, @Nullable String locationHeader); 138 getEncodedSpec()139 public String getEncodedSpec() { 140 return mEncodedSpec; 141 } 142 getUrl()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 RegexMatchProbeSpec( String spec, URL url, Pattern statusRegex, Pattern locationHeaderRegex)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 getResult(int status, String locationHeader)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 safeMatch(@ullable String value, @Nullable Pattern pattern)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