/*
* Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net>
* Distributed under the terms of either:
* - the common development and distribution license (CDDL), v1.0; or
* - the GNU Lesser General Public License, v2.1 or later
*/
package winstone;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringWriter;
import java.io.Writer;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Servlet to handle static resources. Simply finds and sends them, or
* dispatches to the error servlet.
*
* @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a>
* @version $Id: StaticResourceServlet.java,v 1.17 2004/12/31 07:21:00
* rickknowles Exp $
*/
public class StaticResourceServlet extends HttpServlet {
// final String JSP_FILE = "org.apache.catalina.jsp_file";
final static String FORWARD_SERVLET_PATH = "javax.servlet.forward.servlet_path";
final static String INCLUDE_SERVLET_PATH = "javax.servlet.include.servlet_path";
final static String CACHED_RESOURCE_DATE_HEADER = "If-Modified-Since";
final static String LAST_MODIFIED_DATE_HEADER = "Last-Modified";
final static String RANGE_HEADER = "Range";
final static String ACCEPT_RANGES_HEADER = "Accept-Ranges";
final static String CONTENT_RANGE_HEADER = "Content-Range";
final static String RESOURCE_FILE = "winstone.LocalStrings";
private DateFormat sdfFileDate = new SimpleDateFormat("dd-MM-yyyy HH:mm");
private File webRoot;
private String prefix;
private boolean directoryList;
public void init(ServletConfig config) throws ServletException {
super.init(config);
this.webRoot = new File(config.getInitParameter("webRoot"));
this.prefix = config.getInitParameter("prefix");
String dirList = config.getInitParameter("directoryList");
this.directoryList = (dirList == null)
|| dirList.equalsIgnoreCase("true")
|| dirList.equalsIgnoreCase("yes");
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
boolean isInclude = (request.getAttribute(INCLUDE_SERVLET_PATH) != null);
boolean isForward = (request.getAttribute(FORWARD_SERVLET_PATH) != null);
String path = null;
if (isInclude)
path = (String) request.getAttribute(INCLUDE_SERVLET_PATH);
else {
path = request.getServletPath();
}
// URL decode path
path = WinstoneRequest.decodeURLToken(path);
long cachedResDate = request.getDateHeader(CACHED_RESOURCE_DATE_HEADER);
Logger.log(Logger.DEBUG, Launcher.RESOURCES,
"StaticResourceServlet.PathRequested", new String[] {
getServletConfig().getServletName(), path });
// Check for the resource
File res = path.equals("") ? this.webRoot : new File(
this.webRoot, path);
// Send a 404 if not found
if (!res.exists())
response.sendError(HttpServletResponse.SC_NOT_FOUND, Launcher.RESOURCES
.getString("StaticResourceServlet.PathNotFound", path));
// Check we are below the webroot
else if (!isDescendant(this.webRoot, res, this.webRoot)) {
Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES, "StaticResourceServlet.OutsideWebroot",
new String[] {res.getCanonicalPath(), this.webRoot.toString()});
response.sendError(HttpServletResponse.SC_FORBIDDEN, Launcher.RESOURCES
.getString("StaticResourceServlet.PathInvalid", path));
}
// Check we are not below the web-inf
else if (!isInclude && !isForward && isDescendant(new File(this.webRoot, "WEB-INF"), res, this.webRoot))
response.sendError(HttpServletResponse.SC_NOT_FOUND, Launcher.RESOURCES
.getString("StaticResourceServlet.PathInvalid", path));
// Check we are not below the meta-inf
else if (!isInclude && !isForward && isDescendant(new File(this.webRoot, "META-INF"), res, this.webRoot))
response.sendError(HttpServletResponse.SC_NOT_FOUND, Launcher.RESOURCES
.getString("StaticResourceServlet.PathInvalid", path));
// check for the directory case
else if (res.isDirectory()) {
if (path.endsWith("/")) {
// Try to match each of the welcome files
// String matchedWelcome = matchWelcomeFiles(path, res);
// if (matchedWelcome != null)
// response.sendRedirect(this.prefix + path + matchedWelcome);
// else
if (this.directoryList)
generateDirectoryList(request, response, path);
else
response.sendError(HttpServletResponse.SC_FORBIDDEN,
Launcher.RESOURCES.getString("StaticResourceServlet.AccessDenied"));
} else
response.sendRedirect(this.prefix + path + "/");
}
// Send a 304 if not modified
else if (!isInclude && (cachedResDate != -1)
&& (cachedResDate < (System.currentTimeMillis() / 1000L * 1000L))
&& (cachedResDate >= (res.lastModified() / 1000L * 1000L))) {
String mimeType = getServletContext().getMimeType(
res.getName().toLowerCase());
if (mimeType != null)
response.setContentType(mimeType);
response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
response.setContentLength(0);
response.flushBuffer();
}
// Write out the resource if not range or is included
else if ((request.getHeader(RANGE_HEADER) == null) || isInclude) {
String mimeType = getServletContext().getMimeType(
res.getName().toLowerCase());
if (mimeType != null)
response.setContentType(mimeType);
InputStream resStream = new FileInputStream(res);
response.setStatus(HttpServletResponse.SC_OK);
response.setContentLength((int) res.length());
// response.addHeader(ACCEPT_RANGES_HEADER, "bytes");
response.addDateHeader(LAST_MODIFIED_DATE_HEADER, res.lastModified());
OutputStream out = null;
Writer outWriter = null;
try {
out = response.getOutputStream();
} catch (IllegalStateException err) {
outWriter = response.getWriter();
} catch (IllegalArgumentException err) {
outWriter = response.getWriter();
}
byte buffer[] = new byte[4096];
int read = resStream.read(buffer);
while (read > 0) {
if (out != null) {
out.write(buffer, 0, read);
} else {
outWriter.write(new String(buffer, 0, read,
response.getCharacterEncoding()));
}
read = resStream.read(buffer);
}
resStream.close();
} else if (request.getHeader(RANGE_HEADER).startsWith("bytes=")) {
String mimeType = getServletContext().getMimeType(
res.getName().toLowerCase());
if (mimeType != null)
response.setContentType(mimeType);
InputStream resStream = new FileInputStream(res);
List ranges = new ArrayList();
StringTokenizer st = new StringTokenizer(request.getHeader(
RANGE_HEADER).substring(6).trim(), ",", false);
int totalSent = 0;
String rangeText = "";
while (st.hasMoreTokens()) {
String rangeBlock = st.nextToken();
int start = 0;
int end = (int) res.length();
int delim = rangeBlock.indexOf('-');
if (delim != 0)
start = Integer.parseInt(rangeBlock.substring(0, delim)
.trim());
if (delim != rangeBlock.length() - 1)
end = Integer.parseInt(rangeBlock.substring(delim + 1)
.trim());
totalSent += (end - start);
rangeText += "," + start + "-" + end;
ranges.add(start + "-" + end);
}
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.addHeader(CONTENT_RANGE_HEADER, "bytes "
+ rangeText.substring(1) + "/" + res.length());
response.setContentLength(totalSent);
response.addHeader(ACCEPT_RANGES_HEADER, "bytes");
response.addDateHeader(LAST_MODIFIED_DATE_HEADER, res
.lastModified());
OutputStream out = response.getOutputStream();
int bytesRead = 0;
for (Iterator i = ranges.iterator(); i.hasNext();) {
String rangeBlock = (String) i.next();
int delim = rangeBlock.indexOf('-');
int start = Integer.parseInt(rangeBlock.substring(0, delim));
int end = Integer.parseInt(rangeBlock.substring(delim + 1));
int read = 0;
while ((read != -1) && (bytesRead <= res.length())) {
read = resStream.read();
if ((bytesRead >= start) && (bytesRead < end))
out.write(read);
bytesRead++;
}
}
resStream.close();
} else
response
.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
}
/**
* Generate a list of the files in this directory
*/
private void generateDirectoryList(HttpServletRequest request,
HttpServletResponse response, String path) throws ServletException,
IOException {
// Get the file list
File dir = path.equals("") ? this.webRoot : new File(
this.webRoot, path);
File children[] = dir.listFiles();
Arrays.sort(children);
// Build row content
StringWriter rowString = new StringWriter();
String oddColour = Launcher.RESOURCES
.getString("StaticResourceServlet.DirectoryList.OddColour");
String evenColour = Launcher.RESOURCES
.getString("StaticResourceServlet.DirectoryList.EvenColour");
String rowTextColour = Launcher.RESOURCES
.getString("StaticResourceServlet.DirectoryList.RowTextColour");
String directoryLabel = Launcher.RESOURCES
.getString("StaticResourceServlet.DirectoryList.DirectoryLabel");
String parentDirLabel = Launcher.RESOURCES
.getString("StaticResourceServlet.DirectoryList.ParentDirectoryLabel");
String noDateLabel = Launcher.RESOURCES
.getString("StaticResourceServlet.DirectoryList.NoDateLabel");
int rowCount = 0;
// Write the parent dir row
if (!path.equals("") && !path.equals("/")) {
rowString.write(Launcher.RESOURCES.getString(
"StaticResourceServlet.DirectoryList.Row", new String[] {
rowTextColour, evenColour, parentDirLabel, "..",
noDateLabel, directoryLabel }));
rowCount++;
}
// Write the rows for each file
for (int n = 0; n < children.length; n++) {
if (!children[n].getName().equalsIgnoreCase("web-inf") &&
!children[n].getName().equalsIgnoreCase("meta-inf")) {
File file = children[n];
String date = noDateLabel;
String size = directoryLabel;
if (!file.isDirectory()) {
size = "" + file.length();
synchronized (sdfFileDate) {
date = sdfFileDate.format(new Date(file.lastModified()));
}
}
rowString.write(Launcher.RESOURCES.getString(
"StaticResourceServlet.DirectoryList.Row",
new String[] {
rowTextColour,
rowCount % 2 == 0 ? evenColour : oddColour,
file.getName() + (file.isDirectory() ? "/" : ""),
"./" + file.getName() + (file.isDirectory() ? "/" : ""),
date, size}));
rowCount++;
}
}
// Build wrapper body
String out = Launcher.RESOURCES.getString("StaticResourceServlet.DirectoryList.Body",
new String[] {
Launcher.RESOURCES.getString("StaticResourceServlet.DirectoryList.HeaderColour"),
Launcher.RESOURCES.getString("StaticResourceServlet.DirectoryList.HeaderTextColour"),
Launcher.RESOURCES.getString("StaticResourceServlet.DirectoryList.LabelColour"),
Launcher.RESOURCES.getString("StaticResourceServlet.DirectoryList.LabelTextColour"),
new Date() + "",
Launcher.RESOURCES.getString("ServerVersion"),
path.equals("") ? "/" : path,
rowString.toString() });
response.setContentLength(out.getBytes().length);
response.setContentType("text/html");
Writer w = response.getWriter();
w.write(out);
w.close();
}
public static boolean isDescendant(File parent, File child, File commonBase) throws IOException {
if (child.equals(parent)) {
return true;
} else {
// Start by checking canonicals
String canonicalParent = parent.getAbsoluteFile().getCanonicalPath();
String canonicalChild = child.getAbsoluteFile().getCanonicalPath();
if (canonicalChild.startsWith(canonicalParent)) {
return true;
}
// If canonicals don't match, we're dealing with symlinked files, so if we can
// build a path from the parent to the child,
String childOCValue = constructOurCanonicalVersion(child, commonBase);
String parentOCValue = constructOurCanonicalVersion(parent, commonBase);
return childOCValue.startsWith(parentOCValue);
}
}
public static String constructOurCanonicalVersion(File current, File stopPoint) {
int backOnes = 0;
StringBuffer ourCanonicalVersion = new StringBuffer();
while ((current != null) && !current.equals(stopPoint)) {
if (current.getName().equals("..")) {
backOnes++;
} else if (current.getName().equals(".")) {
// skip - do nothing
} else if (backOnes > 0) {
backOnes--;
} else {
ourCanonicalVersion.insert(0, "/" + current.getName());
}
current = current.getParentFile();
}
return ourCanonicalVersion.toString();
}
}