- Created HeaderFactory abstraction

- Implemented different ALLOW-FROM strategies as specified in the proposal.

Conflicts:
	config/src/main/java/org/springframework/security/config/http/HeadersBeanDefinitionParser.java
	config/src/test/groovy/org/springframework/security/config/http/HttpHeadersConfigTests.groovy
This commit is contained in:
Marten Deinum 2013-02-22 20:43:29 +01:00 committed by Rob Winch
parent a63baa8391
commit d0b40cd2ae
21 changed files with 740 additions and 67 deletions

View File

@ -16,17 +16,22 @@
package org.springframework.security.config.http;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.web.headers.HeadersFilter;
import org.springframework.security.web.headers.StaticHeaderFactory;
import org.springframework.security.web.headers.frameoptions.*;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import java.util.HashMap;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.regex.PatternSyntaxException;
/**
* Parser for the {@code HeadersFilter}.
@ -40,10 +45,12 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
private static final String ATT_BLOCK = "block";
private static final String ATT_POLICY = "policy";
private static final String ATT_ORIGIN = "origin";
private static final String ATT_STRATEGY = "strategy";
private static final String ATT_FROM_PARAMETER = "from-parameter";
private static final String ATT_NAME = "name";
private static final String ATT_VALUE = "value";
private static final String ATT_REF = "ref";
private static final String XSS_ELEMENT = "xss-protection";
private static final String CONTENT_TYPE_ELEMENT = "content-type-options";
@ -51,55 +58,107 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
private static final String GENERIC_HEADER_ELEMENT = "header";
private static final String XSS_PROTECTION_HEADER = "X-XSS-Protection";
private static final String FRAME_OPTIONS_HEADER = "X-Frame-Options";
private static final String CONTENT_TYPE_OPTIONS_HEADER = "X-Content-Type-Options";
private static final String ALLOW_FROM = "ALLOW-FROM";
private ManagedList headerFactories;
public BeanDefinition parse(Element element, ParserContext parserContext) {
headerFactories = new ManagedList();
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(HeadersFilter.class);
final Map<String, String> headers = new HashMap<String, String>();
parseXssElement(element, parserContext, headers);
parseFrameOptionsElement(element, parserContext, headers);
parseContentTypeOptionsElement(element, headers);
parseXssElement(element, parserContext);
parseFrameOptionsElement(element, parserContext);
parseContentTypeOptionsElement(element);
parseHeaderElements(element, headers);
parseHeaderElements(element);
builder.addPropertyValue("headers", headers);
builder.addConstructorArgValue(headerFactories);
return builder.getBeanDefinition();
}
private void parseHeaderElements(Element element, Map<String, String> headers) {
List<Element> headerEtls = DomUtils.getChildElementsByTagName(element, GENERIC_HEADER_ELEMENT);
for (Element headerEtl : headerEtls) {
headers.put(headerEtl.getAttribute(ATT_NAME), headerEtl.getAttribute(ATT_VALUE));
private void parseHeaderElements(Element element) {
List<Element> headerElts = DomUtils.getChildElementsByTagName(element, GENERIC_HEADER_ELEMENT);
for (Element headerElt : headerElts) {
String headerFactoryRef = headerElt.getAttribute(ATT_REF);
if (StringUtils.hasText(headerFactoryRef)) {
headerFactories.add(new RuntimeBeanReference(headerFactoryRef));
} else {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(StaticHeaderFactory.class);
builder.addConstructorArgValue(headerElt.getAttribute(ATT_NAME));
builder.addConstructorArgValue(headerElt.getAttribute(ATT_VALUE));
headerFactories.add(builder.getBeanDefinition());
}
}
}
private void parseContentTypeOptionsElement(Element element, Map<String, String> headers) {
private void parseContentTypeOptionsElement(Element element) {
Element contentTypeElt = DomUtils.getChildElementByTagName(element, CONTENT_TYPE_ELEMENT);
if (contentTypeElt != null) {
headers.put(CONTENT_TYPE_OPTIONS_HEADER, "nosniff");
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(StaticHeaderFactory.class);
builder.addConstructorArgValue(CONTENT_TYPE_OPTIONS_HEADER);
builder.addConstructorArgValue("nosniff");
headerFactories.add(builder.getBeanDefinition());
}
}
private void parseFrameOptionsElement(Element element, ParserContext parserContext, Map<String, String> headers) {
private void parseFrameOptionsElement(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FrameOptionsHeaderFactory.class);
Element frameElt = DomUtils.getChildElementByTagName(element, FRAME_OPTIONS_ELEMENT);
if (frameElt != null) {
String header = getAttribute(frameElt, ATT_POLICY, "DENY");
builder.addConstructorArgValue(header);
if (ALLOW_FROM.equals(header) ) {
String origin = frameElt.getAttribute(ATT_ORIGIN);
if (!StringUtils.hasText(origin) ) {
parserContext.getReaderContext().error("<frame-options policy=\"ALLOW-FROM\"/> requires a non-empty string value for the origin attribute to be specified.", frameElt);
String strategyRef = getAttribute(frameElt, ATT_REF, null);
String strategy = getAttribute(frameElt, ATT_STRATEGY, null);
if (StringUtils.hasText(strategy) && StringUtils.hasText(strategyRef)) {
parserContext.getReaderContext().error("Only one of 'strategy' or 'strategy-ref' can be set.",
frameElt);
} else if (strategyRef != null) {
builder.addConstructorArgReference(strategyRef);
} else if (strategy != null) {
String value = getAttribute(frameElt, ATT_VALUE, null);
if (!StringUtils.hasText(value)) {
parserContext.getReaderContext().error("Strategy requires a 'value' to be set.", frameElt);
}
// static, whitelist, regexp
if ("static".equals(strategy)) {
try {
builder.addConstructorArgValue(new StaticAllowFromStrategy(new URI(value)));
} catch (URISyntaxException e) {
parserContext.getReaderContext().error(
"'value' attribute doesn't represent a valid URI.", frameElt, e);
}
} else {
RequestParameterAllowFromStrategy allowFromStrategy = null;
if ("whitelist".equals(strategy)) {
allowFromStrategy = new WhiteListedAllowFromStrategy(
StringUtils.commaDelimitedListToSet(value));
} else {
try {
allowFromStrategy = new RegExpAllowFromStrategy(value);
} catch (PatternSyntaxException e) {
parserContext.getReaderContext().error(
"'value' attribute doesn't represent a valid regular expression.", frameElt, e);
}
}
String fromParameter = getAttribute(frameElt, ATT_FROM_PARAMETER, "from");
allowFromStrategy.setParameterName(fromParameter);
builder.addConstructorArgValue(allowFromStrategy);
}
} else {
parserContext.getReaderContext().error("One of 'strategy' and 'strategy-ref' must be set.",
frameElt);
}
header += " " + origin;
}
headers.put(FRAME_OPTIONS_HEADER, header);
headerFactories.add(builder.getBeanDefinition());
}
}
private void parseXssElement(Element element, ParserContext parserContext, Map<String, String> headers) {
private void parseXssElement(Element element, ParserContext parserContext) {
Element xssElt = DomUtils.getChildElementByTagName(element, XSS_ELEMENT);
if (xssElt != null) {
boolean enabled = Boolean.valueOf(getAttribute(xssElt, ATT_ENABLED, "true"));
@ -109,9 +168,12 @@ public class HeadersBeanDefinitionParser implements BeanDefinitionParser {
if (enabled && block) {
value += "; mode=block";
} else if (!enabled && block) {
parserContext.getReaderContext().error("<xss-protection enabled=\"false\"/> does not allow for the block=\"true\".", xssElt);
parserContext.getReaderContext().error("<xss-protection enabled=\"false\"/> does not allow block=\"true\".", xssElt);
}
headers.put(XSS_PROTECTION_HEADER, value);
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(StaticHeaderFactory.class);
builder.addConstructorArgValue(XSS_PROTECTION_HEADER);
builder.addConstructorArgValue(value);
headerFactories.add(builder.getBeanDefinition());
}
}

View File

@ -729,8 +729,18 @@ frame-options.attlist &=
## Specify the policy to use for the X-Frame-Options-Header.
attribute policy {"DENY","SAMEORIGIN","ALLOW-FROM"}?
frame-options.attlist &=
## Specify the origin to use when ALLOW-FROM is chosen.
attribute origin {xsd:token}?
## Specify the strategy to use when ALLOW-FROM is chosen.
attribute strategy {"static","whitelist","regexp"}?
frame-options.attlist &=
## Specify the a reference to the custom AllowFromStrategy to use when ALLOW-FROM is chosen.
ref?
frame-options.attlist &=
## Specify the a value to use for the chosen strategy.
attribute value {xsd:string}?
frame-options.attlist &=
## Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp' based strategy. Default is 'from'.
attribute from-parameter {xsd:string}?
xss-protection =
## Enable basic XSS browser protection, supported by newer browsers (IE8+), will set the X-XSS-Protection header.
@ -751,10 +761,13 @@ header=
element header {header.attlist}
header.attlist &=
## The name of the header to add.
attribute name {xsd:token}
attribute name {xsd:token}?
header.attlist &=
## The value for the header.
attribute value {xsd:token}
attribute value {xsd:token}?
header.attlist &=
## Reference to a custom HeaderFactory implementation.
ref?
any-user-service = user-service | jdbc-user-service | ldap-user-service

View File

@ -2271,9 +2271,35 @@
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="origin" type="xs:token">
<xs:attribute name="strategy">
<xs:annotation>
<xs:documentation>Specify the origin to use when ALLOW-FROM is chosen.
<xs:documentation>Specify the strategy to use when ALLOW-FROM is chosen.
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="static"/>
<xs:enumeration value="whitelist"/>
<xs:enumeration value="regexp"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
<xs:attribute name="ref" type="xs:token">
<xs:annotation>
<xs:documentation>Defines a reference to a Spring bean Id.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="value" type="xs:string">
<xs:annotation>
<xs:documentation>Specify the a value to use for the chosen strategy.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="from-parameter" type="xs:string">
<xs:annotation>
<xs:documentation>Specify the request parameter to use for the origin when using a 'whitelist' or 'regexp'
based strategy. Default is 'from'.
</xs:documentation>
</xs:annotation>
</xs:attribute>
@ -2319,18 +2345,24 @@
</xs:complexType>
</xs:element>
<xs:attributeGroup name="header.attlist">
<xs:attribute name="name" use="required" type="xs:token">
<xs:attribute name="name" type="xs:token">
<xs:annotation>
<xs:documentation>The name of the header to add.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="value" use="required" type="xs:token">
<xs:attribute name="value" type="xs:token">
<xs:annotation>
<xs:documentation>The value for the header.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="ref" type="xs:token">
<xs:annotation>
<xs:documentation>Defines a reference to a Spring bean Id.
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="any-user-service" abstract="true"/>
<xs:element name="custom-filter">

View File

@ -12,6 +12,8 @@
*/
package org.springframework.security.config.http
import org.springframework.security.util.FieldUtils
import javax.servlet.Filter
import javax.servlet.http.HttpServletRequest
@ -54,10 +56,12 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
createAppContext()
def hf = getFilter(HeadersFilter)
MockHttpServletResponse response = new MockHttpServletResponse();
hf.doFilter(new MockHttpServletRequest(), response);
expect:
hf
hf.headers.isEmpty()
response.headers.isEmpty()
}
def 'http headers content-type-options'() {
@ -69,10 +73,11 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
createAppContext()
def hf = getFilter(HeadersFilter)
MockHttpServletResponse response = new MockHttpServletResponse();
hf.doFilter(new MockHttpServletRequest(), response);
expect:
hf
hf.headers == ['X-Content-Type-Options':'nosniff']
response.headers == ['X-Content-Type-Options':'nosniff']
}
def 'http headers frame-options defaults to DENY'() {
@ -288,6 +293,6 @@ class HttpHeadersConfigTests extends AbstractHttpConfigTests {
then:
BeanDefinitionParsingException e = thrown()
e.message.contains '<xss-protection enabled="false"/> does not allow for the block="true".'
e.message.contains '<xss-protection enabled="false"/> does not allow block="true".'
}
}

View File

@ -319,9 +319,44 @@
including it in a frame it is the same as the one serving the page.
</para>
</section>
<section xml:id="nsa-frame-options-origin">
<title><literal>frame-options-origin</literal></title>
<para>The origin</para>
<section xml:id="nsa=frame-options-strategy">
<title><literal>frame-options-strategy</literal></title>
<para>
Select the <classname>AllowFromStrategy</classname> to use when using the ALLOW-FROM policy.
<itemizedlist>
<listitem><literal>static</literal> Use a single static ALLOW-FROM value. The value can be set
through the <link xlink:href="#nsa-frame-options-value">value</link> attribute.
</listitem>
<listitem><literal>regexp</literal> Use a regelur expression to validate incoming requests and
if they are allowed. The regular expression can be set through the <link xlink:href="#nsa-frame-options-value">value</link>
attribute. The request parameter used to retrieve the value to validate can be specified
using the <link xlink:href="#nsa-frame-options-from-parameter">from-parameter</link>.
</listitem>
<listitem><literal>whitelist</literal>A comma-seperated list containing the allowed domains.
The comma-seperated list can be set through the <link xlink:href="#nsa-frame-options-value">value</link>
attribute. The request parameter used to retrieve the value to validate can be specified
using the <link xlink:href="#nsa-frame-options-from-parameter">from-parameter</link>.
</listitem>
</itemizedlist>
</para>
</section>
<section xml:id="nsa-frame-options-ref">
<title><literal>frame-options-ref</literal></title>
<para>
Instead of using one of the predefined strategies it is also possible to use a custom <classname>AllowFromStrategy</classname>.
The reference to this bean can be specified through this ref attribute.
</para>
</section>
<section xml:id="nsa-frame-options-value">
<title><literal>frame-options-value</literal></title>
<para>The value to use when ALLOW-FROM is used a <link xlink:href="#nsa-frame-options-strategy">strategy</link>.</para>
</section>
<section xml:id="nsa-frame-options-from-parameter">
<title><literal>frame-options-from-parameter</literal></title>
<para>
Specify the name of the request parameter to use when using regexp or whitelist for the ALLOW-FROM
strategy.
</para>
</section>
</section>
<section xml:id="nsa-frame-options-parents">
@ -381,6 +416,10 @@
<title><literal>header-value</literal></title>
<para>The <literal>value</literal> of the header to add.</para>
</section>
<section xml:id="nsa-header-ref">
<title><literal>header-ref</literal></title>
<para>Reference to a custom implementation of the <classname>HeaderFactory</classname> interface.</para>
</section>
</section>
<section xml:id="nsa-header-parents">
<title>Parent Elements of <literal>&lt;header&gt;</literal></title>

View File

@ -0,0 +1,37 @@
package org.springframework.security.web.headers;
import java.util.Arrays;
/**
* Created with IntelliJ IDEA.
* User: marten
* Date: 29-01-13
* Time: 20:26
* To change this template use File | Settings | File Templates.
*/
public final class Header {
private final String name;
private final String[] values;
public Header(String name, String... values) {
this.name = name;
this.values = values;
}
public String getName() {
return this.name;
}
public String[] getValues() {
return this.values;
}
public int hashCode() {
return name.hashCode() + Arrays.hashCode(values);
}
public String toString() {
return "Header [name: " + name + ", values: " + Arrays.toString(values)+"]";
}
}

View File

@ -0,0 +1,23 @@
package org.springframework.security.web.headers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Contract for a factory that creates {@code Header} instances.
*
* @author Marten Deinum
* @since 3.2
* @see HeadersFilter
*/
public interface HeaderFactory {
/**
* Create a {@code Header} instance.
*
* @param request the request
* @param response the response
* @return the created Header or <code>null</code>
*/
Header create(HttpServletRequest request, HttpServletResponse response);
}

View File

@ -22,8 +22,7 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
/**
* Filter implementation to add headers to the current request. Can be useful to add certain headers which enable
@ -35,28 +34,41 @@ import java.util.Map;
*/
public class HeadersFilter extends OncePerRequestFilter {
/** Map of headers to add to a response */
private final Map<String, String> headers = new HashMap<String, String>();
/** Collection of HeaderFactory instances to produce Headers. */
private final List<HeaderFactory> factories;
public HeadersFilter(List<HeaderFactory> factories) {
this.factories = factories;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
for (Map.Entry<String, String> header : headers.entrySet()) {
String name = header.getKey();
String value = header.getValue();
if (logger.isTraceEnabled()) {
logger.trace("Adding header '" + name + "' with value '"+value +"'");
for (HeaderFactory factory : factories) {
Header header = factory.create(request, response);
if (header != null) {
String name = header.getName();
String[] values = header.getValues();
boolean first = true;
for (String value : values) {
if (logger.isDebugEnabled()) {
logger.debug("Adding header '" + name + "' with value '"+value +"'");
}
if (first) {
response.setHeader(name, value);
first = false;
} else {
response.addHeader(name, value);
}
}
} else {
if (logger.isDebugEnabled()) {
logger.debug("Factory produced no header.");
}
}
response.setHeader(header.getKey(), header.getValue());
}
filterChain.doFilter(request, response);
}
public void setHeaders(Map<String, String> headers) {
this.headers.clear();
this.headers.putAll(headers);
}
public void addHeader(String name, String value) {
headers.put(name, value);
}
}

View File

@ -0,0 +1,23 @@
package org.springframework.security.web.headers;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* {@code HeaderFactory} implementation which returns the same {@code Header} instance.
*
* @author Marten Deinum
* @since 3.2
*/
public class StaticHeaderFactory implements HeaderFactory {
private final Header header;
public StaticHeaderFactory(String name, String... values) {
header = new Header(name, values);
}
public Header create(HttpServletRequest request, HttpServletResponse response) {
return header;
}
}

View File

@ -0,0 +1,15 @@
package org.springframework.security.web.headers.frameoptions;
import javax.servlet.http.HttpServletRequest;
/**
* Strategy interfaces used by the {@code FrameOptionsHeaderFactory} to determine the actual value to use for the
* X-Frame-Options header when using the ALLOW-FROM directive.
*
* @author Marten Deinum
* @since 3.2
*/
public interface AllowFromStrategy {
String apply(HttpServletRequest request);
}

View File

@ -0,0 +1,46 @@
package org.springframework.security.web.headers.frameoptions;
import org.springframework.security.web.headers.Header;
import org.springframework.security.web.headers.HeaderFactory;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* {@code HeaderFactory} implementation for the X-Frame-Options headers. When using the ALLOW-FROM directive the actual
* value is determined by a {@code AllowFromStrategy}.
*
* @author Marten Deinum
* @since 3.2
*
* @see AllowFromStrategy
*/
public class FrameOptionsHeaderFactory implements HeaderFactory {
public static final String FRAME_OPTIONS_HEADER = "X-Frame-Options";
private static final String ALLOW_FROM = "ALLOW-FROM";
private final AllowFromStrategy allowFromStrategy;
private final String mode;
public FrameOptionsHeaderFactory(String mode) {
this(mode, new NullAllowFromStrategy());
}
public FrameOptionsHeaderFactory(String mode, AllowFromStrategy allowFromStrategy) {
this.mode=mode;
this.allowFromStrategy=allowFromStrategy;
}
@Override
public Header create(HttpServletRequest request, HttpServletResponse response) {
if (ALLOW_FROM.equals(mode)) {
String value = allowFromStrategy.apply(request);
return new Header(FRAME_OPTIONS_HEADER, value);
} else {
return new Header(FRAME_OPTIONS_HEADER, mode);
}
}
}

View File

@ -0,0 +1,17 @@
package org.springframework.security.web.headers.frameoptions;
import javax.servlet.http.HttpServletRequest;
/**
* Created with IntelliJ IDEA.
* User: marten
* Date: 30-01-13
* Time: 11:06
* To change this template use File | Settings | File Templates.
*/
public class NullAllowFromStrategy implements AllowFromStrategy {
@Override
public String apply(HttpServletRequest request) {
return null;
}
}

View File

@ -0,0 +1,26 @@
package org.springframework.security.web.headers.frameoptions;
import org.springframework.util.Assert;
import java.util.regex.Pattern;
/**
* Implementation which uses a regular expression to validate the supplied origin.
*
* @author Marten Deinum
* @since 3.2
*/
public class RegExpAllowFromStrategy extends RequestParameterAllowFromStrategy {
private final Pattern pattern;
public RegExpAllowFromStrategy(String pattern) {
Assert.hasText(pattern, "Pattern cannot be empty.");
this.pattern = Pattern.compile(pattern);
}
@Override
protected boolean allowed(String from) {
return pattern.matcher(from).matches();
}
}

View File

@ -0,0 +1,51 @@
package org.springframework.security.web.headers.frameoptions;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* Base class for AllowFromStrategy implementations which use a request parameter to retrieve the origin. By default
* the parameter named <code>from</code> is read from the request.
*
* @author Marten Deinum
* @since 3.2
*/
public abstract class RequestParameterAllowFromStrategy implements AllowFromStrategy {
private static final String DEFAULT_ORIGIN_REQUEST_PARAMETER = "from";
private String parameter = DEFAULT_ORIGIN_REQUEST_PARAMETER;
/** Logger for use by subclasses */
protected final Log log = LogFactory.getLog(getClass());
@Override
public String apply(HttpServletRequest request) {
String from = request.getParameter(parameter);
if (log.isDebugEnabled()) {
log.debug("Supplied origin '"+from+"'");
}
if (StringUtils.hasText(from) && allowed(from)) {
return "ALLOW-FROM " + from;
} else {
return "DENY";
}
}
public void setParameterName(String parameter) {
this.parameter=parameter;
}
/**
* Method to be implemented by base classes, used to determine if the supplied origin is allowed.
*
* @param from the supplied origin
* @return <code>true</code> if the supplied origin is allowed.
*/
protected abstract boolean allowed(String from);
}

View File

@ -0,0 +1,21 @@
package org.springframework.security.web.headers.frameoptions;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
/**
* Simple implementation of the {@code AllowFromStrategy}
*/
public class StaticAllowFromStrategy implements AllowFromStrategy {
private final URI uri;
public StaticAllowFromStrategy(URI uri) {
this.uri=uri;
}
@Override
public String apply(HttpServletRequest request) {
return uri.toString();
}
}

View File

@ -0,0 +1,27 @@
package org.springframework.security.web.headers.frameoptions;
import org.springframework.util.Assert;
import java.util.Collection;
import java.util.List;
/**
* Implementation which checks the supplied origin against a list of allowed origins.
*
* @author Marten Deinum
* @since 3.2
*/
public class WhiteListedAllowFromStrategy extends RequestParameterAllowFromStrategy {
private final Collection<String> allowed;
public WhiteListedAllowFromStrategy(Collection<String> allowed) {
Assert.notEmpty(allowed, "Allowed origins cannot be empty.");
this.allowed = allowed;
}
@Override
protected boolean allowed(String from) {
return allowed.contains(from);
}
}

View File

@ -20,9 +20,9 @@ import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
@ -39,7 +39,8 @@ public class HeadersFilterTest {
@Test
public void noHeadersConfigured() throws Exception {
HeadersFilter filter = new HeadersFilter();
List<HeaderFactory> factories = new ArrayList();
HeadersFilter filter = new HeadersFilter(factories);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
MockFilterChain filterChain = new MockFilterChain();
@ -51,11 +52,18 @@ public class HeadersFilterTest {
@Test
public void additionalHeadersShouldBeAddedToTheResponse() throws Exception {
HeadersFilter filter = new HeadersFilter();
Map<String, String> headers = new HashMap<String, String>();
headers.put("X-Header1", "foo");
headers.put("X-Header2", "bar");
filter.setHeaders(headers);
List<HeaderFactory> factories = new ArrayList();
MockHeaderFactory factory1 = new MockHeaderFactory();
factory1.setName("X-Header1");
factory1.setValue("foo");
MockHeaderFactory factory2 = new MockHeaderFactory();
factory2.setName("X-Header2");
factory2.setValue("bar");
factories.add(factory1);
factories.add(factory2);
HeadersFilter filter = new HeadersFilter(factories);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
@ -70,4 +78,24 @@ public class HeadersFilterTest {
assertThat(response.getHeader("X-Header2"), is("bar"));
}
private static final class MockHeaderFactory implements HeaderFactory {
private String name;
private String value;
@Override
public Header create(HttpServletRequest request, HttpServletResponse response) {
return new Header(name, value);
}
public void setName(String name) {
this.name=name;
}
public void setValue(String value) {
this.value=value;
}
}
}

View File

@ -0,0 +1,26 @@
package org.springframework.security.web.headers;
import org.junit.Test;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertSame;
import static org.springframework.test.util.MatcherAssertionErrors.assertThat;
/**
* Test for the {@code StaticHeaderFactory}
*
* @author Marten Deinum
* @since 3.2
*/
public class StaticHeaderFactoryTest {
@Test
public void sameHeaderShouldBeReturned() {
StaticHeaderFactory factory = new StaticHeaderFactory("X-header", "foo");
Header header = factory.create(null, null);
assertThat(header.getName(), is("X-header"));
assertThat(header.getValues()[0], is("foo"));
assertSame(header, factory.create(null, null));
}
}

View File

@ -0,0 +1,66 @@
package org.springframework.security.web.headers.frameoptions;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
/**
* Created with IntelliJ IDEA.
* User: marten
* Date: 01-02-13
* Time: 20:25
* To change this template use File | Settings | File Templates.
*/
public class RegExpAllowFromStrategyTest {
@Test(expected = PatternSyntaxException.class)
public void invalidRegularExpressionShouldLeadToException() {
new RegExpAllowFromStrategy("[a-z");
}
@Test(expected = IllegalArgumentException.class)
public void nullRegularExpressionShouldLeadToException() {
new RegExpAllowFromStrategy(null);
}
@Test
public void subdomainMatchingRegularExpression() {
RegExpAllowFromStrategy strategy = new RegExpAllowFromStrategy("^http://([a-z0-9]*?\\.)test\\.com");
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("from", "http://abc.test.com");
String result1 = strategy.apply(request);
assertThat(result1, is("ALLOW-FROM http://abc.test.com"));
request.setParameter("from", "http://foo.test.com");
String result2 = strategy.apply(request);
assertThat(result2, is("ALLOW-FROM http://foo.test.com"));
request.setParameter("from", "http://test.foobar.com");
String result3 = strategy.apply(request);
assertThat(result3, is("DENY"));
}
@Test
public void noParameterShouldDeny() {
RegExpAllowFromStrategy strategy = new RegExpAllowFromStrategy("^http://([a-z0-9]*?\\.)test\\.com");
MockHttpServletRequest request = new MockHttpServletRequest();
String result1 = strategy.apply(request);
assertThat(result1, is("DENY"));
}
@Test
public void test() {
String pattern = "^http://([a-z0-9]*?\\.)test\\.com";
Pattern p = Pattern.compile(pattern);
String url = "http://abc.test.com";
assertTrue(p.matcher(url).matches());
}
}

View File

@ -0,0 +1,24 @@
package org.springframework.security.web.headers.frameoptions;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import java.net.URI;
import static org.junit.Assert.assertEquals;
/**
* Test for the StaticAllowFromStrategy.
*
* @author Marten Deinum
* @since 3.2
*/
public class StaticAllowFromStrategyTest {
@Test
public void shouldReturnUri() {
String uri = "http://www.test.com";
StaticAllowFromStrategy strategy = new StaticAllowFromStrategy(URI.create(uri));
assertEquals(uri, strategy.apply(new MockHttpServletRequest()));
}
}

View File

@ -0,0 +1,80 @@
package org.springframework.security.web.headers.frameoptions;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import static org.hamcrest.CoreMatchers.is;
import static org.springframework.test.util.MatcherAssertionErrors.assertThat;
/**
* Test for the {@code WhiteListedAllowFromStrategy}.
*
* @author Marten Deinum
* @since 3.2
*/
public class WhiteListedAllowFromStrategyTest {
@Test(expected = IllegalArgumentException.class)
public void emptyListShouldThrowException() {
new WhiteListedAllowFromStrategy(new ArrayList<String>());
}
@Test(expected = IllegalArgumentException.class)
public void nullListShouldThrowException() {
new WhiteListedAllowFromStrategy(null);
}
@Test
public void listWithSingleElementShouldMatch() {
List<String> allowed = new ArrayList<String>();
allowed.add("http://www.test.com");
WhiteListedAllowFromStrategy strategy = new WhiteListedAllowFromStrategy(allowed);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("from", "http://www.test.com");
String result = strategy.apply(request);
assertThat(result, is("ALLOW-FROM http://www.test.com"));
}
@Test
public void listWithMultipleElementShouldMatch() {
List<String> allowed = new ArrayList<String>();
allowed.add("http://www.test.com");
allowed.add("http://www.springsource.org");
WhiteListedAllowFromStrategy strategy = new WhiteListedAllowFromStrategy(allowed);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("from", "http://www.test.com");
String result = strategy.apply(request);
assertThat(result, is("ALLOW-FROM http://www.test.com"));
}
@Test
public void listWithSingleElementShouldNotMatch() {
List<String> allowed = new ArrayList<String>();
allowed.add("http://www.test.com");
WhiteListedAllowFromStrategy strategy = new WhiteListedAllowFromStrategy(allowed);
MockHttpServletRequest request = new MockHttpServletRequest();
request.setParameter("from", "http://www.test123.com");
String result = strategy.apply(request);
assertThat(result, is("DENY"));
}
@Test
public void requestWithoutParameterShouldNotMatch() {
List<String> allowed = new ArrayList<String>();
allowed.add("http://www.test.com");
WhiteListedAllowFromStrategy strategy = new WhiteListedAllowFromStrategy(allowed);
MockHttpServletRequest request = new MockHttpServletRequest();
String result = strategy.apply(request);
assertThat(result, is("DENY"));
}
}