diff --git a/src/main/java/com/alibaba/druid/support/http/WebStatFilter.java b/src/main/java/com/alibaba/druid/support/http/WebStatFilter.java index 589b972d40..7681bb159d 100644 --- a/src/main/java/com/alibaba/druid/support/http/WebStatFilter.java +++ b/src/main/java/com/alibaba/druid/support/http/WebStatFilter.java @@ -239,4 +239,4 @@ public class WebStatFilter implements Filter { } } } -} +} \ No newline at end of file diff --git a/src/main/java/com/alibaba/druid/util/AntPathMatcher.java b/src/main/java/com/alibaba/druid/util/AntPathMatcher.java new file mode 100644 index 0000000000..2629f91907 --- /dev/null +++ b/src/main/java/com/alibaba/druid/util/AntPathMatcher.java @@ -0,0 +1,394 @@ +package com.alibaba.druid.util; + +/** + *

+ * PathMatcher implementation for Ant-style path patterns. Examples are provided below. + *

+ *

+ * Part of this mapping code has been kindly borrowed from Apache Ant. + *

+ * The mapping matches URLs using the following rules:
+ *

+ *

+ * Some examples:
+ *

+ *

+ * N.B.: This class was borrowed (with much appreciation) from the Shiro + * and the Spring Framework with modifications. We didn't want to reinvent + * the wheel of great work they've done, but also didn't want to force every Shiro user to depend on Spring + *

+ *

+ * As per the Apache 2.0 license, the original copyright notice and all author and copyright information have remained + * in tact. + *

+ * + * @since 16.07.2003 + */ +public class AntPathMatcher implements PatternMatcher { + + // TODO - complete JavaDoc + + /** + * Default path separator: "/" + */ + public static final String DEFAULT_PATH_SEPARATOR = "/"; + + private String pathSeparator = DEFAULT_PATH_SEPARATOR; + + /** + * Set the path separator to use for pattern parsing. Default is "/", as in Ant. + */ + public void setPathSeparator(String pathSeparator) { + this.pathSeparator = (pathSeparator != null ? pathSeparator : DEFAULT_PATH_SEPARATOR); + } + + public boolean isPattern(String path) { + return (path.indexOf('*') != -1 || path.indexOf('?') != -1); + } + + public boolean matches(String pattern, String source) { + return match(pattern, source); + } + + public boolean match(String pattern, String path) { + return doMatch(pattern, path, true); + } + + public boolean matchStart(String pattern, String path) { + return doMatch(pattern, path, false); + } + + /** + * Actually match the given path against the given pattern. + * + * @param pattern the pattern to match against + * @param path the path String to test + * @param fullMatch whether a full pattern match is required (else a pattern match as far as the given base path + * goes is sufficient) + * @return true if the supplied path matched, false if it didn't + */ + protected boolean doMatch(String pattern, String path, boolean fullMatch) { + if (path.startsWith(this.pathSeparator) != pattern.startsWith(this.pathSeparator)) { + return false; + } + + String[] pattDirs = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator); + String[] pathDirs = StringUtils.tokenizeToStringArray(path, this.pathSeparator); + + int pattIdxStart = 0; + int pattIdxEnd = pattDirs.length - 1; + int pathIdxStart = 0; + int pathIdxEnd = pathDirs.length - 1; + + // Match all elements up to the first ** + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String patDir = pattDirs[pattIdxStart]; + if ("**".equals(patDir)) { + break; + } + if (!matchStrings(patDir, pathDirs[pathIdxStart])) { + return false; + } + pattIdxStart++; + pathIdxStart++; + } + + if (pathIdxStart > pathIdxEnd) { + // Path is exhausted, only match if rest of pattern is * or **'s + if (pattIdxStart > pattIdxEnd) { + return (pattern.endsWith(this.pathSeparator) ? path.endsWith(this.pathSeparator) : !path.endsWith(this.pathSeparator)); + } + if (!fullMatch) { + return true; + } + if (pattIdxStart == pattIdxEnd && pattDirs[pattIdxStart].equals("*") && path.endsWith(this.pathSeparator)) { + return true; + } + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } else if (pattIdxStart > pattIdxEnd) { + // String not exhausted, but pattern is. Failure. + return false; + } else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { + // Path start definitely matches due to "**" part in pattern. + return true; + } + + // up to last '**' + while (pattIdxStart <= pattIdxEnd && pathIdxStart <= pathIdxEnd) { + String patDir = pattDirs[pattIdxEnd]; + if (patDir.equals("**")) { + break; + } + if (!matchStrings(patDir, pathDirs[pathIdxEnd])) { + return false; + } + pattIdxEnd--; + pathIdxEnd--; + } + if (pathIdxStart > pathIdxEnd) { + // String is exhausted + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + return true; + } + + while (pattIdxStart != pattIdxEnd && pathIdxStart <= pathIdxEnd) { + int patIdxTmp = -1; + for (int i = pattIdxStart + 1; i <= pattIdxEnd; i++) { + if (pattDirs[i].equals("**")) { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == pattIdxStart + 1) { + // '**/**' situation, so skip one + pattIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - pattIdxStart - 1); + int strLength = (pathIdxEnd - pathIdxStart + 1); + int foundIdx = -1; + + strLoop: for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + String subPat = (String) pattDirs[pattIdxStart + j + 1]; + String subStr = (String) pathDirs[pathIdxStart + i + j]; + if (!matchStrings(subPat, subStr)) { + continue strLoop; + } + } + foundIdx = pathIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + pattIdxStart = patIdxTmp; + pathIdxStart = foundIdx + patLength; + } + + for (int i = pattIdxStart; i <= pattIdxEnd; i++) { + if (!pattDirs[i].equals("**")) { + return false; + } + } + + return true; + } + + /** + * Tests whether or not a string matches against a pattern. The pattern may contain two special characters:
+ * '*' means zero or more characters
+ * '?' means one and only one character + * + * @param pattern pattern to match against. Must not be null. + * @param str string which must be matched against the pattern. Must not be null. + * @return true if the string matches against the pattern, or false otherwise. + */ + private boolean matchStrings(String pattern, String str) { + char[] patArr = pattern.toCharArray(); + char[] strArr = str.toCharArray(); + int patIdxStart = 0; + int patIdxEnd = patArr.length - 1; + int strIdxStart = 0; + int strIdxEnd = strArr.length - 1; + char ch; + + boolean containsStar = false; + for (char aPatArr : patArr) { + if (aPatArr == '*') { + containsStar = true; + break; + } + } + + if (!containsStar) { + // No '*'s, so we make a shortcut + if (patIdxEnd != strIdxEnd) { + return false; // Pattern and string do not have the same size + } + for (int i = 0; i <= patIdxEnd; i++) { + ch = patArr[i]; + if (ch != '?') { + if (ch != strArr[i]) { + return false;// Character mismatch + } + } + } + return true; // String matches against pattern + } + + if (patIdxEnd == 0) { + return true; // Pattern contains only '*', which matches anything + } + + // Process characters before first star + while ((ch = patArr[patIdxStart]) != '*' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxStart]) { + return false;// Character mismatch + } + } + patIdxStart++; + strIdxStart++; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // Process characters after last star + while ((ch = patArr[patIdxEnd]) != '*' && strIdxStart <= strIdxEnd) { + if (ch != '?') { + if (ch != strArr[strIdxEnd]) { + return false;// Character mismatch + } + } + patIdxEnd--; + strIdxEnd--; + } + if (strIdxStart > strIdxEnd) { + // All characters in the string are used. Check if only '*'s are + // left in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + return true; + } + + // process pattern between stars. padIdxStart and patIdxEnd point + // always to a '*'. + while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) { + int patIdxTmp = -1; + for (int i = patIdxStart + 1; i <= patIdxEnd; i++) { + if (patArr[i] == '*') { + patIdxTmp = i; + break; + } + } + if (patIdxTmp == patIdxStart + 1) { + // Two stars next to each other, skip the first one. + patIdxStart++; + continue; + } + // Find the pattern between padIdxStart & padIdxTmp in str between + // strIdxStart & strIdxEnd + int patLength = (patIdxTmp - patIdxStart - 1); + int strLength = (strIdxEnd - strIdxStart + 1); + int foundIdx = -1; + strLoop: for (int i = 0; i <= strLength - patLength; i++) { + for (int j = 0; j < patLength; j++) { + ch = patArr[patIdxStart + j + 1]; + if (ch != '?') { + if (ch != strArr[strIdxStart + i + j]) { + continue strLoop; + } + } + } + + foundIdx = strIdxStart + i; + break; + } + + if (foundIdx == -1) { + return false; + } + + patIdxStart = patIdxTmp; + strIdxStart = foundIdx + patLength; + } + + // All characters in the string are used. Check if only '*'s are left + // in the pattern. If so, we succeeded. Otherwise failure. + for (int i = patIdxStart; i <= patIdxEnd; i++) { + if (patArr[i] != '*') { + return false; + } + } + + return true; + } + + /** + * Given a pattern and a full path, determine the pattern-mapped part. + *

+ * For example: + *

+ *

+ * Assumes that {@link #match} returns true for 'pattern' and 'path', but + * does not enforce this. + */ + public String extractPathWithinPattern(String pattern, String path) { + String[] patternParts = StringUtils.tokenizeToStringArray(pattern, this.pathSeparator); + String[] pathParts = StringUtils.tokenizeToStringArray(path, this.pathSeparator); + + StringBuilder buffer = new StringBuilder(); + + // Add any path parts that have a wildcarded pattern part. + int puts = 0; + for (int i = 0; i < patternParts.length; i++) { + String patternPart = patternParts[i]; + if ((patternPart.indexOf('*') > -1 || patternPart.indexOf('?') > -1) && pathParts.length >= i + 1) { + if (puts > 0 || (i == 0 && !pattern.startsWith(this.pathSeparator))) { + buffer.append(this.pathSeparator); + } + buffer.append(pathParts[i]); + puts++; + } + } + + // Append any trailing path parts. + for (int i = patternParts.length; i < pathParts.length; i++) { + if (puts > 0 || i > 0) { + buffer.append(this.pathSeparator); + } + buffer.append(pathParts[i]); + } + + return buffer.toString(); + } + +} diff --git a/src/main/java/com/alibaba/druid/util/PatternMatcher.java b/src/main/java/com/alibaba/druid/util/PatternMatcher.java new file mode 100644 index 0000000000..9b49be34ff --- /dev/null +++ b/src/main/java/com/alibaba/druid/util/PatternMatcher.java @@ -0,0 +1,24 @@ +package com.alibaba.druid.util; + +/** + * Interface for components that can match source strings against a specified pattern string. + *

+ * Different implementations can support different pattern types, for example, Ant style path expressions, or regular + * expressions, or other types of text based patterns. + * + * @see com.alibaba.druid.util.AntPathMatcher AntPathMatcher + * @since 0.2.6 + */ +public interface PatternMatcher { + + /** + * Returns true if the given source matches the specified pattern, + * false otherwise. + * + * @param pattern the pattern to match against + * @param source the source to match + * @return true if the given source matches the specified pattern, + * false otherwise. + */ + boolean matches(String pattern, String source); +} diff --git a/src/main/java/com/alibaba/druid/util/ServletPathMatcher.java b/src/main/java/com/alibaba/druid/util/ServletPathMatcher.java new file mode 100644 index 0000000000..6395e38aec --- /dev/null +++ b/src/main/java/com/alibaba/druid/util/ServletPathMatcher.java @@ -0,0 +1,41 @@ +package com.alibaba.druid.util; + +public class ServletPathMatcher implements PatternMatcher { + + /** + *

+ * three type: endsWithMatch(eg. /xxx*=/xxx/xyz), startsWithMatch(eg. *.xxx=abc.xxx), equals(eg. /xxx=/xxx)。 + *

+ * Notice: *xxx* will match *xxxyyyy. endsWithMatch first. + */ + @Override + public boolean matches(String pattern, String source) { + if (pattern == null || source == null) { + return false; + } + pattern = pattern.trim(); + source = source.trim(); + if (pattern.endsWith("*")) { + // pattern: /druid* source:/druid/index.html + int length = pattern.length() - 1; + if (source.length() >= length) { + if (pattern.substring(0, length).equals(source.substring(0, length))) { + return true; + } + } + } else if (pattern.startsWith("*")) { + // pattern: *.html source:/xx/xx.html + int length = pattern.length() - 1; + if (source.length() >= length && source.endsWith(pattern.substring(1))) { + return true; + } + } else { + // pattern: /druid/index.html source:/druid/index.html + if (pattern.equals(source)) { + return true; + } + } + return false; + } + +} diff --git a/src/main/java/com/alibaba/druid/util/StringUtils.java b/src/main/java/com/alibaba/druid/util/StringUtils.java index 01ce45e10c..1b9f0a8108 100644 --- a/src/main/java/com/alibaba/druid/util/StringUtils.java +++ b/src/main/java/com/alibaba/druid/util/StringUtils.java @@ -1,7 +1,11 @@ package com.alibaba.druid.util; +import java.util.ArrayList; +import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; +import java.util.StringTokenizer; import com.alibaba.druid.support.logging.Log; import com.alibaba.druid.support.logging.LogFactory; @@ -97,6 +101,85 @@ public class StringUtils { return parameters; } + /** + * Tokenize the given String into a String array via a StringTokenizer. Trims tokens and omits empty tokens. + *

+ * The given delimiters string is supposed to consist of any number of delimiter characters. Each of those + * characters can be used to separate tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + *

+ *

+ * Copied from the Spring Framework while retaining all license, copyright and author information. + * + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String (each of those characters is individually + * considered as delimiter). + * @return an array of the tokens + * @see java.util.StringTokenizer + * @see java.lang.String#trim() + */ + public static String[] tokenizeToStringArray(String str, String delimiters) { + return tokenizeToStringArray(str, delimiters, true, true); + } + + /** + * Tokenize the given String into a String array via a StringTokenizer. + *

+ * The given delimiters string is supposed to consist of any number of delimiter characters. Each of those + * characters can be used to separate tokens. A delimiter is always a single character; for multi-character + * delimiters, consider using delimitedListToStringArray + *

+ *

+ * Copied from the Spring Framework while retaining all license, copyright and author information. + * + * @param str the String to tokenize + * @param delimiters the delimiter characters, assembled as String (each of those characters is individually + * considered as delimiter) + * @param trimTokens trim the tokens via String's trim + * @param ignoreEmptyTokens omit empty tokens from the result array (only applies to tokens that are empty after + * trimming; StringTokenizer will not consider subsequent delimiters as token in the first place). + * @return an array of the tokens (null if the input String was null) + * @see java.util.StringTokenizer + * @see java.lang.String#trim() + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static String[] tokenizeToStringArray(String str, String delimiters, boolean trimTokens, + boolean ignoreEmptyTokens) { + + if (str == null) { + return null; + } + StringTokenizer st = new StringTokenizer(str, delimiters); + List tokens = new ArrayList(); + while (st.hasMoreTokens()) { + String token = st.nextToken(); + if (trimTokens) { + token = token.trim(); + } + if (!ignoreEmptyTokens || token.length() > 0) { + tokens.add(token); + } + } + return toStringArray(tokens); + } + + /** + * Copy the given Collection into a String array. The Collection must contain String elements only. + *

+ *

+ * Copied from the Spring Framework while retaining all license, copyright and author information. + * + * @param collection the Collection to copy + * @return the String array (null if the passed-in Collection was null) + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static String[] toStringArray(Collection collection) { + if (collection == null) { + return null; + } + return (String[]) collection.toArray(new String[collection.size()]); + } + public static void main(String args[]) { System.out.println(JSON.toJSONString(getParameters("test?t=1&f=").get("t"))); } diff --git a/src/test/java/com/alibaba/druid/util/ServletPathMathcherTest.java b/src/test/java/com/alibaba/druid/util/ServletPathMathcherTest.java new file mode 100644 index 0000000000..c1f0e68544 --- /dev/null +++ b/src/test/java/com/alibaba/druid/util/ServletPathMathcherTest.java @@ -0,0 +1,99 @@ +package com.alibaba.druid.util; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + + +public class ServletPathMathcherTest { + + /** + * PatternMatcher used in determining which paths to react to for a given request. + */ + protected PatternMatcher pathMatcher = new ServletPathMatcher(); + + @Test + public void testStartsWithPattern() { + String[] bogusPatterns = { "/druid*", "/druid*/what*", "*/druid*" }; + String[] bogusSources = { "/druid", "/druid/index.html", "/druid*/what/xyu" }; + boolean result = false; + for (int i = 0; i < bogusSources.length; i++) { + for (int j = 0; j < bogusPatterns.length; j++) { + String bogusSource = bogusSources[i]; + String bogusPattern = bogusPatterns[j]; + if (pathMatcher.matches(bogusPattern, bogusSource)) { + result = true; + } + if (result == true) { + break; + } + } + assertThat(true, equalTo(result)); + result = false; + } + } + + @Test + public void testEndsWithPattern() { + String[] bogusPatterns = { "*.html", "*.ico", "*.css" }; + String[] bogusSources = { "/index.html", "/favicon.ico", "/druid.css" }; + boolean result = false; + for (int i = 0; i < bogusSources.length; i++) { + for (int j = 0; j < bogusPatterns.length; j++) { + String bogusSource = bogusSources[i]; + String bogusPattern = bogusPatterns[j]; + if (pathMatcher.matches(bogusPattern, bogusSource)) { + result = true; + } + if (result == true) { + break; + } + } + assertThat(true, equalTo(result)); + result = false; + } + } + + @Test + public void testEqualsPattern() { + String[] bogusPatterns = { "/index.html", "/favicon.ico", "/xyz" }; + String[] bogusSources = { "/index.html", "/favicon.ico", "/xyz" }; + boolean result = false; + for (int i = 0; i < bogusSources.length; i++) { + for (int j = 0; j < bogusPatterns.length; j++) { + String bogusSource = bogusSources[i]; + String bogusPattern = bogusPatterns[j]; + if (pathMatcher.matches(bogusPattern, bogusSource)) { + result = true; + } + if (result == true) { + break; + } + } + assertThat(true, equalTo(result)); + result = false; + } + } + + @Test + public void testPatternPriority() { + String[] bogusPatterns = { "*html*", "/favicon.ico*", "*html" }; + String[] bogusSources = { "*html/ok?", "/favicon.ico/ok?", "/index.html" }; + boolean result = false; + for (int i = 0; i < bogusSources.length; i++) { + for (int j = 0; j < bogusPatterns.length; j++) { + String bogusSource = bogusSources[i]; + String bogusPattern = bogusPatterns[j]; + if (pathMatcher.matches(bogusPattern, bogusSource)) { + result = true; + } + if (result == true) { + break; + } + } + assertThat(true, equalTo(result)); + result = false; + } + } +}