// FileServlet - servlet similar to a standard httpd
//
// Copyright (C)1996,1998 by Jef Poskanzer <jef@acme.com>. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE.
//
// Visit the ACME Labs Java page for up-to-date versions of this and other
// fine Java utilities: http://www.acme.com/java/
//
// All enhancements Copyright (C)1998-2005 by Dmitriy Rogatkin
// http://tjws.sourceforge.net
package Acme.Serve;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.zip.GZIPOutputStream; //import java.util.zip.DeflaterOutputStream;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import Acme.Utils;
/// Servlet similar to a standard httpd.
// <P>
// Implements the "GET" and "HEAD" methods for files and directories.
// Handles index.html, index.htm, default.htm, default.html.
// Redirects directory URLs that lack a trailing /.
// Handles If-Modified-Since.
// <P>
// <A HREF="/resources/classes/Acme/Serve/FileServlet.java">Fetch the software.</A><BR>
// <A HREF="/resources/classes/Acme.tar.Z">Fetch the entire Acme package.</A>
// <P>
// @see Acme.Serve.Serve
public class FileServlet extends HttpServlet {
public static final String DEF_USE_COMPRESSION = "tjws.fileservlet.usecompression";
// We keep a single throttle table for all instances of the servlet.
// Normally there is only one instance; the exception is subclasses.
static Acme.WildcardDictionary throttleTab = null;
static final String[] DEFAULTINDEXPAGES = { "index.html", "index.htm", "default.htm", "default.html" };
static final DecimalFormat lengthftm = new DecimalFormat("#");
static final String BYTES_UNIT = "bytes";
protected String charSet = Serve.UTF8;// "iso-8859-1";
private static final boolean logenabled = false;
// true;
private Method canExecute, getFreeSpace;
private boolean useCompression;
// / Constructor.
public FileServlet() {
try {
canExecute = File.class.getMethod("canExecute", Utils.EMPTY_CLASSES);
} catch (SecurityException e) {
} catch (NoSuchMethodException e) {
}
try {
getFreeSpace = File.class.getMethod("getFreeSpace", Utils.EMPTY_CLASSES);
} catch (SecurityException e) {
} catch (NoSuchMethodException e) {
}
useCompression = System.getProperty(DEF_USE_COMPRESSION) != null;
}
// / Constructor with throttling.
// @param throttles filename containing throttle settings
// @param charset used for displaying directory page
// @see ThrottledOutputStream
public FileServlet(String throttles, String charset) throws IOException {
this();
if (charset != null)
this.charSet = charset;
readThrottles(throttles);
}
private void readThrottles(String throttles) throws IOException {
Acme.WildcardDictionary newThrottleTab = ThrottledOutputStream.parseThrottleFile(throttles);
if (throttleTab == null)
throttleTab = newThrottleTab;
else {
// Merge the new one into the old one.
Enumeration keys = newThrottleTab.keys();
Enumeration elements = newThrottleTab.elements();
while (keys.hasMoreElements()) {
Object key = keys.nextElement();
Object element = elements.nextElement();
throttleTab.put(key, element);
}
}
}
// / Returns a string containing information about the author, version, and
// copyright of the servlet.
public String getServletInfo() {
return "File servlet similar to httpd";
}
// / Services a single request from the client.
// @param req the servlet request
// @param req the servlet response
// @exception ServletException when an exception has occurred
public void service(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException {
boolean headOnly;
if (req.getMethod().equalsIgnoreCase("get") || req.getAttribute("javax.servlet.forward.request_uri") != null
|| req.getAttribute("javax.servlet.include.request_uri") != null)
headOnly = false;
else if (req.getMethod().equalsIgnoreCase("head"))
headOnly = true;
else {
res.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, "Method "+req.getMethod());
return;
}
req.setCharacterEncoding(Serve.UTF8);
String path = Utils.canonicalizePath(req.getPathInfo());
log("Canonical path:"+path);
//res.setBufferSize(Utils.COPY_BUF_SIZE/2);
dispatchPathname(req, res, headOnly, path);
}
private void dispatchPathname(HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path)
throws IOException {
log("path trans: " + req.getPathTranslated());
String filename = req.getPathTranslated() != null ? req.getPathTranslated().replace('/', File.separatorChar)
: "";
File file = new File(filename);
log("retrieving '" + filename + "' for path " + path);
if (file.exists()) {
if (!file.isDirectory())
serveFile(req, res, headOnly, path, file);
else {
log("showing dir " + file);
if (redirectDirectory(req, res, path, file) == false)
showIdexFile(req, res, headOnly, path, filename);
}
} else
res.sendError(HttpServletResponse.SC_NOT_FOUND);
}
private void showIdexFile(HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path,
String parent) throws IOException {
log("showing index in directory " + parent);
for (int i = 0; i < DEFAULTINDEXPAGES.length; i++) {
File indexFile = new File(parent, DEFAULTINDEXPAGES[i]);
if (indexFile.exists()) {
serveFile(req, res, headOnly, path, indexFile);
return;
}
}
// index not found
serveDirectory(req, res, headOnly, path, new File(parent));
}
private void serveFile(HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path, File file)
throws IOException {
log("getting " + file);
if (logenabled) {
Enumeration enh = req.getHeaderNames();
while (enh.hasMoreElements()) {
String hn = (String) enh.nextElement();
log("hdr:" + hn + ":" + req.getHeader(hn));
}
}
if (!file.canRead()) {
res.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
} else
// by Niel Markwick
try {
file.getCanonicalPath();
} catch (Exception e) {
res.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden, exception:" + e);
return;
}
// Handle If-Modified-Since.
res.setStatus(HttpServletResponse.SC_OK);
long lastMod = file.lastModified();
long ifModSince = req.getDateHeader("If-Modified-Since");
if (ifModSince != -1 && ifModSince >= lastMod) {
res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
headOnly = true;
}
// TODO add processing If-None-Match, If-Unmodified-Since and If-Match
String contentType = getServletContext().getMimeType(file.getName());
if (contentType != null)
res.setContentType(contentType);
long flen = file.length();
// check for range
String range = req.getHeader("Range");
long sr = 0;
long er = -1;
if (range != null) {
log("Range:" + range);
if (range.regionMatches(true, 0, BYTES_UNIT, 0, BYTES_UNIT.length())) {
int i = range.indexOf('-');
if (i > 0) {
try {
sr = Long.parseLong(range.substring(BYTES_UNIT.length() + 1, i));
if (sr < 0)
throw new NumberFormatException("Invalid start range value:" + sr);
try {
er = Long.parseLong(range.substring(i + 1));
} catch (NumberFormatException nfe) {
er = flen - 1;
}
} catch (NumberFormatException nfe) {
}
} // else invalid range? ignore?
} // else other units not supported
log("range values " + sr + " to " + er);
}
long clen = er < 0 ? flen : (er - sr + 1);
res.setDateHeader("Last-modified", lastMod);
if (er > 0) {
if (sr > er || er >= flen) {
// TODO If-Range presence can change behavior
res.setHeader("Content-Range", BYTES_UNIT + " */" + flen);
res.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return;
}
res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
res.setHeader("Content-Range", BYTES_UNIT + " " + sr + '-' + er + '/' + flen);
log("content-range:" + BYTES_UNIT + " " + sr + '-' + er + '/' + flen);
}
// String ifRange = req.getHeader("If-Range");
// res.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
boolean doCompress = false;
if (useCompression && contentType != null && contentType.startsWith("text")) {
if (Utils.isGzipAccepted(req.getHeader("Accept-Encoding")) > 0) {
res.setHeader("Content-Encoding", "gzip");
doCompress = true;
}
}
if (doCompress == false || headOnly) {
if (clen < Integer.MAX_VALUE)
res.setContentLength((int) clen);
else
res.setHeader("Content-Length", Long.toString(clen));
}
OutputStream out = null;
InputStream in = null;
try {
if (!headOnly) {
out = doCompress ? new GZIPOutputStream(res.getOutputStream()) : (OutputStream) res.getOutputStream();
// Check throttle.
if (throttleTab != null) {
ThrottleItem throttleItem = (ThrottleItem) throttleTab.get(path);
if (throttleItem != null) {
// !!! Need to count for multiple simultaneous fetches.
out = new ThrottledOutputStream(out, throttleItem.getMaxBps());
}
}
in = new FileInputStream(file);
while (sr > 0) {
long sl = in.skip(sr);
if (sl > 0)
sr -= sl;
else {
res.sendError(HttpServletResponse.SC_CONFLICT, "Conflict");
// better can be Internal Server Error
return;
}
}
copyStream(in, out, clen);
if (doCompress)
((GZIPOutputStream) out).finish();
}
} finally {
if (in != null)
try {
in.close();
} catch (IOException ioe) {
}
if (out != null) {
out.flush();
out.close();
}
}
}
// / Copy a file from in to out.
// Sub-classes can override this in order to do filtering of some sort.
public void copyStream(InputStream in, OutputStream out, long len) throws IOException {
Acme.Utils.copyStream(in, out, len);
}
private void serveDirectory(HttpServletRequest req, HttpServletResponse res, boolean headOnly, String path,
File file) throws IOException {
log("indexing " + file);
if (!file.canRead()) {
res.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
res.setStatus(HttpServletResponse.SC_OK);
res.setContentType("text/html;charset=" + charSet);
OutputStream out = res.getOutputStream();
if (!headOnly) {
String[] names = file.list();
if (names == null) {
res.sendError(HttpServletResponse.SC_FORBIDDEN, "Can't access " + req.getRequestURI());
return;
}
PrintStream p = new PrintStream(new BufferedOutputStream(out), false, charSet); // 1.4
p.println("<HTML><HEAD>");
p.println("<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=" + charSet + "\">");
p.println("<TITLE>Index of " + path + "</TITLE>");
p.println("</HEAD><BODY " + Serve.BGCOLOR);
p.println("><H2>Index of " + path + "</H2>");
p.println("<PRE>");
p.println("mode bytes last-changed name");
p.println("<HR>");
// TODO consider not case sensetive search
Arrays.sort(names);
long total = 0;
for (int i = 0; i < names.length; ++i) {
File aFile = new File(file, names[i]);
String aFileType;
long aFileLen;
if (aFile.isDirectory())
aFileType = "d";
else if (aFile.isFile())
aFileType = "-";
else
aFileType = "?";
String aFileRead = (aFile.canRead() ? "r" : "-");
String aFileWrite = (aFile.canWrite() ? "w" : "-");
String aFileExe = "-";
if (canExecute != null)
try {
if (((Boolean) canExecute.invoke(aFile, Utils.EMPTY_OBJECTS)).booleanValue())
aFileExe = "x";
} catch (IllegalArgumentException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
String aFileSize = lengthftm.format(aFileLen = aFile.length());
total += (aFileLen + 1023) / 1024; //
while (aFileSize.length() < 12)
aFileSize = " " + aFileSize;
String aFileDate = Acme.Utils.lsDateStr(new Date(aFile.lastModified()));
while (aFileDate.length() < 14)
aFileDate += " ";
String aFileDirsuf = (aFile.isDirectory() ? "/" : "");
String aFileSuf = (aFile.isDirectory() ? "/" : "");
p.println(aFileType + aFileRead + aFileWrite + aFileExe + " " + aFileSize + " " + aFileDate + " "
+ "<A HREF=\"" + URLEncoder.encode(names[i], charSet) /* 1.4 */
+ aFileDirsuf + "\">" + names[i] + aFileSuf + "</A>");
}
if (total != 0)
total += 3;
p.println("total " + total);
p.println("</PRE>");
p.println("<HR>");
p.print(Serve.Identification.serverIdHtml);
p.println("</BODY></HTML>");
p.flush();
}
out.close();
}
/**
*
* @param req
* http request
* @param res
* http response
* @param path
* web path
* @param file
* file system path
* @return true if redirection required and happened
* @throws IOException
* in redirection
*/
private boolean redirectDirectory(HttpServletRequest req, HttpServletResponse res, String path, File file)
throws IOException {
int pl = path.length();
if (pl > 0 && path.charAt(pl - 1) != '/') {
// relative redirect
int sp = path.lastIndexOf('/');
if (sp < 0)
path += '/';
else
path = path.substring(sp + 1) + '/';
log("redirecting dir " + path);
res.sendRedirect(path);
return true;
}
return false;
}
public void log(String msg) {
if (logenabled)
super.log(msg);
}
}