/* * FileHandler.java * * Brazil project web application toolkit, * export version: 2.3 * Copyright (c) 1998-2008 Sun Microsystems, Inc. * * Sun Public License Notice * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is included as the file "license.terms", * and also available at http://www.sun.com/ * * The Original Code is from: * Brazil project web application toolkit release 2.3. * The Initial Developer of the Original Code is: suhler. * Portions created by suhler are Copyright (C) Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): cstevens, suhler. * * Version: 2.10 * Created by suhler on 98/09/14 * Last modified by suhler on 08/07/02 10:39:51 * * Version Histories: * * 2.10 08/07/02-10:39:51 (suhler) * add strict types * * 2.9 08/03/04-10:59:22 (suhler) * CookieFilter.java * * 2.8 07/01/08-15:15:47 (suhler) * add a lastmodified method to deal with the "lastModified" property * * 2.7 06/04/25-14:22:19 (suhler) * use consolidated mime type handling in FileHandler * * 2.6 05/12/09-14:50:44 (suhler) * Document the partial-content feature (using the "range") header, fix * an off-by-one error when the range "end" is trucated, and add the proper * "content-range" header * * 2.5 05/02/04-11:38:28 (suhler) * redo glob-style mime type matching to look at explicit list of * patterns, so we don't search all the properties (twice) * * 2.4 05/02/04-11:01:53 (suhler) * added globbing to mime typ matching (using search through properties) * * 2.3 04/11/30-15:19:41 (suhler) * fixed sccs version string * * 2.2 03/08/01-16:17:53 (suhler) * fixes for javadoc * * 2.1 02/10/01-16:34:50 (suhler) * version change * * 1.34 02/07/24-10:47:55 (suhler) * doc updates * * 1.33 02/07/11-17:44:33 (suhler) * doc fixes * * 1.32 01/07/23-14:48:25 (suhler) * changed "allow" to "getOnly" and reversed the default: all methods are * accepted unless "getOnly" is specified * . * * 1.31 01/03/12-17:27:16 (cstevens) * spelling error in log message * * 1.30 01/01/07-22:35:43 (suhler) * added byte range support * * 1.29 00/12/11-13:32:13 (suhler) * add class=props for automatic property extraction * * 1.28 00/11/19-10:32:45 (suhler) * Add "lastModified" into request properties * * 1.27 00/10/17-09:30:45 (suhler) * added a "prefix" property. * . * * 1.26 00/10/05-09:18:11 (suhler) * return "403 Forbidden" for existing but not readable files * . * * 1.25 00/05/15-10:09:12 (suhler) * removed "deny" option - use the UrlMapperHandler instead * removed the "result" option - use the FilterHandler instead * added more diagnostics * * 1.24 00/04/24-14:06:12 (cstevens) * doc * * 1.23 00/01/30-17:45:14 (suhler) * Removed platform dependency in redirect check * * 1.22 99/12/09-10:43:11 (suhler) * add "allow" configuration parameter to allow handling other than * GET requests * * 1.21 99/11/17-15:36:49 (cstevens) * broke DirectoryHandler. * * 1.20 99/11/16-19:09:01 (cstevens) * wildcard imports * * 1.19 99/11/16-14:18:14 (cstevens) * FileHandler.java: * 1. documentation. * 2. "blockSize" property was retrieved from Properties but never used, since * the copy operation uses the Server.bufsize. "blockSize" removed. * 3. "deny" property to deny access to files that matched a glob pattern was * broken. Arguments to Glob were reversed, so the code was treating the * URL as the pattern and the deny pattern as the filename. No matches. * 4. URL was never "URL decoded", so something like "/in%64ex%2ehtml" * was not converted to "/index.html". Code would therefore look for the * existence of a file called "in%64ex%2ehtml". * 5. java.net.URL was used to convert a URL to the local file name. However, * java.net.URL does not collapse a trailing "/.." (or "/.") in the URL, * leading to the possibility of escaping from the document root. * 6. In jdk-1.2, java.io.File collapses out multiple consecutive "/" characters * in a file name, while in jdk-1.1, this doesn't happen. It is necessary * to take them out to make it easier to compare if two files are the * same. * 7. Request property "fileName" is set to the name of the file being * fetched. But if the user requested a directory name, and the * FileHandler automatically filled in the name of the index file (e.g., * index.html), "fileName" should have been set to the that file, not * just the name of the directory. * 8. After appending the name of the index file onto the name of the * directory requested in an URL, the FileHandler didn't ensure that that * file existed (and was a regular file). This would cause a * FileNotFoundException, and the server would either send a "500 Server * Error" or just close the connection back to the client. Instead, it * should return a "404 Not Found" message. * * 1.18 99/10/28-17:23:27 (cstevens) * ChangedTemplate * * 1.17 99/10/26-18:13:19 (cstevens) * Get rid of public variables Request.server and Request.sock: * A. In all cases, Request.server was not necessary; it was mainly used for * constructing the absolute URL for a redirect, so Request.redirect() was * rewritten to take an absolute or relative URL and do the right thing. * B. Request.sock was changed to Request.getSock(); it is still rarely used * for diagnostics and logging (e.g., ChainSawHandler). * * 1.16 99/10/25-14:29:49 (suhler) * file handler wasn't sending content type * * 1.15 99/10/14-14:57:34 (cstevens) * resolve wilcard imports. * * 1.14 99/10/14-12:52:04 (cstevens) * FileHandler.sendFile uses new Request.sendResponse(InputStream) method. * * 1.13 99/09/22-16:02:29 (suhler) * The file handler only responds to GET requests * * 1.12 99/09/17-14:02:55 (suhler) * Merged changes between child workspace "/home/suhler/brazil/naws" and * parent workspace "/net/mack.eng/export/ws/brazil/naws". * * 1.11 99/09/15-14:41:02 (cstevens) * Rewritign http server to make it easier to proxy requests. * * 1.10.1.1 99/08/18-08:40:30 (suhler) * doc change * * 1.10 99/08/06-12:05:33 (suhler) * moved glob to util directoy * * 1.9 99/06/29-14:33:31 (suhler) * Fixed redirects on directory names with no trailing '/' * Uses "host:" header if available * . * * 1.8 99/06/28-10:53:16 (suhler) * allow results to be placed in properties (temporary) * * 1.7 99/06/04-13:54:41 (suhler) * added "request" option to place result into request properties * and requrn false. * This allows other handlers to process files more * * 1.6 99/04/27-14:45:09 (suhler) * - added glob style pattern matcher as a utility method * - added "deny" parameter to fall through if url matches a pattern * * 1.5 99/03/30-09:24:31 (suhler) * - documentation updates * - made SendFile public * - set "Directory" and "FileName" properties upon failure * - don't force result code to be 200 * * 1.4 99/02/04-20:38:21 (cstevens) * lint: changed wildcard import statements to explicit imports. * * 1.3 98/10/13-08:05:34 (suhler) * added informational messages * * 1.2 98/09/21-14:50:54 (suhler) * changed the package names * * 1.2 98/09/14-18:03:05 (Codemgr) * SunPro Code Manager data about conflicts, renames, etc... * Name history : 2 1 server/FileHandler.java * Name history : 1 0 FileHandler.java * * 1.1 98/09/14-18:03:04 (suhler) * date and time created 98/09/14 18:03:04 by suhler * */ package sunlabs.brazil.server; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.util.Properties; import java.util.StringTokenizer; import sunlabs.brazil.util.Glob; import sunlabs.brazil.util.http.HttpUtil; /** * Standard handler for fetching static files. * This handler does URL to file conversion, file suffix to mime type * lookup, delivery of index files where providing directory * references, and redirection for missing slashes (/) at the end of * directory requests. *
* The following configuration parameters are used: *
/foo
then the the file
* [root]/foo/bar.html
will be delivered
* in response to the url /bar.html
.
* * mimePatterns=.x* .a? * mime.x*=text/xml * mime.a?=application/octet-stream ** The types corrosponding to mime patterns are searched for * in mimePattern order, first looking for *
prefix.mime.pattern
then
* mime.pattern
. If neither property exists,
* then the type is invalid.
* * The FileHandler sets the following entries in the request properties * as a side-effect: *
* This handler supports a subset of the http range
* header of the form
* range bytes=[start]-[end]
, where start and end
* are byte positions in the file, starting at zero. A large or
* missing end value is treated as the end of the file. If a valid
* range
header is found, the appropriate
* content-range
header is returned, along with the partial
* contents of the file.
*
* @author Stephen Uhler
* @version 2.10
*/
public class FileHandler implements Handler {
private static final String PREFIX = "prefix"; // our prefix
private static final String DEFAULT = "default"; // property for default document, given directory
private static final String GETONLY = "getOnly"; // allow only GETs
private static final String STRICT_TYPES = "strictTypes";
public static final String MIME = "mime"; // property for mime type
public static final String UNKNOWN = "unknown";// make this suffix unknown
public static final String ROOT = "root"; // property for document root
public String urlPrefix = "/";
String prefix;
/**
* Initialize the file handler.
*
* @return The file handler always returns true.
*/
public boolean
init(Server server, String prefix)
{
this.prefix = prefix;
urlPrefix = server.props.getProperty(prefix + PREFIX, urlPrefix);
return true;
}
/**
* Find, read, and deliver via http the requested file.
* The server property root
is used as the document root.
* The document root is recalculated for each request, so an upstream
* handler may change it for that request.
* For URL's ending with "/", the server property default
* (normally index.html) is automatically appended.
*
* If the file suffix is not found as a server property
* mime.suffix
, the file is not delivered.
*/
public boolean
respond(Request request)
throws IOException
{
if (!request.url.startsWith(urlPrefix)) {
return false;
}
if ((request.props.getProperty(prefix + GETONLY)!=null) &&
(!request.method.equals("GET"))) {
request.log(Server.LOG_INFORMATIONAL, prefix,
"Skipping request, only GET's allowed");
return false;
}
String url = request.url.substring(urlPrefix.length());
Properties props = request.props;
String root = props.getProperty(prefix + ROOT,
props.getProperty(ROOT, "."));
boolean strictTypes = (props.getProperty(prefix+STRICT_TYPES) != null);
String name = urlToPath(url);
request.log(Server.LOG_DIAGNOSTIC, prefix, "Looking for file: (" +
root + ")(" + name + ")");
File file = new File(root + name);
String path = file.getPath();
if (file.isDirectory()) {
/*
* Must check if the original name
ends with "/",
* not File.getPath
because in jdk-1.2,
* File.getPath
truncates the terminating "/".
*/
if (request.url.endsWith("/") == false) {
request.redirect(request.url + "/", null);
return true;
}
props.put("DirectoryName", path);
String index = props.getProperty(prefix + DEFAULT, "index.html");
file = new File(file, index);
path = file.getPath();
}
/*
* Put the name of the file in the request object. This
* may be of some use for down stream handlers.
*/
props.put("fileName", path);
if (file.exists() == false) {
request.log(Server.LOG_INFORMATIONAL, prefix,
"no such file: " + path);
return false;
}
String basename = file.getName();
String type = getMimeType(basename, props, prefix, strictTypes);
if (type == null) {
request.log(Server.LOG_INFORMATIONAL, prefix,
"unknown file suffix for " + basename);
return false;
}
sendFile(request, file, 200, type);
return true;
}
/**
* Get the mime type based on the suffix of a String. The
* suffix (e.g. text after the last ".") is used to lookup
* the entry "prefix.mime.[suffix]" then "mime.[suffix]" in
* props
. If neither entry is found, then mime
* glob pattern are used, if available. If there is no suffix,
* then the empty string is used.
*
* If the mime type is set to the special string "unknown", then * the type is unknown. This allows specific types to be undefined * when glob patterns are used. *
* If the property "prefix.mimePatterns" (or "mimePatterns") exists, * then it specifies a white-space delimited set of glob style patterns * for matching file suffixes to types. If a match for a specific file * suffix fails, then the property "mime.[pattern]" is used for type * comparisons. *
* The entries: *
* mimePatterns=*ml * mime.*ml=text/xml ** would associate the type "text/xml" with the file foo.html, foo.xml * and foo.dhtml. * The entries: *
* mimePatterns=* * mime*=application/octet-stream * mime.config=unknown ** Would set the types for all file types not otherwise defined to be * "application/octet-stream", except that files ending in ".config" * would have no type (e.g. they would generate a file not found error). * * @param name The string to compute the mime type for * @param props The properties to look up the mime types in. * @param prefix The properties prefix for the name lookup * @return The type (or null if not found). */ public static String getMimeType(String name, Properties props, String prefix) { return getMimeType(name, props, prefix, false); } /** * If "strict" is set, only return types that specifically * match properties with this handlers prefix. */ public static String getMimeType(String name, Properties props, String prefix, boolean strict) { String type = null; int index = name.lastIndexOf('.'); if (index > 0) { String suffix = name.substring(index); if (strict) { return props.getProperty(prefix + MIME + suffix); } type = props.getProperty(prefix + MIME + suffix, props.getProperty(MIME + suffix)); // check for glob patterns if (type == null) { type = getGlobType(suffix, props, prefix); } else if (type.equals(UNKNOWN)) { return null; } } else if (index < 0) { type = getGlobType("", props, prefix); } return type; } /** * Helper to find mime types for suffixes with glob pattern. * Used only if a static mime type is not found. * @param suffix the file suffix to look for * @param dict where to look * @param prefix prefix in properties table */ static String getGlobType(String suffix, Properties props, String pre) { String list = props.getProperty(pre + "mimePatterns", props.getProperty("mimePatterns")); if (list == null) { return null; } StringTokenizer st = new StringTokenizer(list); while(st.hasMoreTokens()) { String key = st.nextToken(); if (Glob.match(key, suffix)) { String type = props.getProperty(pre + "mime" + key, props.getProperty("mime" + key)); if (UNKNOWN.equals(type)) { break; } else { return type; } } } return null; } /** * Helper function to convert an url into a pathname.
URL(String)
collapses all "/.." (and "/.") sequences,
* except for a trailing "/.." (or "/."), which would lead to the
* possibility of escaping from the document root.
*
* File.getPath
in jdk-1.1 leaves all the "//" constructs
* in, but it collapses them in jdk-1.2, so we have to always take it
* out ourselves, just to be sure.
*
* @param url
* The file path from the URL (that is, minus the "http://host"
* part). May be null
.
*
* @return The path that corresponds to the URL. The returned value
* begins with "/". The caller can concatenate this path
* onto the end of some document root.
*/
public static String
urlToPath(String url)
{
String name = HttpUtil.urlDecode(url);
StringBuffer sb = new StringBuffer();
StringTokenizer st = new StringTokenizer(name, "/");
while (st.hasMoreTokens()) {
String part = st.nextToken();
if (part.equals(".")) {
continue;
} else if (part.equals("..")) {
for (int i = sb.length(); --i >= 0; ) {
if (sb.charAt(i) == '/') {
sb.setLength(i);
break;
}
}
} else {
sb.append(File.separatorChar).append(part);
}
}
if ((sb.length() == 0) || name.endsWith("/")) {
sb.append(File.separatorChar);
}
return sb.toString();
}
/**
* Send a file as a response.
* @param request The request object
* @param file The file to output
* @param code The HTTP status code.
* @param type The mime type of the file
*/
static public void
sendFile(Request request, File file, int code, String type)
throws IOException
{
if (file.isFile() == false) {
request.sendError(404, null, "not a normal file");
return;
}
if (file.canRead() == false) {
request.sendError(403, null, "Permission Denied");
return;
}
FileInputStream in = null;
try {
in = new FileInputStream(file);
request.addHeader("Last-Modified",
HttpUtil.formatTime(file.lastModified()));
setModified(request.props, file.lastModified());
int size = (int) file.length();
request.setStatus(code);
size = range(request, in, size);
request.sendResponse(in, size, type, -1);
} finally {
if (in != null) {
in.close();
}
}
}
/**
* Set the "lastModified" request property. If already set,
* use the most recent value.
* @param props Where to find the "lastModified" property
* @param mod The modidied time, in ms since the epoch
*/
public static void setModified(Properties props, long mod) {
if (mod <= 0) {
return;
}
String current = props.getProperty("lastModified");
if (current == null) {
props.put("lastModified", "" + mod);
} else {
try {
long l = Long.parseLong(current);
if (l > mod) {
props.put("lastModified", "" + l);
}
} catch (NumberFormatException e) {}
}
}
/**
* Compute simple byte ranges. (for gnutella support). Only simple
* ranges (e.g. xxx-yyy) are supported. This skips over the proper
* number of bytes in the input stream, changes the request status to
* 26, and adds a content-range header.
* @return The (potential partial) size.
*
*/
private static int
range(Request request, FileInputStream in, int size) throws IOException {
String range=request.getRequestHeader("range");
int sep = 0;
if (range != null && request.getStatus()==200 &&
range.indexOf("bytes=") == 0 &&
(sep = range.indexOf("-")) > 0 &&
range.indexOf(",")<0) {
int start = -1;
int end = -1;
try {
start = Integer.parseInt(range.substring(6,sep));
} catch (NumberFormatException e) {}
try {
end = Integer.parseInt(range.substring(sep+1));
} catch (NumberFormatException e) {}
if (end == -1) {
end = size - 1;
} else if (end >= size) {
end = size - 1;
}
if (start == -1) {
start = size - end + 1;
end = size;
}
if (end >= start) {
in.skip(start);
request.addHeader("Content-Range",
"bytes " + start + "-" + end + "/" + size);
size = end - start + 1;
request.setStatus(206);
}
}
return size;
}
}