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