diff --git a/apps/jetty/build.xml b/apps/jetty/build.xml
index 7f500eca19..cd59f01587 100644
--- a/apps/jetty/build.xml
+++ b/apps/jetty/build.xml
@@ -304,6 +304,7 @@
+
@@ -318,7 +319,7 @@
debug="true" deprecation="on" source="${javac.version}" target="${javac.version}"
destdir="./build/obj"
includeAntRuntime="false"
- classpath="../../core/java/build/i2p.jar:./jettylib/commons-logging.jar:./jettylib/javax.servlet.jar:./jettylib/org.mortbay.jetty.jar:./jettylib/jetty-http.jar:./jettylib/jetty-io.jar:./jettylib/jetty-security.jar:./jettylib/jetty-util.jar:./jettylib/jetty-xml.jar" >
+ classpath="../../core/java/build/i2p.jar:./jettylib/commons-logging.jar:./jettylib/javax.servlet.jar:./jettylib/org.mortbay.jetty.jar:./jettylib/jetty-http.jar:./jettylib/jetty-io.jar:./jettylib/jetty-security.jar:./jettylib/jetty-servlet.jar:./jettylib/jetty-util.jar:./jettylib/jetty-xml.jar" >
diff --git a/apps/jetty/java/src/net/i2p/servlet/I2PDefaultServlet.java b/apps/jetty/java/src/net/i2p/servlet/I2PDefaultServlet.java
new file mode 100644
index 0000000000..455a8fe498
--- /dev/null
+++ b/apps/jetty/java/src/net/i2p/servlet/I2PDefaultServlet.java
@@ -0,0 +1,316 @@
+// Contains code from Jetty 9.2.21:
+
+//
+// ========================================================================
+// Copyright (c) 1995-2017 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 net.i2p.servlet;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import javax.servlet.ServletContext;
+import javax.servlet.UnavailableException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.util.StringUtil;
+import org.eclipse.jetty.util.URIUtil;
+import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.resource.ResourceCollection;
+
+
+
+/**
+ * Extends DefaultServlet to set locale for the displayed time of directory listings,
+ * to prevent leaking of the locale.
+ *
+ * @since 0.9.31
+ *
+ */
+public class I2PDefaultServlet extends DefaultServlet
+{
+ // shadows of private fields in super
+ private ContextHandler _contextHandler;
+ private boolean _dirAllowed=true;
+ private Resource _resourceBase;
+ private Resource _stylesheet;
+
+ private static final String FORMAT = "yyyy-MM-dd HH:mm";
+
+ /**
+ * Overridden to save local copies of dirAllowed, locale, resourceBase, and stylesheet.
+ * Calls super.
+ */
+ @Override
+ public void init()
+ throws UnavailableException
+ {
+ super.init();
+ _dirAllowed=getInitBoolean("dirAllowed",_dirAllowed);
+
+ String rb=getInitParameter("resourceBase");
+ if (rb!=null)
+ {
+ try{_resourceBase=_contextHandler.newResource(rb);}
+ catch (Exception e)
+ {
+ throw new UnavailableException(e.toString());
+ }
+ }
+
+ String css=getInitParameter("stylesheet");
+ try
+ {
+ if(css!=null)
+ {
+ _stylesheet = Resource.newResource(css);
+ if(!_stylesheet.exists())
+ {
+ _stylesheet = null;
+ }
+ }
+ if(_stylesheet == null)
+ {
+ _stylesheet = Resource.newResource(this.getClass().getResource("/jetty-dir.css"));
+ }
+ }
+ catch(Exception e)
+ {
+ }
+ }
+
+ /**
+ * Overridden to save the result
+ * Calls super.
+ */
+ @Override
+ protected ContextHandler initContextHandler(ServletContext servletContext)
+ {
+ ContextHandler rv = super.initContextHandler(servletContext);
+ _contextHandler = rv;
+ return rv;
+ }
+
+ /* copied from DefaultServlet unchanged */
+ private boolean getInitBoolean(String name, boolean dft)
+ {
+ String value=getInitParameter(name);
+ if (value==null || value.length()==0)
+ return dft;
+ return (value.startsWith("t")||
+ value.startsWith("T")||
+ value.startsWith("y")||
+ value.startsWith("Y")||
+ value.startsWith("1"));
+ }
+
+ /**
+ * Copied and modified from DefaultServlet.java.
+ * Overridden to set the Locale for the dates.
+ *
+ * Get the resource list as a HTML directory listing.
+ * @param base The base URL
+ * @param parent True if the parent directory should be included
+ * @return String of HTML
+ */
+ @Override
+ protected void sendDirectory(HttpServletRequest request,
+ HttpServletResponse response,
+ Resource resource,
+ String pathInContext)
+ throws IOException
+ {
+ if (!_dirAllowed)
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
+
+ byte[] data=null;
+ String base = URIUtil.addPaths(request.getRequestURI(),URIUtil.SLASH);
+
+ //If the DefaultServlet has a resource base set, use it
+ if (_resourceBase != null)
+ {
+ // handle ResourceCollection
+ if (_resourceBase instanceof ResourceCollection)
+ resource=_resourceBase.addPath(pathInContext);
+ }
+ //Otherwise, try using the resource base of its enclosing context handler
+ else if (_contextHandler.getBaseResource() instanceof ResourceCollection)
+ resource=_contextHandler.getBaseResource().addPath(pathInContext);
+
+ String dir = getListHTML(resource, base, pathInContext.length()>1);
+ if (dir==null)
+ {
+ response.sendError(HttpServletResponse.SC_FORBIDDEN,
+ "No directory");
+ return;
+ }
+
+ data=dir.getBytes("UTF-8");
+ response.setContentType("text/html; charset=UTF-8");
+ response.setContentLength(data.length);
+ response.getOutputStream().write(data);
+ }
+
+ /**
+ * Copied and modified from Resource.java
+ * Modified to set the Locale for the dates.
+ *
+ * Get the resource list as a HTML directory listing.
+ * @param base The base URL
+ * @param parent True if the parent directory should be included
+ * @return String of HTML
+ */
+ private static String getListHTML(Resource res, String base, boolean parent)
+ throws IOException
+ {
+ base=URIUtil.canonicalPath(base);
+ if (base==null || !res.isDirectory())
+ return null;
+
+ String[] ls = res.list();
+ if (ls==null)
+ return null;
+ Arrays.sort(ls);
+
+ String decodedBase = URIUtil.decodePath(base);
+ String title = "Directory: "+deTag(decodedBase);
+
+ StringBuilder buf=new StringBuilder(4096);
+ buf.append("
");
+ buf.append("");
+ buf.append(title);
+ buf.append(" \n");
+ buf.append(title);
+ buf.append(" \n\n");
+
+ if (parent)
+ {
+ buf.append("Parent Directory \n");
+ }
+
+ String encodedBase = hrefEncodeURI(base);
+
+ DateFormat dfmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.UK);
+ TimeZone utc = TimeZone.getTimeZone("GMT");
+ dfmt.setTimeZone(utc);
+ for (int i=0 ; i< ls.length ; i++)
+ {
+ Resource item = res.addPath(ls[i]);
+
+ buf.append("\n");
+ buf.append(deTag(ls[i]));
+ buf.append(" ");
+ buf.append(" ");
+ buf.append(item.length());
+ buf.append(" bytes ");
+ buf.append(dfmt.format(new Date(item.lastModified())));
+ buf.append(" UTC ");
+ }
+ buf.append("
\n");
+ buf.append("\n");
+
+ return buf.toString();
+ }
+
+ /**
+ * Copied unchanged from Resource.java
+ *
+ * Encode any characters that could break the URI string in an HREF.
+ * Such as ">Link
+ *
+ * The above example would parse incorrectly on various browsers as the "<" or '"' characters
+ * would end the href attribute value string prematurely.
+ *
+ * @param raw the raw text to encode.
+ * @return the defanged text.
+ */
+ private static String hrefEncodeURI(String raw)
+ {
+ StringBuffer buf = null;
+
+ loop:
+ for (int i=0;i':
+ buf=new StringBuffer(raw.length()<<1);
+ break loop;
+ }
+ }
+ if (buf==null)
+ return raw;
+
+ for (int i=0;i':
+ buf.append("%3E");
+ continue;
+ default:
+ buf.append(c);
+ continue;
+ }
+ }
+
+ return buf.toString();
+ }
+
+ /**
+ * Copied unchanged from Resource.java
+ */
+ private static String deTag(String raw)
+ {
+ return StringUtil.sanitizeXmlString(raw);
+ }
+}
diff --git a/history.txt b/history.txt
index 70f984a1b3..e88f9933f3 100644
--- a/history.txt
+++ b/history.txt
@@ -1,6 +1,8 @@
2017-05-05 zzz
* Blockfile: Move from i2p.jar to addressbook.jar
* i2psnark: Initial support for ut_comment, no UI yet
+ * Jetty: New default servlet for eepsite, with
+ locale-independent directory listing (ticket #1965)
* 2017-05-03 0.9.30 released
diff --git a/installer/resources/eepsite/contexts/base-context.xml b/installer/resources/eepsite/contexts/base-context.xml
index ef18daf499..70b681ba58 100644
--- a/installer/resources/eepsite/contexts/base-context.xml
+++ b/installer/resources/eepsite/contexts/base-context.xml
@@ -34,7 +34,7 @@ to serve static html files and images.
- org.eclipse.jetty.servlet.DefaultServlet
+ net.i2p.servlet.I2PDefaultServlet
/
diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java
index 282c18b422..0725033fa6 100644
--- a/router/java/src/net/i2p/router/RouterVersion.java
+++ b/router/java/src/net/i2p/router/RouterVersion.java
@@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */
public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION;
- public final static long BUILD = 2;
+ public final static long BUILD = 3;
/** for example "-test" */
public final static String EXTRA = "";