From fca8bf6088fb984784da5a29806b52f36f4db9c4 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 12 Sep 2017 14:46:42 -0500 Subject: [PATCH] Add MediaTypeServerWebExchangeMatcher Fixes gh-4536 --- .../MediaTypeServerWebExchangeMatcher.java | 155 ++++++++++++++++++ ...ediaTypeServerWebExchangeMatcherTests.java | 138 ++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 webflux/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java create mode 100644 webflux/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java diff --git a/webflux/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java b/webflux/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java new file mode 100644 index 0000000000..6f0f9627f1 --- /dev/null +++ b/webflux/src/main/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcher.java @@ -0,0 +1,155 @@ +/* + * + * * Copyright 2002-2017 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.server.util.matcher; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import reactor.core.publisher.Mono; + +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.NotAcceptableStatusException; +import org.springframework.web.server.ServerWebExchange; + +/** + * @author Rob Winch + * @since 5.0 + */ +public class MediaTypeServerWebExchangeMatcher implements ServerWebExchangeMatcher { + private final Log logger = LogFactory.getLog(getClass()); + private RequestedContentTypeResolver requestedContentTypeResolver = new HeaderContentTypeResolver(); + + private final Collection matchingMediaTypes; + private boolean useEquals; + private Set ignoredMediaTypes = Collections.emptySet(); + + public MediaTypeServerWebExchangeMatcher(MediaType... matchingMediaTypes) { + Assert.notEmpty(matchingMediaTypes, "matchingMediaTypes cannot be null"); + Assert.noNullElements(matchingMediaTypes, "matchingMediaTypes cannot contain null"); + this.matchingMediaTypes = Arrays.asList(matchingMediaTypes); + } + + public MediaTypeServerWebExchangeMatcher(Collection matchingMediaTypes) { + Assert.notEmpty(matchingMediaTypes, "matchingMediaTypes cannot be null"); + Assert.isTrue(!matchingMediaTypes.contains(null), () -> "matchingMediaTypes cannot contain null. Got " + matchingMediaTypes); + this.matchingMediaTypes = matchingMediaTypes; + } + + @Override + public Mono matches(ServerWebExchange exchange) { + List httpRequestMediaTypes; + try { + httpRequestMediaTypes = this.requestedContentTypeResolver.resolveMediaTypes(exchange); + } + catch (NotAcceptableStatusException e) { + this.logger.debug("Failed to parse MediaTypes, returning false", e); + return MatchResult.notMatch(); + } + if (this.logger.isDebugEnabled()) { + this.logger.debug("httpRequestMediaTypes=" + httpRequestMediaTypes); + } + for (MediaType httpRequestMediaType : httpRequestMediaTypes) { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Processing " + httpRequestMediaType); + } + if (shouldIgnore(httpRequestMediaType)) { + this.logger.debug("Ignoring"); + continue; + } + if (this.useEquals) { + boolean isEqualTo = this.matchingMediaTypes + .contains(httpRequestMediaType); + this.logger.debug("isEqualTo " + isEqualTo); + return isEqualTo ? MatchResult.match() : MatchResult.notMatch(); + } + for (MediaType matchingMediaType : this.matchingMediaTypes) { + boolean isCompatibleWith = matchingMediaType + .isCompatibleWith(httpRequestMediaType); + if (this.logger.isDebugEnabled()) { + this.logger.debug(matchingMediaType + " .isCompatibleWith " + + httpRequestMediaType + " = " + isCompatibleWith); + } + if (isCompatibleWith) { + return MatchResult.match(); + } + } + } + this.logger.debug("Did not match any media types"); + return MatchResult.notMatch(); + } + + + private boolean shouldIgnore(MediaType httpRequestMediaType) { + for (MediaType ignoredMediaType : this.ignoredMediaTypes) { + if (httpRequestMediaType.includes(ignoredMediaType)) { + 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; + } + + /** + * Sets the {@link RequestedContentTypeResolver} to be used + * @param requestedContentTypeResolver the resolver to use. Default is {@link HeaderContentTypeResolver} + */ + public void setRequestedContentTypeResolver( + RequestedContentTypeResolver requestedContentTypeResolver) { + Assert.notNull(requestedContentTypeResolver, "requestedContentTypeResolver cannot be null"); + this.requestedContentTypeResolver = requestedContentTypeResolver; + } + + /** + * 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; + } + + @Override + public String toString() { + return "MediaTypeRequestMatcher [requestedContentTypeResolver=" + + this.requestedContentTypeResolver + ", matchingMediaTypes=" + + this.matchingMediaTypes + ", useEquals=" + this.useEquals + + ", ignoredMediaTypes=" + this.ignoredMediaTypes + "]"; + } +} diff --git a/webflux/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java b/webflux/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java new file mode 100644 index 0000000000..602ca3701e --- /dev/null +++ b/webflux/src/test/java/org/springframework/security/web/server/util/matcher/MediaTypeServerWebExchangeMatcherTests.java @@ -0,0 +1,138 @@ +/* + * + * * Copyright 2002-2017 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.server.util.matcher; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +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.http.server.reactive.MockServerHttpRequest; +import org.springframework.web.reactive.accept.RequestedContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + * @since 5.0 + */ +@RunWith(MockitoJUnitRunner.class) +public class MediaTypeServerWebExchangeMatcherTests { + @Mock + private RequestedContentTypeResolver resolver; + + private MediaTypeServerWebExchangeMatcher matcher; + + @Test(expected = IllegalArgumentException.class) + public void constructorMediaTypeArrayWhenNullThenThrowsIllegalArgumentException() { + MediaType[] types = null; + new MediaTypeServerWebExchangeMatcher(types); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorMediaTypeArrayWhenContainsNullThenThrowsIllegalArgumentException() { + MediaType[] types = { null }; + new MediaTypeServerWebExchangeMatcher(types); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorMediaTypeListWhenNullThenThrowsIllegalArgumentException() { + List types = null; + new MediaTypeServerWebExchangeMatcher(types); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorMediaTypeListWhenContainsNullThenThrowsIllegalArgumentException() { + List types = Collections.singletonList(null); + new MediaTypeServerWebExchangeMatcher(types); + } + + @Test + public void matchWhenDefaultResolverAndAcceptEqualThenMatch() { + MediaType acceptType = MediaType.TEXT_HTML; + MediaTypeServerWebExchangeMatcher matcher = new MediaTypeServerWebExchangeMatcher(acceptType); + + assertThat(matcher.matches(exchange(acceptType)).block().isMatch()).isTrue(); + } + + @Test + public void matchWhenDefaultResolverAndAcceptEqualAndIgnoreThenMatch() { + MediaType acceptType = MediaType.TEXT_HTML; + MediaTypeServerWebExchangeMatcher matcher = new MediaTypeServerWebExchangeMatcher(acceptType); + matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + + assertThat(matcher.matches(exchange(acceptType)).block().isMatch()).isTrue(); + } + + @Test + public void matchWhenDefaultResolverAndAcceptEqualAndIgnoreThenNotMatch() { + MediaType acceptType = MediaType.TEXT_HTML; + MediaTypeServerWebExchangeMatcher matcher = new MediaTypeServerWebExchangeMatcher(acceptType); + matcher.setIgnoredMediaTypes(Collections.singleton(MediaType.ALL)); + + assertThat(matcher.matches(exchange(MediaType.ALL)).block().isMatch()).isFalse(); + } + + @Test + public void matchWhenDefaultResolverAndAcceptImpliedThenMatch() { + MediaTypeServerWebExchangeMatcher matcher = new MediaTypeServerWebExchangeMatcher(MediaType.parseMediaTypes("text/*")); + + assertThat(matcher.matches(exchange(MediaType.TEXT_HTML)).block().isMatch()).isTrue(); + } + + @Test + public void matchWhenDefaultResolverAndAcceptImpliedAndUseEqualsThenNotMatch() { + MediaTypeServerWebExchangeMatcher matcher = new MediaTypeServerWebExchangeMatcher(MediaType.ALL); + matcher.setUseEquals(true); + + assertThat(matcher.matches(exchange(MediaType.TEXT_HTML)).block().isMatch()).isFalse(); + } + + @Test + public void matchWhenCustomResolverAndAcceptEqualThenMatch() { + MediaType acceptType = MediaType.TEXT_HTML; + when(this.resolver.resolveMediaTypes(any())).thenReturn(Arrays.asList(acceptType)); + MediaTypeServerWebExchangeMatcher matcher = new MediaTypeServerWebExchangeMatcher(acceptType); + matcher.setRequestedContentTypeResolver(this.resolver); + + assertThat(matcher.matches(exchange(acceptType)).block().isMatch()).isTrue(); + } + + @Test + public void matchWhenCustomResolverAndDifferentAcceptThenNotMatch() { + MediaType acceptType = MediaType.TEXT_HTML; + when(this.resolver.resolveMediaTypes(any())).thenReturn(Arrays.asList(acceptType)); + MediaTypeServerWebExchangeMatcher matcher = new MediaTypeServerWebExchangeMatcher(MediaType.APPLICATION_JSON); + matcher.setRequestedContentTypeResolver(this.resolver); + + assertThat(matcher.matches(exchange(acceptType)).block().isMatch()).isFalse(); + } + + private static ServerWebExchange exchange(MediaType... accept) { + return MockServerHttpRequest.get("/").accept(accept).toExchange(); + } +}