// ======================================================================== // 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 org.klomp.snark.web; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.Enumeration; import java.util.List; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.UnavailableException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.i2p.I2PAppContext; import net.i2p.data.ByteArray; import net.i2p.data.DataHelper; import net.i2p.util.ByteCache; import net.i2p.util.Log; import net.i2p.util.SystemVersion; /* ------------------------------------------------------------ */ /** * Based on DefaultServlet from Jetty 6.1.26, heavily simplified * and modified to remove all dependencies on Jetty libs. * * Supports HEAD and GET only, for resources from the .war and local files. * Supports files and resource only. * Supports MIME types with local overrides and additions. * Supports Last-Modified. * Supports single request ranges. * * Does not support directories or "welcome files". * Does not support gzip. * Does not support multiple request ranges. * Does not cache. * * POST returns 405. * Directories return 403. * Jar resources are sent with a long cache directive. * * ------------------------------------------------------------ * * 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, these can be set * on the servlet itself: * <PRE> * * resourceBase Set to replace the context resource base * warBase Path allowed for resource in war * * </PRE> * * * @author Greg Wilkins (gregw) * @author Nigel Canonizado * * @since Jetty 7 */ class BasicServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected transient final I2PAppContext _context; protected transient final Log _log; protected File _resourceBase; private String _warBase; private transient final MimeTypes _mimeTypes; /** same as PeerState.PARTSIZE */ private static final int BUFSIZE = 16*1024; private transient ByteCache _cache = ByteCache.getInstance(16, BUFSIZE); private static final int WAR_CACHE_CONTROL_SECS = 24*60*60; private static final int FILE_CACHE_CONTROL_SECS = 24*60*60; public BasicServlet() { super(); _context = I2PAppContext.getGlobalContext(); _log = _context.logManager().getLog(getClass()); _mimeTypes = new MimeTypes(); } /* ------------------------------------------------------------ */ public void init(ServletConfig cfg) throws ServletException { super.init(cfg); String rb=getInitParameter("resourceBase"); if (rb!=null) { File f = new File(rb); setResourceBase(f); } String wb = getInitParameter("warBase"); if (wb != null) setWarBase(wb); } /** * Files are served from here */ protected void setResourceBase(File base) throws UnavailableException { if (!base.isDirectory()) { _log.log(Log.CRIT, "Configured i2psnark directory " + base + " does not exist"); throw new UnavailableException("Resource base does not exist: " + base); } _resourceBase = base; if (_log.shouldLog(Log.INFO)) _log.info("Resource base is " + _resourceBase); } /** * Only paths starting with this in the path are served */ protected void setWarBase(String base) { if (!base.startsWith("/")) base = '/' + base; if (!base.endsWith("/")) base = base + '/'; _warBase = base; if (_log.shouldLog(Log.INFO)) _log.info("War base is " + _warBase); } /** 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 or null if not existing */ public File getResource(String pathInContext) { if (_resourceBase==null) return null; File r = null; if (!pathInContext.contains("..") && !pathInContext.endsWith("/")) { File f = new File(_resourceBase, pathInContext); if (f.exists()) r = f; } return r; } /** 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 or null. Returns null for directories */ public HttpContent getContent(String pathInContext) { if (_resourceBase==null) return null; HttpContent r = null; if (_warBase != null && pathInContext.startsWith(_warBase)) { r = new JarContent(pathInContext); } else { File f = getResource(pathInContext); // exists && !directory if (f != null && f.isFile()) r = new FileContent(f); } return r; } /* ------------------------------------------------------------ */ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // always starts with a '/' String servletpath = request.getServletPath(); String pathInfo=request.getPathInfo(); // ??? right?? String pathInContext = addPaths(servletpath, pathInfo); // Find the resource and content try { HttpContent content = getContent(pathInContext); // Handle resource if (content == null) { if (_log.shouldLog(Log.WARN)) _log.warn("Not found: " + pathInContext); response.sendError(404); } else { if (passConditionalHeaders(request, response, content)) { if (_log.shouldLog(Log.INFO)) _log.info("Sending: " + content); sendData(request, response, content); } else { if (_log.shouldLog(Log.INFO)) _log.info("Not modified: " + content); } } } catch(IllegalArgumentException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Error sending " + pathInContext, e); if(!response.isCommitted()) response.sendError(500, e.getMessage()); } catch(IOException e) { if (_log.shouldLog(Log.WARN)) // typical browser abort //_log.warn("Error sending", e); _log.warn("Error sending " + pathInContext + ": " + e); throw e; } } /* ------------------------------------------------------------ */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.sendError(405); } /* ------------------------------------------------------------ */ /* (non-Javadoc) * @see javax.servlet.http.HttpServlet#doTrace(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse) */ protected void doTrace(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.sendError(405); } protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.sendError(405); } protected void doDelete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.sendError(405); } /* ------------------------------------------------------------ */ /** Check modification date headers. * @return true to keep going, false if handled here */ protected boolean passConditionalHeaders(HttpServletRequest request,HttpServletResponse response, HttpContent content) throws IOException { try { if (!request.getMethod().equals("HEAD") ) { long ifmsl=request.getDateHeader("If-Modified-Since"); if (ifmsl!=-1) { if (content.getLastModified()/1000 <= ifmsl/1000) { try { response.reset(); } catch (IllegalStateException ise) { // committed return true; } response.setStatus(304); response.flushBuffer(); return false; } } } } catch(IllegalArgumentException iae) { if(!response.isCommitted()) response.sendError(400, iae.getMessage()); throw iae; } return true; } /* ------------------------------------------------------------ */ protected void sendData(HttpServletRequest request, HttpServletResponse response, HttpContent content) throws IOException { InputStream in =null; try { in = content.getInputStream(); } catch (IOException e) { if (_log.shouldLog(Log.WARN)) _log.warn("Not found: " + content); response.sendError(404); return; } OutputStream out =null; try { out = response.getOutputStream(); } catch (IllegalStateException e) { out = new WriterOutputStream(response.getWriter()); } long content_length = content.getContentLength(); // see if there are any range headers Enumeration<?> reqRanges = request.getHeaders("Range"); if (reqRanges == null || !reqRanges.hasMoreElements()) { // if there were no ranges, send entire entity // Write content normally writeHeaders(response,content,content_length); if (content_length >= 0 && request.getMethod().equals("HEAD")) { // if we know the content length, don't send it to be counted if (_log.shouldLog(Log.INFO)) _log.info("HEAD: " + content); } else { // GET or unknown size for HEAD copy(in, out); } return; } // Parse the satisfiable ranges List<InclusiveByteRange> ranges = InclusiveByteRange.satisfiableRanges(reqRanges, content_length); // if there are no satisfiable ranges, send 416 response // Completely punt on multiple ranges (unlike Default) if (ranges == null || ranges.size() != 1) { writeHeaders(response, content, content_length); response.setStatus(416); response.setHeader("Content-Range", InclusiveByteRange.to416HeaderRangeString(content_length)); in.close(); return; } // if there is only a single valid range (must be satisfiable // since were here now), send that range with a 216 response InclusiveByteRange singleSatisfiableRange = ranges.get(0); long singleLength = singleSatisfiableRange.getSize(content_length); writeHeaders(response, content, singleLength); response.setStatus(206); response.setHeader("Content-Range", singleSatisfiableRange.toHeaderRangeString(content_length)); copy(in, singleSatisfiableRange.getFirst(content_length), out, singleLength); } /* ------------------------------------------------------------ */ protected void writeHeaders(HttpServletResponse response,HttpContent content,long count) throws IOException { if (content.getContentType()!=null && response.getContentType()==null) response.setContentType(content.getContentType()); response.setHeader("X-Content-Type-Options", "nosniff"); long lml = content.getLastModified(); if (lml > 0) response.setDateHeader("Last-Modified",lml); if (count != -1) { if (count<Integer.MAX_VALUE) response.setContentLength((int)count); else response.setHeader("Content-Length", Long.toString(count)); } long ct = content.getCacheTime(); if (ct>=0) response.setHeader("Cache-Control", "public, max-age=" + ct); } /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */ /* I2P additions below here */ /** from Jetty HttpContent.java */ public interface HttpContent { String getContentType(); long getLastModified(); /** in seconds */ int getCacheTime(); long getContentLength(); InputStream getInputStream() throws IOException; } private class FileContent implements HttpContent { private final File _file; public FileContent(File file) { _file = file; } /* ------------------------------------------------------------ */ public String getContentType() { //return _mimeTypes.getMimeByExtension(_file.toString()); return getMimeType(_file.toString()); } /* ------------------------------------------------------------ */ public long getLastModified() { return _file.lastModified(); } public int getCacheTime() { return FILE_CACHE_CONTROL_SECS; } /* ------------------------------------------------------------ */ public long getContentLength() { return _file.length(); } /* ------------------------------------------------------------ */ public InputStream getInputStream() throws IOException { return new BufferedInputStream(new FileInputStream(_file)); } @Override public String toString() { return "File \"" + _file + '"'; } } private class JarContent implements HttpContent { private final String _path; public JarContent(String path) { _path = path; } /* ------------------------------------------------------------ */ public String getContentType() { return getMimeType(_path); } /* ------------------------------------------------------------ */ public long getLastModified() { String cpath = getServletContext().getContextPath(); // this won't work if we aren't at top level String cname = "".equals(cpath) ? "i2psnark" : cpath.substring(1).replace("/", "_"); return (new File(_context.getBaseDir(), "webapps/" + cname + ".war")).lastModified(); } public int getCacheTime() { return WAR_CACHE_CONTROL_SECS; } /* ------------------------------------------------------------ */ public long getContentLength() { return -1; } /* ------------------------------------------------------------ */ public InputStream getInputStream() throws IOException { InputStream rv = getServletContext().getResourceAsStream(_path); if (rv == null) throw new IOException("Not found"); return rv; } @Override public String toString() { return "Jar resource \"" + _path + '"'; } } /** * @param resourcePath in the classpath, without ".properties" extension */ protected void loadMimeMap(String resourcePath) { _mimeTypes.loadMimeMap(resourcePath); } /* ------------------------------------------------------------ */ /** Get the MIME type by filename extension. * @param filename A file name * @return MIME type matching the longest dot extension of the * file name. */ protected String getMimeType(String filename) { String rv = _mimeTypes.getMimeByExtension(filename); if (rv != null) return rv; return getServletContext().getMimeType(filename); } protected void addMimeMapping(String extension, String type) { _mimeTypes.addMimeMapping(extension, type); } /** * Simple version of URIUtil.addPaths() * @param path may be null */ protected static String addPaths(String base, String path) { if (path == null) return base; String rv = (new File(base, path)).toString(); if (SystemVersion.isWindows()) rv = rv.replace("\\", "/"); return rv; } /** * Simple version of URIUtil.decodePath() */ protected static String decodePath(String path) throws MalformedURLException { if (!path.contains("%")) return path; try { URI uri = new URI(path); return uri.getPath(); } catch (URISyntaxException use) { // for ease of use, since a USE is not an IOE but a MUE is... throw new MalformedURLException(use.getMessage()); } } /** * Simple version of URIUtil.encodePath() */ protected static String encodePath(String path) /* throws MalformedURLException */ { // Does NOT handle a ':' correctly, throws MUE. // Can't convert to %3a before hand or the % gets escaped //try { // URI uri = new URI(null, null, path, null); // return uri.toString(); //} catch (URISyntaxException use) { // // for ease of use, since a USE is not an IOE but a MUE is... // throw new MalformedURLException(use.getMessage()); //} return URIUtil.encodePath(path); } /** * Write from in to out */ private void copy(InputStream in, OutputStream out) throws IOException { copy(in, 0, out, -1); } /** * Write from in to out */ private void copy(InputStream in, long skip, OutputStream out, final long len) throws IOException { ByteArray ba = _cache.acquire(); byte[] buf = ba.getData(); try { if (skip > 0) DataHelper.skip(in, skip); int read = 0; long tot = 0; boolean done = false; while ( (read = in.read(buf)) != -1 && !done) { if (len >= 0) { tot += read; if (tot >= len) { read -= (int) (tot - len); done = true; } } out.write(buf, 0, read); } } finally { _cache.release(ba, false); if (in != null) try { in.close(); } catch (IOException ioe) {} if (out != null) try { out.close(); } catch (IOException ioe) {} } } }