diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml
index fdecd0f46..025c3f1db 100644
--- a/apps/i2psnark/java/build.xml
+++ b/apps/i2psnark/java/build.xml
@@ -104,9 +104,11 @@
+
+
-
+
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/BasicServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/BasicServlet.java
new file mode 100644
index 000000000..3fea3a0b0
--- /dev/null
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/BasicServlet.java
@@ -0,0 +1,547 @@
+// ========================================================================
+// 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 javax.servlet.ServletConfig;
+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 net.i2p.I2PAppContext;
+import net.i2p.data.ByteArray;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+
+
+/* ------------------------------------------------------------ */
+/**
+ * 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.
+ *
+ * Does not support directories or "welcome files".
+ * Does not support gzip.
+ * Does not support request ranges.
+ * Does not cache.
+ *
+ * HEAD and POST return 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 either
+ * on the servlet itself or as ServletContext initParameters with a prefix
+ * of org.mortbay.jetty.servlet.Default. :
+ *
+ *
+ * resourceBase Set to replace the context resource base
+
+ * warBase Path allowed for resource in war
+ *
+ *
+ *
+ *
+ * @author Greg Wilkins (gregw)
+ * @author Nigel Canonizado
+ *
+ * @since Jetty 7
+ */
+class BasicServlet extends HttpServlet
+{
+ protected final I2PAppContext _context;
+ protected final Log _log;
+ protected File _resourceBase;
+ private String _warBase;
+
+ private final MimeTypes _mimeTypes;
+
+ /** same as PeerState.PARTSIZE */
+ private static final int BUFSIZE = 16*1024;
+ private 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())
+ 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 if (!pathInContext.contains("..") &&
+ !pathInContext.endsWith("/")) {
+ File f = new File(_resourceBase, pathInContext);
+ // exists && !directory
+ if (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)
+ {
+ response.reset();
+ 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());
+ }
+
+ // Write content normally
+ long content_length = content.getContentLength();
+ 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;
+ }
+
+ /* ------------------------------------------------------------ */
+ protected void writeHeaders(HttpServletResponse response,HttpContent content,long count)
+ throws IOException
+ {
+ if (content.getContentType()!=null && response.getContentType()==null)
+ response.setContentType(content.getContentType());
+
+ long lml = content.getLastModified();
+ if (lml>=0)
+ response.setDateHeader("Last-Modified",lml);
+
+ if (count != -1)
+ {
+ if (count=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()
+ {
+ return (new File(_context.getBaseDir(), "webapps/i2psnark.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;
+ return (new File(base, path)).toString();
+ }
+
+ /**
+ * 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 {
+ 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());
+ }
+ }
+
+ /**
+ * Write from in to out
+ */
+ private void copy(InputStream in, OutputStream out) throws IOException {
+ ByteArray ba = _cache.acquire();
+ byte[] buf = ba.getData();
+ try {
+ int read = 0;
+ while ( (read = in.read(buf)) != -1) {
+ 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) {}
+ }
+ }
+}
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index c8aa8e1c1..ea8875626 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -24,6 +24,7 @@ import java.util.TreeSet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -44,31 +45,28 @@ import org.klomp.snark.Tracker;
import org.klomp.snark.TrackerClient;
import org.klomp.snark.dht.DHT;
-import org.eclipse.jetty.servlet.DefaultServlet;
-import org.eclipse.jetty.util.URIUtil;
-import org.eclipse.jetty.util.resource.Resource;
-
/**
- * We extend Default instead of HTTPServlet so we can handle
- * i2psnark/ file requests with http:// instead of the flaky and
- * often-blocked-by-the-browser file://
+ * Refactored to eliminate Jetty dependencies
*/
-public class I2PSnarkServlet extends DefaultServlet {
- private I2PAppContext _context;
- private Log _log;
+public class I2PSnarkServlet extends BasicServlet {
+ /** generally "/i2psnark" */
+ private String _contextPath;
private SnarkManager _manager;
private static long _nonce;
- private Resource _resourceBase;
private String _themePath;
private String _imgPath;
private String _lastAnnounceURL;
public static final String PROP_CONFIG_FILE = "i2psnark.configFile";
+ public I2PSnarkServlet() {
+ super();
+ }
+
@Override
public void init(ServletConfig cfg) throws ServletException {
- _context = I2PAppContext.getGlobalContext();
- _log = _context.logManager().getLog(I2PSnarkServlet.class);
+ super.init(cfg);
+ _contextPath = getServletContext().getContextPath();
_nonce = _context.random().nextLong();
_manager = new SnarkManager(_context);
String configFile = _context.getProperty(PROP_CONFIG_FILE);
@@ -76,10 +74,9 @@ public class I2PSnarkServlet extends DefaultServlet {
configFile = "i2psnark.config";
_manager.loadConfig(configFile);
_manager.start();
- try {
- _resourceBase = Resource.newResource(_manager.getDataDir().getAbsolutePath());
- } catch (IOException ioe) {}
- super.init(cfg);
+ loadMimeMap("org/klomp/snark/web/mime");
+ setResourceBase(_manager.getDataDir());
+ setWarBase("/.icons/");
}
@Override
@@ -95,17 +92,13 @@ public class I2PSnarkServlet extends DefaultServlet {
* and we can't get any resources (like icons) out of the .war
*/
@Override
- public Resource getResource(String pathInContext)
+ public File getResource(String pathInContext)
{
if (pathInContext == null || pathInContext.equals("/") || pathInContext.equals("/index.jsp") ||
pathInContext.equals("/index.html") || pathInContext.startsWith("/.icons/"))
return super.getResource(pathInContext);
// files in the i2psnark/ directory
- try {
- return _resourceBase.addPath(pathInContext);
- } catch (IOException ioe) {
- throw new RuntimeException(ioe);
- }
+ return new File(_resourceBase, pathInContext);
}
/**
@@ -139,6 +132,7 @@ public class I2PSnarkServlet extends DefaultServlet {
*/
@Override
public void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+ //_log.error("Service " + req.getMethod() + " \"" + req.getContextPath() + "\" \"" + req.getServletPath() + "\" \"" + req.getPathInfo() + '"');
// since we are not overriding handle*(), do this here
String method = req.getMethod();
if (!(method.equals("GET") || method.equals("HEAD") || method.equals("POST"))) {
@@ -178,15 +172,15 @@ public class I2PSnarkServlet extends DefaultServlet {
if (path.endsWith("/")) {
// bypass the horrid Resource.getListHTML()
String pathInfo = req.getPathInfo();
- String pathInContext = URIUtil.addPaths(path, pathInfo);
+ String pathInContext = addPaths(path, pathInfo);
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html; charset=UTF-8");
- Resource resource = getResource(pathInContext);
- if (resource == null || (!resource.exists())) {
+ File resource = getResource(pathInContext);
+ if (resource == null) {
resp.sendError(404);
} else {
- String base = URIUtil.addPaths(req.getRequestURI(), "/");
+ String base = addPaths(req.getRequestURI(), "/");
String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null);
if (method.equals("POST")) {
// P-R-G
@@ -2031,7 +2025,7 @@ public class I2PSnarkServlet extends DefaultServlet {
* @return String of HTML or null if postParams != null
* @since 0.7.14
*/
- private String getListHTML(Resource r, String base, boolean parent, Map postParams)
+ private String getListHTML(File r, String base, boolean parent, Map postParams)
throws IOException
{
String[] ls = null;
@@ -2040,7 +2034,7 @@ public class I2PSnarkServlet extends DefaultServlet {
Arrays.sort(ls, Collator.getInstance());
} // if r is not a directory, we are only showing torrent info section
- String title = URIUtil.decodePath(base);
+ String title = decodePath(base);
if (title.startsWith("/i2psnark/"))
title = title.substring("/i2psnark/".length());
@@ -2228,7 +2222,7 @@ public class I2PSnarkServlet extends DefaultServlet {
.append("\">\n");
buf.append("\n\n");
buf.append("