Index: test/org/apache/catalina/filters/TesterFilterChain.java =================================================================== --- test/org/apache/catalina/filters/TesterFilterChain.java (revision 0) +++ test/org/apache/catalina/filters/TesterFilterChain.java (revision 0) @@ -0,0 +1,36 @@ +/** + * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. + * + * 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.apache.catalina.filters; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +/** + * A mock {@link FilterChain}. + */ +public class TesterFilterChain implements FilterChain { + + @Override + public void doFilter(ServletRequest request, ServletResponse response) + throws IOException, ServletException { + // NoOp + } + +} \ No newline at end of file Property changes on: test/org/apache/catalina/filters/TesterFilterChain.java ___________________________________________________________________ Added: svn:eol-style + native Index: test/org/apache/catalina/filters/TesterServletContext.java =================================================================== --- test/org/apache/catalina/filters/TesterServletContext.java (revision 0) +++ test/org/apache/catalina/filters/TesterServletContext.java (revision 0) @@ -0,0 +1,316 @@ +/** + * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. + * + * 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.apache.catalina.filters; + +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Enumeration; +import java.util.EventListener; +import java.util.Map; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterRegistration; +import javax.servlet.RequestDispatcher; +import javax.servlet.Servlet; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRegistration; +import javax.servlet.ServletRegistration.Dynamic; +import javax.servlet.SessionCookieConfig; +import javax.servlet.SessionTrackingMode; +import javax.servlet.descriptor.JspConfigDescriptor; + +/** + * A Mock {@link ServletContext}. + * + * @author mosoni + * + */ +public class TesterServletContext implements ServletContext { + + @Override + public String getContextPath() { + throw new RuntimeException("Not implemented"); + } + + @Override + public ServletContext getContext(String uripath) { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getMajorVersion() { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getMinorVersion() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getMimeType(String file) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Set getResourcePaths(String path) { + throw new RuntimeException("Not implemented"); + } + + @Override + public URL getResource(String path) throws MalformedURLException { + throw new RuntimeException("Not implemented"); + } + + @Override + public InputStream getResourceAsStream(String path) { + throw new RuntimeException("Not implemented"); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + throw new RuntimeException("Not implemented"); + } + + @Override + public RequestDispatcher getNamedDispatcher(String name) { + + throw new RuntimeException("Not implemented"); + } + + @Override + public Servlet getServlet(String name) throws ServletException { + + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getServlets() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getServletNames() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void log(String msg) { + // NOOP + } + + @Override + public void log(Exception exception, String msg) { + // NOOP + } + + @Override + public void log(String message, Throwable throwable) { + // NOOP + } + + @Override + public String getRealPath(String path) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getServerInfo() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getInitParameter(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getInitParameterNames() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Object getAttribute(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getAttributeNames() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setAttribute(String name, Object object) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void removeAttribute(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getServletContextName() { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getEffectiveMajorVersion() { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getEffectiveMinorVersion() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean setInitParameter(String name, String value) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Dynamic addServlet(String servletName, String className) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Dynamic addServlet(String servletName, Servlet servlet) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Dynamic addServlet(String servletName, + Class servletClass) { + throw new RuntimeException("Not implemented"); + } + + @Override + public T createServlet(Class c) + throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public ServletRegistration getServletRegistration(String servletName) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Map getServletRegistrations() { + throw new RuntimeException("Not implemented"); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + String filterName, String className) { + throw new RuntimeException("Not implemented"); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + String filterName, Filter filter) { + throw new RuntimeException("Not implemented"); + } + + @Override + public javax.servlet.FilterRegistration.Dynamic addFilter( + String filterName, Class filterClass) { + throw new RuntimeException("Not implemented"); + } + + @Override + public T createFilter(Class c) + throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public FilterRegistration getFilterRegistration(String filterName) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Map getFilterRegistrations() { + throw new RuntimeException("Not implemented"); + } + + @Override + public SessionCookieConfig getSessionCookieConfig() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setSessionTrackingModes( + Set sessionTrackingModes) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Set getDefaultSessionTrackingModes() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Set getEffectiveSessionTrackingModes() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void addListener(String className) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void addListener(T t) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void addListener(Class listenerClass) { + throw new RuntimeException("Not implemented"); + } + + @Override + public T createListener(Class c) + throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public JspConfigDescriptor getJspConfigDescriptor() { + throw new RuntimeException("Not implemented"); + } + + @Override + public ClassLoader getClassLoader() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void declareRoles(String... roleNames) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getVirtualServerName() { + throw new RuntimeException("Not implemented"); + } + +} \ No newline at end of file Property changes on: test/org/apache/catalina/filters/TesterServletContext.java ___________________________________________________________________ Added: svn:eol-style + native Index: test/org/apache/catalina/filters/TesterHttpServletRequest.java =================================================================== --- test/org/apache/catalina/filters/TesterHttpServletRequest.java (revision 0) +++ test/org/apache/catalina/filters/TesterHttpServletRequest.java (revision 0) @@ -0,0 +1,444 @@ +/** + * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. + * + * 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.apache.catalina.filters; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.AsyncContext; +import javax.servlet.DispatcherType; +import javax.servlet.RequestDispatcher; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletInputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpUpgradeHandler; +import javax.servlet.http.Part; + +/** + * A mock {@link HttpServletRequest}. + */ +public class TesterHttpServletRequest implements HttpServletRequest { + + private final Map attributes = new HashMap(); + private final Map> headers = new HashMap>(); + private String method; + private String contentType; + + @Override + public Object getAttribute(String name) { + return attributes.get(name); + } + + @Override + public Enumeration getAttributeNames() { + return Collections.enumeration(attributes.keySet()); + } + + @Override + public String getCharacterEncoding() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setCharacterEncoding(String env) + throws UnsupportedEncodingException { + // NO-OP. + } + + @Override + public int getContentLength() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getContentType() { + return this.contentType; + } + + /** + * Method to set content type for test. + * + * @param contentType + * The type of content. + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + @Override + public ServletInputStream getInputStream() throws IOException { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getParameter(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getParameterNames() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String[] getParameterValues(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Map getParameterMap() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getProtocol() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getScheme() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getServerName() { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getServerPort() { + throw new RuntimeException("Not implemented"); + } + + @Override + public BufferedReader getReader() throws IOException { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRemoteAddr() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRemoteHost() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setAttribute(String name, Object o) { + attributes.put(name, o); + } + + @Override + public void removeAttribute(String name) { + attributes.remove(name); + } + + @Override + public Locale getLocale() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getLocales() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isSecure() { + throw new RuntimeException("Not implemented"); + } + + @Override + public RequestDispatcher getRequestDispatcher(String path) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRealPath(String path) { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getRemotePort() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getLocalName() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getLocalAddr() { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getLocalPort() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getAuthType() { + throw new RuntimeException("Not implemented"); + } + + @Override + public Cookie[] getCookies() { + throw new RuntimeException("Not implemented"); + } + + @Override + public long getDateHeader(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getHeader(String name) { + List list = headers.get(name); + if (list != null) { + return CORSFilter.join(new HashSet(list), ","); + } + return null; + } + + /** + * Method to set header name and value for test. + * + * @param name + * Name of header. + * @param value + * Value of header. + */ + public void setHeader(String name, String value) { + List values = new ArrayList(); + values.add(value); + headers.put(name, values); + } + + @Override + public Enumeration getHeaders(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Enumeration getHeaderNames() { + return Collections.enumeration(headers.keySet()); + } + + @Override + public int getIntHeader(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getMethod() { + return method; + } + + /** + * Method to set HTTP method type, for test. + * + * @param method + * The type of HTTP method. + */ + public void setMethod(String method) { + this.method = method; + } + + @Override + public String getPathInfo() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getPathTranslated() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getContextPath() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getQueryString() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRemoteUser() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isUserInRole(String role) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Principal getUserPrincipal() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRequestedSessionId() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getRequestURI() { + throw new RuntimeException("Not implemented"); + } + + @Override + public StringBuffer getRequestURL() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getServletPath() { + throw new RuntimeException("Not implemented"); + } + + @Override + public HttpSession getSession(boolean create) { + throw new RuntimeException("Not implemented"); + } + + @Override + public HttpSession getSession() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isRequestedSessionIdValid() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isRequestedSessionIdFromCookie() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isRequestedSessionIdFromURL() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isRequestedSessionIdFromUrl() { + throw new RuntimeException("Not implemented"); + } + + @Override + public long getContentLengthLong() { + throw new RuntimeException("Not implemented"); + } + + @Override + public ServletContext getServletContext() { + throw new RuntimeException("Not implemented"); + } + + @Override + public AsyncContext startAsync() throws IllegalStateException { + throw new RuntimeException("Not implemented"); + } + + @Override + public AsyncContext startAsync(ServletRequest servletRequest, + ServletResponse servletResponse) throws IllegalStateException { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isAsyncStarted() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean isAsyncSupported() { + throw new RuntimeException("Not implemented"); + } + + @Override + public AsyncContext getAsyncContext() { + throw new RuntimeException("Not implemented"); + } + + @Override + public DispatcherType getDispatcherType() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String changeSessionId() { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean authenticate(HttpServletResponse response) + throws IOException, ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public void login(String username, String password) throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public void logout() throws ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public Collection getParts() throws IOException, ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public Part getPart(String name) throws IOException, ServletException { + throw new RuntimeException("Not implemented"); + } + + @Override + public T upgrade( + Class httpUpgradeHandlerClass) throws IOException { + throw new RuntimeException("Not implemented"); + } + +} \ No newline at end of file Property changes on: test/org/apache/catalina/filters/TesterHttpServletRequest.java ___________________________________________________________________ Added: svn:eol-style + native Index: test/org/apache/catalina/filters/TesterFilterConfigs.java =================================================================== --- test/org/apache/catalina/filters/TesterFilterConfigs.java (revision 0) +++ test/org/apache/catalina/filters/TesterFilterConfigs.java (revision 0) @@ -0,0 +1,355 @@ +/** + * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. + * + * 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.apache.catalina.filters; + +import java.util.Enumeration; + +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; + +/** + * A collection of mock {@link FilterConfig}. + */ +public class TesterFilterConfigs { + /** + * A HTTPS origin. + */ + public static final String HTTPS_WWW_APACHE_ORG = "https://www.apache.org"; + /** + * A HTTP origin. + */ + public static final String HTTP_TOMCAT_APACHE_ORG = "http://tomcat.apache.org"; + /** + * A sample header to test exposed headers. + */ + public static final String EXPOSED_HEADERS = "X-CUSTOM-HEADER"; + /** + * Any origin + */ + public static final String ANY_ORIGIN = "*"; + + /** + * A mock {@link ServletContext} object. + */ + public static final TesterServletContext mockServletContext = new TesterServletContext(); + + /** + * Returns a {@link FilterConfig} object, with default configuration. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getDefaultFilterConfig() { + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS; + final String allowedOrigins = CORSFilter.DEFAULT_ALLOWED_ORIGINS; + final String exposedHeaders = CORSFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CORSFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = CORSFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String loggingEnabled = CORSFilter.DEFAULT_LOGGING_ENABLED; + final String decorateRequest = CORSFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A variation of default {@link FilterConfig}, with support of PUT method + * and credentials. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getFilterConfigAnyOriginAndSupportsCredentials() { + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS + + ",PUT"; + final String allowedOrigins = CORSFilter.DEFAULT_ALLOWED_ORIGINS; + final String exposedHeaders = CORSFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = "true"; + final String preflightMaxAge = CORSFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String loggingEnabled = CORSFilter.DEFAULT_LOGGING_ENABLED; + final String decorateRequest = CORSFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A variation of default {@link FilterConfig}, with support of PUT method + * and credentials disabled.. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getFilterConfigAnyOriginAndSupportsCredentialsDisabled() { + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS + + ",PUT"; + final String allowedOrigins = CORSFilter.DEFAULT_ALLOWED_ORIGINS; + final String exposedHeaders = CORSFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = "false"; + final String preflightMaxAge = CORSFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String loggingEnabled = CORSFilter.DEFAULT_LOGGING_ENABLED; + final String decorateRequest = CORSFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A variation of default {@link FilterConfig}, with support of PUT method + * and any origin disabled, that is configured using specific origins. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getFilterConfigSpecificOriginAndSupportsCredentialsDisabled() { + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS + + ",PUT"; + final String allowedOrigins = HTTP_TOMCAT_APACHE_ORG + "," + + HTTPS_WWW_APACHE_ORG; + final String exposedHeaders = CORSFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = "false"; + final String preflightMaxAge = CORSFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String loggingEnabled = CORSFilter.DEFAULT_LOGGING_ENABLED; + final String decorateRequest = CORSFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A variation of default {@link FilterConfig}, with exposed headers. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getFilterConfigWithExposedHeaders() { + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS; + final String allowedOrigins = CORSFilter.DEFAULT_ALLOWED_ORIGINS; + final String exposedHeaders = EXPOSED_HEADERS; + final String supportCredentials = CORSFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = CORSFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String loggingEnabled = CORSFilter.DEFAULT_LOGGING_ENABLED; + final String decorateRequest = CORSFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A variation of default {@link FilterConfig}, with support of PUT method, + * a HTTPS origin + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getSecureFilterConfig() { + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS + + ",PUT"; + final String allowedOrigins = HTTPS_WWW_APACHE_ORG; + final String exposedHeaders = CORSFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = "true"; + final String preflightMaxAge = CORSFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String loggingEnabled = "true"; + final String decorateRequest = CORSFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A {@link FilterConfig} with all null attributes, i.e. will + * use default config. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getNullFilterConfig() { + return generateFilterConfig(null, null, null, null, null, null, null, + null); + } + + /** + * A variation of default {@link FilterConfig}, with support of PUT method, + * a HTTPS origin and a HTTP origin. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getSpecificOriginFilterConfig() { + final String allowedOrigins = HTTPS_WWW_APACHE_ORG + "," + + HTTP_TOMCAT_APACHE_ORG; + + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS + + ",PUT"; + final String exposedHeaders = CORSFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CORSFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = CORSFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String loggingEnabled = CORSFilter.DEFAULT_LOGGING_ENABLED; + final String decorateRequest = CORSFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A variation of default {@link FilterConfig}, with support of PUT method, + * a HTTPS origin, a HTTP origin, and a negative max age header, which + * indicates browsers to not cache pre-flight response. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getSpecificOriginFilterConfigNegativeMaxAge() { + final String allowedOrigins = HTTPS_WWW_APACHE_ORG + "," + + HTTP_TOMCAT_APACHE_ORG; + + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS + + ",PUT"; + final String exposedHeaders = CORSFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CORSFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = "-1"; + final String loggingEnabled = CORSFilter.DEFAULT_LOGGING_ENABLED; + final String decorateRequest = CORSFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A variation of default {@link FilterConfig}, with an invalid pre-flight + * max age value. It should be a integer. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getFilterConfigInvalidMaxPreflightAge() { + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS; + final String allowedOrigins = CORSFilter.DEFAULT_ALLOWED_ORIGINS; + final String exposedHeaders = CORSFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CORSFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = "abc"; + final String loggingEnabled = CORSFilter.DEFAULT_LOGGING_ENABLED; + final String decorateRequest = CORSFilter.DEFAULT_DECORATE_REQUEST; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A {@link FilterConfig} with all properties as empty strings. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getEmptyFilterConfig() { + final String allowedHttpHeaders = ""; + final String allowedHttpMethods = ""; + final String allowedOrigins = ""; + final String exposedHeaders = ""; + final String supportCredentials = ""; + final String preflightMaxAge = ""; + final String loggingEnabled = ""; + final String decorateRequest = ""; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + /** + * A variation of default {@link FilterConfig}, that disables adding + * attributes to HttpServletRequest. + * + * @return A {@link FilterConfig} object. + */ + public static FilterConfig getFilterConfigDecorateRequestDisabled() { + final String allowedHttpHeaders = CORSFilter.DEFAULT_ALLOWED_HTTP_HEADERS; + final String allowedHttpMethods = CORSFilter.DEFAULT_ALLOWED_HTTP_METHODS; + final String allowedOrigins = CORSFilter.DEFAULT_ALLOWED_ORIGINS; + final String exposedHeaders = CORSFilter.DEFAULT_EXPOSED_HEADERS; + final String supportCredentials = CORSFilter.DEFAULT_SUPPORTS_CREDENTIALS; + final String preflightMaxAge = CORSFilter.DEFAULT_PREFLIGHT_MAXAGE; + final String loggingEnabled = CORSFilter.DEFAULT_LOGGING_ENABLED; + final String decorateRequest = "false"; + + return generateFilterConfig(allowedHttpHeaders, allowedHttpMethods, + allowedOrigins, exposedHeaders, supportCredentials, + preflightMaxAge, loggingEnabled, decorateRequest); + } + + private static FilterConfig generateFilterConfig( + final String allowedHttpHeaders, final String allowedHttpMethods, + final String allowedOrigins, final String exposedHeaders, + final String supportCredentials, final String preflightMaxAge, + final String loggingEnabled, final String decorateRequest) { + FilterConfig filterConfig = new FilterConfig() { + + @Override + public String getFilterName() { + return "cors-filter"; + } + + @Override + public ServletContext getServletContext() { + return mockServletContext; + } + + @Override + public String getInitParameter(String name) { + if (CORSFilter.PARAM_CORS_ALLOWED_HEADERS + .equalsIgnoreCase(name)) { + return allowedHttpHeaders; + } else if (CORSFilter.PARAM_CORS_ALLOWED_METHODS + .equalsIgnoreCase(name)) { + return allowedHttpMethods; + } else if (CORSFilter.PARAM_CORS_ALLOWED_ORIGINS + .equalsIgnoreCase(name)) { + return allowedOrigins; + } else if (CORSFilter.PARAM_CORS_EXPOSED_HEADERS + .equalsIgnoreCase(name)) { + return exposedHeaders; + } else if (CORSFilter.PARAM_CORS_SUPPORT_CREDENTIALS + .equalsIgnoreCase(name)) { + return supportCredentials; + } else if (CORSFilter.PARAM_CORS_PREFLIGHT_MAXAGE + .equalsIgnoreCase(name)) { + return preflightMaxAge; + } else if (CORSFilter.PARAM_CORS_LOGGING_ENABLED + .equalsIgnoreCase(name)) { + return loggingEnabled; + } else if (CORSFilter.PARAM_CORS_REQUEST_DECORATE + .equalsIgnoreCase(name)) { + return decorateRequest; + } + return null; + } + + @Override + public Enumeration getInitParameterNames() { + return null; + } + }; + + return filterConfig; + } +} \ No newline at end of file Property changes on: test/org/apache/catalina/filters/TesterFilterConfigs.java ___________________________________________________________________ Added: svn:eol-style + native Index: test/org/apache/catalina/filters/TesterHttpServletResponse.java =================================================================== --- test/org/apache/catalina/filters/TesterHttpServletResponse.java (revision 0) +++ test/org/apache/catalina/filters/TesterHttpServletResponse.java (revision 0) @@ -0,0 +1,238 @@ +/** + * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. + * + * 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.apache.catalina.filters; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import javax.servlet.ServletOutputStream; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +/** + * A mock {@link HttpServletResponse}. + */ +public class TesterHttpServletResponse implements HttpServletResponse { + List headerNames = new ArrayList(); + List headerValues = new ArrayList(); + PrintWriter pw; + int status; + + @Override + public String getCharacterEncoding() { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getContentType() { + throw new RuntimeException("Not implemented"); + } + + @Override + public ServletOutputStream getOutputStream() throws IOException { + throw new RuntimeException("Not implemented"); + } + + @Override + public PrintWriter getWriter() throws IOException { + if (pw == null) { + pw = new PrintWriter(new StringWriter()); + } + return pw; + } + + @Override + public void setCharacterEncoding(String charset) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setContentLength(int len) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setContentType(String type) { + // NO-OP + } + + @Override + public void setBufferSize(int size) { + throw new RuntimeException("Not implemented"); + } + + @Override + public int getBufferSize() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void flushBuffer() throws IOException { + throw new RuntimeException("Not implemented"); + } + + @Override + public void resetBuffer() { + // NO-OP + } + + @Override + public boolean isCommitted() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void reset() { + // NO-OP + } + + @Override + public void setLocale(Locale loc) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Locale getLocale() { + throw new RuntimeException("Not implemented"); + } + + @Override + public void addCookie(Cookie cookie) { + throw new RuntimeException("Not implemented"); + } + + @Override + public boolean containsHeader(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String encodeURL(String url) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String encodeRedirectURL(String url) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String encodeUrl(String url) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String encodeRedirectUrl(String url) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void sendError(int sc, String msg) throws IOException { + // NO-OP + } + + @Override + public void sendError(int sc) throws IOException { + // NO-OP + } + + @Override + public void sendRedirect(String location) throws IOException { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setDateHeader(String name, long date) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void addDateHeader(String name, long date) { + throw new RuntimeException("Not implemented"); + } + + @Override + public String getHeader(String name) { + int index = headerNames.indexOf(name); + if (index != -1) { + return headerValues.get(index); + } + return null; + } + + @Override + public void setHeader(String name, String value) { + int index = headerNames.indexOf(name); + if (index != -1) { + headerValues.set(index, value); + } else { + headerNames.add(name); + headerValues.add(value); + } + } + + @Override + public void addHeader(String name, String value) { + headerNames.add(name); + headerValues.add(value); + } + + @Override + public void setIntHeader(String name, int value) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void addIntHeader(String name, int value) { + throw new RuntimeException("Not implemented"); + } + + @Override + public void setStatus(int sc) { + this.status = sc; + } + + @Override + public int getStatus() { + return this.status; + } + + @Override + public void setStatus(int sc, String sm) { + // NO-OP + } + + @Override + public void setContentLengthLong(long length) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Collection getHeaders(String name) { + throw new RuntimeException("Not implemented"); + } + + @Override + public Collection getHeaderNames() { + throw new RuntimeException("Not implemented"); + } + +} \ No newline at end of file Property changes on: test/org/apache/catalina/filters/TesterHttpServletResponse.java ___________________________________________________________________ Added: svn:eol-style + native Index: test/org/apache/catalina/filters/TestCORSFilter.java =================================================================== --- test/org/apache/catalina/filters/TestCORSFilter.java (revision 0) +++ test/org/apache/catalina/filters/TestCORSFilter.java (revision 0) @@ -0,0 +1,1562 @@ +/** + * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. + * + * 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.apache.catalina.filters; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Assert; +import org.junit.Test; + +/** + * Unit tests for {@link CORSFilter}. + */ +public class TestCORSFilter { + private final FilterChain filterChain = new TesterFilterChain(); + + /** + * Tests if a GET request is treated as simple request. + * + * @See http://www.w3.org/TR/cors/#simple-method + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterSimpleGET() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + "https://www.apache.org")); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.SIMPLE.name().toLowerCase())); + } + + /** + * Tests if a POST request is treated as simple request. + * + * @See http://www.w3.org/TR/cors/#simple-method + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterSimplePOST() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setContentType("text/plain"); + request.setMethod("POST"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + "https://www.apache.org")); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.SIMPLE.name().toLowerCase())); + } + + /** + * Tests if a HEAD request is treated as simple request. + * + * @See http://www.w3.org/TR/cors/#simple-method + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterSimpleHEAD() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("HEAD"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + "https://www.apache.org")); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.SIMPLE.name().toLowerCase())); + } + + /** + * Test the presence of specific origin in response, when '*' is not used. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterSimpleSpecificHeader() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("POST"); + request.setContentType("text/plain"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.SIMPLE.name().toLowerCase())); + } + + /** + * Tests the prsence of the origin (and not '*') in the response, when + * supports credentials is enabled alongwith any origin, '*'. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterSimpleAnyOriginAndSupportsCredentials() + throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs + .getFilterConfigAnyOriginAndSupportsCredentials()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS) + .equals("true")); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.SIMPLE.name().toLowerCase())); + } + + /** + * Tests the presence of the origin (and not '*') in the response, when + * supports credentials is enabled alongwith any origin, '*'. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterSimpleAnyOriginAndSupportsCredentialsDisabled() + throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs + .getFilterConfigAnyOriginAndSupportsCredentialsDisabled()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + TesterFilterConfigs.ANY_ORIGIN)); + Assert.assertNull(response + .getHeader(CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.SIMPLE.name().toLowerCase())); + } + + /** + * Tests the presence of exposed headers in response, if configured. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterSimpleWithExposedHeaders() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("POST"); + request.setContentType("text/plain"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter + .init(TesterFilterConfigs.getFilterConfigWithExposedHeaders()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + "https://www.apache.org")); + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS) + .equals(TesterFilterConfigs.EXPOSED_HEADERS)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.SIMPLE.name().toLowerCase())); + } + + /** + * Checks if an OPTIONS request is processed as pre-flight. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterPreflight() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase())); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals( + "Content-Type")); + } + + /** + * Checks if an OPTIONS request is processed as pre-flight where any origin + * is enabled. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterPreflightAnyOrigin() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase())); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals( + "Content-Type")); + } + + /** + * Checks if an OPTIONS request is processed as pre-flight. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterPreflightInvalidOrigin() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "http://www.example.com"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertEquals(response.getStatus(), + HttpServletResponse.SC_FORBIDDEN); + } + + /** + * Tests the case when a negative max-age header is provided. In that case + * the browser should not cache the response. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterPreflightNegativeMaxAge() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs + .getSpecificOriginFilterConfigNegativeMaxAge()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertNull(response + .getHeader(CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase())); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals( + "Content-Type")); + } + + /** + * Tests a preflight request with credentials enabled. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterPreflightWithCredentials() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSecureFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS) + .equals("true")); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase())); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals( + "Content-Type")); + } + + /** + * Tests a preflight request, when specific origin is enabled and + * credentials are disabled. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterPreflightWithoutCredentialsAndSpecificOrigin() + throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs + .getFilterConfigSpecificOriginAndSupportsCredentialsDisabled()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertNull(response + .getHeader(CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.PRE_FLIGHT.name().toLowerCase())); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS).equals( + "Content-Type")); + } + + /** + * Negative test, when a CORS request arrives, with a null origin. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterNullOrigin() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + + request.setMethod("POST"); + request.setContentType("text/plain"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.NOT_CORS, requestType); + + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.FALSE)); + } + + /** + * Tests a CORS request, from an origin that's not allowed. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDoFilterInvalidCORSOriginNotAllowed() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, "www.google.com"); + request.setMethod("POST"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, + response.getStatus()); + } + + /** + * Negative case, when a null request and null response is used. + * + * @throws IOException + * @throws ServletException + */ + @Test(expected = ServletException.class) + public void testDoFilterNullRequestNullResponse() throws IOException, + ServletException { + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(null, null, filterChain); + } + + /** + * Negative case, when a null request is used. + * + * @throws IOException + * @throws ServletException + */ + @Test(expected = ServletException.class) + public void testDoFilterNullRequestResponse() throws IOException, + ServletException { + TesterHttpServletResponse response = new TesterHttpServletResponse(); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(null, response, filterChain); + } + + /** + * Negative case, when a null response is used. + * + * @throws IOException + * @throws ServletException + */ + @Test(expected = ServletException.class) + public void testDoFilterRequestNullResponse() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, null, filterChain); + } + + /** + * Tests filter init for defaults. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testInitDefaultFilterConfig() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(null); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + "https://www.apache.org")); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.TRUE)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN).equals( + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG)); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE).equals( + CORSFilter.CORSRequestType.SIMPLE.name().toLowerCase())); + } + + /** + * Tests filter init when an invalid filter config is provided with invalid + * max age. + * + * @throws IOException + * @throws ServletException + */ + @Test(expected = ServletException.class) + public void testInitInvalidFilterConfig() throws IOException, + ServletException { + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs + .getFilterConfigInvalidMaxPreflightAge()); + // If we don't get an exception at this point, then all mocked objects + // worked as expected. + } + + /** + * Tests if a non-simple request is given to simple request handler. + * + * @throws IOException + * @throws ServletException + */ + @Test(expected = IllegalArgumentException.class) + public void testNotSimple() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "Content-Type"); + request.setMethod("OPTIONS"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.handleSimpleCORS(request, response, filterChain); + } + + /** + * When a non-preflight request is given to a pre-flight requets handler. + * + * @throws IOException + * @throws ServletException + */ + @Test(expected = IllegalArgumentException.class) + public void testNotPreflight() throws IOException, ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.handlePreflightCORS(request, response, filterChain); + } + + /** + * Tests when a null request and a null response is provided. + */ + @Test(expected = IllegalArgumentException.class) + public void testDecorateCORSPropertiesNullRequestNullCORSRequestType() { + CORSFilter.decorateCORSProperties(null, null); + } + + /** + * Tests when a null request is provided. + */ + @Test(expected = IllegalArgumentException.class) + public void testDecorateCORSPropertiesNullRequestValidCORSRequestType() { + CORSFilter.decorateCORSProperties(null, + CORSFilter.CORSRequestType.SIMPLE); + } + + /** + * Tests when a null response is provided. + */ + @Test(expected = IllegalArgumentException.class) + public void testDecorateCORSPropertiesValidRequestNullRequestType() { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + CORSFilter.decorateCORSProperties(request, null); + } + + /** + * Test a normal non cross-origin request. + */ + @Test + public void testDecorateCORSPropertiesCORSRequestTypeNotCORS() { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + CORSFilter.decorateCORSProperties(request, + CORSFilter.CORSRequestType.NOT_CORS); + Assert.assertTrue(request.getAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST).equals( + Boolean.FALSE)); + } + + /** + * Tests an invalid request type. + * + * @throws ServletException + */ + @Test + public void testDecorateCORSPropertiesCORSRequestTypeInvalidCORS() { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + CORSFilter.decorateCORSProperties(request, + CORSFilter.CORSRequestType.INVALID_CORS); + Assert.assertNull(request + .getAttribute(CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)); + } + + /** + * Tests a simple request type when any origin is enabled. + * + * @throws ServletException + */ + @Test + public void testCheckSimpleRequestTypeAnyOrigin() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, "http://www.w3.org"); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.SIMPLE, requestType); + } + + /** + * Happy path test, when a valid CORS Simple request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckSimpleRequestType() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.SIMPLE, requestType); + } + + /** + * Happy path test, when a valid CORS Simple request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckActualRequestType() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setMethod("PUT"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.ACTUAL, requestType); + } + + /** + * Happy path test, when a valid CORS Simple request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckActualRequestTypeMethodPOSTNotSimpleHeaders() + throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setMethod("POST"); + request.setContentType("application/json"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.ACTUAL, requestType); + } + + /** + * Happy path test, when a valid CORS Pre-flight request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckPreFlightRequestType() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "Content-Type"); + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.PRE_FLIGHT, requestType); + } + + /** + * when a valid CORS Pre-flight request arrives, with no + * Access-Control-Request-Method + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeNoACRM() throws ServletException, + IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.ACTUAL, requestType); + } + + /** + * when a valid CORS Pre-flight request arrives, with empty + * Access-Control-Request-Method + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeEmptyACRM() + throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, ""); + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.INVALID_CORS, + requestType); + } + + /** + * Happy path test, when a valid CORS Pre-flight request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckPreFlightRequestTypeNoHeaders() + throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.PRE_FLIGHT, requestType); + } + + /** + * Section 6.2.3 + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeInvalidRequestMethod() + throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, + "POLITE"); + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, + response.getStatus()); + } + + /** + * Section Section 6.2.5 + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeUnsupportedRequestMethod() + throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, + "TRACE"); + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, + response.getStatus()); + } + + /** + * Section Section 6.2.6 + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeUnsupportedRequestHeaders() + throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "X-ANSWER"); + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSecureFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, + response.getStatus()); + } + + /** + * Section Section 6.2.7 + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeAnyOriginNoWithCredentials() + throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, + "Origin"); + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs + .getFilterConfigAnyOriginAndSupportsCredentialsDisabled()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + "*")); + Assert.assertNull(response + .getHeader(CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS)); + } + + /** + * Test when a preflight request is made from an origin that's not allowed. + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckPreFlightRequestTypeOriginNotAllowed() + throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, "www.ebay.com"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSecureFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, + response.getStatus()); + } + + /** + * Happy path test, when a valid CORS Pre-flight request arrives. + * + * @throws ServletException + */ + @Test + public void testCheckPreFlightRequestTypeEmptyHeaders() + throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTP_TOMCAT_APACHE_ORG); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + request.setHeader( + CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS, ""); + request.setMethod("OPTIONS"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.PRE_FLIGHT, requestType); + } + + /** + * Negative test, when a CORS request arrives, with an empty origin. + * + * @throws ServletException + */ + @Test + public void testCheckNotCORSRequestTypeEmptyOrigin() + throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, ""); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.INVALID_CORS, + requestType); + } + + /** + * Tests for failure, when a different domain is used, that's not in the + * allowed list of origins. + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckInvalidOrigin() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, "www.example.com"); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, + response.getStatus()); + } + + /** + * Tests for failure, when a different sub-domain is used, that's not in the + * allowed list of origins. + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckInvalidOriginNotAllowedSubdomain() + throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "http://commons.apache.org"); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, + response.getStatus()); + } + + /** + * PUT is not an allowed request method. + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckInvalidRequestMethod() throws ServletException, + IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "http://tomcat.apache.org"); + request.setMethod("PUT"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, + response.getStatus()); + } + + /** + * When requestMethod is null + * + * @throws ServletException + */ + @Test + public void testCheckNullRequestMethod() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "http://tomcat.apache.org"); + request.setMethod(null); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.INVALID_CORS, + requestType); + } + + /** + * "http://tomcat.apache.org" is an allowed origin and + * "https://tomcat.apache.org" is not, because scheme doesn't match + * + * @throws ServletException + */ + @Test + public void testCheckForSchemeVariance() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "https://tomcat.apache.org"); + request.setMethod("POST"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.INVALID_CORS, + requestType); + } + + /** + * "http://tomcat.apache.org" is an allowed origin and + * "http://tomcat.apache.org:8080" is not, because ports doesn't match + * + * @throws ServletException + * @throws IOException + */ + @Test + public void testCheckForPortVariance() throws ServletException, IOException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "http://tomcat.apache.org:8080"); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getSpecificOriginFilterConfig()); + corsFilter.doFilter(request, response, filterChain); + Assert.assertEquals(HttpServletResponse.SC_FORBIDDEN, + response.getStatus()); + } + + /** + * Tests for failure, when an invalid {@link HttpServletRequest} is + * encountered. + * + * @throws ServletException + */ + @Test(expected = IllegalArgumentException.class) + public void testCheckRequestTypeNull() throws ServletException { + HttpServletRequest request = null; + CORSFilter corsFilter = new CORSFilter(); + corsFilter.checkRequestType(request); + } + + /** + * Tests a normal join of multiple elements, with comma separator. + */ + @Test + public void testJoin() { + Set elements = new LinkedHashSet(); + String separator = ","; + elements.add("world"); + elements.add("peace"); + String join = CORSFilter.join(elements, separator); + Assert.assertTrue("world,peace".equals(join)); + } + + /** + * Tests join with only 1 element. + */ + @Test + public void testJoinSingleElement() { + Set elements = new LinkedHashSet(); + String separator = ","; + elements.add("world"); + String join = CORSFilter.join(elements, separator); + Assert.assertTrue("world".equals(join)); + } + + /** + * Tests join when a null separator is provided, the default comma is used. + */ + @Test + public void testJoinSepNull() { + Set elements = new LinkedHashSet(); + String separator = null; + elements.add("world"); + elements.add("peace"); + String join = CORSFilter.join(elements, separator); + Assert.assertTrue("world,peace".equals(join)); + } + + /** + * Tests join when elements collection is null. + */ + @Test + public void testJoinElementsNull() { + Set elements = null; + String separator = ","; + String join = CORSFilter.join(elements, separator); + + Assert.assertNull(join); + } + + /** + * Tests join when atleast one null elements is there. + */ + @Test + public void testJoinOneNullElement() { + Set elements = new LinkedHashSet(); + String separator = ","; + elements.add(null); + elements.add("peace"); + String join = CORSFilter.join(elements, separator); + Assert.assertTrue(",peace".equals(join)); + } + + /** + * Tests join when null elements are there. + */ + @Test + public void testJoinAllNullElements() { + Set elements = new LinkedHashSet(); + String separator = ","; + elements.add(null); + elements.add(null); + String join = CORSFilter.join(elements, separator); + Assert.assertTrue("".equals(join)); + } + + /** + * Tests join, for empty strings. + */ + @Test + public void testJoinAllEmptyElements() { + Set elements = new LinkedHashSet(); + String separator = ","; + elements.add(""); + elements.add(""); + String join = CORSFilter.join(elements, separator); + Assert.assertTrue("".equals(join)); + } + + /** + * Tests join, with a pipe separator. + */ + @Test + public void testJoinPipeSeparator() { + Set elements = new LinkedHashSet(); + String separator = "|"; + elements.add("world"); + elements.add("peace"); + String join = CORSFilter.join(elements, separator); + Assert.assertTrue("world|peace".equals(join)); + } + + /** + * Tests default filter config. + * + * @throws ServletException + */ + @Test + public void testWithFilterConfig() throws ServletException { + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + Assert.assertTrue(corsFilter.getAllowedHttpHeaders().size() == 6); + Assert.assertTrue(corsFilter.getAllowedHttpMethods().size() == 4); + Assert.assertTrue(corsFilter.getAllowedOrigins().size() == 0); + Assert.assertTrue(corsFilter.isAnyOriginAllowed()); + Assert.assertTrue(corsFilter.getExposedHeaders().size() == 0); + Assert.assertTrue(corsFilter.isSupportsCredentials()); + Assert.assertTrue(corsFilter.getPreflightMaxAge() == 1800); + Assert.assertTrue(!corsFilter.isLoggingEnabled()); + } + + /** + * Tests an invalid non-number max age. + * + * @throws ServletException + */ + @Test(expected = ServletException.class) + public void testWithFilterConfigInvalidPreflightAge() + throws ServletException { + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs + .getFilterConfigInvalidMaxPreflightAge()); + } + + /** + * Tests empty config object. + * + * @throws ServletException + */ + @Test + public void testWithStringParserEmpty() throws ServletException { + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getEmptyFilterConfig()); + Assert.assertTrue(corsFilter.getAllowedHttpHeaders().size() == 0); + Assert.assertTrue(corsFilter.getAllowedHttpMethods().size() == 0); + Assert.assertTrue(corsFilter.getAllowedOrigins().size() == 0); + Assert.assertTrue(corsFilter.getExposedHeaders().size() == 0); + Assert.assertFalse(corsFilter.isSupportsCredentials()); + Assert.assertTrue(corsFilter.getPreflightMaxAge() == 0); + Assert.assertTrue(!corsFilter.isLoggingEnabled()); + } + + /** + * If an init param is null, it's default value will be used. + * + * @throws ServletException + */ + @Test + public void testWithStringParserNull() throws ServletException { + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getNullFilterConfig()); + Assert.assertTrue(corsFilter.getAllowedHttpHeaders().size() == 6); + Assert.assertTrue(corsFilter.getAllowedHttpMethods().size() == 4); + Assert.assertTrue(corsFilter.getAllowedOrigins().size() == 0); + Assert.assertTrue(corsFilter.isAnyOriginAllowed()); + Assert.assertTrue(corsFilter.getExposedHeaders().size() == 0); + Assert.assertTrue(corsFilter.isSupportsCredentials()); + Assert.assertTrue(corsFilter.getPreflightMaxAge() == 1800); + Assert.assertTrue(!corsFilter.isLoggingEnabled()); + } + + /** + * A valid origin. + */ + @Test + public void testValidOrigin() { + Assert.assertTrue(CORSFilter.isValidOrigin("http://www.w3.org")); + } + + /** + * Invalid origin, \r\n + */ + @Test + public void testInValidOriginCRLF() { + Assert.assertFalse(CORSFilter.isValidOrigin("http://www.w3.org\r\n")); + } + + /** + * Invalid origin, encoded chars + */ + @Test + public void testInValidOriginEncodedCRLF1() { + Assert.assertFalse(CORSFilter.isValidOrigin("http://www.w3.org%0d%0a")); + } + + /** + * Invalid origin, encoded chars in caps. + */ + @Test + public void testInValidOriginEncodedCRLF2() { + Assert.assertFalse(CORSFilter.isValidOrigin("http://www.w3.org%0D%0A")); + } + + /** + * Invalid origin, double encoded chars. + */ + @Test + public void testInValidOriginEncodedCRLF3() { + Assert.assertFalse(CORSFilter + .isValidOrigin("http://www.w3.org%0%0d%0ad%0%0d%0aa")); + } + + /** + * Test for CRLF, an invalid request. \r\n characters. + * + * @throws ServletException + */ + @Test + public void testCheckInvalidCRLF1() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "http://www.w3.org\r\n"); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.INVALID_CORS, + requestType); + } + + /** + * Test for CRLF, an invalid request. + * + * @throws ServletException + */ + @Test + public void testCheckInvalidCRLF2() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "http://www.w3.org\r"); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.INVALID_CORS, + requestType); + } + + /** + * Test for CRLF, an invalid request. encoded chars + * + * @throws ServletException + */ + @Test + public void testCheckInvalidCRLF3() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "http://www.w3.org%0d%0a"); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.INVALID_CORS, + requestType); + } + + /** + * Test for CRLF, an invalid request. encoded chars in caps. + * + * @throws ServletException + */ + @Test + public void testCheckInvalidCRLF4() throws ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + "http://www.w3.org%0D%0A"); + request.setMethod("GET"); + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs.getDefaultFilterConfig()); + CORSFilter.CORSRequestType requestType = corsFilter + .checkRequestType(request); + Assert.assertEquals(CORSFilter.CORSRequestType.INVALID_CORS, + requestType); + } + + /** + * Request should not be decorated. + * + * @throws IOException + * @throws ServletException + */ + @Test + public void testDecorateRequestDisabled() throws IOException, + ServletException { + TesterHttpServletRequest request = new TesterHttpServletRequest(); + request.setHeader(CORSFilter.REQUEST_HEADER_ORIGIN, + TesterFilterConfigs.HTTPS_WWW_APACHE_ORG); + request.setMethod("GET"); + TesterHttpServletResponse response = new TesterHttpServletResponse(); + + CORSFilter corsFilter = new CORSFilter(); + corsFilter.init(TesterFilterConfigs + .getFilterConfigDecorateRequestDisabled()); + corsFilter.doFilter(request, response, filterChain); + + Assert.assertTrue(response.getHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN).equals( + "https://www.apache.org")); + Assert.assertNull(request + .getAttribute(CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST)); + Assert.assertNull(request + .getAttribute(CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN)); + Assert.assertNull(request + .getAttribute(CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS)); + Assert.assertNull(request + .getAttribute(CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE)); + } + + /** + * Not sure how to test doing nothing. Sigh! + */ + @Test + public void testDestroy() { + // Nothing to test. + // NO-OP + } +} \ No newline at end of file Property changes on: test/org/apache/catalina/filters/TestCORSFilter.java ___________________________________________________________________ Added: svn:eol-style + native Index: java/org/apache/catalina/filters/CORSFilter.java =================================================================== --- java/org/apache/catalina/filters/CORSFilter.java (revision 0) +++ java/org/apache/catalina/filters/CORSFilter.java (revision 0) @@ -0,0 +1,1155 @@ +/** + * Copyright 2012-2013 eBay Software Foundation, All Rights Reserved. + * + * 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.apache.catalina.filters; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + *

+ * A {@link Filter} that enable client-side cross-origin requests by + * implementing W3C's CORS (Cross-Origin Resource + * Sharing) specification for resources. Each {@link HttpServletRequest} + * request is inspected as per specification, and appropriate response headers + * are added to {@link HttpServletResponse}. + *

+ * + *

+ * By default, it also sets following request attributes, that helps to + * determine nature of request downstream. + *

    + *
  • cors.isCorsRequest: Flag to determine if request is a CORS + * request. Set to true if CORS request; false + * otherwise.
  • + *
  • cors.request.origin: The Origin URL, i.e. the URL of the page from + * where the request is originated.
  • + *
  • + * cors.request.type: Type of request. Possible values: + *
      + *
    • SIMPLE: A request which is not preceded by a pre-flight request.
    • + *
    • ACTUAL: A request which is preceded by a pre-flight request.
    • + *
    • PRE_FLIGHT: A pre-flight request.
    • + *
    • NOT_CORS: A normal same-origin request.
    • + *
    • INVALID_CORS: A cross-origin request, which is invalid.
    • + *
    + *
  • + *
  • cors.request.headers: Request headers sent as + * 'Access-Control-Request-Headers' header, for pre-flight request.
  • + *
+ *

+ * + * @author Mohit Soni + * @see CORS specification + * + */ +public final class CORSFilter implements Filter { + // ----------------------------------------------------- Instance variables + /** + * Holds filter configuration. + */ + private FilterConfig filterConfig; + + /** + * A {@link Collection} of origins consisting of zero or more origins that + * are allowed access to the resource. + */ + private final Collection allowedOrigins; + + /** + * Determines if any origin is allowed to make request. + */ + private boolean anyOriginAllowed; + + /** + * A {@link Collection} of methods consisting of zero or more methods that + * are supported by the resource. + */ + private final Collection allowedHttpMethods; + + /** + * A {@link Collection} of headers consisting of zero or more header field + * names that are supported by the resource. + */ + private final Collection allowedHttpHeaders; + + /** + * A {@link Collection} of exposed headers consisting of zero or more header + * field names of headers other than the simple response headers that the + * resource might use and can be exposed. + */ + private final Collection exposedHeaders; + + /** + * A supports credentials flag that indicates whether the resource supports + * user credentials in the request. It is true when the resource does and + * false otherwise. + */ + private boolean supportsCredentials; + + /** + * Indicates (in seconds) how long the results of a pre-flight request can + * be cached in a pre-flight result cache. + */ + private long preflightMaxAge; + + /** + * Controls access log logging. + */ + private boolean loggingEnabled; + + /** + * Determines if the request should be decorated or not. + */ + private boolean decorateRequest; + + // --------------------------------------------------------- Constructor(s) + /** + * Initializes configuration {@link Collection} objects. + */ + public CORSFilter() { + this.allowedOrigins = new HashSet(); + this.allowedHttpMethods = new HashSet(); + this.allowedHttpHeaders = new HashSet(); + this.exposedHeaders = new HashSet(); + } + + // --------------------------------------------------------- Public methods + @Override + public void doFilter(final ServletRequest servletRequest, + final ServletResponse servletResponse, final FilterChain filterChain) + throws IOException, ServletException { + if (!(servletRequest instanceof HttpServletRequest) + || !(servletResponse instanceof HttpServletResponse)) { + String message = "CORS doesn't support non-HTTP request or response."; + throw new ServletException(message); + } + + // Safe to downcast at this point. + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + // Determines the CORS request type. + CORSFilter.CORSRequestType requestType = checkRequestType(request); + + // Adds CORS specific attributes to request. + if (decorateRequest) { + CORSFilter.decorateCORSProperties(request, requestType); + } + switch (requestType) { + case SIMPLE: + // Handles a Simple CORS request. + this.handleSimpleCORS(request, response, filterChain); + break; + case ACTUAL: + // Handles an Actual CORS request. + this.handleSimpleCORS(request, response, filterChain); + break; + case PRE_FLIGHT: + // Handles a Pre-flight CORS request. + this.handlePreflightCORS(request, response, filterChain); + break; + case NOT_CORS: + // Handles a Normal request that is not a cross-origin request. + this.handleNonCORS(request, response, filterChain); + break; + default: + // Handles a CORS request that violates specification. + this.handleInvalidCORS(request, response, filterChain); + break; + } + } + + @Override + public void init(final FilterConfig filterConfig) throws ServletException { + // Initialize defaults + parseAndStore(DEFAULT_ALLOWED_ORIGINS, DEFAULT_ALLOWED_HTTP_METHODS, + DEFAULT_ALLOWED_HTTP_HEADERS, DEFAULT_EXPOSED_HEADERS, + DEFAULT_SUPPORTS_CREDENTIALS, DEFAULT_PREFLIGHT_MAXAGE, + DEFAULT_LOGGING_ENABLED, DEFAULT_DECORATE_REQUEST); + + this.filterConfig = filterConfig; + this.loggingEnabled = false; + + if (filterConfig != null) { + String configAllowedOrigins = filterConfig + .getInitParameter(PARAM_CORS_ALLOWED_ORIGINS); + String configAllowedHttpMethods = filterConfig + .getInitParameter(PARAM_CORS_ALLOWED_METHODS); + String configAllowedHttpHeaders = filterConfig + .getInitParameter(PARAM_CORS_ALLOWED_HEADERS); + String configExposedHeaders = filterConfig + .getInitParameter(PARAM_CORS_EXPOSED_HEADERS); + String configSupportsCredentials = filterConfig + .getInitParameter(PARAM_CORS_SUPPORT_CREDENTIALS); + String configPreflightMaxAge = filterConfig + .getInitParameter(PARAM_CORS_PREFLIGHT_MAXAGE); + String configLoggingEnabled = filterConfig + .getInitParameter(PARAM_CORS_LOGGING_ENABLED); + String configDecorateRequest = filterConfig + .getInitParameter(PARAM_CORS_REQUEST_DECORATE); + + parseAndStore(configAllowedOrigins, configAllowedHttpMethods, + configAllowedHttpHeaders, configExposedHeaders, + configSupportsCredentials, configPreflightMaxAge, + configLoggingEnabled, configDecorateRequest); + } + } + + // --------------------------------------------------------------- Handlers + /** + * Handles a CORS request of type {@link CORSRequestType}.SIMPLE. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + * @see Simple + * Cross-Origin Request, Actual Request, and Redirects + */ + public void handleSimpleCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + CORSFilter.CORSRequestType requestType = checkRequestType(request); + if (!(requestType == CORSFilter.CORSRequestType.SIMPLE || requestType == CORSFilter.CORSRequestType.ACTUAL)) { + String message = "Expects a HttpServletRequest object of type " + + CORSFilter.CORSRequestType.SIMPLE + " or " + + CORSFilter.CORSRequestType.ACTUAL; + throw new IllegalArgumentException(message); + } + + final String origin = request + .getHeader(CORSFilter.REQUEST_HEADER_ORIGIN); + final String method = request.getMethod(); + + // Section 6.1.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + if (!allowedHttpMethods.contains(method)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.1.3 + // Add a single Access-Control-Allow-Origin header. + if (anyOriginAllowed && !supportsCredentials) { + // If resource doesn't support credentials and if any origin is + // allowed + // to make CORS request, return header with '*'. + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + } else { + // If the resource supports credentials add a single + // Access-Control-Allow-Origin header, with the value of the Origin + // header as value. + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + } + // Section 6.1.3 + // If the resource supports credentials, add a single + // Access-Control-Allow-Credentials header with the case-sensitive + // string "true" as value. + if (supportsCredentials) { + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true"); + } + + // Section 6.1.4 + // If the list of exposed headers is not empty add one or more + // Access-Control-Expose-Headers headers, with as values the header + // field names given in the list of exposed headers. + if ((exposedHeaders != null) && (exposedHeaders.size() > 0)) { + String exposedHeadersString = join(exposedHeaders, ","); + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS, + exposedHeadersString); + } + + // Forward the request down the filter chain. + filterChain.doFilter(request, response); + } + + /** + * Handles CORS pre-flight request. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + public void handlePreflightCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + CORSRequestType requestType = checkRequestType(request); + if (requestType != CORSRequestType.PRE_FLIGHT) { + throw new IllegalArgumentException( + "Expects a HttpServletRequest object of type " + + CORSRequestType.PRE_FLIGHT.name().toLowerCase()); + } + + final String origin = request + .getHeader(CORSFilter.REQUEST_HEADER_ORIGIN); + + // Section 6.2.2 + if (!isOriginAllowed(origin)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.3 + String accessControlRequestMethod = request + .getHeader(CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethod == null + || (!HTTP_METHODS.contains(accessControlRequestMethod.trim()))) { + handleInvalidCORS(request, response, filterChain); + return; + } else { + accessControlRequestMethod = accessControlRequestMethod.trim(); + } + + // Section 6.2.4 + String accessControlRequestHeadersHeader = request + .getHeader(CORSFilter.REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + List accessControlRequestHeaders = new LinkedList(); + if (accessControlRequestHeadersHeader != null + && !accessControlRequestHeadersHeader.trim().isEmpty()) { + String[] headers = accessControlRequestHeadersHeader.trim().split( + ","); + for (String header : headers) { + accessControlRequestHeaders.add(header.trim().toLowerCase()); + } + } + + // Section 6.2.5 + if (!allowedHttpMethods.contains(accessControlRequestMethod)) { + handleInvalidCORS(request, response, filterChain); + return; + } + + // Section 6.2.6 + if (!accessControlRequestHeaders.isEmpty()) { + for (String header : accessControlRequestHeaders) { + if (!allowedHttpHeaders.contains(header)) { + handleInvalidCORS(request, response, filterChain); + return; + } + } + } + + // Section 6.2.7 + if (supportsCredentials) { + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, + "true"); + } else { + if (anyOriginAllowed) { + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + "*"); + } else { + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, + origin); + } + } + + // Section 6.2.8 + if (preflightMaxAge > 0) { + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE, + String.valueOf(preflightMaxAge)); + } + + // Section 6.2.9 + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS, + accessControlRequestMethod); + + // Section 6.2.10 + if ((allowedHttpHeaders != null) && (!allowedHttpHeaders.isEmpty())) { + response.addHeader( + CORSFilter.RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS, + join(allowedHttpHeaders, ",")); + } + + // Do not forward the request down the filter chain. + } + + /** + * Handles a request, that's not a CORS request, but is a valid request i.e. + * it is not a cross-origin request. This implementation, just forwards the + * request down the filter chain. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + public void handleNonCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) + throws IOException, ServletException { + // Let request pass. + filterChain.doFilter(request, response); + } + + /** + * Handles a CORS request that violates specification. + * + * @param request + * The {@link HttpServletRequest} object. + * @param response + * The {@link HttpServletResponse} object. + * @param filterChain + * The {@link FilterChain} object. + * @throws IOException + * @throws ServletException + */ + public void handleInvalidCORS(final HttpServletRequest request, + final HttpServletResponse response, final FilterChain filterChain) { + String origin = request.getHeader(CORSFilter.REQUEST_HEADER_ORIGIN); + String method = request.getMethod(); + String accessControlRequestHeaders = request + .getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + + String message = "Invalid CORS request; Origin=" + origin + ";Method=" + + method; + if (accessControlRequestHeaders != null) { + message = message + ";Access-Control-Request-Headers=" + + accessControlRequestHeaders; + } + response.setContentType("text/plain"); + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + response.resetBuffer(); + + log(message); + } + + @Override + public void destroy() { + // NOOP + } + + // -------------------------------------------------------- Utility methods + /** + * Decorates the {@link HttpServletRequest}, with CORS attributes. + *
    + *
  • cors.isCorsRequest: Flag to determine if request is a CORS + * request. Set to true if CORS request; false + * otherwise.
  • + *
  • cors.request.origin: The Origin URL.
  • + *
  • cors.request.type: Type of request. Values: + * simple or preflight or not_cors or + * invalid_cors
  • + *
  • cors.request.headers: Request headers sent as + * 'Access-Control-Request-Headers' header, for pre-flight request.
  • + *
+ * + * @param request + * The {@link HttpServletRequest} object. + * @param corsRequestType + * The {@link CORSRequestType} object. + */ + public static void decorateCORSProperties(final HttpServletRequest request, + final CORSRequestType corsRequestType) { + if (request == null) { + throw new IllegalArgumentException( + "HttpServletRequest object is null"); + } + + if (corsRequestType == null) { + throw new IllegalArgumentException("CORSRequestType object is null"); + } + + switch (corsRequestType) { + case SIMPLE: + request.setAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, + Boolean.TRUE); + request.setAttribute(CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CORSFilter.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + break; + case ACTUAL: + request.setAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, + Boolean.TRUE); + request.setAttribute(CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CORSFilter.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + break; + case PRE_FLIGHT: + request.setAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, + Boolean.TRUE); + request.setAttribute(CORSFilter.HTTP_REQUEST_ATTRIBUTE_ORIGIN, + request.getHeader(CORSFilter.REQUEST_HEADER_ORIGIN)); + request.setAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE, + corsRequestType.name().toLowerCase()); + String headers = request + .getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS); + if (headers == null) { + headers = ""; + } + request.setAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS, headers); + break; + case NOT_CORS: + request.setAttribute( + CORSFilter.HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST, + Boolean.FALSE); + break; + default: + // Don't set any attributes + break; + } + } + + /** + * Joins elements of {@link Set} into a string, where each element is + * separated by the provided separator. + * + * @param elements + * The {@link Set} containing elements to join together. + * @param joinSeparator + * The character to be used for separating elements. + * @return The joined {@link String}; null if elements + * {@link Set} is null. + */ + public static String join(final Collection elements, + final String joinSeparator) { + String separator = ","; + if (elements == null) { + return null; + } + if (joinSeparator != null) { + separator = joinSeparator; + } + StringBuilder buffer = new StringBuilder(); + boolean isFirst = true; + for (String element : elements) { + if (!isFirst) { + buffer.append(separator); + } else { + isFirst = false; + } + + if (element != null) { + buffer.append(element); + } + } + + return buffer.toString(); + } + + /** + * Determines the request type. + * + * @param request + * @return {@link CORSRequestType} The type of request. + */ + public CORSRequestType checkRequestType(final HttpServletRequest request) { + CORSRequestType requestType = CORSRequestType.INVALID_CORS; + if (request == null) { + throw new IllegalArgumentException( + "HttpServletRequest object is null"); + } + String originHeader = request.getHeader(REQUEST_HEADER_ORIGIN); + // Section 6.1.1 and Section 6.2.1 + if (originHeader != null) { + if (originHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else if (!isValidOrigin(originHeader)) { + requestType = CORSRequestType.INVALID_CORS; + } else { + String method = request.getMethod(); + if (method != null && HTTP_METHODS.contains(method)) { + if ("OPTIONS".equals(method)) { + String accessControlRequestMethodHeader = request + .getHeader(REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethodHeader != null + && !accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.PRE_FLIGHT; + } else if (accessControlRequestMethodHeader != null + && accessControlRequestMethodHeader.isEmpty()) { + requestType = CORSRequestType.INVALID_CORS; + } else { + requestType = CORSRequestType.ACTUAL; + } + } else if ("GET".equals(method) || "HEAD".equals(method)) { + requestType = CORSRequestType.SIMPLE; + } else if ("POST".equals(method)) { + String contentType = request.getContentType(); + if (contentType != null) { + contentType = contentType.toLowerCase().trim(); + if (SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES + .contains(contentType)) { + requestType = CORSRequestType.SIMPLE; + } else { + requestType = CORSRequestType.ACTUAL; + } + } + } else if (COMPLEX_HTTP_METHODS.contains(method)) { + requestType = CORSRequestType.ACTUAL; + } + } + } + } else { + requestType = CORSRequestType.NOT_CORS; + } + + return requestType; + } + + /** + * Checks if the Origin is allowed to make a CORS request. + * + * @param origin + * The Origin. + * @return true if origin is allowed; false + * otherwise. + */ + private boolean isOriginAllowed(final String origin) { + if (anyOriginAllowed) { + return true; + } + + // If 'Origin' header is a case-sensitive match of any of allowed + // origins, then return true, else return false. + return allowedOrigins.contains(origin); + } + + private void log(String message) { + if (loggingEnabled) { + filterConfig.getServletContext().log(message); + } + } + + /** + * Parses each param-value and populates configuration variables. If a param + * is provided, it overrides the default. + * + * @param allowedOrigins + * A {@link String} of comma separated origins. + * @param allowedHttpMethods + * A {@link String} of comma separated HTTP methods. + * @param allowedHttpHeaders + * A {@link String} of comma separated HTTP headers. + * @param exposedHeaders + * A {@link String} of comma separated headers that needs to be + * exposed. + * @param supportsCredentials + * "true" if support credentials needs to be enabled. + * @param preflightMaxAge + * The amount of seconds the user agent is allowed to cache the + * result of the pre-flight request. + * @param loggingEnabled + * Flag to control logging to access log. + * @throws ServletException + */ + private void parseAndStore(final String allowedOrigins, + final String allowedHttpMethods, final String allowedHttpHeaders, + final String exposedHeaders, final String supportsCredentials, + final String preflightMaxAge, final String loggingEnabled, + final String decorateRequest) throws ServletException { + if (allowedOrigins != null) { + if (allowedOrigins.trim().equals("*")) { + this.anyOriginAllowed = true; + } else { + this.anyOriginAllowed = false; + Set setAllowedOrigins = parseStringToSet(allowedOrigins); + this.allowedOrigins.clear(); + this.allowedOrigins.addAll(setAllowedOrigins); + } + } + + if (allowedHttpMethods != null) { + Set setAllowedHttpMethods = parseStringToSet(allowedHttpMethods); + this.allowedHttpMethods.clear(); + this.allowedHttpMethods.addAll(setAllowedHttpMethods); + } + + if (allowedHttpHeaders != null) { + Set setAllowedHttpHeaders = parseStringToSet(allowedHttpHeaders); + Set lowerCaseHeaders = new HashSet(); + for (String header : setAllowedHttpHeaders) { + String lowerCase = header.toLowerCase(); + lowerCaseHeaders.add(lowerCase); + } + this.allowedHttpHeaders.clear(); + this.allowedHttpHeaders.addAll(lowerCaseHeaders); + } + + if (exposedHeaders != null) { + Set setExposedHeaders = parseStringToSet(exposedHeaders); + this.exposedHeaders.clear(); + this.exposedHeaders.addAll(setExposedHeaders); + } + + if (supportsCredentials != null) { + // For any value other then 'true' this will be false. + this.supportsCredentials = Boolean + .parseBoolean(supportsCredentials); + } + + if (preflightMaxAge != null) { + try { + if (!preflightMaxAge.isEmpty()) { + this.preflightMaxAge = Long.parseLong(preflightMaxAge); + } else { + this.preflightMaxAge = 0L; + } + } catch (NumberFormatException e) { + throw new ServletException("Unable to parse preflightMaxAge", e); + } + } + + if (loggingEnabled != null) { + // For any value other then 'true' this will be false. + this.loggingEnabled = Boolean.parseBoolean(loggingEnabled); + } + + if (decorateRequest != null) { + // For any value other then 'true' this will be false. + this.decorateRequest = Boolean.parseBoolean(decorateRequest); + } + } + + /** + * Takes a comma separated list and returns a Set. + * + * @param data + * A comma separated list of strings. + * @return Set + */ + private Set parseStringToSet(final String data) { + String[] splits; + + if (data != null && data.length() > 0) { + splits = data.split(","); + } else { + splits = new String[] {}; + } + + Set set = new HashSet(); + if (splits.length > 0) { + for (String split : splits) { + set.add(split.trim()); + } + } + + return set; + } + + /** + * Checks if a given origin is valid or not. Criteria: + *
    + *
  • If an encoded character is present in origin, it's not valid.
  • + *
  • Origin should be a valid {@link URI}
  • + *
+ * + * @param origin + * @see RFC952 + * @return true, if origin is a valid URI; false, if either it contains CRLF + * characters, or is not a valid URI or is missing scheme. + */ + public static boolean isValidOrigin(String origin) { + // Checks for encoded characters. Helps prevent CRLF injection. + if (origin.contains("%")) { + return false; + } + + URI originURI; + + try { + originURI = new URI(origin); + } catch (URISyntaxException e) { + return false; + } + // If scheme for URI is null, return false. Return true otherwise. + return originURI.getScheme() != null; + + } + + // -------------------------------------------------------------- Accessors + /** + * Determines if logging is enabled or not. + * + * @return true if it's enabled; false otherwise. + */ + public boolean isLoggingEnabled() { + return loggingEnabled; + } + + /** + * Determines if any origin is allowed to make CORS request. + * + * @return true if it's enabled; false otherwise. + */ + public boolean isAnyOriginAllowed() { + return anyOriginAllowed; + } + + /** + * Returns a {@link Collection} of headers that should be exposed by + * browser. + * + * @return A {@link Collection} of {@link String}. + */ + public Collection getExposedHeaders() { + return exposedHeaders; + } + + /** + * Determines is supports credentials is enabled + * + * @return true if supported, false otherwise. + */ + public boolean isSupportsCredentials() { + return supportsCredentials; + } + + /** + * Returns the preflight response cache time in seconds. + * + * @return Time to cache in seconds. + */ + public long getPreflightMaxAge() { + return preflightMaxAge; + } + + /** + * Returns the {@link Set} of allowed origins that are allowed to make + * requests. + * + * @return {@link Set} of {@link String} + */ + public Collection getAllowedOrigins() { + return allowedOrigins; + } + + /** + * Returns a {@link Set} of HTTP methods that are allowed to make requests. + * + * @return {@link Set} of {@link String} + */ + public Collection getAllowedHttpMethods() { + return allowedHttpMethods; + } + + /** + * Returns a {@link Set} of headers support by resource. + * + * @return {@link Set} of {@link String} + */ + public Collection getAllowedHttpHeaders() { + return allowedHttpHeaders; + } + + // -------------------------------------------------- CORS Response Headers + /** + * The Access-Control-Allow-Origin header indicates whether a resource can + * be shared based by returning the value of the Origin request header in + * the response. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + + /** + * The Access-Control-Allow-Credentials header indicates whether the + * response to request can be exposed when the omit credentials flag is + * unset. When part of the response to a preflight request it indicates that + * the actual request can include user credentials. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + + /** + * The Access-Control-Expose-Headers header indicates which headers are safe + * to expose to the API of a CORS API specification + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"; + + /** + * The Access-Control-Max-Age header indicates how long the results of a + * preflight request can be cached in a preflight result cache. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_MAX_AGE = "Access-Control-Max-Age"; + + /** + * The Access-Control-Allow-Methods header indicates, as part of the + * response to a preflight request, which methods can be used during the + * actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + + /** + * The Access-Control-Allow-Headers header indicates, as part of the + * response to a preflight request, which header field names can be used + * during the actual request. + */ + public static final String RESPONSE_HEADER_ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + + // -------------------------------------------------- CORS Request Headers + /** + * The Origin header indicates where the cross-origin request or preflight + * request originates from. + */ + public static final String REQUEST_HEADER_ORIGIN = "Origin"; + + /** + * The Access-Control-Request-Method header indicates which method will be + * used in the actual request as part of the preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + + /** + * The Access-Control-Request-Headers header indicates which headers will be + * used in the actual request as part of the preflight request. + */ + public static final String REQUEST_HEADER_ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + + // ----------------------------------------------------- Request attributes + /** + * The prefix to a CORS request attribute. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_PREFIX = "cors."; + + /** + * Attribute that contains the origin of the request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_ORIGIN = HTTP_REQUEST_ATTRIBUTE_PREFIX + + "request.origin"; + + /** + * Boolean value, suggesting if the request is a CORS request or not. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_IS_CORS_REQUEST = HTTP_REQUEST_ATTRIBUTE_PREFIX + + "isCorsRequest"; + + /** + * Type of CORS request, of type {@link CORSRequestType}. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_TYPE = HTTP_REQUEST_ATTRIBUTE_PREFIX + + "request.type"; + + /** + * Request headers sent as 'Access-Control-Request-Headers' header, for + * pre-flight request. + */ + public static final String HTTP_REQUEST_ATTRIBUTE_REQUEST_HEADERS = HTTP_REQUEST_ATTRIBUTE_PREFIX + + "request.headers"; + + // -------------------------------------------------------------- Constants + /** + * Enumerates varies types of CORS requests. Also, provides utility methods + * to determine the request type. + */ + public static enum CORSRequestType { + /** + * A simple HTTP request, i.e. it shouldn't be pre-flighted. + */ + SIMPLE, + /** + * A HTTP request that needs to be pre-flighted. + */ + ACTUAL, + /** + * A pre-flight CORS request, to get meta information, before a + * non-simple HTTP request is sent. + */ + PRE_FLIGHT, + /** + * Not a CORS request, but a normal request. + */ + NOT_CORS, + /** + * An invalid CORS request, i.e. it qualifies to be a CORS request, but + * fails to be a valid one. + */ + INVALID_CORS + } + + /** + * {@link Collection} of HTTP methods. Case sensitive. + * + * @see RFC2616 + */ + public static final Collection HTTP_METHODS = new HashSet( + Arrays.asList("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", + "TRACE", "CONNECT")); + /** + * {@link Collection} of non-simple HTTP methods. Case sensitive. + */ + public static final Collection COMPLEX_HTTP_METHODS = new HashSet( + Arrays.asList("PUT", "DELETE", "TRACE", "CONNECT")); + /** + * {@link Collection} of Simple HTTP methods. Case sensitive. + * + * @see Terminology + */ + public static final Collection SIMPLE_HTTP_METHODS = new HashSet( + Arrays.asList("GET", "POST", "HEAD")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see Terminology + */ + public static final Collection SIMPLE_HTTP_REQUEST_HEADERS = new HashSet( + Arrays.asList("Accept", "Accept-Language", "Content-Language")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see Terminology + */ + public static final Collection SIMPLE_HTTP_RESPONSE_HEADERS = new HashSet( + Arrays.asList("Cache-Control", "Content-Language", "Content-Type", + "Expires", "Last-Modified", "Pragma")); + + /** + * {@link Collection} of Simple HTTP request headers. Case in-sensitive. + * + * @see Terminology + */ + public static final Collection SIMPLE_HTTP_REQUEST_CONTENT_TYPE_VALUES = new HashSet( + Arrays.asList("application/x-www-form-urlencoded", + "multipart/form-data", "text/plain")); + + // ------------------------------------------------ Configuration Defaults + /** + * By default, all origins are allowed to make requests. + */ + public static final String DEFAULT_ALLOWED_ORIGINS = "*"; + + /** + * By default, following methods are supported: GET, POST, HEAD and OPTIONS. + */ + public static final String DEFAULT_ALLOWED_HTTP_METHODS = "GET,POST,HEAD,OPTIONS"; + + /** + * By default, time duration to cache pre-flight response is 30 mins. + */ + public static final String DEFAULT_PREFLIGHT_MAXAGE = "1800"; + + /** + * By default, support credentials is turned on. + */ + public static final String DEFAULT_SUPPORTS_CREDENTIALS = "true"; + + /** + * By default, following headers are supported: + * Origin,Accept,X-Requested-With, Content-Type, + * Access-Control-Request-Method, and Access-Control-Request-Headers. + */ + public static final String DEFAULT_ALLOWED_HTTP_HEADERS = "Origin,Accept,X-Requested-With,Content-Type," + + "Access-Control-Request-Method,Access-Control-Request-Headers"; + + /** + * By default, none of the headers are exposed in response. + */ + public static final String DEFAULT_EXPOSED_HEADERS = ""; + + /** + * By default, access log logging is turned off + */ + public static final String DEFAULT_LOGGING_ENABLED = "false"; + + /** + * By default, request is decorated with CORS attributes. + */ + public static final String DEFAULT_DECORATE_REQUEST = "true"; + + // ----------------------------------------Filter Config Init param-name(s) + /** + * Key to retrieve allowed origins from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_ORIGINS = "cors.allowed.origins"; + + /** + * Key to retrieve support credentials from {@link FilterConfig}. + */ + public static final String PARAM_CORS_SUPPORT_CREDENTIALS = "cors.support.credentials"; + + /** + * Key to retrieve exposed headers from {@link FilterConfig}. + */ + public static final String PARAM_CORS_EXPOSED_HEADERS = "cors.exposed.headers"; + + /** + * Key to retrieve allowed headers from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_HEADERS = "cors.allowed.headers"; + + /** + * Key to retrieve allowed methods from {@link FilterConfig}. + */ + public static final String PARAM_CORS_ALLOWED_METHODS = "cors.allowed.methods"; + + /** + * Key to retrieve preflight max age from {@link FilterConfig}. + */ + public static final String PARAM_CORS_PREFLIGHT_MAXAGE = "cors.preflight.maxage"; + + /** + * Key to retrieve access log logging flag. + */ + public static final String PARAM_CORS_LOGGING_ENABLED = "cors.logging.enabled"; + + /** + * Key to determine if request should be decorated. + */ + public static final String PARAM_CORS_REQUEST_DECORATE = "cors.request.decorate"; +} \ No newline at end of file Property changes on: java/org/apache/catalina/filters/CORSFilter.java ___________________________________________________________________ Added: svn:eol-style + native Index: webapps/docs/config/filter.xml =================================================================== --- webapps/docs/config/filter.xml (revision 1488477) +++ webapps/docs/config/filter.xml (working copy) @@ -1309,6 +1309,118 @@ +
+ +

This filter is an implementation of W3C's CORS (Cross-Origin Resource Sharing) specification, which is a mechanism that enables cross-origin requests.

+

The filter works by adding required Access-Control-* headers to HttpServletResponse object. The filter also protects against HTTP response splitting. If request is invalid, or is not permitted, then request is rejected with HTTP status code 403 (Forbidden). A flowchart that demonstrate request processing by this filter is available here.

+

The minimal configuration required to use this filter is:

+ +<filter> + <filter-name>CORSFilter</filter-name> + <filter-class>org.apache.catalina.filters.CORSFilter</filter-class> +</filter> +<filter-mapping> + <filter-name>CORSFilter</filter-name> + <url-pattern>/*</url-pattern> +</filter-mapping> + +
+ +

The filter class name for the CORS Filter is org.apache.catalina.filters.CORSFilter.

+
+ +

The CORS Filter supports following initialisation parameters:

+ + +

A list of origins that are allowed to access the resource. A '*' can be specified to enable access to resource from any origin. Otherwise, a whitelist of comma separated origins can be provided. Ex: http://www.w3.org, https://www.apache.org. Defaults: * (Any origin is allowed to access the resource).

+
+ +

A comma separated list of HTTP methods that can be used to access the resource, using cross-origin requests. These are the methods which will also be included as part of 'Access-Control-Allow-Methods' header in a pre-flight response. Ex: GET,POST. Defaults: GET,POST,HEAD,OPTIONS

+
+ +

A comma separated list of request headers that can be used when making an actual request. These header will also be returned as part of 'Access-Control-Allow-Headers' header in a pre-flight response. Ex: Origin,Accept. Defaults: Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers

+
+ +

A comma separated list of headers other than the simple response headers that browsers are allowed to access. These are the headers which will also be included as part of 'Access-Control-Expose-Headers' header in the pre-flight response. Ex: X-CUSTOM-HEADER-PING,X-CUSTOM-HEADER-PONG. Default: None. Non-simple headers are not exposed by default.

+
+ +

The amount of seconds, browser is allowed to cache the result of the pre-flight request. This will be included as part of 'Access-Control-Max-Age' header in the pre-flight response. A negative value will prevent CORS Filter from adding this response header from pre-flight response. Defaults: 1800

+
+ +

A flag that indicates whether the resource supports user credentials. This flag is exposed as part of 'Access-Control-Allow-Credentials' header in a pre-flight response. It helps browser determine whether or not an actual request can be made using credentials. Defaults: true

+
+ +

A flag to control logging to container logs. Defaults: false

+
+ +

A flag to control if CORS specific attributes should be added to HttpServletRequest object or not. Defaults: true

+
+
+

Here's an example of a more advanced configuration, that overrides defaults:

+ +<filter> + <filter-name>CORSFilter</filter-name> + <filter-class>org.apache.catalina.filters.CORSFilter</filter-class> + <init-param> + <description>A comma separated list of allowed origins</description> + <param-name>cors.allowed.origins</param-name> + <param-value>*</param-value> + </init-param> + <init-param> + <description>A comma separated list of HTTP verbs, using which a cross-origin request can be made.</description> + <param-name>cors.allowed.methods</param-name> + <param-value>GET,POST,HEAD,OPTIONS,PUT</param-value> + </init-param> + <init-param> + <description>A comma separated list of allowed headers when making a non simple cross-origin request.</description> + <param-name>cors.allowed.headers</param-name> + <param-value>Content-Type,X-Requested-With,accept,Origin,Access-Control-Request-Method,Access-Control-Request-Headers</param-value> + </init-param> + <init-param> + <description>A comma separated list of non-standard response headers that will be exposed to XMLHttpRequest object.</description> + <param-name>cors.exposed.headers</param-name> + <param-value>Access-Control-Allow-Origin,Access-Control-Allow-Credentials</param-value> + </init-param> + <init-param> + <description>A flag that suggests if a cross-origin request is supported with cookies, BASIC Authentication, etc.</description> + <param-name>cors.support.credentials</param-name> + <param-value>true</param-value> + </init-param> + <init-param> + <description>A flag control logging</description> + <param-name>cors.logging.enabled</param-name> + <param-value>true</param-value> + </init-param> + <init-param> + <description>Indicates how long (in seconds) the results of a preflight request can be cached in a preflight result cache.</description> + <param-name>cors.preflight.maxage</param-name> + <param-value>10</param-value> + </init-param> +</filter> +<filter-mapping> + <filter-name>CORS Filter</filter-name> + <url-pattern>/*</url-pattern> +</filter-mapping> + +
+ +

CORS Filter adds information about a request, in the HttpServletRequest object, for consumption downstream. Following attributes are set, if cors.request.decorate initialisation parameter is true:

+
    +
  • cors.isCorsRequest: Flag to determine if a request is a CORS request.
  • +
  • cors.request.origin: The Origin URL, i.e. the URL of the page from where the request is originated.
  • +
  • cors.request.type: Type of CORS request. Possible values: +
      +
    • SIMPLE: A request which is not preceded by a pre-flight request.
    • +
    • ACTUAL: A request which is preceded by a pre-flight request.
    • +
    • PRE_FLIGHT: A pre-flight request.
    • +
    • NOT_CORS: A normal same-origin request.
    • +
    • INVALID_CORS: A cross-origin request, which is invalid.
    • +
    +
  • +
  • cors.request.headers: Request headers sent as 'Access-Control-Request-Headers' header, for a pre-flight request.
  • +
+
+