// ======================================================================== // $Id: Default.java,v 1.51 2006/10/08 14:13:18 gregwilkins Exp $ // Copyright 199-2004 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ======================================================================== package net.lightbody.bmp.proxy.jetty.jetty.servlet; import net.lightbody.bmp.proxy.jetty.http.*; import net.lightbody.bmp.proxy.jetty.log.LogFactory; import net.lightbody.bmp.proxy.jetty.util.*; import org.apache.commons.logging.Log; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.UnavailableException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Enumeration; import java.util.List; /* ------------------------------------------------------------ */ /** * The default servlet. This servlet, normally mapped to /, provides the handling for static * content, OPTION and TRACE methods for the context. The following initParameters are supported: * * <PRE> * * acceptRanges If true, range requests and responses are supported * * dirAllowed If true, directory listings are returned if no welcome file is found. Else 403 * Forbidden. * * putAllowed If true, the PUT method is allowed * * delAllowed If true, the DELETE method is allowed * * redirectWelcome If true, welcome files are redirected rather than forwarded to. * * minGzipLength If set to a positive integer, then static content larger than this will be served * as gzip content encoded if a matching resource is found ending with ".gz" * * resourceBase Set to replace the context resource base * * relativeResourceBase Set with a pathname relative to the base of the servlet context root. Useful * for only serving static content out of only specific subdirectories. * * </PRE> * * The MOVE method is allowed if PUT and DELETE are allowed * * @version $Id: Default.java,v 1.51 2006/10/08 14:13:18 gregwilkins Exp $ * @author Greg Wilkins (gregw) */ public class Default extends HttpServlet { private static Log log = LogFactory.getLog(Default.class); private HttpContext _httpContext; private ServletHandler _servletHandler; private String _AllowString = "GET, POST, HEAD, OPTIONS, TRACE"; private boolean _acceptRanges = true; private boolean _dirAllowed; private boolean _putAllowed; private boolean _delAllowed; private boolean _redirectWelcomeFiles; private int _minGzipLength = -1; private Resource _resourceBase; /* ------------------------------------------------------------ */ public void init() throws UnavailableException { ServletContext config = getServletContext(); _servletHandler = ((ServletHandler.Context) config).getServletHandler(); _httpContext = _servletHandler.getHttpContext(); _acceptRanges = getInitBoolean("acceptRanges"); _dirAllowed = getInitBoolean("dirAllowed"); _putAllowed = getInitBoolean("putAllowed"); _delAllowed = getInitBoolean("delAllowed"); _redirectWelcomeFiles = getInitBoolean("redirectWelcome"); _minGzipLength = getInitInt("minGzipLength"); String rrb = getInitParameter("relativeResourceBase"); if (rrb != null) { try { _resourceBase = _httpContext.getBaseResource().addPath(rrb); } catch (Exception e) { log.warn(LogSupport.EXCEPTION, e); throw new UnavailableException(e.toString()); } } String rb = getInitParameter("resourceBase"); if (rrb != null && rb != null) throw new UnavailableException("resourceBase & relativeResourceBase"); if (rb != null) { try { _resourceBase = Resource.newResource(rb); } catch (Exception e) { log.warn(LogSupport.EXCEPTION, e); throw new UnavailableException(e.toString()); } } if (log.isDebugEnabled()) log.debug("resource base = " + _resourceBase); if (_putAllowed) _AllowString += ", PUT"; if (_delAllowed) _AllowString += ", DELETE"; if (_putAllowed && _delAllowed) _AllowString += ", MOVE"; } /* ------------------------------------------------------------ */ private boolean getInitBoolean(String name) { String value = getInitParameter(name); return value != null && value.length() > 0 && (value.startsWith("t") || value.startsWith("T") || value.startsWith("y") || value.startsWith("Y") || value.startsWith("1")); } /* ------------------------------------------------------------ */ private int getInitInt(String name) { String value = getInitParameter(name); if (value != null && value.length() > 0) return Integer.parseInt(value); return -1; } /* ------------------------------------------------------------ */ /** * get Resource to serve. Map a path to a resource. The default implementation calls * HttpContext.getResource but derived servlets may provide their own mapping. * * @param pathInContext The path to find a resource for. * @return The resource to serve. */ protected Resource getResource(String pathInContext) throws IOException { Resource r = (_resourceBase == null) ? _httpContext.getResource(pathInContext) : _resourceBase.addPath(pathInContext); if (log.isDebugEnabled()) log.debug("RESOURCE=" + r); return r; } /* ------------------------------------------------------------ */ protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String servletPath = (String) request.getAttribute(Dispatcher.__INCLUDE_SERVLET_PATH); String pathInfo = null; if (servletPath == null) { servletPath = request.getServletPath(); pathInfo = request.getPathInfo(); } else pathInfo = (String) request.getAttribute(Dispatcher.__INCLUDE_PATH_INFO); String pathInContext = URI.addPaths(servletPath, pathInfo); boolean endsWithSlash = pathInContext.endsWith("/"); Resource resource = getResource(pathInContext); // Is the method allowed? String method = request.getMethod(); if (_AllowString.indexOf(method) < 0) { if (resource != null && resource.exists()) { response.setHeader(HttpFields.__Allow, _AllowString); response.sendError(HttpResponse.__405_Method_Not_Allowed); } else response.sendError(HttpResponse.__404_Not_Found); return; } // Handle the request try { // handle by method. if (method.equals(HttpRequest.__GET) || method.equals(HttpRequest.__POST) || method.equals(HttpRequest.__HEAD)) handleGet(request, response, pathInContext, resource, endsWithSlash); else if (_putAllowed && method.equals(HttpRequest.__PUT)) handlePut(request, response, pathInContext, resource); else if (_delAllowed && method.equals(HttpRequest.__DELETE)) handleDelete(request, response, pathInContext, resource); else if (_putAllowed && _delAllowed && method.equals(HttpRequest.__MOVE)) handleMove(request, response, pathInContext, resource); else if (method.equals(HttpRequest.__OPTIONS)) handleOptions(request, response); else if (method.equals(HttpRequest.__TRACE)) _servletHandler.handleTrace(request, response); else { // anything else... try { if (resource.exists()) response.sendError(HttpResponse.__501_Not_Implemented); else _servletHandler.notFound(request, response); } catch (Exception e) { LogSupport.ignore(log, e); } } } catch (IllegalArgumentException e) { LogSupport.ignore(log, e); } finally { if (resource != null && !(resource instanceof CachedResource)) resource.release(); } } /* ------------------------------------------------------------------- */ public void handleGet(HttpServletRequest request, HttpServletResponse response, String pathInContext, Resource resource, boolean endsWithSlash) throws ServletException, IOException { if (resource == null || !resource.exists()) response.sendError(HttpResponse.__404_Not_Found); else { // check if directory if (resource.isDirectory()) { if (!endsWithSlash && !pathInContext.equals("/")) { String q = request.getQueryString(); StringBuffer buf = request.getRequestURL(); if (q != null && q.length() != 0) { buf.append('?'); buf.append(q); } response.setContentLength(0); response.sendRedirect(response.encodeRedirectURL(URI.addPaths(buf.toString(), "/"))); return; } // See if index file exists String welcome = _httpContext.getWelcomeFile(resource); if (welcome != null) { String ipath = URI.addPaths(pathInContext, welcome); if (_redirectWelcomeFiles) { // Redirect to the index response.setContentLength(0); response.sendRedirect(URI.addPaths(_httpContext.getContextPath(), ipath)); } else { // Forward to the index RequestDispatcher dispatcher = _servletHandler.getRequestDispatcher(ipath); dispatcher.forward(request, response); } return; } // Check modified dates if (!passConditionalHeaders(request, response, resource)) return; // If we got here, no forward to index took place sendDirectory(request, response, resource, pathInContext.length() > 1); } else { // Check modified dates if (!passConditionalHeaders(request, response, resource)) return; // just send it sendData(request, response, pathInContext, resource); } } } /* ------------------------------------------------------------------- */ public void handlePut(HttpServletRequest request, HttpServletResponse response, String pathInContext, Resource resource) throws ServletException, IOException { boolean exists = resource != null && resource.exists(); if (exists && !passConditionalHeaders(request, response, resource)) return; if (pathInContext.endsWith("/")) { if (!exists) { if (!resource.getFile().mkdirs()) response.sendError(HttpResponse.__403_Forbidden, "Directories could not be created"); else { response.setStatus(HttpResponse.__201_Created); response.flushBuffer(); } } else { response.setStatus(HttpResponse.__200_OK); response.flushBuffer(); } } else { try { int toRead = request.getContentLength(); InputStream in = request.getInputStream(); OutputStream out = resource.getOutputStream(); if (toRead >= 0) IO.copy(in, out, toRead); else IO.copy(in, out); out.close(); response.setStatus(exists ? HttpResponse.__200_OK : HttpResponse.__201_Created); response.flushBuffer(); } catch (Exception ex) { log.warn(LogSupport.EXCEPTION, ex); response.sendError(HttpResponse.__403_Forbidden, ex.getMessage()); } } } /* ------------------------------------------------------------------- */ public void handleDelete(HttpServletRequest request, HttpServletResponse response, String pathInContext, Resource resource) throws ServletException, IOException { if (!resource.exists() || !passConditionalHeaders(request, response, resource)) return; try { // delete the file if (resource.delete()) { response.setStatus(HttpResponse.__204_No_Content); response.flushBuffer(); } else response.sendError(HttpResponse.__403_Forbidden); } catch (SecurityException sex) { log.warn(LogSupport.EXCEPTION, sex); response.sendError(HttpResponse.__403_Forbidden, sex.getMessage()); } } /* ------------------------------------------------------------------- */ public void handleMove(HttpServletRequest request, HttpServletResponse response, String pathInContext, Resource resource) throws ServletException, IOException { if (!resource.exists() || !passConditionalHeaders(request, response, resource)) return; String newPath = URI.canonicalPath(request.getHeader("new-uri")); if (newPath == null) { response.sendError(HttpResponse.__400_Bad_Request, "No new-uri"); return; } String contextPath = _httpContext.getContextPath(); if (contextPath != null && !newPath.startsWith(contextPath)) { response.sendError(HttpResponse.__405_Method_Not_Allowed, "Not in context"); return; } try { String newInfo = newPath; if (contextPath != null) newInfo = newInfo.substring(contextPath.length()); Resource newFile = _httpContext.getBaseResource().addPath(newInfo); resource.renameTo(newFile); response.setStatus(HttpResponse.__204_No_Content); response.flushBuffer(); } catch (Exception ex) { log.warn(LogSupport.EXCEPTION, ex); response.sendError(HttpResponse.__500_Internal_Server_Error, "Error:" + ex); return; } } /* ------------------------------------------------------------ */ public void handleOptions(HttpServletRequest request, HttpServletResponse response) throws IOException { // Handle OPTIONS request for entire server // 9.2 response.setIntHeader(HttpFields.__ContentLength, 0); response.setHeader(HttpFields.__Allow, _AllowString); response.flushBuffer(); } /* ------------------------------------------------------------ */ /* * Check modification date headers. */ protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, Resource resource) throws IOException { if (!request.getMethod().equals(HttpRequest.__HEAD) && request.getAttribute(Dispatcher.__INCLUDE_REQUEST_URI) == null) { // If we have meta data for the file // Try a direct match for most common requests. Avoids // parsing the date. ResourceCache.ResourceMetaData metaData = _httpContext.getResourceMetaData(resource); if (metaData != null) { String ifms = request.getHeader(HttpFields.__IfModifiedSince); String mdlm = metaData.getLastModified(); if (ifms != null && mdlm != null && ifms.equals(mdlm)) { response.reset(); response.setStatus(HttpResponse.__304_Not_Modified); response.flushBuffer(); return false; } } long date = 0; // Parse the if[un]modified dates and compare to resource if ((date = request.getDateHeader(HttpFields.__IfUnmodifiedSince)) > 0) { if (resource.lastModified() / 1000 > date / 1000) { response.sendError(HttpResponse.__412_Precondition_Failed); return false; } } if ((date = request.getDateHeader(HttpFields.__IfModifiedSince)) > 0) { if (resource.lastModified() / 1000 <= date / 1000) { response.reset(); response.setStatus(HttpResponse.__304_Not_Modified); response.flushBuffer(); return false; } } } return true; } /* ------------------------------------------------------------------- */ protected void sendDirectory(HttpServletRequest request, HttpServletResponse response, Resource resource, boolean parent) throws IOException { if (!_dirAllowed) { response.sendError(HttpResponse.__403_Forbidden); return; } byte[] data = null; if (resource instanceof CachedResource) data = ((CachedResource) resource).getCachedData(); if (data == null) { String base = URI.addPaths(request.getRequestURI(), "/"); String dir = resource.getListHTML(base, parent); if (dir == null) { response.sendError(HttpResponse.__403_Forbidden, "No directory"); return; } data = dir.getBytes("UTF-8"); if (resource instanceof CachedResource) ((CachedResource) resource).setCachedData(data); } response.setContentType("text/html; charset=UTF-8"); response.setContentLength(data.length); if (!request.getMethod().equals(HttpRequest.__HEAD)) response.getOutputStream().write(data); } /* ------------------------------------------------------------ */ protected void sendData(HttpServletRequest request, HttpServletResponse response, String pathInContext, Resource resource) throws IOException { long resLength = resource.length(); boolean include = request.getAttribute(Dispatcher.__INCLUDE_REQUEST_URI) != null; // Get the output stream (or writer) OutputStream out = null; try { out = response.getOutputStream(); } catch (IllegalStateException e) { out = new WriterOutputStream(response.getWriter()); } // see if there are any range headers Enumeration reqRanges = include ? null : request.getHeaders(HttpFields.__Range); if (reqRanges == null || !reqRanges.hasMoreElements()) { // if there were no ranges, send entire entity Resource data = resource; if (!include) { // look for a gziped content. if (_minGzipLength > 0) { String accept = request.getHeader(HttpFields.__AcceptEncoding); if (accept != null && resLength > _minGzipLength && !pathInContext.endsWith(".gz")) { Resource gz = getResource(pathInContext + ".gz"); if (gz.exists() && accept.indexOf("gzip") >= 0 && request.getAttribute(Dispatcher.__INCLUDE_REQUEST_URI) == null) { response.setHeader(HttpFields.__ContentEncoding, "gzip"); data = gz; resLength = data.length(); } } } writeHeaders(response, resource, resLength); } data.writeTo(out, 0, resLength); return; } // Parse the satisfiable ranges List ranges = InclusiveByteRange.satisfiableRanges(reqRanges, resLength); // if there are no satisfiable ranges, send 416 response if (ranges == null || ranges.size() == 0) { writeHeaders(response, resource, resLength); response.setStatus(HttpResponse.__416_Requested_Range_Not_Satisfiable); response.setHeader(HttpFields.__ContentRange, InclusiveByteRange.to416HeaderRangeString(resLength)); resource.writeTo(out, 0, resLength); return; } // if there is only a single valid range (must be satisfiable // since were here now), send that range with a 216 response if (ranges.size() == 1) { InclusiveByteRange singleSatisfiableRange = (InclusiveByteRange) ranges.get(0); long singleLength = singleSatisfiableRange.getSize(resLength); writeHeaders(response, resource, singleLength); response.setStatus(HttpResponse.__206_Partial_Content); response.setHeader(HttpFields.__ContentRange, singleSatisfiableRange.toHeaderRangeString(resLength)); resource.writeTo(out, singleSatisfiableRange.getFirst(resLength), singleLength); return; } // multiple non-overlapping valid ranges cause a multipart // 216 response which does not require an overall // content-length header // writeHeaders(response, resource, -1); ResourceCache.ResourceMetaData metaData = _httpContext.getResourceMetaData(resource); String encoding = metaData.getMimeType(); MultiPartResponse multi = new MultiPartResponse(response.getOutputStream()); response.setStatus(HttpResponse.__206_Partial_Content); // If the request has a "Request-Range" header then we need to // send an old style multipart/x-byteranges Content-Type. This // keeps Netscape and acrobat happy. This is what Apache does. String ctp; if (request.getHeader(HttpFields.__RequestRange) != null) ctp = "multipart/x-byteranges; boundary="; else ctp = "multipart/byteranges; boundary="; response.setContentType(ctp + multi.getBoundary()); InputStream in = (resource instanceof CachedResource) ? null : resource.getInputStream(); long pos = 0; for (int i = 0; i < ranges.size(); i++) { InclusiveByteRange ibr = (InclusiveByteRange) ranges.get(i); String header = HttpFields.__ContentRange + ": " + ibr.toHeaderRangeString(resLength); multi.startPart(encoding, new String[] { header}); long start = ibr.getFirst(resLength); long size = ibr.getSize(resLength); if (in != null) { // Handle non cached resource if (start < pos) { in.close(); in = resource.getInputStream(); pos = 0; } if (pos < start) { in.skip(start - pos); pos = start; } IO.copy(in, out, size); pos += size; } else // Handle cached resource (resource).writeTo(out, start, size); } if (in != null) in.close(); multi.close(); return; } /* ------------------------------------------------------------ */ protected void writeHeaders(HttpServletResponse response, Resource resource, long count) throws IOException { ResourceCache.ResourceMetaData metaData = _httpContext.getResourceMetaData(resource); response.setContentType(metaData.getMimeType()); if (count != -1) { if (count == resource.length() && response instanceof ServletHttpResponse ) response.setHeader(HttpFields.__ContentLength, metaData.getLength()); else response.setContentLength((int) count); } response.setHeader(HttpFields.__LastModified, metaData.getLastModified()); if (_acceptRanges) response.setHeader(HttpFields.__AcceptRanges, "bytes"); } }