diff --git a/webui/build.gradle b/webui/build.gradle
index b7d347a4..6b71cab1 100644
--- a/webui/build.gradle
+++ b/webui/build.gradle
@@ -20,6 +20,9 @@ war {
from ('src/main/js', {
into "js"
})
+ from ('src/main/resources', {
+ into "WEB-INF/classes/com/muwire/webui"
+ })
webInf {
from "$buildDir/compiledJsps"
into "classes"
@@ -100,6 +103,7 @@ task poupdate {
precompileJsp.dependsOn compileJava
generateWebXML.dependsOn precompileJsp
bundle.dependsOn precompileJsp
+poupdate.dependsOn precompileJsp
war.dependsOn generateWebXML, bundle
artifacts {
diff --git a/webui/src/main/java/com/muwire/webui/BasicServlet.java b/webui/src/main/java/com/muwire/webui/BasicServlet.java
new file mode 100644
index 00000000..1f9f2055
--- /dev/null
+++ b/webui/src/main/java/com/muwire/webui/BasicServlet.java
@@ -0,0 +1,534 @@
+// ========================================================================
+// 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 com.muwire.webui;
+
+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.servlet.util.WriterOutputStream;
+import net.i2p.util.ByteCache;
+import net.i2p.util.Log;
+import net.i2p.util.SecureFile;
+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:
+ *
+ *
+ * resourceBase Set to replace the context resource base
+
+ *
+ *
+ *
+ * @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 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 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 SecureFile(rb);
+ setResourceBase(f);
+ }
+ }
+
+ /**
+ * Files are served from here
+ */
+ protected synchronized void setResourceBase(File base) throws UnavailableException {
+ if (!base.isDirectory()) {
+ _log.error("Configured 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);
+ }
+
+ /** 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)
+ {
+ File r = null;
+ if (!pathInContext.contains("..") &&
+ !pathInContext.endsWith("/")) {
+ File f;
+ synchronized (this) {
+ if (_resourceBase==null)
+ return null;
+ 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)
+ {
+ HttpContent r = null;
+ 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.getOutputStream().close();
+ 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 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
+ {
+ String rtype = response.getContentType();
+ String ctype = content.getContentType();
+ if (rtype != null) {
+ if (rtype.equals("application/javascript"))
+ response.setCharacterEncoding("ISO-8859-1");
+ } else if (ctype != null) {
+ response.setContentType(ctype);
+ if (ctype.equals("application/javascript"))
+ response.setCharacterEncoding("ISO-8859-1");
+ }
+ 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));
+ response.setHeader("Accept-Ranges", "bytes");
+ } else {
+ response.setHeader("Accept-Ranges", "none");
+ }
+
+ // add name header for muwire since the URL is just the hash
+ String name = content.getContentName();
+ String name2 = FilenameUtil.sanitizeFilename(name);
+ String name3 = FilenameUtil.encodeFilenameRFC5987(name);
+ response.addHeader("Content-Disposition", "inline; filename=\"" + name2 + "\"; " +
+ "filename*=" + name3);
+
+ 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();
+ String getContentName();
+ 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 String getContentName()
+ {
+ return _file.getName();
+ }
+
+ /* ------------------------------------------------------------ */
+ 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 + '"'; }
+ }
+
+ /**
+ * @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;
+ }
+
+ /**
+ * 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) {}
+ }
+ }
+}
diff --git a/webui/src/main/java/com/muwire/webui/DownloadedContentServlet.java b/webui/src/main/java/com/muwire/webui/DownloadedContentServlet.java
new file mode 100644
index 00000000..48b0f23d
--- /dev/null
+++ b/webui/src/main/java/com/muwire/webui/DownloadedContentServlet.java
@@ -0,0 +1,29 @@
+package com.muwire.webui;
+
+import java.io.File;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+
+public class DownloadedContentServlet extends BasicServlet {
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ super.init(config);
+ loadMimeMap("com/muwire/webui/mime");
+ }
+
+ /**
+ * Find the file for the hash.
+ *
+ * @param pathInContext should always start with /
+ * @return file or null
+ */
+ @Override
+ public File getResource(String pathInContext)
+ {
+ File r = null;
+ // TODO
+ return r;
+ }
+}
diff --git a/webui/src/main/java/com/muwire/webui/FilenameUtil.java b/webui/src/main/java/com/muwire/webui/FilenameUtil.java
new file mode 100644
index 00000000..bd7c993b
--- /dev/null
+++ b/webui/src/main/java/com/muwire/webui/FilenameUtil.java
@@ -0,0 +1,87 @@
+package com.muwire.webui;
+
+import net.i2p.data.DataHelper;
+
+/**
+ * File name encoding methods
+ *
+ * From SusiMail. GPLv2 or any later version.
+ */
+public class FilenameUtil {
+
+ /**
+ * Convert the UTF-8 to ASCII suitable for inclusion in a header
+ * and for use as a cross-platform filename.
+ * Replace chars likely to be illegal in filenames,
+ * and non-ASCII chars, with _
+ *
+ * Ref: RFC 6266, RFC 5987, i2psnark Storage.ILLEGAL
+ *
+ * @since 0.9.18
+ */
+ public static String sanitizeFilename(String name) {
+ name = name.trim();
+ StringBuilder buf = new StringBuilder(name.length());
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ // illegal filename chars
+ if (c <= 32 || c >= 0x7f ||
+ c == '<' || c == '>' || c == ':' || c == '"' ||
+ c == '/' || c == '\\' || c == '|' || c == '?' ||
+ c == '*')
+ buf.append('_');
+ else
+ buf.append(c);
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Encode the UTF-8 suitable for inclusion in a header
+ * as a RFC 5987/6266 filename* value, and for use as a cross-platform filename.
+ * Replace chars likely to be illegal in filenames with _
+ *
+ * Ref: RFC 6266, RFC 5987, i2psnark Storage.ILLEGAL
+ *
+ * This does NOT do multiline, e.g. filename*0* (RFC 2231)
+ *
+ * ref: https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
+ * ref: RFC 2231
+ *
+ * @since 0.9.33
+ */
+ public static String encodeFilenameRFC5987(String name) {
+ name = name.trim();
+ StringBuilder buf = new StringBuilder(name.length());
+ buf.append("utf-8''");
+ for (int i = 0; i < name.length(); i++) {
+ char c = name.charAt(i);
+ // illegal filename chars
+ if (c < 32 || (c >= 0x7f && c <= 0x9f) ||
+ c == '<' || c == '>' || c == ':' || c == '"' ||
+ c == '/' || c == '\\' || c == '|' || c == '?' ||
+ c == '*' ||
+ // unicode newlines
+ c == 0x2028 || c == 0x2029) {
+ buf.append('_');
+ } else if (c == ' ' || c == '\'' || c == '%' || // not in 5987 attr-char
+ c == '(' || c == ')' || c == '@' || // 2616 separators
+ c == ',' || c == ';' || c == '[' || c == ']' ||
+ c == '=' || c == '{' || c == '}') {
+ // single byte encoding
+ buf.append(HexTable.table[c].replace('=', '%'));
+ } else if (c < 0x7f) {
+ // single byte char, as-is
+ buf.append(c);
+ } else {
+ // multi-byte encoding
+ byte[] utf = DataHelper.getUTF8(String.valueOf(c));
+ for (int j = 0; j < utf.length; j++) {
+ int b = utf[j] & 0xff;
+ buf.append(HexTable.table[b].replace('=', '%'));
+ }
+ }
+ }
+ return buf.toString();
+ }
+}
diff --git a/webui/src/main/java/com/muwire/webui/HexTable.java b/webui/src/main/java/com/muwire/webui/HexTable.java
new file mode 100644
index 00000000..0148191e
--- /dev/null
+++ b/webui/src/main/java/com/muwire/webui/HexTable.java
@@ -0,0 +1,73 @@
+/*
+ * Created on Nov 12, 2004
+ *
+ * This file is part of susimail project, see http://susi.i2p/
+ *
+ * Copyright (C) 2004-2005
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * $Revision: 1.2 $
+ */
+package com.muwire.webui;
+
+/**
+ * From SusiMail.
+ *
+ * @author susi
+ */
+public class HexTable {
+
+ /**
+ * Three character strings, upper case, e.g. "=0A"
+ */
+ public static final String[] table = new String[256];
+
+ static {
+ for( int i = 0; i < 256; i++ ) {
+ String str = intToHex( i );
+ if( str.length() == 1 )
+ str = "0" + str;
+ table[i] = "=" + str;
+ }
+ }
+
+ private static String intToHex( int b )
+ {
+ if( b == 0 )
+ return "0";
+ else {
+ String str = "";
+ while( b > 0 ) {
+ byte c = (byte)(b % 16);
+ if( c < 10 )
+ c += '0';
+ else
+ c += 'A' - 10;
+ str = "" + (char)c + str;
+ b = (byte)(b / 16);
+ }
+ return str;
+ }
+ }
+
+/****
+ public static void main(String[] args) {
+ for( int i = 0; i < 256; i++ ) {
+ System.out.println(i + ": " + table[i]);
+ }
+ }
+****/
+}
diff --git a/webui/src/main/java/com/muwire/webui/InclusiveByteRange.java b/webui/src/main/java/com/muwire/webui/InclusiveByteRange.java
new file mode 100644
index 00000000..4065774c
--- /dev/null
+++ b/webui/src/main/java/com/muwire/webui/InclusiveByteRange.java
@@ -0,0 +1,218 @@
+//
+// ========================================================================
+// Copyright (c) 1995-2013 Mort Bay Consulting Pty. Ltd.
+// ------------------------------------------------------------------------
+// All rights reserved. This program and the accompanying materials
+// are made available under the terms of the Eclipse Public License v1.0
+// and Apache License v2.0 which accompanies this distribution.
+//
+// The Eclipse Public License is available at
+// http://www.eclipse.org/legal/epl-v10.html
+//
+// The Apache License v2.0 is available at
+// http://www.opensource.org/licenses/apache2.0.php
+//
+// You may elect to redistribute this code under either of these licenses.
+// ========================================================================
+//
+
+package com.muwire.webui;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.StringTokenizer;
+
+/* ------------------------------------------------------------ */
+/** Byte range inclusive of end points.
+ *
+ *
+ * parses the following types of byte ranges:
+ *
+ * bytes=100-499
+ * bytes=-300
+ * bytes=100-
+ * bytes=1-2,2-3,6-,-2
+ *
+ * given an entity length, converts range to string
+ *
+ * bytes 100-499/500
+ *
+ *
+ *
+ * Based on RFC2616 3.12, 14.16, 14.35.1, 14.35.2
+ * @version $version$
+ *
+ */
+public class InclusiveByteRange
+{
+ long first = 0;
+ long last = 0;
+
+ public InclusiveByteRange(long first, long last)
+ {
+ this.first = first;
+ this.last = last;
+ }
+
+ public long getFirst()
+ {
+ return first;
+ }
+
+ public long getLast()
+ {
+ return last;
+ }
+
+
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @param headers Enumeration of Range header fields.
+ * @param size Size of the resource.
+ * @return List of satisfiable ranges
+ */
+ public static List satisfiableRanges(Enumeration> headers, long size)
+ {
+ List satRanges = null;
+
+ // walk through all Range headers
+ headers:
+ while (headers.hasMoreElements())
+ {
+ String header = (String) headers.nextElement();
+ StringTokenizer tok = new StringTokenizer(header,"=,",false);
+ String t=null;
+ try
+ {
+ // read all byte ranges for this header
+ while (tok.hasMoreTokens())
+ {
+ try
+ {
+ t = tok.nextToken().trim();
+
+ long first = -1;
+ long last = -1;
+ int d = t.indexOf('-');
+ if (d < 0 || t.indexOf('-',d + 1) >= 0)
+ {
+ if ("bytes".equals(t))
+ continue;
+ continue headers;
+ }
+ else if (d == 0)
+ {
+ if (d + 1 < t.length())
+ last = Long.parseLong(t.substring(d + 1).trim());
+ else
+ {
+ continue;
+ }
+ }
+ else if (d + 1 < t.length())
+ {
+ first = Long.parseLong(t.substring(0,d).trim());
+ last = Long.parseLong(t.substring(d + 1).trim());
+ }
+ else
+ first = Long.parseLong(t.substring(0,d).trim());
+
+ if (first == -1 && last == -1)
+ continue headers;
+
+ if (first != -1 && last != -1 && (first > last))
+ continue headers;
+
+ if (first < size)
+ {
+ if (satRanges == null)
+ satRanges = new ArrayList(4);
+ InclusiveByteRange range = new InclusiveByteRange(first,last);
+ satRanges.add(range);
+ }
+ }
+ catch (NumberFormatException e)
+ {
+ continue;
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ }
+ }
+ return satRanges;
+ }
+
+ /* ------------------------------------------------------------ */
+ public long getFirst(long size)
+ {
+ if (first<0)
+ {
+ long tf=size-last;
+ if (tf<0)
+ tf=0;
+ return tf;
+ }
+ return first;
+ }
+
+ /* ------------------------------------------------------------ */
+ public long getLast(long size)
+ {
+ if (first<0)
+ return size-1;
+
+ if (last<0 ||last>=size)
+ return size-1;
+ return last;
+ }
+
+ /* ------------------------------------------------------------ */
+ public long getSize(long size)
+ {
+ return getLast(size)-getFirst(size)+1;
+ }
+
+
+ /* ------------------------------------------------------------ */
+ public String toHeaderRangeString(long size)
+ {
+ StringBuilder sb = new StringBuilder(40);
+ sb.append("bytes ");
+ sb.append(getFirst(size));
+ sb.append('-');
+ sb.append(getLast(size));
+ sb.append("/");
+ sb.append(size);
+ return sb.toString();
+ }
+
+ /* ------------------------------------------------------------ */
+ public static String to416HeaderRangeString(long size)
+ {
+ StringBuilder sb = new StringBuilder(40);
+ sb.append("bytes */");
+ sb.append(size);
+ return sb.toString();
+ }
+
+
+ /* ------------------------------------------------------------ */
+ @Override
+ public String toString()
+ {
+ StringBuilder sb = new StringBuilder(60);
+ sb.append(Long.toString(first));
+ sb.append(":");
+ sb.append(Long.toString(last));
+ return sb.toString();
+ }
+
+
+}
+
+
+
diff --git a/webui/src/main/java/com/muwire/webui/MimeTypes.java b/webui/src/main/java/com/muwire/webui/MimeTypes.java
new file mode 100644
index 00000000..c9c1666d
--- /dev/null
+++ b/webui/src/main/java/com/muwire/webui/MimeTypes.java
@@ -0,0 +1,129 @@
+// ========================================================================
+// Copyright 2000-2005 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 com.muwire.webui;
+
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+import java.util.concurrent.ConcurrentHashMap;
+
+
+/* ------------------------------------------------------------ */
+/**
+ * Based on MimeTypes from Jetty 6.1.26, heavily simplified
+ * and modified to remove all dependencies on Jetty libs.
+ *
+ * Supports mime types only, not encodings.
+ * Does not support a default "*" mapping.
+ *
+ * This is only for local mappings.
+ * Caller should use getServletContext().getMimeType() if this returns null.
+ *
+ *
+ * ------------------------------------------------------------
+ *
+ * @author Greg Wilkins
+ *
+ * @since Jetty 7
+ */
+class MimeTypes
+{
+
+ private final Map _mimeMap;
+
+ public MimeTypes() {
+ _mimeMap = new ConcurrentHashMap();
+ }
+
+ /* ------------------------------------------------------------ */
+ /**
+ * @param resourcePath A Map of file extension to mime-type.
+ */
+ public void loadMimeMap(String resourcePath) {
+ loadMimeMap(_mimeMap, resourcePath);
+ }
+
+ /**
+ * Tries both webapp and system class loader, since Jetty blocks
+ * its classes from the webapp class loader.
+ */
+ private static void loadMimeMap(Map map, String resourcePath) {
+ try
+ {
+ ResourceBundle mime;
+ try {
+ mime = ResourceBundle.getBundle(resourcePath);
+ } catch(MissingResourceException e) {
+ // Jetty 7 webapp classloader blocks jetty classes
+ // http://wiki.eclipse.org/Jetty/Reference/Jetty_Classloading
+ //System.out.println("No mime types loaded from " + resourcePath + ", trying system classloader");
+ mime = ResourceBundle.getBundle(resourcePath, Locale.getDefault(), ClassLoader.getSystemClassLoader());
+ }
+ Enumeration i = mime.getKeys();
+ while(i.hasMoreElements())
+ {
+ String ext = i.nextElement();
+ String m = mime.getString(ext);
+ map.put(ext.toLowerCase(Locale.US), m);
+ }
+ //System.out.println("Loaded " + map.size() + " mime types from " + resourcePath);
+ } catch(MissingResourceException e) {
+ //System.out.println("No mime types loaded from " + resourcePath);
+ }
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Get the MIME type by filename extension.
+ *
+ * Returns ONLY local mappings.
+ * Caller should use getServletContext().getMimeType() if this returns null.
+ *
+ * @param filename A file name
+ * @return MIME type matching the longest dot extension of the
+ * file name.
+ */
+ public String getMimeByExtension(String filename)
+ {
+ String type=null;
+
+ if (filename!=null)
+ {
+ int i=-1;
+ while(type==null)
+ {
+ i=filename.indexOf('.',i+1);
+
+ if (i<0 || i>=filename.length())
+ break;
+
+ String ext=filename.substring(i+1).toLowerCase(Locale.US);
+ type = _mimeMap.get(ext);
+ }
+ }
+ return type;
+ }
+
+ /* ------------------------------------------------------------ */
+ /** Set a mime mapping
+ * @param extension
+ * @param type
+ */
+ public void addMimeMapping(String extension, String type)
+ {
+ _mimeMap.put(extension.toLowerCase(Locale.US), type);
+ }
+}
diff --git a/webui/src/main/java/com/muwire/webui/SharedContentServlet.java b/webui/src/main/java/com/muwire/webui/SharedContentServlet.java
new file mode 100644
index 00000000..239393d5
--- /dev/null
+++ b/webui/src/main/java/com/muwire/webui/SharedContentServlet.java
@@ -0,0 +1,29 @@
+package com.muwire.webui;
+
+import java.io.File;
+
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+
+public class SharedContentServlet extends BasicServlet {
+
+ @Override
+ public void init(ServletConfig config) throws ServletException {
+ super.init(config);
+ loadMimeMap("com/muwire/webui/mime");
+ }
+
+ /**
+ * Find the file for the hash.
+ *
+ * @param pathInContext should always start with /
+ * @return file or null
+ */
+ @Override
+ public File getResource(String pathInContext)
+ {
+ File r = null;
+ // TODO
+ return r;
+ }
+}
diff --git a/webui/src/main/resources/mime.properties b/webui/src/main/resources/mime.properties
new file mode 100644
index 00000000..a4c7b8b0
--- /dev/null
+++ b/webui/src/main/resources/mime.properties
@@ -0,0 +1,59 @@
+3gp = video/3gpp
+3gpp = video/3gpp
+7z = application/x-7z-compressed
+ape = audio/x-monkeys-audio
+bz2 = application/x-bzip2
+cue = application/x-cue
+dmg = application/apple-diskimage
+epub = application/epub+zip
+flac = audio/flac
+flv = video/x-flv
+iso = application/x-iso9660-image
+m3u = audio/mpegurl
+m3u8 = audio/mpegurl
+m4a = audio/mp4a-latm
+m4b = audio/mp4a-latm
+m4v = video/x-m4v
+mka = audio/x-matroska
+mkv = video/x-matroska
+mobi = application/x-mobipocket-ebook
+mp4 = video/mp4
+mpc = audio/x-musepack
+nfo = text/plain
+odb = application/vnd.oasis.opendocument.database
+odc = application/vnd.oasis.opendocument.chart
+odf = application/vnd.oasis.opendocument.formula
+odg = application/vnd.oasis.opendocument.graphics
+odi = application/vnd.oasis.opendocument.image
+odm = application/vnd.oasis.opendocument.text-master
+odp = application/vnd.oasis.opendocument.presentation
+ods = application/vnd.oasis.opendocument.spreadsheet
+odt = application/vnd.oasis.opendocument.text
+ogm = video/ogg
+ogv = video/ogg
+oga = audio/ogg
+opus = audio/ogg
+otc = application/vnd.oasis.opendocument.chart-template
+otf = application/vnd.oasis.opendocument.formula-template
+otg = application/vnd.oasis.opendocument.graphics-template
+oth = application/vnd.oasis.opendocument.text-web
+oti = application/vnd.oasis.opendocument.image-template
+otp = application/vnd.oasis.opendocument.presentation-template
+ots = application/vnd.oasis.opendocument.spreadsheet-template
+ott = application/vnd.oasis.opendocument.text-template
+pls = audio/x-scpls
+rar = application/x-rar-compressed
+sfv = text/x-sfv
+su2 = application/zip
+su3 = application/zip
+sud = application/zip
+tbz = application/x-bzip2
+torrent = application/x-bittorrent
+txt = text/plain
+war = application/java-archive
+webm = video/webm
+wma = audio/x-ms-wma
+wmv = video/x-ms-wmv
+wpl = application/vnd.ms-wpl
+xspf = application/xspf+xml
+xz = application/x-xz