/* Copyright 2014 Groupon, 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.groupon.odo; import com.groupon.odo.plugin.PluginHelper; import com.groupon.odo.proxylib.Constants; import com.groupon.odo.proxylib.models.History; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HttpMethod; import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; import org.apache.commons.httpclient.methods.EntityEnclosingMethod; import org.apache.commons.httpclient.methods.InputStreamRequestEntity; import org.apache.commons.httpclient.methods.RequestEntity; import org.apache.commons.io.IOUtils; import org.msgpack.core.MessagePack; import org.msgpack.core.MessageUnpacker; import org.msgpack.value.ImmutableValue; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class HttpUtilities { private static final Logger logger = LoggerFactory.getLogger(HttpUtilities.class); /** * Key for content type header. */ public static final String STRING_CONTENT_TYPE_HEADER_NAME = "Content-Type"; /** * Transfer Encoding header value */ public static final String STRING_TRANSFER_ENCODING = "Transfer-Encoding"; /** * MessagePack content type value */ public static final String STRING_CONTENT_TYPE_MESSAGEPACK = "binary/messagepack"; /** * Connection header value */ public static final String STRING_CONNECTION = "Connection"; /** * Chunked value */ public static final String STRING_CHUNKED = "chunked"; /** * Application JSON content type value */ public static final String STRING_CONTENT_TYPE_JSON = "application/json"; /** * Form encoded content type value */ public static final String STRING_CONTENT_TYPE_FORM_URLENCODED = "application/x-www-form-urlencoded"; /** * @param url full url containing hostname * @return hostname */ public static String getHostNameFromURL(String url) { int urlLeftPos = url.indexOf("//"); String hostName = url.substring(urlLeftPos + 2); int urlRightPos = hostName.indexOf("/"); if (urlRightPos != -1) { hostName = hostName.substring(0, urlRightPos); } // now look for a port int portPos = hostName.indexOf(":"); if (portPos != -1) { hostName = hostName.substring(0, portPos); } return hostName; } public static int getPortFromURL(String url) { int urlLeftPos = url.indexOf("//"); Boolean isHttps = url.startsWith("https"); // set port defaults int port = 80; if (isHttps) { port = 443; } String portStr = null; String hostName = url.substring(urlLeftPos + 2); int urlRightPos = hostName.indexOf("/"); if (urlRightPos != -1) { hostName = hostName.substring(0, urlRightPos); } // now look for a port int portPos = hostName.indexOf(":"); if (portPos != -1) { portStr = hostName.substring(portPos + 1, urlRightPos); } if (portStr != null) { port = Integer.parseInt(portStr); } return port; } public static String removePortFromHostHeaderString(String host) { String hostName = host; int portPos = host.indexOf(":"); if (portPos != -1) { hostName = host.substring(0, portPos); } return hostName; } /** * Obtain collection of Parameters from request * * @param dataArray request parameters * @return Map of parameters * @throws Exception exception */ public static Map<String, String[]> mapUrlEncodedParameters(byte[] dataArray) throws Exception { Map<String, String[]> mapPostParameters = new HashMap<String, String[]>(); try { ByteArrayOutputStream byteout = new ByteArrayOutputStream(); for (int x = 0; x < dataArray.length; x++) { // split the data up by & to get the parts if (dataArray[x] == '&' || x == (dataArray.length - 1)) { if (x == (dataArray.length - 1)) { byteout.write(dataArray[x]); } // find '=' and split the data up into key value pairs int equalsPos = -1; ByteArrayOutputStream key = new ByteArrayOutputStream(); ByteArrayOutputStream value = new ByteArrayOutputStream(); byte[] byteArray = byteout.toByteArray(); for (int xx = 0; xx < byteArray.length; xx++) { if (byteArray[xx] == '=') { equalsPos = xx; } else { if (equalsPos == -1) { key.write(byteArray[xx]); } else { value.write(byteArray[xx]); } } } ArrayList<String> values = new ArrayList<String>(); if (mapPostParameters.containsKey(key.toString())) { values = new ArrayList<String>(Arrays.asList(mapPostParameters.get(key.toString()))); mapPostParameters.remove(key.toString()); } values.add(value.toString()); /** * If equalsPos is not -1, then there was a '=' for the key * If value.size is 0, then there is no value so want to add in the '=' * Since it will not be added later like params with keys and valued */ if (equalsPos != -1 && value.size() == 0) { key.write((byte) '='); } mapPostParameters.put(key.toString(), values.toArray(new String[values.size()])); byteout = new ByteArrayOutputStream(); } else { byteout.write(dataArray[x]); } } } catch (Exception e) { throw new Exception("Could not parse request data: " + e.getMessage()); } return mapPostParameters; } public static HttpServletResponse addHeader(HttpServletResponse response, Object[] headerPair) { // set header response.setHeader(headerPair[0].toString(), headerPair[1].toString()); return response; } /***** * Pretty formatting methods for storing History */ /** * Retrieve URL without parameters * * @param sourceURI source URI * @return URL without parameters */ public static String getURL(String sourceURI) { String retval = sourceURI; int qPos = sourceURI.indexOf("?"); if (qPos != -1) { retval = retval.substring(0, qPos); } return retval; } /** * Obtain newline-delimited headers from method * * @param method HttpMethod to scan * @return newline-delimited headers */ public static String getHeaders(HttpMethod method) { String headerString = ""; Header[] headers = method.getRequestHeaders(); for (Header header : headers) { String name = header.getName(); if (name.equals(Constants.ODO_PROXY_HEADER)) { // skip.. don't want to log this continue; } if (headerString.length() != 0) { headerString += "\n"; } headerString += header.getName() + ": " + header.getValue(); } return headerString; } /** * Obtain newline-delimited headers from request * * @param request HttpServletRequest to scan * @return newline-delimited headers */ public static String getHeaders(HttpServletRequest request) { String headerString = ""; Enumeration<String> headerNames = request.getHeaderNames(); while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); if (name.equals(Constants.ODO_PROXY_HEADER)) { // skip.. don't want to log this continue; } if (headerString.length() != 0) { headerString += "\n"; } headerString += name + ": " + request.getHeader(name); } return headerString; } /** * Obtain newline-delimited headers from response * * @param response HttpServletResponse to scan * @return newline-delimited headers */ public static String getHeaders(HttpServletResponse response) { String headerString = ""; Collection<String> headerNames = response.getHeaderNames(); for (String headerName : headerNames) { // there may be multiple headers per header name for (String headerValue : response.getHeaders(headerName)) { if (headerString.length() != 0) { headerString += "\n"; } headerString += headerName + ": " + headerValue; } } return headerString; } /** * Obtain parameters from query * * @param query query to scan * @return Map of parameters */ public static HashMap<String, String> getParameters(String query) { HashMap<String, String> params = new HashMap<String, String>(); if (query == null || query.length() == 0) { return params; } String[] splitQuery = query.split("&"); for (String splitItem : splitQuery) { String[] items = splitItem.split("="); if (items.length == 1) { params.put(items[0], ""); } else { params.put(items[0], items[1]); } } return params; } /** * Sets up the given {@link org.apache.commons.httpclient.methods.PostMethod} to send the same multipart POST data * as was sent in the given {@link HttpServletRequest} * * @param postMethodProxyRequest The {@link org.apache.commons.httpclient.methods.PostMethod} that we are configuring to send a * multipart POST request * @param httpServletRequest The {@link HttpServletRequest} that contains the multipart * POST data to be sent via the {@link org.apache.commons.httpclient.methods.PostMethod} */ @SuppressWarnings("unchecked") public static void handleMultipartPost( EntityEnclosingMethod postMethodProxyRequest, HttpServletRequest httpServletRequest, DiskFileItemFactory diskFileItemFactory) throws ServletException { // TODO: this function doesn't set any history data try { // just pass back the binary data InputStreamRequestEntity ire = new InputStreamRequestEntity(httpServletRequest.getInputStream()); postMethodProxyRequest.setRequestEntity(ire); postMethodProxyRequest.setRequestHeader(STRING_CONTENT_TYPE_HEADER_NAME, httpServletRequest.getHeader(STRING_CONTENT_TYPE_HEADER_NAME)); } catch (Exception e) { throw new ServletException(e); } } /** * Sets up the given {@link org.apache.commons.httpclient.methods.PostMethod} to send the same standard POST data * as was sent in the given {@link HttpServletRequest} * * @param methodProxyRequest The {@link org.apache.commons.httpclient.methods.PostMethod} that we are configuring to send a * standard POST request * @param httpServletRequest The {@link HttpServletRequest} that contains the POST data to * be sent via the {@link org.apache.commons.httpclient.methods.PostMethod} * @param history The {@link com.groupon.odo.proxylib.models.History} log for this request */ @SuppressWarnings("unchecked") public static void handleStandardPost(EntityEnclosingMethod methodProxyRequest, HttpServletRequest httpServletRequest, History history) throws Exception { String deserialisedMessages = ""; byte[] requestByteArray = null; // Create a new StringBuffer with the data to be passed StringBuilder requestBody = new StringBuilder(); InputStream body = httpServletRequest.getInputStream(); RequestEntity requestEntity = null; if (httpServletRequest.getContentType() != null && httpServletRequest.getContentType().contains(STRING_CONTENT_TYPE_FORM_URLENCODED) && httpServletRequest.getHeader("content-encoding") == null) { requestByteArray = IOUtils.toByteArray(body); history.setRawPostData(requestByteArray); // this is binary.. just return it as is requestEntity = new ByteArrayRequestEntity(requestByteArray); // Get the client POST data as a Map if content type is: application/x-www-form-urlencoded // We do this manually since some data is not properly parseable by the servlet request Map<String, String[]> mapPostParameters = HttpUtilities.mapUrlEncodedParameters(requestByteArray); // Iterate the parameter names for (String stringParameterName : mapPostParameters.keySet()) { // Iterate the values for each parameter name String[] stringArrayParameterValues = mapPostParameters .get(stringParameterName); for (String stringParameterValue : stringArrayParameterValues) { // Create a NameValuePair and store in list // add an & if there is already data if (requestBody.length() > 0) { requestBody.append("&"); } requestBody.append(stringParameterName); // not everything has a value so lets check if (stringParameterValue.length() > 0) { requestBody.append("="); requestBody.append(stringParameterValue); } } } /** * Process the post data string so it can be added to history * Separates individual params out and applies post data override as applicable */ Proxy.QueryInformation queryInformation = Proxy.processPostDataString(requestBody.toString()); // Set request body which is added to history requestBody = new StringBuilder(queryInformation.queryString); // Rewrite the post data if it is modified if (queryInformation.modified) { String postData = queryInformation.queryString; requestBody = new StringBuilder(postData); requestByteArray = postData.getBytes(); requestEntity = new ByteArrayRequestEntity(requestByteArray); } } else if (httpServletRequest.getContentType() != null && httpServletRequest.getContentType().contains(STRING_CONTENT_TYPE_MESSAGEPACK)) { /** * Convert input stream to bytes for it to be read by the deserializer * Unpack and iterate the list to see the contents */ requestByteArray = IOUtils.toByteArray(body); history.setRawPostData(requestByteArray); requestEntity = new ByteArrayRequestEntity(requestByteArray); ByteArrayInputStream byteArrayIS = new ByteArrayInputStream(requestByteArray); MessageUnpacker unpacker = MessagePack.newDefaultUnpacker(byteArrayIS); while (unpacker.hasNext()) { ImmutableValue message = unpacker.unpackValue(); deserialisedMessages += message; deserialisedMessages += "\n"; } history.setRequestBodyDecoded(true); requestBody = new StringBuilder(Proxy.processPostDataString(requestBody.toString()).queryString); } else { requestByteArray = IOUtils.toByteArray(body); history.setRawPostData(requestByteArray); // this is binary.. just return it as is requestEntity = new ByteArrayRequestEntity(requestByteArray); // decode this for history if it is encoded String requestBodyString = PluginHelper.getByteArrayDataAsString(httpServletRequest.getHeader("content-encoding"), requestByteArray); requestBody.append(requestBodyString); // mark in history if the body has been decoded if (!requestBodyString.equals(new String(requestByteArray))) { history.setRequestBodyDecoded(true); } requestBody = new StringBuilder(Proxy.processPostDataString(requestBody.toString()).queryString); } // set post body in history object history.setRequestPostData(requestBody.toString()); // set post body in proxy request object methodProxyRequest.setRequestEntity(requestEntity); /** * Set the history to have decoded messagepack. Pass the byte data back to request */ if (httpServletRequest.getContentType() != null && httpServletRequest.getContentType().contains(STRING_CONTENT_TYPE_MESSAGEPACK)) { history.setRequestPostData(deserialisedMessages); ByteArrayRequestEntity byteRequestEntity = new ByteArrayRequestEntity(requestByteArray); methodProxyRequest.setRequestEntity(byteRequestEntity); } } }