/*
* Copyright 2015 herd contributors
*
* 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.finra.herd.ui;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.regex.Pattern;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.WebUtils;
/**
* A servlet filter that logs incoming HTTP requests. This approach is similar to the Spring CommonsRequestLoggingFilter, but is customized to ensure that the
* full request body is always read and logged. In addition, this filter only has the concept of "before" request logging.
*/
public class RequestLoggingFilter extends OncePerRequestFilter
{
private static final Logger LOGGER = LoggerFactory.getLogger(RequestLoggingFilter.class);
public static final String DEFAULT_LOG_MESSAGE_PREFIX = "HTTP Request [";
public static final String DEFAULT_LOG_MESSAGE_SUFFIX = "]";
private static final Integer DEFAULT_MAX_PAYLOAD_LENGTH = null; // Default to unlimited.
private boolean includeQueryString = true;
private boolean includeClientInfo = true;
private boolean includePayload = true;
private Integer maxPayloadLength = DEFAULT_MAX_PAYLOAD_LENGTH;
private String logMessagePrefix = DEFAULT_LOG_MESSAGE_PREFIX;
private String logMessageSuffix = DEFAULT_LOG_MESSAGE_SUFFIX;
/**
* Set whether or not the query string should be included in the log message.
*/
public void setIncludeQueryString(boolean includeQueryString)
{
this.includeQueryString = includeQueryString;
}
/**
* Return whether or not the query string should be included in the log message.
*/
protected boolean isIncludeQueryString()
{
return this.includeQueryString;
}
/**
* Set whether or not the client address and session id should be included in the log message.
*/
public void setIncludeClientInfo(boolean includeClientInfo)
{
this.includeClientInfo = includeClientInfo;
}
/**
* Return whether or not the client address and session id should be included in the log message.
*/
protected boolean isIncludeClientInfo()
{
return this.includeClientInfo;
}
/**
* Set whether or not the request payload (body) should be included in the log message.
*/
public void setIncludePayload(boolean includePayload)
{
this.includePayload = includePayload;
}
/**
* Return whether or not the request payload (body) should be included in the log message.
*/
protected boolean isIncludePayload()
{
return includePayload;
}
/**
* Sets the maximum length of the payload body to be included in the log message. Default (i.e. null) is unlimited characters.
*/
public void setMaxPayloadLength(Integer maxPayloadLength)
{
this.maxPayloadLength = maxPayloadLength;
}
/**
* Return the maximum length of the payload body to be included in the log message.
*/
protected Integer getMaxPayloadLength()
{
return maxPayloadLength;
}
/**
* Set the value that should be prepended to the log message.
*/
public void setLogMessagePrefix(String logMessagePrefix)
{
this.logMessagePrefix = logMessagePrefix;
}
/**
* Set the value that should be appended to the log message.
*/
public void setLogMessageSuffix(String logMessageSuffix)
{
this.logMessageSuffix = logMessageSuffix;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException
{
HttpServletRequest requestLocal = request;
// Determine if this is the first request or not. We only want to wrap the request to log on the first request.
boolean isFirstRequest = !isAsyncDispatch(requestLocal);
if (isFirstRequest)
{
requestLocal = new RequestLoggingFilterWrapper(requestLocal);
((RequestLoggingFilterWrapper) requestLocal).logRequest(request);
}
// Move onto the next filter while wrapping the request with our own custom logging class.
filterChain.doFilter(requestLocal, response);
}
/**
* A request wrapper that logs incoming requests.
*/
public class RequestLoggingFilterWrapper extends HttpServletRequestWrapper
{
private byte[] payload = null;
private BufferedReader reader;
/**
* Constructs a request logging filter wrapper.
*
* @param request the request to wrap.
*
* @throws IOException if any problems were encountered while reading from the stream.
*/
public RequestLoggingFilterWrapper(HttpServletRequest request) throws IOException
{
// Perform super class processing.
super(request);
// Only grab the payload if debugging is enabled. Otherwise, we'd always be pre-reading the entire payload for no reason which cause a slight
// performance degradation for no reason.
if (LOGGER.isDebugEnabled())
{
// Read the original payload into the payload variable.
InputStream inputStream = null;
try
{
// Get the input stream.
inputStream = request.getInputStream();
if (inputStream != null)
{
// Read the payload from the input stream.
payload = IOUtils.toByteArray(request.getInputStream());
}
}
finally
{
if (inputStream != null)
{
try
{
inputStream.close();
}
catch (IOException iox)
{
LOGGER.warn("Unable to close request input stream.", iox);
}
}
}
}
}
/**
* Log the request message.
*
* @param request the request.
*/
public void logRequest(HttpServletRequest request)
{
StringBuilder message = new StringBuilder();
// Append the log message prefix.
message.append(logMessagePrefix);
// Append the URI.
message.append("uri=").append(request.getRequestURI());
// Append the query string if present.
if (isIncludeQueryString() && StringUtils.hasText(request.getQueryString()))
{
message.append('?').append(request.getQueryString());
}
// Append the HTTP method.
message.append(";method=").append(request.getMethod());
// Append the client information.
if (isIncludeClientInfo())
{
// The client remote address.
String client = request.getRemoteAddr();
if (StringUtils.hasLength(client))
{
message.append(";client=").append(client);
}
// The HTTP session information.
HttpSession session = request.getSession(false);
if (session != null)
{
message.append(";session=").append(session.getId());
}
// The remote user information.
String user = request.getRemoteUser();
if (user != null)
{
message.append(";user=").append(user);
}
}
// Get the request payload.
String payloadString = "";
try
{
if (payload != null && payload.length > 0)
{
payloadString = new String(payload, 0, payload.length, getCharacterEncoding());
}
}
catch (UnsupportedEncodingException e)
{
payloadString = "[Unknown]";
}
// Append the request payload if present.
if (isIncludePayload() && StringUtils.hasLength(payloadString))
{
String sanitizedPayloadString = payloadString;
/*
* TODO Remove this logic once proper way of securing sensitive data is implemented.
* Replaces the payload if it contains the word "password" for requests to jobDefinitions and jobs.
*/
if (request.getRequestURI().endsWith("/jobDefinitions") || request.getRequestURI().endsWith("/jobs") ||
request.getRequestURI().endsWith("/jobs/signal"))
{
Pattern pattern = Pattern.compile("password", Pattern.CASE_INSENSITIVE);
if (pattern.matcher(payloadString).find())
{
sanitizedPayloadString = "<hidden because it may contain sensitive information>";
}
}
/*
* Limit logged payload length if max length is set
*/
else if (getMaxPayloadLength() != null)
{
sanitizedPayloadString = payloadString.substring(0, Math.min(payloadString.length(), getMaxPayloadLength()));
}
message.append(";payload=").append(sanitizedPayloadString);
}
// Append the log message suffix.
message.append(logMessageSuffix);
// Log the actual message.
LOGGER.debug(message.toString());
}
@Override
public ServletInputStream getInputStream() throws IOException
{
if (payload == null)
{
// If no payload is present (i.e. debug logging isn't enabled), then perform the standard super class functionality.
return super.getInputStream();
}
else
{
return new ServletInputStream()
{
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload);
public int read() throws IOException
{
// Read bytes from the previously read payload.
return byteArrayInputStream.read();
}
};
}
}
@Override
public int getContentLength()
{
if (payload == null)
{
return super.getContentLength();
}
else
{
return payload.length;
}
}
@Override
public String getCharacterEncoding()
{
String enc = super.getCharacterEncoding();
return (enc != null ? enc : WebUtils.DEFAULT_CHARACTER_ENCODING);
}
@Override
public BufferedReader getReader() throws IOException
{
if (payload == null)
{
return super.getReader();
}
else
{
if (reader == null)
{
this.reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(payload), getCharacterEncoding()));
}
return reader;
}
}
}
}