package org.ovirt.engine.core.utils.servlet;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import javax.activation.MimetypesFileTypeMap;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ServletUtils {
// The log:
private static final Logger log = LoggerFactory.getLogger(ServletUtils.class);
// Map of MIME types:
private static MimetypesFileTypeMap mimeMap;
// Anything longer than this is considered a large file and a warning
// will be generating when serving it:
private static final long LARGE = 1048576; // 1 MiB
static {
// Load the system wide MIME types map:
try {
mimeMap = new MimetypesFileTypeMap(System.getProperty("org.ovirt.engine.mime.types", "/etc/mime.types"));
}
catch (IOException exception) {
log.error("Can't load system mime types file.", exception);
mimeMap = new MimetypesFileTypeMap();
}
}
// The max size of path names (this is less than supported in Linux, but we
// don't use paths larger than this, and this way we are a bit safer):
private static final long PATH_MAX = 512;
public static final String CONTEXT_TO_ROOT_MODIFIER = "contextToRootModifier";
private ServletUtils() {
// No instances allowed.
}
public static MimetypesFileTypeMap getMimeMap() {
return mimeMap;
}
/**
* Contruct ETag to file.
* @param file File.
* @return ETag.
* Here to allow UT.
*/
protected static String getETag(File file) {
return String.format(
"W/\"%s-%s\"",
file.length(),
file.lastModified()
);
}
/**
* Send a file to the output stream of the response passed into the method.
* @param request The {@code HttpServletRequest} so we can get the path of the file.
* @param response The {@code HttpServletResponse} so we can get the output stream and set response headers.
* @param file The {@code File} to write to the response output stream.
* @param type The MIME type of the file.
*/
public static void sendFile(final HttpServletRequest request, final HttpServletResponse response, final File file, final String defaultType) throws IOException {
sendFile(request, response, file, defaultType, true);
}
public static void sendFile(final HttpServletRequest request, final HttpServletResponse response, final File file, final String defaultType, boolean cache) throws IOException {
sendFile(request, response, file, defaultType, cache, true);
}
public static void sendFile(final HttpServletRequest request,
final HttpServletResponse response,
final File file,
final String defaultType,
boolean cache,
boolean required) throws IOException {
// Make sure the file exits and is readable and send a 404 error
// response if it doesn't:
if (!canReadFile(file)) {
if (required) {
log.error("Can't read file '{}' for request '{}', will send a 404 error response.",
file != null ? file.getAbsolutePath() : "",
request.getRequestURI());
}
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
else {
boolean send = true;
if (cache) {
String eTag = getETag(file);
// Always include ETag on response
response.setHeader("ETag", eTag);
String IfNoneMatch = request.getHeader("If-None-Match");
if ("*".equals(IfNoneMatch)) {
response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
send = false;
}
else if (eTag.equals(IfNoneMatch)) {
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
send = false;
}
}
if (send) {
// Send metadata
String mime = defaultType;
if (mime == null) {
mime = getMimeMap().getContentType(file);
}
response.setContentType(mime);
response.setContentLength((int) getFileSize(file));
// Send content
writeFileToStream(response.getOutputStream(), file);
}
}
}
/**
* Check if the file is readable.
*/
public static boolean canReadFile(final File file) {
return file != null && file.exists() && file.canRead() && !file.isDirectory();
}
/**
* Write the file passed in out to the output stream passed in.
* @param out The {@code OutputStream} to write to.
* @param file The {@code File} to read.
* @throws IOException If there is a problem reading the file or writing to the stream.
*/
public static void writeFileToStream(final OutputStream out, final File file) throws IOException {
// Send the content of the file
try (InputStream in = new FileInputStream(file)) {
byte[] buffer = new byte[4096];
int count = 0;
while ((count = in.read(buffer)) != -1) {
out.write(buffer, 0, count);
}
}
catch (IOException exception) {
final String message = "Error sending file '" + file.getAbsolutePath() + "'.";
log.error(message, exception);
throw new IOException(message, exception);
}
}
/**
* Returns the size of the {@code File} passed in.
* @param file The file to get the length for.
* @return The length of the file as a {@code long}
*/
public static long getFileSize(File file) {
// Advice against large files:
final long length = file.length();
if (length > LARGE) {
log.warn("File '{}' is {} bytes long. Please reconsider using this servlet for files larger than {} bytes.",
file.getAbsolutePath(),
length,
LARGE);
}
return length;
}
/**
* Check if the path passed in is sane. This method does the following checks:
* <ol>
* <li>Checks if the path length is longer than the maximum allowed</li>
* <li>Checks if the path contains potentially dangerous characters (.., //, ./)</li>
* </ol>
* @param path The path to check.
* @return {@code true} if the path is sane, {@code false} otherwise.
*/
public static boolean isSane(String path) {
// Check that the path is not too long:
final int length = path.length();
if (length > PATH_MAX) {
log.error("The path '{}' is {} characters long, which is longer than the maximum allowed {}.",
path,
length,
PATH_MAX);
return false;
}
// Check that there aren't potentially dangerous directory navigation sequences:
if (path.contains("..") || path.contains("//") || path.contains("./")) {
log.error("The path contains potentially dangerous directory navigation sequences.");
return false;
}
// All checks passed, the path is sane:
return true;
}
/**
* Get a {@code File} object from the passed in path and base location. This
* method will do a sanity check to make sure the passed in path is not try
* to read file it should. see isSane for all checks that are done.
* @param path The path to check for the file.
* @param base The base path.
* @return A {@code File} object pointing to the file, or null if the file
* cannot be found.
* @see #isSane
*/
public static File makeFileFromSanePath(String path, File base) {
File file = null;
if (path == null) {
file = base;
}
else if (!isSane(path)) {
log.error("The path '{}' is not sane, will return null.", path);
}
else {
file = new File(base, path);
}
return file;
}
public static String getBaseContextPath(final HttpServletRequest request) {
return getAsAbsoluteContext(request.getContextPath(),
request.getSession().getServletContext().getInitParameter(CONTEXT_TO_ROOT_MODIFIER));
}
/**
* Calculate the absolute path based on context path combined with the relative path passed in.
* The relative path is allowed to contain . and ..
* @param servletContextPath The context path of the context containing this servlet.
* @param relativePath The path relative to the context path, is allowed to start with / at which point it is
* an absolute path and the result of this method.
* @return The calculated absolute path.
*/
public static String getAsAbsoluteContext(String servletContextPath, String relativePath) {
String result = null;
if (relativePath != null && relativePath.startsWith("/")) { //$NON-NLS-1$
result = relativePath;
} else if (relativePath != null) {
result = URI.create(servletContextPath + "/" + relativePath).normalize().toString(); //$NON-NLS-1$
}
return result;
}
}