diff --git hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/CrossOriginFilter.java hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/CrossOriginFilter.java index e69de29..8fc6041 100644 --- hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/CrossOriginFilter.java +++ hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/CrossOriginFilter.java @@ -0,0 +1,158 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.hadoop.http; + +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; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class CrossOriginFilter implements Filter { + + private static final Log LOG = LogFactory.getLog(CrossOriginFilter.class); + + // HTTP CORS Request Headers + static final String ORIGIN = "Origin"; + static final String ACCESS_CONTROL_REQUEST_METHOD = "Access-Control-Request-Method"; + static final String ACCESS_CONTROL_REQUEST_HEADERS = "Access-Control-Request-Headers"; + + // HTTP CORS Response Headers + static final String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + static final String ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + static final String ACCESS_CONTROL_ALLOW_METHODS = "Access-Control-Allow-Methods"; + static final String ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + + // Filter configuration + public static final String ALLOWED_ORIGINS = "access.control.allowed.origins"; + + private List allowedMethods = new ArrayList(); + private List allowedHeaders = new ArrayList(); + private List allowedOrigins = new ArrayList(); + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + initializeAllowedMethods(filterConfig); + initializeAllowedHeaders(filterConfig); + initializeAllowedOrigins(filterConfig); + } + + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + doCrossFilter((HttpServletRequest) req, (HttpServletResponse) res); + chain.doFilter(req, res); + } + + @Override + public void destroy() { + allowedMethods.clear(); + allowedHeaders.clear(); + allowedOrigins.clear(); + } + + private void doCrossFilter(HttpServletRequest req, HttpServletResponse res) { + + String origin = req.getHeader(ORIGIN); + if (!isCrossOrigin(origin)) { + return; + } + if (!isOriginAllowed(origin)) { + return; + } + if (!isMethodAllowed(req)) { + return; + } + if (!areHeadersAllowed(req)) { + LOG.info("Header Disallowed"); + return; + } + + res.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, origin); + res.setHeader(ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString()); + res.setHeader(ACCESS_CONTROL_ALLOW_METHODS, getAllowedMethodsHeader()); + res.setHeader(ACCESS_CONTROL_ALLOW_HEADERS, getAllowedHeadersHeader()); + } + + String getAllowedHeadersHeader() { + return StringUtils.join(allowedHeaders, ','); + } + + String getAllowedMethodsHeader() { + return StringUtils.join(allowedMethods, ','); + } + + void initializeAllowedMethods(FilterConfig filterConfig) { + allowedMethods.add("GET"); + allowedMethods.add("POST"); + allowedMethods.add("HEAD"); + LOG.info("Allowed Methods: " + getAllowedMethodsHeader()); + } + + void initializeAllowedHeaders(FilterConfig filterConfig) { + allowedHeaders.add("X-Requested-With"); + allowedHeaders.add("Content-Type"); + allowedHeaders.add("Accept"); + allowedHeaders.add("Origin"); + LOG.info("Allowed Headers: " + getAllowedHeadersHeader()); + } + + void initializeAllowedOrigins(FilterConfig filterConfig) { + String allowedOriginsConfig = filterConfig.getInitParameter(ALLOWED_ORIGINS); + allowedOrigins = Arrays.asList(allowedOriginsConfig.trim().split("\\s*,\\s*")); + LOG.info("Allowed Origins: " + StringUtils.join(allowedOrigins, ',')); + } + + boolean isCrossOrigin(String origin) { + return origin != null; + } + + boolean areHeadersAllowed(HttpServletRequest req) { + String accessControlRequestHeaders = req.getHeader(ACCESS_CONTROL_REQUEST_HEADERS); + if (accessControlRequestHeaders == null) { + return true; + } + String headers[] = accessControlRequestHeaders.trim().split("\\s*,\\s*"); + return allowedHeaders.containsAll(Arrays.asList(headers)); + } + + boolean isOriginAllowed(String origin) { + return allowedOrigins.contains(origin); + } + + boolean isMethodAllowed(HttpServletRequest req) { + String accessControlRequestMethod = req.getHeader(ACCESS_CONTROL_REQUEST_METHOD); + if (accessControlRequestMethod == null) { + return false; + } + return allowedMethods.contains(accessControlRequestMethod); + } +} diff --git hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/CrossOriginFilterInitializer.java hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/CrossOriginFilterInitializer.java index e69de29..2a34411 100644 --- hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/CrossOriginFilterInitializer.java +++ hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/http/CrossOriginFilterInitializer.java @@ -0,0 +1,69 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.hadoop.http; + +import java.util.Map; + +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.http.FilterContainer; +import org.apache.hadoop.http.FilterInitializer; + +/** + *

+ * Initializes {@link CrossOriginFilter} which provides support for Kerberos + * HTTP SPNEGO authentication. + *

+ *

+ * It enables Kerberos HTTP SPNEGO plus delegation token authentication for the + * timeline server. + *

+ * Refer to the core-default.xml file, after the comment 'HTTP + * Authentication' for details on the configuration options. All related + * configuration properties have 'hadoop.http.authentication.' as prefix. + */ +public class CrossOriginFilterInitializer extends FilterInitializer { + + public static final String PREFIX = "hadoop.http.filter.cross.origin."; + + /** + *

+ * Initializes {@link CrossOriginFilter} + *

+ *

+ * Propagates to {@link CrossOriginFilter} configuration all YARN + * configuration properties prefixed with + * "yarn.timeline-service.authentication." + *

+ * + * @param container + * The filter container + * @param conf + * Configuration for run-time parameters + */ + @Override + public void initFilter(FilterContainer container, Configuration conf) { + + container.addGlobalFilter("Cross Origin Filter", + CrossOriginFilter.class.getName(), getFilterParameters(conf)); + } + + static Map getFilterParameters(Configuration conf) { + return conf.getValByRegex(PREFIX); + } +} diff --git hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestCrossOriginFilter.java hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestCrossOriginFilter.java index e69de29..889326a 100644 --- hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestCrossOriginFilter.java +++ hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestCrossOriginFilter.java @@ -0,0 +1,203 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.hadoop.http; + +import java.io.IOException; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.Test; + +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; + +public class TestCrossOriginFilter { + + @Test + public void testSameOrigin() throws ServletException, IOException { + + // Setup the configuration settings of the server + Map conf = new HashMap(); + conf.put(CrossOriginFilter.ALLOWED_ORIGINS, ""); + FilterConfig filterConfig = new FilterConfigTest(conf); + + // Origin is not specified for same origin requests + HttpServletRequest mockReq = mock(HttpServletRequest.class); + when(mockReq.getHeader(CrossOriginFilter.ORIGIN)).thenReturn(null); + + // Objects to verify interactions based on request + HttpServletResponse mockRes = mock(HttpServletResponse.class); + FilterChain mockChain = mock(FilterChain.class); + + // Object under test + CrossOriginFilter filter = new CrossOriginFilter(); + filter.init(filterConfig); + filter.doFilter(mockReq, mockRes, mockChain); + + verifyZeroInteractions(mockRes); + verify(mockChain).doFilter(mockReq, mockRes); + } + + @Test + public void testDisallowedOrigin() throws ServletException, IOException { + + // Setup the configuration settings of the server + Map conf = new HashMap(); + conf.put(CrossOriginFilter.ALLOWED_ORIGINS, "example.com"); + FilterConfig filterConfig = new FilterConfigTest(conf); + + // Origin is not specified for same origin requests + HttpServletRequest mockReq = mock(HttpServletRequest.class); + when(mockReq.getHeader(CrossOriginFilter.ORIGIN)).thenReturn("example.org"); + + // Objects to verify interactions based on request + HttpServletResponse mockRes = mock(HttpServletResponse.class); + FilterChain mockChain = mock(FilterChain.class); + + // Object under test + CrossOriginFilter filter = new CrossOriginFilter(); + filter.init(filterConfig); + filter.doFilter(mockReq, mockRes, mockChain); + + verifyZeroInteractions(mockRes); + verify(mockChain).doFilter(mockReq, mockRes); + } + + @Test + public void testDisallowedMethod() throws ServletException, IOException { + + // Setup the configuration settings of the server + Map conf = new HashMap(); + conf.put(CrossOriginFilter.ALLOWED_ORIGINS, "example.com"); + FilterConfig filterConfig = new FilterConfigTest(conf); + + // Origin is not specified for same origin requests + HttpServletRequest mockReq = mock(HttpServletRequest.class); + when(mockReq.getHeader(CrossOriginFilter.ORIGIN)).thenReturn("example.com"); + when(mockReq.getHeader(CrossOriginFilter.ACCESS_CONTROL_REQUEST_METHOD)).thenReturn("DISALLOWED_METHOD"); + + // Objects to verify interactions based on request + HttpServletResponse mockRes = mock(HttpServletResponse.class); + FilterChain mockChain = mock(FilterChain.class); + + // Object under test + CrossOriginFilter filter = new CrossOriginFilter(); + filter.init(filterConfig); + filter.doFilter(mockReq, mockRes, mockChain); + + verifyZeroInteractions(mockRes); + verify(mockChain).doFilter(mockReq, mockRes); + } + + @Test + public void testDisallowedHeader() throws ServletException, IOException { + + // Setup the configuration settings of the server + Map conf = new HashMap(); + conf.put(CrossOriginFilter.ALLOWED_ORIGINS, "example.com"); + FilterConfig filterConfig = new FilterConfigTest(conf); + + // Origin is not specified for same origin requests + HttpServletRequest mockReq = mock(HttpServletRequest.class); + when(mockReq.getHeader(CrossOriginFilter.ORIGIN)).thenReturn("example.com"); + when(mockReq.getHeader(CrossOriginFilter.ACCESS_CONTROL_REQUEST_METHOD)).thenReturn("GET"); + when(mockReq.getHeader(CrossOriginFilter.ACCESS_CONTROL_REQUEST_HEADERS)).thenReturn("Disallowed-Header"); + + // Objects to verify interactions based on request + HttpServletResponse mockRes = mock(HttpServletResponse.class); + FilterChain mockChain = mock(FilterChain.class); + + // Object under test + CrossOriginFilter filter = new CrossOriginFilter(); + filter.init(filterConfig); + filter.doFilter(mockReq, mockRes, mockChain); + + verifyZeroInteractions(mockRes); + verify(mockChain).doFilter(mockReq, mockRes); + } + + @Test + public void testCrossOriginFilter() throws ServletException, IOException { + + // Setup the configuration settings of the server + Map conf = new HashMap(); + conf.put(CrossOriginFilter.ALLOWED_ORIGINS, "example.com"); + FilterConfig filterConfig = new FilterConfigTest(conf); + + // Origin is not specified for same origin requests + HttpServletRequest mockReq = mock(HttpServletRequest.class); + when(mockReq.getHeader(CrossOriginFilter.ORIGIN)).thenReturn("example.com"); + when(mockReq.getHeader(CrossOriginFilter.ACCESS_CONTROL_REQUEST_METHOD)).thenReturn("GET"); + when(mockReq.getHeader(CrossOriginFilter.ACCESS_CONTROL_REQUEST_HEADERS)).thenReturn("X-Requested-With"); + + // Objects to verify interactions based on request + HttpServletResponse mockRes = mock(HttpServletResponse.class); + FilterChain mockChain = mock(FilterChain.class); + + // Object under test + CrossOriginFilter filter = new CrossOriginFilter(); + filter.init(filterConfig); + filter.doFilter(mockReq, mockRes, mockChain); + + verify(mockRes).setHeader(CrossOriginFilter.ACCESS_CONTROL_ALLOW_ORIGIN, "example.com"); + verify(mockRes).setHeader(CrossOriginFilter.ACCESS_CONTROL_ALLOW_CREDENTIALS, Boolean.TRUE.toString()); + verify(mockRes).setHeader(CrossOriginFilter.ACCESS_CONTROL_ALLOW_METHODS, filter.getAllowedMethodsHeader()); + verify(mockRes).setHeader(CrossOriginFilter.ACCESS_CONTROL_ALLOW_HEADERS, filter.getAllowedHeadersHeader()); + verify(mockChain).doFilter(mockReq, mockRes); + } + + private static class FilterConfigTest implements FilterConfig { + final Map map; + + FilterConfigTest(Map map) { + this.map = map; + } + + @Override + public String getFilterName() { + return "dummy"; + } + + @Override + public String getInitParameter(String arg0) { + return map.get(arg0); + } + + @Override + public Enumeration getInitParameterNames() { + return Collections.enumeration(map.keySet()); + } + + @Override + public ServletContext getServletContext() { + return null; + } + } +} diff --git hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestCrossOriginFilterInitializer.java hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestCrossOriginFilterInitializer.java index e69de29..315b812 100644 --- hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestCrossOriginFilterInitializer.java +++ hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/http/TestCrossOriginFilterInitializer.java @@ -0,0 +1,53 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.hadoop.http; + +import java.util.Map; + +import org.apache.hadoop.conf.Configuration; + +import org.junit.Assert; +import org.junit.Test; + +public class TestCrossOriginFilterInitializer { + + @Test + public void testGetFilterParameters() { + + // Initialize configuration object + Configuration conf = new Configuration(); + conf.set(CrossOriginFilterInitializer.PREFIX + "rootparam", "rootvalue"); + conf.set(CrossOriginFilterInitializer.PREFIX + "nested.param", "nestedvalue"); + conf.set("outofscopeparam", "outofscopevalue"); + + // call function under test + Map filterParameters = + CrossOriginFilterInitializer.getFilterParameters(conf); + + // retrieve values + String rootvalue = filterParameters.get(CrossOriginFilterInitializer.PREFIX + "rootparam"); + String nestedvalue = filterParameters.get(CrossOriginFilterInitializer.PREFIX + "nested.param"); + String outofscopeparam = filterParameters.get("outofscopeparam"); + + // verify expected values are in place + Assert.assertEquals("Could not find filter parameter", "rootvalue", rootvalue); + Assert.assertEquals("Could not find filter parameter", "nestedvalue", nestedvalue); + Assert.assertNull("Found unexpected value in filter parameters", outofscopeparam); + } +}