diff --git a/web/src/main/java/org/springframework/security/web/util/AntPathRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/AntPathRequestMatcher.java index 45d66768b2..fd4082624c 100644 --- a/web/src/main/java/org/springframework/security/web/util/AntPathRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/AntPathRequestMatcher.java @@ -14,7 +14,12 @@ package org.springframework.security.web.util; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpMethod; import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; /** * Matcher which compares a pre-defined ant-style pattern against the URL @@ -39,7 +44,13 @@ import org.springframework.util.AntPathMatcher; * @see org.springframework.util.AntPathMatcher */ public final class AntPathRequestMatcher implements RequestMatcher { - private final org.springframework.security.web.util.matchers.AntPathRequestMatcher delegate; + private static final Log logger = LogFactory.getLog(AntPathRequestMatcher.class); + private static final String MATCH_ALL = "/**"; + + private final Matcher matcher; + private final String pattern; + private final HttpMethod httpMethod; + private final boolean caseSensitive; /** * Creates a matcher with the specific pattern which will match all HTTP @@ -79,7 +90,28 @@ public final class AntPathRequestMatcher implements RequestMatcher { * true if the matcher should consider case, else false */ public AntPathRequestMatcher(String pattern, String httpMethod, boolean caseSensitive) { - this.delegate = new org.springframework.security.web.util.matchers.AntPathRequestMatcher(pattern, httpMethod, caseSensitive); + Assert.hasText(pattern, "Pattern cannot be null or empty"); + this.caseSensitive = caseSensitive; + + if (pattern.equals(MATCH_ALL) || pattern.equals("**")) { + pattern = MATCH_ALL; + matcher = null; + } else { + if(!caseSensitive) { + pattern = pattern.toLowerCase(); + } + + // If the pattern ends with {@code /**} and has no other wildcards, then optimize to a sub-path match + if (pattern.endsWith(MATCH_ALL) && pattern.indexOf('?') == -1 && + pattern.indexOf("*") == pattern.length() - 2) { + matcher = new SubpathMatcher(pattern.substring(0, pattern.length() - 3)); + } else { + matcher = new SpringAntMatcher(pattern); + } + } + + this.pattern = pattern; + this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod) : null; } /** @@ -89,29 +121,125 @@ public final class AntPathRequestMatcher implements RequestMatcher { * {@code servletPath} + {@code pathInfo} of the request. */ public boolean matches(HttpServletRequest request) { - return this.delegate.matches(request); + if (httpMethod != null && request.getMethod() != null && httpMethod != HttpMethod.valueOf(request.getMethod())) { + if (logger.isDebugEnabled()) { + logger.debug("Request '" + request.getMethod() + " " + getRequestPath(request) + "'" + + " doesn't match '" + httpMethod + " " + pattern); + } + + return false; + } + + if (pattern.equals(MATCH_ALL)) { + if (logger.isDebugEnabled()) { + logger.debug("Request '" + getRequestPath(request) + "' matched by universal pattern '/**'"); + } + + return true; + } + + String url = getRequestPath(request); + + if (logger.isDebugEnabled()) { + logger.debug("Checking match of request : '" + url + "'; against '" + pattern + "'"); + } + + return matcher.matches(url); } - public org.springframework.security.web.util.matchers.AntPathRequestMatcher getDelegate() { - return delegate; + private String getRequestPath(HttpServletRequest request) { + String url = request.getServletPath(); + + if (request.getPathInfo() != null) { + url += request.getPathInfo(); + } + + if(!caseSensitive) { + url = url.toLowerCase(); + } + + return url; } public String getPattern() { - return delegate.getPattern(); + return pattern; + } + + public HttpMethod getHttpMethod() { + return httpMethod; + } + + public boolean isCaseSensitive() { + return caseSensitive; } @Override public boolean equals(Object obj) { - return delegate.equals(obj); + if (!(obj instanceof AntPathRequestMatcher)) { + return false; + } + AntPathRequestMatcher other = (AntPathRequestMatcher)obj; + return this.pattern.equals(other.pattern) && + this.httpMethod == other.httpMethod && + this.caseSensitive == other.caseSensitive; } @Override public int hashCode() { - return delegate.hashCode(); + int code = 31 ^ pattern.hashCode(); + if (httpMethod != null) { + code ^= httpMethod.hashCode(); + } + return code; } @Override public String toString() { - return delegate.toString(); + StringBuilder sb = new StringBuilder(); + sb.append("Ant [pattern='").append(pattern).append("'"); + + if (httpMethod != null) { + sb.append(", ").append(httpMethod); + } + + sb.append("]"); + + return sb.toString(); + } + + private static interface Matcher { + boolean matches(String path); + } + + private static class SpringAntMatcher implements Matcher { + private static final AntPathMatcher antMatcher = new AntPathMatcher(); + + private final String pattern; + + private SpringAntMatcher(String pattern) { + this.pattern = pattern; + } + + public boolean matches(String path) { + return antMatcher.match(pattern, path); + } + } + + /** + * Optimized matcher for trailing wildcards + */ + private static class SubpathMatcher implements Matcher { + private final String subpath; + private final int length; + + private SubpathMatcher(String subpath) { + assert !subpath.contains("*"); + this.subpath = subpath; + this.length = subpath.length(); + } + + public boolean matches(String path) { + return path.startsWith(subpath) && (path.length() == length || path.charAt(length) == '/'); + } } } diff --git a/web/src/main/java/org/springframework/security/web/util/AnyRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/AnyRequestMatcher.java index e819dbf19e..b136b4db48 100644 --- a/web/src/main/java/org/springframework/security/web/util/AnyRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/AnyRequestMatcher.java @@ -10,19 +10,18 @@ import javax.servlet.http.HttpServletRequest; * @deprecated use org.springframework.security.web.util.matchers.AnyRequestMatcher.INSTANCE instead */ public final class AnyRequestMatcher implements RequestMatcher { - private final RequestMatcher delegate = org.springframework.security.web.util.matchers.AnyRequestMatcher.INSTANCE; public boolean matches(HttpServletRequest request) { - return delegate.matches(request); + return true; } @Override public boolean equals(Object obj) { - return delegate.equals(obj); + return obj instanceof AnyRequestMatcher; } @Override public int hashCode() { - return delegate.hashCode(); + return 1; } } diff --git a/web/src/main/java/org/springframework/security/web/util/ELRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/ELRequestMatcher.java index 2bc410c687..a126e767c4 100644 --- a/web/src/main/java/org/springframework/security/web/util/ELRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/ELRequestMatcher.java @@ -19,6 +19,9 @@ package org.springframework.security.web.util; import javax.servlet.http.HttpServletRequest; import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; /** @@ -36,14 +39,16 @@ import org.springframework.security.web.authentication.DelegatingAuthenticationE */ public class ELRequestMatcher implements RequestMatcher { - private final org.springframework.security.web.util.matchers.ELRequestMatcher delegate; + private final Expression expression; public ELRequestMatcher(String el) { - delegate = new org.springframework.security.web.util.matchers.ELRequestMatcher(el); + SpelExpressionParser parser = new SpelExpressionParser(); + expression = parser.parseExpression(el); } public boolean matches(HttpServletRequest request) { - return delegate.matches(request); + EvaluationContext context = createELContext(request); + return expression.getValue(context, Boolean.class).booleanValue(); } /** @@ -52,7 +57,7 @@ public class ELRequestMatcher implements RequestMatcher { * @return EL root context which is used to evaluate the expression */ public EvaluationContext createELContext(HttpServletRequest request) { - return delegate.createELContext(request); + return new StandardEvaluationContext(new ELRequestMatcherContext(request)); } } diff --git a/web/src/main/java/org/springframework/security/web/util/ELRequestMatcherContext.java b/web/src/main/java/org/springframework/security/web/util/ELRequestMatcherContext.java new file mode 100644 index 0000000000..ab6fa70e84 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/util/ELRequestMatcherContext.java @@ -0,0 +1,48 @@ +/* + * Copyright 2009 the original author or authors. + * + * 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 org.springframework.security.web.util; + + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.StringUtils; + +class ELRequestMatcherContext { + + private final HttpServletRequest request; + + public ELRequestMatcherContext(HttpServletRequest request) { + this.request = request; + } + + public boolean hasIpAddress(String ipAddress) { + return (new IpAddressMatcher(ipAddress).matches(request)); + } + + public boolean hasHeader(String headerName, String value) { + String header = request.getHeader(headerName); + if (!StringUtils.hasText(header)) { + return false; + } + + if (header.contains(value)) { + return true; + } + + return false; + } + +} diff --git a/web/src/main/java/org/springframework/security/web/util/IpAddressMatcher.java b/web/src/main/java/org/springframework/security/web/util/IpAddressMatcher.java new file mode 100644 index 0000000000..f56af291f2 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/util/IpAddressMatcher.java @@ -0,0 +1,91 @@ +package org.springframework.security.web.util; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Arrays; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.StringUtils; + +/** + * Matches a request based on IP Address or subnet mask matching against the remote address. + *
+ * Both IPv6 and IPv4 addresses are supported, but a matcher which is configured with an IPv4 address will + * never match a request which returns an IPv6 address, and vice-versa. + * + * @deprecated use {@link org.springframework.security.web.util.matchers.IpAddressMatcher} + * @author Luke Taylor + * @since 3.0.2 + */ +public final class IpAddressMatcher implements RequestMatcher { + private final int nMaskBits; + private final InetAddress requiredAddress; + + /** + * Takes a specific IP address or a range specified using the + * IP/Netmask (e.g. 192.168.1.0/24 or 202.24.0.0/14). + * + * @param ipAddress the address or range of addresses from which the request must come. + */ + public IpAddressMatcher(String ipAddress) { + + if (ipAddress.indexOf('/') > 0) { + String[] addressAndMask = StringUtils.split(ipAddress, "/"); + ipAddress = addressAndMask[0]; + nMaskBits = Integer.parseInt(addressAndMask[1]); + } else { + nMaskBits = -1; + } + requiredAddress = parseAddress(ipAddress); + } + + public boolean matches(HttpServletRequest request) { + return matches(request.getRemoteAddr()); + } + + public boolean matches(String address) { + InetAddress remoteAddress = parseAddress(address); + + if (!requiredAddress.getClass().equals(remoteAddress.getClass())) { + return false; + } + + if (nMaskBits < 0) { + return remoteAddress.equals(requiredAddress); + } + + byte[] remAddr = remoteAddress.getAddress(); + byte[] reqAddr = requiredAddress.getAddress(); + + int oddBits = nMaskBits % 8; + int nMaskBytes = nMaskBits/8 + (oddBits == 0 ? 0 : 1); + byte[] mask = new byte[nMaskBytes]; + + Arrays.fill(mask, 0, oddBits == 0 ? mask.length : mask.length - 1, (byte)0xFF); + + if (oddBits != 0) { + int finalByte = (1 << oddBits) - 1; + finalByte <<= 8-oddBits; + mask[mask.length - 1] = (byte) finalByte; + } + + // System.out.println("Mask is " + new sun.misc.HexDumpEncoder().encode(mask)); + + for (int i=0; i < mask.length; i++) { + if ((remAddr[i] & mask[i]) != (reqAddr[i] & mask[i])) { + return false; + } + } + + return true; + } + + private InetAddress parseAddress(String address) { + try { + return InetAddress.getByName(address); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("Failed to parse address" + address, e); + } + } +} diff --git a/web/src/main/java/org/springframework/security/web/util/RegexRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/RegexRequestMatcher.java index 92d02f4331..2145c1d797 100644 --- a/web/src/main/java/org/springframework/security/web/util/RegexRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/RegexRequestMatcher.java @@ -16,6 +16,11 @@ import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.HttpMethod; +import org.springframework.util.StringUtils; + /** * Uses a regular expression to decide whether a supplied the URL of a supplied {@code HttpServletRequest}. * @@ -25,13 +30,17 @@ import javax.servlet.http.HttpServletRequest; * by default. Case-insensitive matching can be used by using the constructor which takes the {@code caseInsensitive} * argument. * + * @deprecated use {@link org.springframework.security.web.util.matchers.RegexRequestMatcher} + * * @author Luke Taylor * @author Rob Winch * @since 3.1 - * @deprecated use org.springframework.security.web.util.matchers.RegexRequestMatcher */ public final class RegexRequestMatcher implements RequestMatcher { - private final org.springframework.security.web.util.matchers.RegexRequestMatcher delegate; + private final static Log logger = LogFactory.getLog(RegexRequestMatcher.class); + + private final Pattern pattern; + private final HttpMethod httpMethod; /** * Creates a case-sensitive {@code Pattern} instance to match against the request. @@ -51,7 +60,12 @@ public final class RegexRequestMatcher implements RequestMatcher { * @param caseInsensitive if true, the pattern will be compiled with the {@link Pattern#CASE_INSENSITIVE} flag set. */ public RegexRequestMatcher(String pattern, String httpMethod, boolean caseInsensitive) { - this.delegate = new org.springframework.security.web.util.matchers.RegexRequestMatcher(pattern, httpMethod, caseInsensitive); + if (caseInsensitive) { + this.pattern = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE); + } else { + this.pattern = Pattern.compile(pattern); + } + this.httpMethod = StringUtils.hasText(httpMethod) ? HttpMethod.valueOf(httpMethod) : null; } /** @@ -62,6 +76,31 @@ public final class RegexRequestMatcher implements RequestMatcher { * @return true if the pattern matches the URL, false otherwise. */ public boolean matches(HttpServletRequest request) { - return delegate.matches(request); + if (httpMethod != null && request.getMethod() != null && httpMethod != HttpMethod.valueOf(request.getMethod())) { + return false; + } + + String url = request.getServletPath(); + String pathInfo = request.getPathInfo(); + String query = request.getQueryString(); + + if (pathInfo != null || query != null) { + StringBuilder sb = new StringBuilder(url); + + if (pathInfo != null) { + sb.append(pathInfo); + } + + if (query != null) { + sb.append('?').append(query); + } + url = sb.toString(); + } + + if (logger.isDebugEnabled()) { + logger.debug("Checking match of request : '" + url + "'; against '" + pattern + "'"); + } + + return pattern.matcher(url).matches(); } } diff --git a/web/src/main/java/org/springframework/security/web/util/RequestMatcherEditor.java b/web/src/main/java/org/springframework/security/web/util/RequestMatcherEditor.java index 446385529c..3afac56177 100644 --- a/web/src/main/java/org/springframework/security/web/util/RequestMatcherEditor.java +++ b/web/src/main/java/org/springframework/security/web/util/RequestMatcherEditor.java @@ -18,7 +18,6 @@ package org.springframework.security.web.util; import java.beans.PropertyEditorSupport; -import org.springframework.security.web.util.matchers.ELRequestMatcher; import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint; /** @@ -34,7 +33,7 @@ public class RequestMatcherEditor extends PropertyEditorSupport { @Override public void setAsText(String text) throws IllegalArgumentException { - setValue(new ELRequestMatcher(text)); + setValue(new org.springframework.security.web.util.matchers.ELRequestMatcher(text)); } } diff --git a/web/src/main/java/org/springframework/security/web/util/matchers/AntPathRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/matchers/AntPathRequestMatcher.java index 2668a80fab..fe9aa8228e 100644 --- a/web/src/main/java/org/springframework/security/web/util/matchers/AntPathRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/util/matchers/AntPathRequestMatcher.java @@ -169,17 +169,19 @@ public final class AntPathRequestMatcher implements RequestMatcher { @SuppressWarnings("deprecation") @Override public boolean equals(Object obj) { - org.springframework.security.web.util.matchers.AntPathRequestMatcher other; + if (obj instanceof org.springframework.security.web.util.AntPathRequestMatcher) { - other = ((org.springframework.security.web.util.AntPathRequestMatcher) obj).getDelegate(); + org.springframework.security.web.util.AntPathRequestMatcher other = (org.springframework.security.web.util.AntPathRequestMatcher) obj; + return this.pattern.equals(other.getPattern()) && + this.httpMethod == other.getHttpMethod() && + this.caseSensitive == other.isCaseSensitive(); } else if(obj instanceof AntPathRequestMatcher) { - other = (AntPathRequestMatcher) obj; - } else { - return false; + org.springframework.security.web.util.matchers.AntPathRequestMatcher other = (AntPathRequestMatcher) obj; + return this.pattern.equals(other.pattern) && + this.httpMethod == other.httpMethod && + this.caseSensitive == other.caseSensitive; } - return this.pattern.equals(other.pattern) && - this.httpMethod == other.httpMethod && - this.caseSensitive == other.caseSensitive; + return false; } @Override