/* * Copyright 2013-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.cloud.netflix.zuul.filters.post; import java.io.ByteArrayInputStream; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.List; import java.util.zip.GZIPInputStream; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.util.ReflectionUtils; import com.netflix.config.DynamicBooleanProperty; import com.netflix.config.DynamicIntProperty; import com.netflix.config.DynamicPropertyFactory; import com.netflix.util.Pair; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.constants.ZuulConstants; import com.netflix.zuul.constants.ZuulHeaders; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.util.HTTPRequestUtils; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.POST_TYPE; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.ROUTING_DEBUG_KEY; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SEND_RESPONSE_FILTER_ORDER; import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.X_ZUUL_DEBUG_HEADER; /** * Post {@link ZuulFilter} that writes responses from proxied requests to the current response. * * @author Spencer Gibb * @author Dave Syer * @author Ryan Baxter */ public class SendResponseFilter extends ZuulFilter { private static final Log log = LogFactory.getLog(SendResponseFilter.class); private static DynamicBooleanProperty INCLUDE_DEBUG_HEADER = DynamicPropertyFactory .getInstance() .getBooleanProperty(ZuulConstants.ZUUL_INCLUDE_DEBUG_HEADER, false); private static DynamicIntProperty INITIAL_STREAM_BUFFER_SIZE = DynamicPropertyFactory .getInstance() .getIntProperty(ZuulConstants.ZUUL_INITIAL_STREAM_BUFFER_SIZE, 8192); private static DynamicBooleanProperty SET_CONTENT_LENGTH = DynamicPropertyFactory .getInstance() .getBooleanProperty(ZuulConstants.ZUUL_SET_CONTENT_LENGTH, false); private boolean useServlet31 = true; public SendResponseFilter() { super(); // To support Servlet API 3.0.1 we need to check if setcontentLengthLong exists try { HttpServletResponse.class.getMethod("setContentLengthLong"); } catch(NoSuchMethodException e) { useServlet31 = false; } } private ThreadLocal<byte[]> buffers = new ThreadLocal<byte[]>() { @Override protected byte[] initialValue() { return new byte[INITIAL_STREAM_BUFFER_SIZE.get()]; } }; @Override public String filterType() { return POST_TYPE; } @Override public int filterOrder() { return SEND_RESPONSE_FILTER_ORDER; } @Override public boolean shouldFilter() { RequestContext context = RequestContext.getCurrentContext(); return context.getThrowable() == null && (!context.getZuulResponseHeaders().isEmpty() || context.getResponseDataStream() != null || context.getResponseBody() != null); } @Override public Object run() { try { addResponseHeaders(); writeResponse(); } catch (Exception ex) { ReflectionUtils.rethrowRuntimeException(ex); } return null; } private void writeResponse() throws Exception { RequestContext context = RequestContext.getCurrentContext(); // there is no body to send if (context.getResponseBody() == null && context.getResponseDataStream() == null) { return; } HttpServletResponse servletResponse = context.getResponse(); if (servletResponse.getCharacterEncoding() == null) { // only set if not set servletResponse.setCharacterEncoding("UTF-8"); } OutputStream outStream = servletResponse.getOutputStream(); InputStream is = null; try { if (RequestContext.getCurrentContext().getResponseBody() != null) { String body = RequestContext.getCurrentContext().getResponseBody(); writeResponse( new ByteArrayInputStream( body.getBytes(servletResponse.getCharacterEncoding())), outStream); return; } boolean isGzipRequested = false; final String requestEncoding = context.getRequest() .getHeader(ZuulHeaders.ACCEPT_ENCODING); if (requestEncoding != null && HTTPRequestUtils.getInstance().isGzipped(requestEncoding)) { isGzipRequested = true; } is = context.getResponseDataStream(); InputStream inputStream = is; if (is != null) { if (context.sendZuulResponse()) { // if origin response is gzipped, and client has not requested gzip, // decompress stream // before sending to client // else, stream gzip directly to client if (context.getResponseGZipped() && !isGzipRequested) { // If origin tell it's GZipped but the content is ZERO bytes, // don't try to uncompress final Long len = context.getOriginContentLength(); if (len == null || len > 0) { try { inputStream = new GZIPInputStream(is); } catch (java.util.zip.ZipException ex) { log.debug( "gzip expected but not " + "received assuming unencoded response " + RequestContext.getCurrentContext() .getRequest().getRequestURL() .toString()); inputStream = is; } } else { // Already done : inputStream = is; } } else if (context.getResponseGZipped() && isGzipRequested) { servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip"); } writeResponse(inputStream, outStream); } } } finally { /** * Closing the wrapping InputStream itself has no effect on closing the underlying tcp connection since it's a wrapped stream. I guess for http * keep-alive. When closing the wrapping stream it tries to reach the end of the current request, which is impossible for infinite http streams. So * instead of closing the InputStream we close the HTTP response. * * @author Johannes Edmeier */ try { Object zuulResponse = RequestContext.getCurrentContext() .get("zuulResponse"); if (zuulResponse instanceof Closeable) { ((Closeable) zuulResponse).close(); } outStream.flush(); // The container will close the stream for us } catch (IOException ex) { log.warn("Error while sending response to client: " + ex.getMessage()); } } } private void writeResponse(InputStream zin, OutputStream out) throws Exception { byte[] bytes = buffers.get(); int bytesRead = -1; while ((bytesRead = zin.read(bytes)) != -1) { out.write(bytes, 0, bytesRead); } } private void addResponseHeaders() { RequestContext context = RequestContext.getCurrentContext(); HttpServletResponse servletResponse = context.getResponse(); if (INCLUDE_DEBUG_HEADER.get()) { @SuppressWarnings("unchecked") List<String> rd = (List<String>) context.get(ROUTING_DEBUG_KEY); if (rd != null) { StringBuilder debugHeader = new StringBuilder(); for (String it : rd) { debugHeader.append("[[[" + it + "]]]"); } servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString()); } } List<Pair<String, String>> zuulResponseHeaders = context.getZuulResponseHeaders(); if (zuulResponseHeaders != null) { for (Pair<String, String> it : zuulResponseHeaders) { servletResponse.addHeader(it.first(), it.second()); } } // Only inserts Content-Length if origin provides it and origin response is not // gzipped if (SET_CONTENT_LENGTH.get()) { Long contentLength = context.getOriginContentLength(); if ( contentLength != null && !context.getResponseGZipped()) { if(useServlet31) { servletResponse.setContentLengthLong(contentLength); } else { //Try and set some kind of content length if we can safely convert the Long to an int if (isLongSafe(contentLength)) { servletResponse.setContentLength(contentLength.intValue()); } } } } } private boolean isLongSafe(long value) { return value <= Integer.MAX_VALUE && value >= Integer.MIN_VALUE; } }