From 4411ae3ff63db941e218416c681080d17c655978 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 18 Jul 2013 11:36:57 -0500 Subject: [PATCH] SEC-2221: Add MediaTypeRequestMatcher --- .../web/util/MediaTypeRequestMatcher.java | 219 ++++++++++++++++++ ...diaTypeRequestMatcherRequestHCNSTests.java | 149 ++++++++++++ .../util/MediaTypeRequestMatcherTests.java | 186 +++++++++++++++ 3 files changed, 554 insertions(+) create mode 100644 web/src/main/java/org/springframework/security/web/util/MediaTypeRequestMatcher.java create mode 100644 web/src/test/java/org/springframework/security/web/util/MediaTypeRequestMatcherRequestHCNSTests.java create mode 100644 web/src/test/java/org/springframework/security/web/util/MediaTypeRequestMatcherTests.java diff --git a/web/src/main/java/org/springframework/security/web/util/MediaTypeRequestMatcher.java b/web/src/main/java/org/springframework/security/web/util/MediaTypeRequestMatcher.java new file mode 100644 index 0000000000..7ba700989b --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/util/MediaTypeRequestMatcher.java @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2013 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 java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * Allows matching {@link HttpServletRequest} based upon the {@link MediaType}'s + * resolved from a {@link ContentNegotiationStrategy}. + * + * By default, the matching process will perform the following: + * + * + * + * For example, consider the following example + * + *
+ * GET /
+ * Accept: application/json
+ *
+ * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
+ * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
+ * assert matcher.matches(request) == true // returns true
+ * 
+ * + * The following will also return true + * + *
+ * GET /
+ * Accept: */*
+ *
+ * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
+ * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
+ * assert matcher.matches(request) == true // returns true
+ * 
+ * + *

Ignoring Media Types

+ * + * Sometimes you may want to ignore certain types of media types. For example, + * you may want to match on "application/json" but ignore "*/" sent by a web + * browser. + * + *
+ * GET /
+ * Accept: */*
+ *
+ * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
+ * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
+ * matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
+ * assert matcher.matches(request) == false // returns false
+ * 
+ * + *
+ * GET /
+ * Accept: application/json
+ *
+ * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
+ * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
+ * matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL));
+ * assert matcher.matches(request) == true // returns true
+ * 
+ * + *

Exact media type comparison

+ * + * By default as long as the {@link MediaType} discovered by + * {@link ContentNegotiationStrategy} returns true for + * {@link MediaType#isCompatibleWith(MediaType)} on the matchingMediaTypes, the + * result of the match is true. However, sometimes you may want to perform an + * exact match. This can be done with the following examples: + * + *
+ * GET /
+ * Accept: application/json
+ *
+ * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
+ * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
+ * matcher.setUseEquals(true);
+ * assert matcher.matches(request) == true // returns true
+ * 
+ * + *
+ * GET /
+ * Accept: application/*
+ *
+ * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
+ * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
+ * matcher.setUseEquals(true);
+ * assert matcher.matches(request) == false // returns false
+ * 
+ * + *
+ * GET /
+ * Accept: */*
+ *
+ * ContentNegotiationStrategy negotiationStrategy = new HeaderContentNegotiationStrategy()
+ * MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON);
+ * matcher.setUseEquals(true);
+ * assert matcher.matches(request) == false // returns false
+ * 
+ * + * @author Rob Winch + * @since 3.2 + */ + +public final class MediaTypeRequestMatcher implements RequestMatcher { + private final Log logger = LogFactory.getLog(getClass()); + private final ContentNegotiationStrategy contentNegotiationStrategy; + private final Collection matchingMediaTypes; + private boolean useEquals; + private Set ignoredMediaTypes = Collections.emptySet(); + + /** + * Creates an instance + * @param contentNegotiationStrategy the {@link ContentNegotiationStrategy} to use + * @param matchingMediaTypes the {@link MediaType} that will make the {@link RequestMatcher} return true + */ + public MediaTypeRequestMatcher(ContentNegotiationStrategy contentNegotiationStrategy, MediaType... matchingMediaTypes) { + this(contentNegotiationStrategy, Arrays.asList(matchingMediaTypes)); + } + + /** + * Creates an instance + * @param contentNegotiationStrategy the {@link ContentNegotiationStrategy} to use + * @param matchingMediaTypes the {@link MediaType} that will make the {@link RequestMatcher} return true + */ + public MediaTypeRequestMatcher(ContentNegotiationStrategy contentNegotiationStrategy, Collection matchingMediaTypes) { + Assert.notNull(contentNegotiationStrategy, "ContentNegotiationStrategy cannot be null"); + Assert.notEmpty(matchingMediaTypes, "matchingMediaTypes cannot be null or empty"); + this.contentNegotiationStrategy = contentNegotiationStrategy; + this.matchingMediaTypes = matchingMediaTypes; + } + + @Override + public boolean matches(HttpServletRequest request) { + List httpRequestMediaTypes; + try { + httpRequestMediaTypes = contentNegotiationStrategy.resolveMediaTypes(new ServletWebRequest(request)); + } + catch (HttpMediaTypeNotAcceptableException e) { + logger.debug("Failed to parse MediaTypes, returning false", e); + return false; + } + for(MediaType httpRequestMediaType : httpRequestMediaTypes) { + if(ignoredMediaTypes.contains(httpRequestMediaType)) { + continue; + } + if(useEquals) { + return matchingMediaTypes.contains(httpRequestMediaType); + } + for(MediaType matchingMediaType : matchingMediaTypes) { + if(matchingMediaType.isCompatibleWith(httpRequestMediaType)) { + return true; + } + } + } + return false; + } + + /** + * If set to true, matches on exact {@link MediaType}, else uses + * {@link MediaType#isCompatibleWith(MediaType)}. + * + * @param useEquals + * specify if equals comparison should be used. + */ + public void setUseEquals(boolean useEquals) { + this.useEquals = useEquals; + } + + /** + * Set the {@link MediaType} to ignore from the + * {@link ContentNegotiationStrategy}. This is useful if for example, you + * want to match on {@link MediaType#APPLICATION_JSON} but want to ignore + * {@link MediaType#ALL}. + * + * @param ignoredMediaTypes + * the {@link MediaType}'s to ignore from the + * {@link ContentNegotiationStrategy} + */ + public void setIgnoredMediaTypes(Set ignoredMediaTypes) { + this.ignoredMediaTypes = ignoredMediaTypes; + } +} \ No newline at end of file diff --git a/web/src/test/java/org/springframework/security/web/util/MediaTypeRequestMatcherRequestHCNSTests.java b/web/src/test/java/org/springframework/security/web/util/MediaTypeRequestMatcherRequestHCNSTests.java new file mode 100644 index 0000000000..d7397f3157 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/util/MediaTypeRequestMatcherRequestHCNSTests.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2013 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 static org.fest.assertions.Assertions.assertThat; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; + +/** + * Verify how integrates with {@link HeaderContentNegotiationStrategy}. + * + * @author Rob Winch + * + */ +public class MediaTypeRequestMatcherRequestHCNSTests { + private MediaTypeRequestMatcher matcher; + private MockHttpServletRequest request; + + private ContentNegotiationStrategy negotiationStrategy; + + @Before + public void setup() { + negotiationStrategy = new HeaderContentNegotiationStrategy(); + request = new MockHttpServletRequest(); + } + + @Test + public void mediaAllMatches() { + request.addHeader("Accept", MediaType.ALL_VALUE); + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.TEXT_HTML); + + assertThat(matcher.matches(request)).isTrue(); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_XHTML_XML); + assertThat(matcher.matches(request)).isTrue(); + } + + // ignoreMediaTypeAll + + @Test + public void mediaAllIgnoreMediaTypeAll() { + request.addHeader("Accept", MediaType.ALL_VALUE); + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.TEXT_HTML); + matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + public void mediaAllAndTextHtmlIgnoreMediaTypeAll() { + request.addHeader("Accept", MediaType.ALL_VALUE + "," + MediaType.TEXT_HTML_VALUE); + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.TEXT_HTML); + matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + + assertThat(matcher.matches(request)).isTrue(); + } + + // JavaDoc + + @Test + public void javadocJsonJson() { + request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE); + MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON); + + assertThat(matcher.matches(request)).isTrue(); + } + + + @Test + public void javadocAllJson() { + request.addHeader("Accept", MediaType.ALL_VALUE); + MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON); + + assertThat(matcher.matches(request)).isTrue(); + } + + + @Test + public void javadocAllJsonIgnoreAll() { + request.addHeader("Accept", MediaType.ALL_VALUE); + MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON); + matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + assertThat(matcher.matches(request)).isFalse(); + } + + + @Test + public void javadocJsonJsonIgnoreAll() { + request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE); + MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON); + matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + assertThat(matcher.matches(request)).isTrue(); + } + + + @Test + public void javadocJsonJsonUseEquals() { + request.addHeader("Accept", MediaType.APPLICATION_JSON_VALUE); + MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON); + matcher.setUseEquals(true); + assertThat(matcher.matches(request)).isTrue(); + } + + @Test + public void javadocAllJsonUseEquals() { + request.addHeader("Accept", MediaType.ALL_VALUE); + MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON); + matcher.setUseEquals(true); + assertThat(matcher.matches(request)).isFalse(); + } + + + @Test + public void javadocApplicationAllJsonUseEquals() { + request.addHeader("Accept", new MediaType("application","*")); + MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON); + matcher.setUseEquals(true); + assertThat(matcher.matches(request)).isFalse(); + } + + + @Test + public void javadocAllJsonUseFalse() { + request.addHeader("Accept", MediaType.ALL_VALUE); + MediaTypeRequestMatcher matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_JSON); + matcher.setUseEquals(true); + assertThat(matcher.matches(request)).isFalse(); + } +} \ No newline at end of file diff --git a/web/src/test/java/org/springframework/security/web/util/MediaTypeRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/util/MediaTypeRequestMatcherTests.java new file mode 100644 index 0000000000..c685428334 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/util/MediaTypeRequestMatcherTests.java @@ -0,0 +1,186 @@ +/* + * Copyright 2002-2013 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 static org.fest.assertions.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * @author Rob Winch + * + */ +@RunWith(MockitoJUnitRunner.class) +public class MediaTypeRequestMatcherTests { + private MediaTypeRequestMatcher matcher; + private MockHttpServletRequest request; + + @Mock + private ContentNegotiationStrategy negotiationStrategy; + + @Before + public void setup() { + request = new MockHttpServletRequest(); + } + + @Test(expected=IllegalArgumentException.class) + public void constructorNullCNSVarargs() { + new MediaTypeRequestMatcher(null, MediaType.ALL); + } + + @Test(expected=IllegalArgumentException.class) + public void constructorNullCNSSet() { + new MediaTypeRequestMatcher(null, Collections.singleton(MediaType.ALL)); + } + + @Test(expected=IllegalArgumentException.class) + public void constructorNoVarargs() { + new MediaTypeRequestMatcher(negotiationStrategy); + } + + @Test(expected=IllegalArgumentException.class) + public void constructorNullMediaTypes() { + Collection mediaTypes = null; + new MediaTypeRequestMatcher(negotiationStrategy, mediaTypes); + } + + @Test(expected=IllegalArgumentException.class) + public void constructorEmtpyMediaTypes() { + new MediaTypeRequestMatcher(negotiationStrategy, Collections.emptyList()); + } + + @Test + public void negotiationStrategyThrowsHMTNAE() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenThrow(new HttpMediaTypeNotAcceptableException("oops")); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.ALL); + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + public void mediaAllMatches() throws Exception { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(MediaType.ALL)); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.TEXT_HTML); + assertThat(matcher.matches(request)).isTrue(); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_XHTML_XML); + assertThat(matcher.matches(request)).isTrue(); + } + + @Test + public void multipleMediaType() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(MediaType.TEXT_PLAIN,MediaType.APPLICATION_XHTML_XML,MediaType.TEXT_HTML)); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_ATOM_XML, MediaType.TEXT_HTML); + assertThat(matcher.matches(request)).isTrue(); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_XHTML_XML, MediaType.APPLICATION_JSON); + assertThat(matcher.matches(request)).isTrue(); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.APPLICATION_FORM_URLENCODED, MediaType.APPLICATION_JSON); + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + public void resolveTextPlainMatchesTextAll() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(MediaType.TEXT_PLAIN)); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, new MediaType("text","*")); + assertThat(matcher.matches(request)).isTrue(); + } + + @Test + public void resolveTextAllMatchesTextPlain() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(new MediaType("text","*"))); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.TEXT_PLAIN); + assertThat(matcher.matches(request)).isTrue(); + } + + // useEquals + + @Test + public void useEqualsResolveTextAllMatchesTextPlain() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(new MediaType("text","*"))); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.TEXT_PLAIN); + matcher.setUseEquals(true); + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + public void useEqualsResolveTextPlainMatchesTextAll() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(MediaType.TEXT_PLAIN)); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, new MediaType("text","*")); + matcher.setUseEquals(true); + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + public void useEqualsSame() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(MediaType.TEXT_PLAIN)); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.TEXT_PLAIN); + matcher.setUseEquals(true); + assertThat(matcher.matches(request)).isTrue(); + } + + @Test + public void useEqualsWithCustomMediaType() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(new MediaType("text","unique"))); + + matcher = new MediaTypeRequestMatcher(negotiationStrategy, new MediaType("text","unique")); + matcher.setUseEquals(true); + assertThat(matcher.matches(request)).isTrue(); + } + + // ignoreMediaTypeAll + + @Test + public void mediaAllIgnoreMediaTypeAll() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(MediaType.ALL)); + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.TEXT_HTML); + matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + + assertThat(matcher.matches(request)).isFalse(); + } + + @Test + public void mediaAllAndTextHtmlIgnoreMediaTypeAll() throws HttpMediaTypeNotAcceptableException { + when(negotiationStrategy.resolveMediaTypes(any(NativeWebRequest.class))).thenReturn(Arrays.asList(MediaType.ALL,MediaType.TEXT_HTML)); + matcher = new MediaTypeRequestMatcher(negotiationStrategy, MediaType.TEXT_HTML); + matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + + assertThat(matcher.matches(request)).isTrue(); + } +}