001: /*
002: * Copyright 2003-2006 Rick Knowles <winstone-devel at lists sourceforge net>
003: * Distributed under the terms of either:
004: * - the common development and distribution license (CDDL), v1.0; or
005: * - the GNU Lesser General Public License, v2.1 or later
006: */
007: package winstone;
008:
009: import java.io.File;
010: import java.io.FileInputStream;
011: import java.io.IOException;
012: import java.io.InputStream;
013: import java.io.OutputStream;
014: import java.io.StringWriter;
015: import java.io.Writer;
016: import java.text.DateFormat;
017: import java.text.SimpleDateFormat;
018: import java.util.ArrayList;
019: import java.util.Arrays;
020: import java.util.Date;
021: import java.util.Iterator;
022: import java.util.List;
023: import java.util.StringTokenizer;
024:
025: import javax.servlet.ServletConfig;
026: import javax.servlet.ServletException;
027: import javax.servlet.http.HttpServlet;
028: import javax.servlet.http.HttpServletRequest;
029: import javax.servlet.http.HttpServletResponse;
030:
031: /**
032: * Servlet to handle static resources. Simply finds and sends them, or
033: * dispatches to the error servlet.
034: *
035: * @author <a href="mailto:rick_knowles@hotmail.com">Rick Knowles</a>
036: * @version $Id: StaticResourceServlet.java,v 1.17 2004/12/31 07:21:00
037: * rickknowles Exp $
038: */
039: public class StaticResourceServlet extends HttpServlet {
040: // final String JSP_FILE = "org.apache.catalina.jsp_file";
041: final static String FORWARD_SERVLET_PATH = "javax.servlet.forward.servlet_path";
042: final static String INCLUDE_SERVLET_PATH = "javax.servlet.include.servlet_path";
043: final static String CACHED_RESOURCE_DATE_HEADER = "If-Modified-Since";
044: final static String LAST_MODIFIED_DATE_HEADER = "Last-Modified";
045: final static String RANGE_HEADER = "Range";
046: final static String ACCEPT_RANGES_HEADER = "Accept-Ranges";
047: final static String CONTENT_RANGE_HEADER = "Content-Range";
048: final static String RESOURCE_FILE = "winstone.LocalStrings";
049: private DateFormat sdfFileDate = new SimpleDateFormat(
050: "dd-MM-yyyy HH:mm");
051: private File webRoot;
052: private String prefix;
053: private boolean directoryList;
054:
055: public void init(ServletConfig config) throws ServletException {
056: super .init(config);
057: this .webRoot = new File(config.getInitParameter("webRoot"));
058: this .prefix = config.getInitParameter("prefix");
059: String dirList = config.getInitParameter("directoryList");
060: this .directoryList = (dirList == null)
061: || dirList.equalsIgnoreCase("true")
062: || dirList.equalsIgnoreCase("yes");
063: }
064:
065: public void doPost(HttpServletRequest request,
066: HttpServletResponse response) throws ServletException,
067: IOException {
068: doGet(request, response);
069: }
070:
071: public void doGet(HttpServletRequest request,
072: HttpServletResponse response) throws ServletException,
073: IOException {
074: boolean isInclude = (request.getAttribute(INCLUDE_SERVLET_PATH) != null);
075: boolean isForward = (request.getAttribute(FORWARD_SERVLET_PATH) != null);
076: String path = null;
077:
078: if (isInclude)
079: path = (String) request.getAttribute(INCLUDE_SERVLET_PATH);
080: else {
081: path = request.getServletPath();
082: }
083:
084: // URL decode path
085: path = WinstoneRequest.decodeURLToken(path);
086:
087: long cachedResDate = request
088: .getDateHeader(CACHED_RESOURCE_DATE_HEADER);
089: Logger.log(Logger.DEBUG, Launcher.RESOURCES,
090: "StaticResourceServlet.PathRequested", new String[] {
091: getServletConfig().getServletName(), path });
092:
093: // Check for the resource
094: File res = path.equals("") ? this .webRoot : new File(
095: this .webRoot, path);
096:
097: // Send a 404 if not found
098: if (!res.exists())
099: response
100: .sendError(
101: HttpServletResponse.SC_NOT_FOUND,
102: Launcher.RESOURCES
103: .getString(
104: "StaticResourceServlet.PathNotFound",
105: path));
106:
107: // Check we are below the webroot
108: else if (!isDescendant(this .webRoot, res, this .webRoot)) {
109: Logger.log(Logger.FULL_DEBUG, Launcher.RESOURCES,
110: "StaticResourceServlet.OutsideWebroot",
111: new String[] { res.getCanonicalPath(),
112: this .webRoot.toString() });
113: response.sendError(HttpServletResponse.SC_FORBIDDEN,
114: Launcher.RESOURCES.getString(
115: "StaticResourceServlet.PathInvalid", path));
116: }
117:
118: // Check we are not below the web-inf
119: else if (!isInclude
120: && !isForward
121: && isDescendant(new File(this .webRoot, "WEB-INF"), res,
122: this .webRoot))
123: response.sendError(HttpServletResponse.SC_NOT_FOUND,
124: Launcher.RESOURCES.getString(
125: "StaticResourceServlet.PathInvalid", path));
126:
127: // Check we are not below the meta-inf
128: else if (!isInclude
129: && !isForward
130: && isDescendant(new File(this .webRoot, "META-INF"),
131: res, this .webRoot))
132: response.sendError(HttpServletResponse.SC_NOT_FOUND,
133: Launcher.RESOURCES.getString(
134: "StaticResourceServlet.PathInvalid", path));
135:
136: // check for the directory case
137: else if (res.isDirectory()) {
138: if (path.endsWith("/")) {
139: // Try to match each of the welcome files
140: // String matchedWelcome = matchWelcomeFiles(path, res);
141: // if (matchedWelcome != null)
142: // response.sendRedirect(this.prefix + path + matchedWelcome);
143: // else
144: if (this .directoryList)
145: generateDirectoryList(request, response, path);
146: else
147: response
148: .sendError(
149: HttpServletResponse.SC_FORBIDDEN,
150: Launcher.RESOURCES
151: .getString("StaticResourceServlet.AccessDenied"));
152: } else
153: response.sendRedirect(this .prefix + path + "/");
154: }
155:
156: // Send a 304 if not modified
157: else if (!isInclude
158: && (cachedResDate != -1)
159: && (cachedResDate < (System.currentTimeMillis() / 1000L * 1000L))
160: && (cachedResDate >= (res.lastModified() / 1000L * 1000L))) {
161: String mimeType = getServletContext().getMimeType(
162: res.getName().toLowerCase());
163: if (mimeType != null)
164: response.setContentType(mimeType);
165: response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
166: response.setContentLength(0);
167: response.flushBuffer();
168: }
169:
170: // Write out the resource if not range or is included
171: else if ((request.getHeader(RANGE_HEADER) == null) || isInclude) {
172: String mimeType = getServletContext().getMimeType(
173: res.getName().toLowerCase());
174: if (mimeType != null)
175: response.setContentType(mimeType);
176: InputStream resStream = new FileInputStream(res);
177:
178: response.setStatus(HttpServletResponse.SC_OK);
179: response.setContentLength((int) res.length());
180: // response.addHeader(ACCEPT_RANGES_HEADER, "bytes");
181: response.addDateHeader(LAST_MODIFIED_DATE_HEADER, res
182: .lastModified());
183: OutputStream out = null;
184: Writer outWriter = null;
185: try {
186: out = response.getOutputStream();
187: } catch (IllegalStateException err) {
188: outWriter = response.getWriter();
189: } catch (IllegalArgumentException err) {
190: outWriter = response.getWriter();
191: }
192: byte buffer[] = new byte[4096];
193: int read = resStream.read(buffer);
194: while (read > 0) {
195: if (out != null) {
196: out.write(buffer, 0, read);
197: } else {
198: outWriter.write(new String(buffer, 0, read,
199: response.getCharacterEncoding()));
200: }
201: read = resStream.read(buffer);
202: }
203: resStream.close();
204: } else if (request.getHeader(RANGE_HEADER).startsWith("bytes=")) {
205: String mimeType = getServletContext().getMimeType(
206: res.getName().toLowerCase());
207: if (mimeType != null)
208: response.setContentType(mimeType);
209: InputStream resStream = new FileInputStream(res);
210:
211: List ranges = new ArrayList();
212: StringTokenizer st = new StringTokenizer(request.getHeader(
213: RANGE_HEADER).substring(6).trim(), ",", false);
214: int totalSent = 0;
215: String rangeText = "";
216: while (st.hasMoreTokens()) {
217: String rangeBlock = st.nextToken();
218: int start = 0;
219: int end = (int) res.length();
220: int delim = rangeBlock.indexOf('-');
221: if (delim != 0)
222: start = Integer.parseInt(rangeBlock.substring(0,
223: delim).trim());
224: if (delim != rangeBlock.length() - 1)
225: end = Integer.parseInt(rangeBlock.substring(
226: delim + 1).trim());
227: totalSent += (end - start);
228: rangeText += "," + start + "-" + end;
229: ranges.add(start + "-" + end);
230: }
231: response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
232: response.addHeader(CONTENT_RANGE_HEADER, "bytes "
233: + rangeText.substring(1) + "/" + res.length());
234: response.setContentLength(totalSent);
235:
236: response.addHeader(ACCEPT_RANGES_HEADER, "bytes");
237: response.addDateHeader(LAST_MODIFIED_DATE_HEADER, res
238: .lastModified());
239: OutputStream out = response.getOutputStream();
240: int bytesRead = 0;
241: for (Iterator i = ranges.iterator(); i.hasNext();) {
242: String rangeBlock = (String) i.next();
243: int delim = rangeBlock.indexOf('-');
244: int start = Integer.parseInt(rangeBlock.substring(0,
245: delim));
246: int end = Integer.parseInt(rangeBlock
247: .substring(delim + 1));
248: int read = 0;
249: while ((read != -1) && (bytesRead <= res.length())) {
250: read = resStream.read();
251: if ((bytesRead >= start) && (bytesRead < end))
252: out.write(read);
253: bytesRead++;
254: }
255: }
256: resStream.close();
257: } else
258: response
259: .sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
260: }
261:
262: /**
263: * Generate a list of the files in this directory
264: */
265: private void generateDirectoryList(HttpServletRequest request,
266: HttpServletResponse response, String path)
267: throws ServletException, IOException {
268: // Get the file list
269: File dir = path.equals("") ? this .webRoot : new File(
270: this .webRoot, path);
271: File children[] = dir.listFiles();
272: Arrays.sort(children);
273:
274: // Build row content
275: StringWriter rowString = new StringWriter();
276: String oddColour = Launcher.RESOURCES
277: .getString("StaticResourceServlet.DirectoryList.OddColour");
278: String evenColour = Launcher.RESOURCES
279: .getString("StaticResourceServlet.DirectoryList.EvenColour");
280: String rowTextColour = Launcher.RESOURCES
281: .getString("StaticResourceServlet.DirectoryList.RowTextColour");
282:
283: String directoryLabel = Launcher.RESOURCES
284: .getString("StaticResourceServlet.DirectoryList.DirectoryLabel");
285: String parentDirLabel = Launcher.RESOURCES
286: .getString("StaticResourceServlet.DirectoryList.ParentDirectoryLabel");
287: String noDateLabel = Launcher.RESOURCES
288: .getString("StaticResourceServlet.DirectoryList.NoDateLabel");
289:
290: int rowCount = 0;
291:
292: // Write the parent dir row
293: if (!path.equals("") && !path.equals("/")) {
294: rowString.write(Launcher.RESOURCES.getString(
295: "StaticResourceServlet.DirectoryList.Row",
296: new String[] { rowTextColour, evenColour,
297: parentDirLabel, "..", noDateLabel,
298: directoryLabel }));
299: rowCount++;
300: }
301:
302: // Write the rows for each file
303: for (int n = 0; n < children.length; n++) {
304: if (!children[n].getName().equalsIgnoreCase("web-inf")
305: && !children[n].getName().equalsIgnoreCase(
306: "meta-inf")) {
307: File file = children[n];
308: String date = noDateLabel;
309: String size = directoryLabel;
310: if (!file.isDirectory()) {
311: size = "" + file.length();
312: synchronized (sdfFileDate) {
313: date = sdfFileDate.format(new Date(file
314: .lastModified()));
315: }
316: }
317: rowString.write(Launcher.RESOURCES.getString(
318: "StaticResourceServlet.DirectoryList.Row",
319: new String[] {
320: rowTextColour,
321: rowCount % 2 == 0 ? evenColour
322: : oddColour,
323: file.getName()
324: + (file.isDirectory() ? "/"
325: : ""),
326: "./"
327: + file.getName()
328: + (file.isDirectory() ? "/"
329: : ""), date, size }));
330: rowCount++;
331: }
332: }
333:
334: // Build wrapper body
335: String out = Launcher.RESOURCES
336: .getString(
337: "StaticResourceServlet.DirectoryList.Body",
338: new String[] {
339: Launcher.RESOURCES
340: .getString("StaticResourceServlet.DirectoryList.HeaderColour"),
341: Launcher.RESOURCES
342: .getString("StaticResourceServlet.DirectoryList.HeaderTextColour"),
343: Launcher.RESOURCES
344: .getString("StaticResourceServlet.DirectoryList.LabelColour"),
345: Launcher.RESOURCES
346: .getString("StaticResourceServlet.DirectoryList.LabelTextColour"),
347: new Date() + "",
348: Launcher.RESOURCES
349: .getString("ServerVersion"),
350: path.equals("") ? "/" : path,
351: rowString.toString() });
352:
353: response.setContentLength(out.getBytes().length);
354: response.setContentType("text/html");
355: Writer w = response.getWriter();
356: w.write(out);
357: w.close();
358: }
359:
360: public static boolean isDescendant(File parent, File child,
361: File commonBase) throws IOException {
362: if (child.equals(parent)) {
363: return true;
364: } else {
365: // Start by checking canonicals
366: String canonicalParent = parent.getAbsoluteFile()
367: .getCanonicalPath();
368: String canonicalChild = child.getAbsoluteFile()
369: .getCanonicalPath();
370: if (canonicalChild.startsWith(canonicalParent)) {
371: return true;
372: }
373:
374: // If canonicals don't match, we're dealing with symlinked files, so if we can
375: // build a path from the parent to the child,
376: String childOCValue = constructOurCanonicalVersion(child,
377: commonBase);
378: String parentOCValue = constructOurCanonicalVersion(parent,
379: commonBase);
380: return childOCValue.startsWith(parentOCValue);
381: }
382: }
383:
384: public static String constructOurCanonicalVersion(File current,
385: File stopPoint) {
386: int backOnes = 0;
387: StringBuffer ourCanonicalVersion = new StringBuffer();
388: while ((current != null) && !current.equals(stopPoint)) {
389: if (current.getName().equals("..")) {
390: backOnes++;
391: } else if (current.getName().equals(".")) {
392: // skip - do nothing
393: } else if (backOnes > 0) {
394: backOnes--;
395: } else {
396: ourCanonicalVersion.insert(0, "/" + current.getName());
397: }
398: current = current.getParentFile();
399: }
400: return ourCanonicalVersion.toString();
401: }
402: }
|