blob: c269d1b941906a5185c6ccc3d2db148f9a096fa4 [file] [log] [blame]
/*
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.net;
import android.content.ContentResolver;
import android.database.Cursor;
import android.provider.Checkin;
import android.provider.Settings;
import android.util.Log;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* A set of rules rewriting and blocking URLs. Used to offer a point of
* control for redirecting HTTP requests, often to the Android proxy server.
*
* <p>Each rule has the following format:
*
* <pre><em>url-prefix</em> [REWRITE <em>new-prefix</em>] [BLOCK]</pre>
*
* <p>Any URL which starts with <em>url-prefix</em> will trigger the rule.
* If BLOCK is specified, requests to that URL will be blocked and fail.
* If REWRITE is specified, the matching prefix will be removed and replaced
* with <em>new-prefix</em>. (If both are specified, BLOCK wins.) Case is
* insensitive for the REWRITE and BLOCK keywords, but sensitive for URLs.
*
* <p>In Gservices, the value of any key that starts with "url:" will be
* interpreted as a rule. The full name of the key is unimportant (but can
* be used to document the intent of the rule, and must be unique).
* Example gservices keys:
*
* <pre>
* url:use_proxy_for_calendar = "http://www.google.com/calendar/ REWRITE http://android.clients.google.com/proxy/calendar/"
* url:stop_crash_reports = "http://android.clients.google.com/crash/ BLOCK"
* url:use_ssl_for_contacts = "http://www.google.com/m8/ REWRITE https://www.google.com/m8/"
* </pre>
*/
public class UrlRules {
/** Thrown when the rewrite rules can't be parsed. */
public static class RuleFormatException extends Exception {
public RuleFormatException(String msg) { super(msg); }
}
/** A single rule specifying actions for URLs matching a certain prefix. */
public static class Rule implements Comparable {
/** Name assigned to the rule (for logging and debugging). */
public final String mName;
/** Prefix required to match this rule. */
public final String mPrefix;
/** Text to replace mPrefix with (null to leave alone). */
public final String mRewrite;
/** True if matching URLs should be blocked. */
public final boolean mBlock;
/** Default rule that does nothing. */
public static final Rule DEFAULT = new Rule();
/** Parse a rewrite rule as given in a Gservices value. */
public Rule(String name, String rule) throws RuleFormatException {
mName = name;
String[] words = PATTERN_SPACE_PLUS.split(rule);
if (words.length == 0) throw new RuleFormatException("Empty rule");
mPrefix = words[0];
String rewrite = null;
boolean block = false;
for (int pos = 1; pos < words.length; ) {
String word = words[pos].toLowerCase();
if (word.equals("rewrite") && pos + 1 < words.length) {
rewrite = words[pos + 1];
pos += 2;
} else if (word.equals("block")) {
block = true;
pos += 1;
} else {
throw new RuleFormatException("Illegal rule: " + rule);
}
// TODO: Parse timeout specifications, etc.
}
mRewrite = rewrite;
mBlock = block;
}
/** Create the default Rule. */
private Rule() {
mName = "DEFAULT";
mPrefix = "";
mRewrite = null;
mBlock = false;
}
/**
* Apply the rule to a particular URL (assumed to match the rule).
* @param url to rewrite or modify.
* @return modified URL, or null if the URL is blocked.
*/
public String apply(String url) {
if (mBlock) {
return null;
} else if (mRewrite != null) {
return mRewrite + url.substring(mPrefix.length());
} else {
return url;
}
}
/** More generic rules are greater than more specific rules. */
public int compareTo(Object o) {
return ((Rule) o).mPrefix.compareTo(mPrefix);
}
}
/** Cached rule set from Gservices. */
private static UrlRules sCachedRules = new UrlRules(new Rule[] {});
private static final Pattern PATTERN_SPACE_PLUS = Pattern.compile(" +");
private static final Pattern RULE_PATTERN = Pattern.compile("\\W");
/** Gservices digest when sCachedRules was cached. */
private static String sCachedDigest = null;
/** Currently active set of Rules. */
private final Rule[] mRules;
/** Regular expression with one capturing group for each Rule. */
private final Pattern mPattern;
/**
* Create a rewriter from an array of Rules. Normally used only for
* testing. Instead, use {@link #getRules} to get rules from Gservices.
* @param rules to use.
*/
public UrlRules(Rule[] rules) {
// Sort the rules to put the most specific rules first.
Arrays.sort(rules);
// Construct a regular expression, escaping all the prefix strings.
StringBuilder pattern = new StringBuilder("(");
for (int i = 0; i < rules.length; ++i) {
if (i > 0) pattern.append(")|(");
pattern.append(RULE_PATTERN.matcher(rules[i].mPrefix).replaceAll("\\\\$0"));
}
mPattern = Pattern.compile(pattern.append(")").toString());
mRules = rules;
}
/**
* Match a string against every Rule and find one that matches.
* @param uri to match against the Rules in the rewriter.
* @return the most specific matching Rule, or Rule.DEFAULT if none match.
*/
public Rule matchRule(String url) {
Matcher matcher = mPattern.matcher(url);
if (matcher.lookingAt()) {
for (int i = 0; i < mRules.length; ++i) {
if (matcher.group(i + 1) != null) {
return mRules[i]; // Rules are sorted most specific first.
}
}
}
return Rule.DEFAULT;
}
/**
* Get the (possibly cached) UrlRules based on the rules in Gservices.
* @param resolver to use for accessing the Gservices database.
* @return an updated UrlRules instance
*/
public static synchronized UrlRules getRules(ContentResolver resolver) {
String digest = Settings.Gservices.getString(resolver,
Settings.Gservices.PROVISIONING_DIGEST);
if (sCachedDigest != null && sCachedDigest.equals(digest)) {
// The digest is the same, so the rules are the same.
return sCachedRules;
}
// Get all the Gservices settings with names starting with "url:".
Cursor cursor = resolver.query(Settings.Gservices.CONTENT_URI,
new String[] {
Settings.Gservices.NAME,
Settings.Gservices.VALUE
},
Settings.Gservices.NAME + " like \"url:%\"", null,
Settings.Gservices.NAME);
try {
ArrayList<Rule> rules = new ArrayList<Rule>();
while (cursor.moveToNext()) {
try {
String name = cursor.getString(0).substring(4); // "url:X"
String value = cursor.getString(1);
if (value == null || value.length() == 0) continue;
rules.add(new Rule(name, value));
} catch (RuleFormatException e) {
// Oops, Gservices has an invalid rule! Skip it.
Log.e("UrlRules", "Invalid rule from Gservices", e);
Checkin.logEvent(resolver,
Checkin.Events.Tag.GSERVICES_ERROR, e.toString());
}
}
sCachedRules = new UrlRules(rules.toArray(new Rule[rules.size()]));
sCachedDigest = digest;
} finally {
cursor.close();
}
return sCachedRules;
}
}