/* * Copyright 2007 Google Inc. * * 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.gwt.dev.shell; import com.google.gwt.core.ext.TreeLogger; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.HashSet; import java.util.Iterator; import java.util.Locale; import java.util.Set; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; /** * This class contains utility functions to do whitelist/blacklist handling. */ public class BrowserWidgetHostChecker { /** * The set of always allowed URLs, which are immune to blacklisting. */ private static final Pattern alwaysValidHttpHosts; /** * The set of blacklisted URLs. */ private static final Set<String> invalidHttpHosts = new HashSet<String>(); private static String oldBlackList = null; private static String oldWhiteList = null; /** * The set of whitelisted URLs. */ private static final Set<String> validHttpHosts = new HashSet<String>(); static { Set<String> regexes = new HashSet<String>(); // Regular URLs may or may not have a port, and must either end with // the host+port, or be followed by a slash (to avoid attacks like // localhost.evildomain.org). String portSuffix = "(:\\d+)?(/.*)?"; regexes.add("https?://localhost" + portSuffix); regexes.add("https?://localhost[.]localdomain" + portSuffix); regexes.add("https?://127[.]0[.]0[.]1" + portSuffix); String hostName; try { hostName = InetAddress.getLocalHost().getHostName(); if (hostName != null) { hostName = hostName.replace(".", "[.]"); regexes.add("https?://" + hostName + portSuffix); } } catch (UnknownHostException e) { // Ignore } String addr; try { addr = InetAddress.getLocalHost().getHostAddress(); if (addr != null) { addr = addr.replace(".", "[.]"); regexes.add("https?://" + addr + portSuffix); } } catch (UnknownHostException e) { // Ignore } regexes.add("file:.*"); regexes.add("about:.*"); regexes.add("res:.*"); regexes.add("javascript:.*"); regexes.add("([a-z][:])[/\\\\].*"); // matches c:\ and c:/ StringBuilder buf = new StringBuilder(); String prefix = "("; for (String regex : regexes) { buf.append(prefix).append('(').append(regex).append(')'); prefix = "|"; } buf.append(")"); alwaysValidHttpHosts = Pattern.compile(buf.toString(), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); } /** * This method blacklists the supplied regexes, separated by comma or space. * * @param regexes the regexes to be forbidden */ public static boolean blacklistRegexes(String regexes) { return addRegex(regexes, false); } /** * This method blacklists the supplied URL, and any that share the same host. * * @param url the host to be forbidden */ public static void blacklistURL(String url) { String hostRegex = computeHostRegex(url); blacklistRegexes(hostRegex); } /** * This method checks the host to see if it is in the supplied set of regexes. * * @param hostUnderConsideration the host to be checked * @param hosts the set of regexes to be checked against * @return true if the host matches */ public static String checkHost(String hostUnderConsideration, Set<String> hosts) { // TODO(jat): build a single regex instead of looping hostUnderConsideration = hostUnderConsideration.toLowerCase(Locale.ENGLISH); for (String rule : hosts) { rule = rule.toLowerCase(Locale.ENGLISH); // match on lowercased regex if (hostUnderConsideration.matches(".*" + rule + ".*")) { return rule; } } return null; } /** * This method computes the host regular expression for the given url. * * @param url the url to be allowed or disallowed * @return the regex that matches the host in the url */ public static String computeHostRegex(String url) { // the entire URL up to the first slash not prefixed by a slash or colon. String raw = url.split("(?<![:/])/")[0]; // escape the dots and put a begin line specifier on the result return "^" + escapeString(raw); } /** * This method formats the blacklist for display in the treeLogger. * * @return the list of regexes as a String */ public static String formatBlackList() { return formatRules(invalidHttpHosts); } /** * This method formats the list of rules for display in the treeLogger. * * @param hosts the set of regexes that match hosts * @return the list of regexes as a String */ public static String formatRules(Set<String> hosts) { StringBuffer out = new StringBuffer(); for (Iterator<String> i = hosts.iterator(); i.hasNext();) { String rule = i.next(); out.append(rule); out.append(" "); } return out.toString(); } /** * This method formats the whitelist for display in the treeLogger. * * @return the list of regexes as a String */ public static String formatWhiteList() { return formatRules(validHttpHosts); } /** * This method returns true if the host is always admissible, regardless of * the blacklist. * * @param url the URL to be verified * @return returns true if the host is always admissible */ public static boolean isAlwaysWhitelisted(String url) { return alwaysValidHttpHosts.matcher(url).matches(); } /** * This method returns non-null if the host is forbidden. * * @param url the URL to be verified * @return returns the regex that specified the host matches the blacklist */ public static String matchBlacklisted(String url) { oldBlackList = formatBlackList(); return checkHost(url, invalidHttpHosts); } /** * This method returns null if the host is admissible, provided it is not on * the blacklist. * * @param url the URL to be verified * @return returns the regex that specified the host matches the whitelist */ public static String matchWhitelisted(String url) { oldWhiteList = formatWhiteList(); return checkHost(url, validHttpHosts); } /** * This method formats a message, and logs it to the treelogger, stating that * the url was blocked. * * @param url the URL that was disallowed * @param header the treelogger under which these messages will be put * @param msgType either a caution or an error */ public static void notifyBlacklistedHost(String blacklistRuleFound, String url, TreeLogger header, TreeLogger.Type msgType) { TreeLogger reason = header.branch(msgType, "reason: " + url + " is blacklisted", null); reason.log(msgType, "To fix: remove \"" + blacklistRuleFound + "\" from system property gwt.hosts.blacklist", null); } /** * This method formats a message, and logs it to the treelogger, stating that * the url was not trusted. * * @param url the URL that provoked the dialog box * @param header the treelogger under which these messages will be put * @param msgType either a caution or an error */ public static void notifyUntrustedHost(String url, TreeLogger header, TreeLogger.Type msgType) { String whiteListStr = oldWhiteList; String blackListStr = oldBlackList; String hostRegex = computeHostRegex(url); TreeLogger reason = header.branch(msgType, "reason: " + url + " is not in the whitelist", null); reason.log(msgType, "whitelist: " + whiteListStr, null); reason.log(msgType, "blacklist: " + blackListStr, null); TreeLogger fix = header.branch(msgType, "To fix: add regex matching " + "URL to -whitelist command line argument", null); fix.log(msgType, "Example: -whitelist \"" + whiteListStr + " " + hostRegex + "\"", null); TreeLogger reject = header.branch(msgType, "To reject automatically: add regex matching " + "URL to -blacklist command line argument", null); reject.log(msgType, "Example: -blacklist \"" + blackListStr + " " + hostRegex + "\"", null); } /** * This method whitelists the supplied String of regexes, separated by comma * or space. * * @param regexes the regexes to be allowed */ public static boolean whitelistRegexes(String regexes) { return addRegex(regexes, true); } /** * This method whitelists the supplied URL, and any that share the same host. * * @param url the host to be allowed */ public static void whitelistURL(String url) { String hostRegex = computeHostRegex(url); whitelistRegexes(hostRegex); } /** * This method blacklists or whitelists the supplied regexes, and any that * share the same host. * * @param whitelist if <code>true</code> the host will be whitelisted * @param regexes the regular expressions to be forbidden, seperated by comma * or space */ private static boolean addRegex(String regexes, boolean whitelist) { if (regexes.equals("")) { return true; // adding empty string is harmless and happens by default } String[] items = regexes.split("[ ,]"); for (int i = 0; i < items.length; i++) { try { Pattern.compile(items[i]); } catch (PatternSyntaxException e) { System.err.println("The regex '" + items[i] + " has syntax errors."); System.err.println(e.toString()); return false; } if (whitelist) { validHttpHosts.add(items[i]); } else { invalidHttpHosts.add(items[i]); } } return true; } private static String escapeString(String raw) { StringBuffer out = new StringBuffer(); for (int i = 0; i < raw.length(); i++) { char c = raw.charAt(i); if (Character.isLetterOrDigit(c) || c == '-' || c == '_') { out.append(c); } else if (c == '\\') { out.append("[\\\\]"); } else if (c == ']') { out.append("[\\]]"); } else if (c == '^') { out.append("[\\^]"); } else if (c == '[') { out.append("[\\[]"); } else { out.append("["); out.append(c); out.append("]"); } } return out.toString(); } }