/* * 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: *

*
root
property for document root (.) * Since the document root is common to many handlers, if no * root property is found with the supplied prefix, then the * root property with the empty prefix ("") is used instead. * This allows many handlers to share the common property. *
default
The document to deliver * if the URL ends in "/". (defaults to index.html.) *
prefix
Only url's that start with this are allowed. * defaults to "". The prefix is removed from the * url before looking it up in the file system. * So, if prefix is /foo then the the file * [root]/foo/bar.html will be delivered * in response to the url /bar.html. *
mime
property for mime type * For each file suffix .XX, the property mime.XX is used to * determine the mime type. If no property exists (or its * value is "unknown", the document * will not be delivered. *
mimePatterns *
List of glob patterns that match file name suffixes * for matching mime types. For example: *
 *		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. *
getOnly
If defined, only "GET" requests will be processed. By * default, all request types are handled. (Note: this is the * inverse of the previous policy, defined by the undocumented * "allow" parameter). *
strictTypes *
If defined, only mime types specifically listed with * The file handlers prefix will be delivered. *
*

* The FileHandler sets the following entries in the request properties * as a side-effect: *

*
fileName
The absolute path of the file * that couldn't be found. *
DirectoryName
If the URL specified is a directory name, * its absolute path is placed here. *
lastModified
The Time stamp of the last modified time *
*

* 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 * rangeheader 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; } }