This is an automated email from the ASF dual-hosted git repository. wuzhiguo pushed a commit to branch trunk in repository https://gitbox.apache.org/repos/asf/ambari.git
The following commit(s) were added to refs/heads/trunk by this push: new fa9e846e55 AMBARI-9016: Ambari API uses HTTP Header Content-Type:text/plain when… (#3481) fa9e846e55 is described below commit fa9e846e55baf91bbe4544a1b9506a6ac3568329 Author: Zhiguo Wu <wuzhi...@apache.org> AuthorDate: Mon Nov 14 23:00:28 2022 +0800 AMBARI-9016: Ambari API uses HTTP Header Content-Type:text/plain when… (#3481) --- .../server/api/ContentTypeOverrideFilter.java | 182 +++++++++++++++++++++ .../ambari/server/controller/AmbariServer.java | 2 + .../server/api/ContentTypeOverrideFilterTest.java | 87 ++++++++++ 3 files changed, 271 insertions(+) diff --git a/ambari-server/src/main/java/org/apache/ambari/server/api/ContentTypeOverrideFilter.java b/ambari-server/src/main/java/org/apache/ambari/server/api/ContentTypeOverrideFilter.java new file mode 100644 index 0000000000..f69d124a3b --- /dev/null +++ b/ambari-server/src/main/java/org/apache/ambari/server/api/ContentTypeOverrideFilter.java @@ -0,0 +1,182 @@ +/* + * 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.ambari.server.api; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +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.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import javax.ws.rs.Consumes; +import javax.ws.rs.Path; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableSet; +import com.google.common.reflect.ClassPath; + + +/** + * Filter to work around the original limitation of the REST API implementation of Amabri, + * where the Content-Type is always text/plain, despite the accepted/returned JSON content. + * When the request uses application/json as Content-Type, Ambari fails. + * + * This workaround is replacing application/json Content-Type to text/plain in requests, thus + * preventing the failure. + * + * Furthermore the response is also tweaked by changing the Content-Type to application/json + * when the original Content-Type of the request was application/json. + */ +public class ContentTypeOverrideFilter implements Filter { + + private static final Logger logger = LoggerFactory.getLogger(ContentTypeOverrideFilter.class); + + private final Set<String> excludedUrls = new HashSet<>(); + + class ContentTypeOverrideRequestWrapper extends HttpServletRequestWrapper { + + public ContentTypeOverrideRequestWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public Enumeration<String> getHeaders(String name) { + Enumeration<String> headerValues = super.getHeaders(name); + if (HttpHeaders.CONTENT_TYPE.equals(name)) { + Set<String> newContentTypeValues = new HashSet<>(); + while (headerValues.hasMoreElements()) { + String value = headerValues.nextElement(); + if(value != null && value.startsWith(MediaType.APPLICATION_JSON)) { + newContentTypeValues.add(value.replace(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN)); + } else { + newContentTypeValues.add(value); + } + } + return Collections.enumeration(newContentTypeValues); + } + return headerValues; + } + + @Override + public String getHeader(String name) { + if (HttpHeaders.CONTENT_TYPE.equals(name)) { + String header = super.getHeader(name); + if (header != null && header.startsWith(MediaType.APPLICATION_JSON)) { + return header.replace(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN); + } + } + return super.getHeader(name); + } + } + + class ContentTypeOverrideResponseWrapper extends HttpServletResponseWrapper { + + public ContentTypeOverrideResponseWrapper(HttpServletResponse response) { + super(response); + super.setContentType(MediaType.APPLICATION_JSON); + } + + @Override + public void setHeader(String name, String value) { + if (!HttpHeaders.CONTENT_TYPE.equals(name)) { + super.setHeader(name, value); + } + } + + @Override + public void addHeader(String name, String value) { + if (!HttpHeaders.CONTENT_TYPE.equals(name)) { + super.addHeader(name, value); + } + } + + @Override + public void setContentType(String type) { + // Ignore changing the content type + } + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (request instanceof HttpServletRequest) { + HttpServletRequest httpServletRequest = (HttpServletRequest) request; + String contentType = httpServletRequest.getContentType(); + + if (contentType != null && contentType.startsWith(MediaType.APPLICATION_JSON) && !excludedUrls.contains(httpServletRequest.getPathInfo())) { + ContentTypeOverrideRequestWrapper requestWrapper = new ContentTypeOverrideRequestWrapper(httpServletRequest); + ContentTypeOverrideResponseWrapper responseWrapper = new ContentTypeOverrideResponseWrapper((HttpServletResponse) response); + + chain.doFilter(requestWrapper, responseWrapper); + return; + } + } + + chain.doFilter(request, response); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + try { + ClassPath classPath = ClassPath.from(ClassLoader.getSystemClassLoader()); + ImmutableSet<ClassPath.ClassInfo> classes = classPath.getTopLevelClassesRecursive("org.apache.ambari.server.api"); + + restart: + for (ClassPath.ClassInfo classInfo: classes) { + Class<?> clazz = classInfo.load(); + if (clazz.isAnnotationPresent(Path.class)) { + Path path = clazz.getAnnotation(Path.class); + for (Method method : clazz.getMethods()) { + if (method.isAnnotationPresent(Consumes.class)) { + Consumes consumesAnnotation = method.getAnnotation(Consumes.class); + for (String consume : consumesAnnotation.value()) { + if (MediaType.APPLICATION_JSON.equals(consume)) { + excludedUrls.add(path.value()); + continue restart; + } + } + } + } + } + } + } catch (Exception e) { + logger.error("Failed to discover URLs that are excluded from Content-Type override. Falling back to pre-defined list of exluded URLs.", e); + + /* Do not fail here, but fallback to manual definition of excluded endpoints. */ + excludedUrls.add("/bootstrap"); + } + } + + @Override + public void destroy() { + } +} diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java index fbb571caa4..28c1863c8e 100644 --- a/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java +++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/AmbariServer.java @@ -42,6 +42,7 @@ import org.apache.ambari.server.agent.HeartBeatHandler; import org.apache.ambari.server.agent.rest.AgentResource; import org.apache.ambari.server.api.AmbariErrorHandler; import org.apache.ambari.server.api.AmbariPersistFilter; +import org.apache.ambari.server.api.ContentTypeOverrideFilter; import org.apache.ambari.server.api.MethodOverrideFilter; import org.apache.ambari.server.api.UserNameOverrideFilter; import org.apache.ambari.server.api.rest.BootStrapResource; @@ -419,6 +420,7 @@ public class AmbariServer { // session-per-request strategy for api root.addFilter(new FilterHolder(injector.getInstance(AmbariPersistFilter.class)), "/api/*", DISPATCHER_TYPES); root.addFilter(new FilterHolder(new MethodOverrideFilter()), "/api/*", DISPATCHER_TYPES); + root.addFilter(new FilterHolder(new ContentTypeOverrideFilter()), "/api/*", DISPATCHER_TYPES); // register listener to capture request context root.addEventListener(new RequestContextListener()); diff --git a/ambari-server/src/test/java/org/apache/ambari/server/api/ContentTypeOverrideFilterTest.java b/ambari-server/src/test/java/org/apache/ambari/server/api/ContentTypeOverrideFilterTest.java new file mode 100644 index 0000000000..048f6c8f25 --- /dev/null +++ b/ambari-server/src/test/java/org/apache/ambari/server/api/ContentTypeOverrideFilterTest.java @@ -0,0 +1,87 @@ +/* + * 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.ambari.server.api; + +import static org.easymock.EasyMock.expect; + +import java.io.IOException; +import java.util.Vector; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; + +import org.easymock.EasyMockRule; +import org.easymock.EasyMockSupport; +import org.easymock.Mock; +import org.easymock.MockType; +import org.junit.Rule; +import org.junit.Test; + +import junit.framework.Assert; + + +public class ContentTypeOverrideFilterTest extends EasyMockSupport { + + private class FilterChainMock implements FilterChain { + HttpServletResponse response; + HttpServletRequest request; + + @Override + public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { + this.request = (HttpServletRequest) request; + this.response = (HttpServletResponse) response; + } + } + + @Rule + public EasyMockRule mock = new EasyMockRule(this); + + @Mock(type = MockType.NICE) + private HttpServletRequest request; + + @Mock(type = MockType.NICE) + private HttpServletResponse response; + + private final ContentTypeOverrideFilter filter = new ContentTypeOverrideFilter(); + + @Test + public void testJSONContentTypeRequest() throws Exception { + Vector<String> headers = new Vector<>(1); + headers.add(MediaType.APPLICATION_JSON); + + expect(request.getContentType()).andReturn(MediaType.APPLICATION_JSON).atLeastOnce(); + expect(request.getHeader(HttpHeaders.CONTENT_TYPE)).andReturn(MediaType.APPLICATION_JSON).atLeastOnce(); + expect(request.getHeaders(HttpHeaders.CONTENT_TYPE)).andReturn(headers.elements()).atLeastOnce(); + replayAll(); + + FilterChainMock chain = new FilterChainMock(); + filter.doFilter(request, response, chain); + + Assert.assertEquals(MediaType.TEXT_PLAIN, chain.request.getHeader(HttpHeaders.CONTENT_TYPE)); + Assert.assertEquals(MediaType.TEXT_PLAIN, chain.request.getHeaders(HttpHeaders.CONTENT_TYPE).nextElement()); + + verifyAll(); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@ambari.apache.org For additional commands, e-mail: commits-h...@ambari.apache.org