All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.zuul.http.HttpServletRequestWrapper Maven / Gradle / Ivy

/*
 * Copyright 2013 Netflix, Inc.
 *
 *      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 com.netflix.zuul.http;


import com.netflix.zuul.constants.ZuulHeaders;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.util.HTTPRequestUtils;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.net.SocketTimeoutException;
import java.net.URLDecoder;
import java.util.*;
import java.util.zip.*;

import static org.junit.Assert.*;
import static org.mockito.Mockito.when;

/**
 * This class implements the Wrapper or Decorator pattern.
* Methods default to calling through to the wrapped request object, * except the ones that read the request's content (parameters, stream or reader). *

* This class provides a buffered content reading that allows the methods * {@link #getReader()}, {@link #getInputStream()} and any of the getParameterXXX to be called * safely and repeatedly with the same results. *

* This class is intended to wrap relatively small HttpServletRequest instances. * * @author pgurov */ public class HttpServletRequestWrapper extends javax.servlet.http.HttpServletRequestWrapper { private final static HashMap EMPTY_MAP = new HashMap(); protected static final Logger LOG = LoggerFactory.getLogger(HttpServletRequestWrapper.class); private HttpServletRequest req; private byte[] contentData = null; private HashMap parameters = null; private long bodyBufferingTimeNs = 0; public HttpServletRequestWrapper() { super(groovyTrick()); } private static HttpServletRequest groovyTrick() { //a trick for Groovy throw new IllegalArgumentException("Please use HttpServletRequestWrapper(HttpServletRequest request) constructor!"); } private HttpServletRequestWrapper(HttpServletRequest request, byte[] contentData, HashMap parameters) { super(request); req = request; this.contentData = contentData; this.parameters = parameters; } public HttpServletRequestWrapper(HttpServletRequest request) { super(request); req = request; } /** * Returns the wrapped HttpServletRequest. * Using the getParameterXXX(), getInputStream() or getReader() methods may interfere * with this class operation. * * @return The wrapped HttpServletRequest. */ @Override public HttpServletRequest getRequest() { try { parseRequest(); } catch (IOException e) { throw new IllegalStateException("Cannot parse the request!", e); } return req; } /** * This method is safe to use multiple times. * * @return The request body data. */ public byte[] getContentData() { return contentData; } /** * This method is safe to use multiple times. * Changing the returned map or the array of any of the map's values will not * interfere with this class operation. * * @return The cloned parameters map. */ public HashMap getParameters() { if (parameters == null) return EMPTY_MAP; HashMap map = new HashMap(parameters.size() * 2); for (String key : parameters.keySet()) { map.put(key, parameters.get(key).clone()); } return map; } private void parseRequest() throws IOException { if (parameters != null) return; //already parsed HashMap> mapA = new HashMap>(); List list; Map> query = HTTPRequestUtils.getInstance().getQueryParams(); if (query != null) { for (String key : query.keySet()) { list = query.get(key); mapA.put(key, list); } } if (shouldBufferBody()) { // Read the request body inputstream into a byte array. ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { // Copy all bytes from inputstream to byte array, and record time taken. long bufferStartTime = System.nanoTime(); IOUtils.copy(req.getInputStream(), baos); bodyBufferingTimeNs = System.nanoTime() - bufferStartTime; contentData = baos.toByteArray(); } catch (SocketTimeoutException e) { // This can happen if the request body is smaller than the size specified in the // Content-Length header, and using tomcat APR connector. LOG.error("SocketTimeoutException reading request body from inputstream. error=" + String.valueOf(e.getMessage())); if (contentData == null) { contentData = new byte[0]; } } try { LOG.debug("Length of contentData byte array = " + contentData.length); if (req.getContentLength() != contentData.length) { LOG.warn("Content-length different from byte array length! cl=" + req.getContentLength() + ", array=" + contentData.length); } } catch(Exception e) { LOG.error("Error checking if request body gzipped!", e); } final boolean isPost = req.getMethod().equals("POST"); String contentType = req.getContentType(); final boolean isFormBody = contentType != null && contentType.contains("application/x-www-form-urlencoded"); // only does magic body param parsing for POST form bodies if (isPost && isFormBody) { String enc = req.getCharacterEncoding(); if (enc == null) enc = "UTF-8"; String s = new String(contentData, enc), name, value; StringTokenizer st = new StringTokenizer(s, "&"); int i; boolean decode = req.getContentType() != null; while (st.hasMoreTokens()) { s = st.nextToken(); i = s.indexOf("="); if (i > 0 && s.length() > i + 1) { name = s.substring(0, i); value = s.substring(i + 1); if (decode) { try { name = URLDecoder.decode(name, "UTF-8"); } catch (Exception e) { } try { value = URLDecoder.decode(value, "UTF-8"); } catch (Exception e) { } } list = mapA.get(name); if (list == null) { list = new LinkedList(); mapA.put(name, list); } list.add(value); } } } } HashMap map = new HashMap(mapA.size() * 2); for (String key : mapA.keySet()) { list = mapA.get(key); map.put(key, list.toArray(new String[list.size()])); } parameters = map; } private boolean shouldBufferBody() { if (LOG.isDebugEnabled()) { LOG.debug("Path = " + req.getPathInfo()); LOG.debug("Transfer-Encoding = " + String.valueOf(req.getHeader(ZuulHeaders.TRANSFER_ENCODING))); LOG.debug("Content-Encoding = " + String.valueOf(req.getHeader(ZuulHeaders.CONTENT_ENCODING))); LOG.debug("Content-Length header = " + req.getContentLength()); } boolean should = false; if (req.getContentLength() > 0) { should = true; } else if (req.getContentLength() == -1) { final String transferEncoding = req.getHeader(ZuulHeaders.TRANSFER_ENCODING); if (transferEncoding != null && transferEncoding.equals(ZuulHeaders.CHUNKED)) { RequestContext.getCurrentContext().setChunkedRequestBody(); should = true; } } return should; } /** * Time taken to buffer the request body in nanoseconds. * @return */ public long getBodyBufferingTimeNs() { return bodyBufferingTimeNs; } /** * This method is safe to call multiple times. * Calling it will not interfere with getParameterXXX() or getReader(). * Every time a new ServletInputStream is returned that reads data from the begining. * * @return A new ServletInputStream. */ @Override public ServletInputStream getInputStream() throws IOException { parseRequest(); return new ServletInputStreamWrapper(contentData); } /** * This method is safe to call multiple times. * Calling it will not interfere with getParameterXXX() or getInputStream(). * Every time a new BufferedReader is returned that reads data from the begining. * * @return A new BufferedReader with the wrapped request's character encoding (or UTF-8 if null). */ @Override public BufferedReader getReader() throws IOException { parseRequest(); String enc = req.getCharacterEncoding(); if (enc == null) enc = "UTF-8"; byte[] data = contentData; if (data == null) data = new byte[0]; return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(data), enc)); } /** * This method is safe to execute multiple times. * * @see javax.servlet.ServletRequest#getParameter(java.lang.String) */ @Override public String getParameter(String name) { try { parseRequest(); } catch (IOException e) { throw new IllegalStateException("Cannot parse the request!", e); } if (parameters == null) return null; String[] values = parameters.get(name); if (values == null || values.length == 0) return null; return values[0]; } /** * This method is safe. * * @see {@link #getParameters()} * @see javax.servlet.ServletRequest#getParameterMap() */ @SuppressWarnings("unchecked") @Override public Map getParameterMap() { try { parseRequest(); } catch (IOException e) { throw new IllegalStateException("Cannot parse the request!", e); } return getParameters(); } /** * This method is safe to execute multiple times. * * @see javax.servlet.ServletRequest#getParameterNames() */ @SuppressWarnings("unchecked") @Override public Enumeration getParameterNames() { try { parseRequest(); } catch (IOException e) { throw new IllegalStateException("Cannot parse the request!", e); } return new Enumeration() { private String[] arr = getParameters().keySet().toArray(new String[0]); private int idx = 0; @Override public boolean hasMoreElements() { return idx < arr.length; } @Override public String nextElement() { return arr[idx++]; } }; } /** * This method is safe to execute multiple times. * Changing the returned array will not interfere with this class operation. * * @see javax.servlet.ServletRequest#getParameterValues(java.lang.String) */ @Override public String[] getParameterValues(String name) { try { parseRequest(); } catch (IOException e) { throw new IllegalStateException("Cannot parse the request!", e); } if (parameters == null) return null; String[] arr = parameters.get(name); if (arr == null) return null; return arr.clone(); } public static final class UnitTest { @Mock HttpServletRequest request; @Before public void before() { RequestContext.getCurrentContext().unset(); MockitoAnnotations.initMocks(this); RequestContext.getCurrentContext().setRequest(request); method("GET"); contentType("zuul/test-content-type"); } private void body(byte[] body) throws IOException { when(request.getInputStream()).thenReturn(new ServletInputStreamWrapper(body)); when(request.getContentLength()).thenReturn(body.length); } private void method(String s) { when(request.getMethod()).thenReturn(s); } private void contentType(String s) { when(request.getContentType()).thenReturn(s); } private static String readZipInputStream(InputStream input) throws IOException { byte[] uploadedBytes = getBytesFromInputStream(input); input.close(); /* try to read it as a zip file */ String uploadFileTxt = null; ZipInputStream zInput = new ZipInputStream(new ByteArrayInputStream(uploadedBytes)); ZipEntry zipEntry = zInput.getNextEntry(); if (zipEntry != null) { // we have a ZipEntry, so this is a zip file while (zipEntry != null) { byte[] fileBytes = getBytesFromInputStream(zInput); uploadFileTxt = new String(fileBytes); zipEntry = zInput.getNextEntry(); } } return uploadFileTxt; } private static byte[] getBytesFromInputStream(InputStream input) throws IOException { int v = 0; ByteArrayOutputStream bos = new ByteArrayOutputStream(); while ((v = input.read()) != -1) { bos.write(v); } bos.close(); return bos.toByteArray(); } @Test public void handlesDuplicateParams() { when(request.getQueryString()).thenReturn("path=one&key1=val1&path=two"); final HttpServletRequestWrapper w = new HttpServletRequestWrapper(request); // getParameters doesn't call parseRequest internally, not sure why // so I'm forcing it here w.getParameterMap(); final Map params = w.getParameters(); assertFalse("params should not be empty", params.isEmpty()); final String[] paths = params.get("path"); assertTrue("paths param should not be empty", paths.length > 0); assertEquals("one", paths[0]); assertEquals("two", paths[1]); } @Test public void handlesPlainRequestBody() throws IOException { final String body = "hello"; body(body.getBytes()); final HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request); assertEquals(body, IOUtils.toString(wrapper.getInputStream())); } @Test public void handlesGzipRequestBody() throws IOException { // creates string, gzips into byte array which will be mocked as InputStream of request final String body = "hello"; final byte[] bodyBytes = body.getBytes(); // in this case the compressed stream is actually larger - need to allocate enough space final ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(0); final GZIPOutputStream gzipOutStream = new GZIPOutputStream(byteOutStream); gzipOutStream.write(bodyBytes); gzipOutStream.finish(); gzipOutStream.flush(); body(byteOutStream.toByteArray()); final HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request); assertEquals(body, IOUtils.toString(new GZIPInputStream(wrapper.getInputStream()))); } @Test public void handlesZipRequestBody() throws IOException { final String body = "hello"; final byte[] bodyBytes = body.getBytes(); final ByteArrayOutputStream byteOutStream = new ByteArrayOutputStream(0); ZipOutputStream zOutput = new ZipOutputStream(byteOutStream); zOutput.putNextEntry(new ZipEntry("f1")); zOutput.write(bodyBytes); zOutput.finish(); zOutput.flush(); body(byteOutStream.toByteArray()); final HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request); assertEquals(body, readZipInputStream(wrapper.getInputStream())); } @Test public void parsesParamsFromFormBody() throws Exception { method("POST"); body("one=1&two=2".getBytes()); contentType("application/x-www-form-urlencoded"); final HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request); final Map params = wrapper.getParameterMap(); assertTrue(params.containsKey("one")); assertTrue(params.containsKey("two")); } @Test public void ignoresParamsInBodyForNonPosts() throws Exception { method("PUT"); body("one=1&two=2".getBytes()); contentType("application/x-www-form-urlencoded"); final HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request); final Map params = wrapper.getParameterMap(); assertFalse(params.containsKey("one")); } @Test public void ignoresParamsInBodyForNonForms() throws Exception { method("POST"); body("one=1&two=2".getBytes()); contentType("application/json"); final HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request); final Map params = wrapper.getParameterMap(); assertFalse(params.containsKey("one")); } @Test public void handlesPostsWithNoContentTypeHeader() throws Exception { method("POST"); body("one=1&two=2".getBytes()); contentType(null); final HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request); final Map params = wrapper.getParameterMap(); assertFalse(params.containsKey("one")); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy