diff --git a/LICENSE.txt b/LICENSE.txt index 11dd51e38..7681ce65a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -182,7 +182,7 @@ Applications: By welterde. See licenses/LICENSE-GPLv2.txt - Jetty 8.1.15.v20140411: + Jetty 8.1.16.v20140903: See licenses/ABOUT-Jetty.html See licenses/NOTICE-Jetty.html See licenses/LICENSE-Apache2.0.txt diff --git a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java index 47e9bb0fb..5fda52940 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java @@ -64,10 +64,11 @@ class ConfigParser { if (inputLine.startsWith(";")) { return ""; } - if (inputLine.split("#").length > 0) { - return inputLine.split("#")[0]; + int hash = inputLine.indexOf('#'); + if (hash >= 0) { + return inputLine.substring(0, hash); } else { - return ""; + return inputLine; } } diff --git a/apps/i2psnark/java/build.xml b/apps/i2psnark/java/build.xml index a4fd04c06..5a4d3dd24 100644 --- a/apps/i2psnark/java/build.xml +++ b/apps/i2psnark/java/build.xml @@ -100,15 +100,15 @@ - - + + - + @@ -121,7 +121,7 @@ - + diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java index ab15bb85c..a7c4f4127 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java @@ -57,8 +57,8 @@ public class Peer implements Comparable private DataOutputStream dout; /** running counters */ - private long downloaded; - private long uploaded; + private final AtomicLong downloaded = new AtomicLong(); + private final AtomicLong uploaded = new AtomicLong(); // Keeps state for in/out connections. Non-null when the handshake // was successful, the connection setup and runs @@ -618,7 +618,7 @@ public class Peer implements Comparable * @since 0.8.4 */ public void downloaded(int size) { - downloaded += size; + downloaded.addAndGet(size); } /** @@ -626,7 +626,7 @@ public class Peer implements Comparable * @since 0.8.4 */ public void uploaded(int size) { - uploaded += size; + uploaded.addAndGet(size); } /** @@ -635,7 +635,7 @@ public class Peer implements Comparable */ public long getDownloaded() { - return downloaded; + return downloaded.get(); } /** @@ -644,7 +644,7 @@ public class Peer implements Comparable */ public long getUploaded() { - return uploaded; + return uploaded.get(); } /** @@ -652,8 +652,8 @@ public class Peer implements Comparable */ public void resetCounters() { - downloaded = 0; - uploaded = 0; + downloaded.set(0); + uploaded.set(0); } public long getInactiveTime() { diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 740b7c2be..2bb841329 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -27,7 +27,6 @@ import java.io.InputStream; import java.util.Collections; import java.util.List; import java.util.Properties; -import java.util.Random; import java.util.StringTokenizer; import net.i2p.I2PAppContext; @@ -245,16 +244,19 @@ public class Snark * * @deprecated unused */ +/**** Snark(I2PSnarkUtil util, String torrent, String ip, int user_port, StorageListener slistener, CoordinatorListener clistener) { this(util, torrent, ip, user_port, slistener, clistener, null, null, null, true, "."); } +****/ /** * single torrent - via router * * @deprecated unused */ +/**** public Snark(I2PAppContext ctx, Properties opts, String torrent, StorageListener slistener, boolean start, String rootDir) { this(new I2PSnarkUtil(ctx), torrent, null, -1, slistener, null, null, null, null, false, rootDir); @@ -284,6 +286,7 @@ public class Snark if (start) this.startTorrent(); } +****/ /** * multitorrent @@ -515,18 +518,13 @@ public class Snark // Create a new ID and fill it with something random. First nine // zeros bytes, then three bytes filled with snark and then - // sixteen random bytes. + // eight random bytes. byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17; byte[] rv = new byte[20]; - Random random = I2PAppContext.getGlobalContext().random(); - int i; - for (i = 0; i < 9; i++) - rv[i] = 0; - rv[i++] = snark; - rv[i++] = snark; - rv[i++] = snark; - while (i < 20) - rv[i++] = (byte)random.nextInt(256); + rv[9] = snark; + rv[10] = snark; + rv[11] = snark; + I2PAppContext.getGlobalContext().random().nextBytes(rv, 12, 8); return rv; } @@ -958,6 +956,7 @@ public class Snark * non-valid argument list. The given listeners will be * passed to all components that take one. */ +/**** private static Snark parseArguments(String[] args, StorageListener slistener, CoordinatorListener clistener) @@ -972,6 +971,7 @@ public class Snark int i = 0; while (i < args.length) { +****/ /* if (args[i].equals("--debug")) { @@ -993,7 +993,9 @@ public class Snark catch (NumberFormatException nfe) { } } } - else */ if (args[i].equals("--port")) + else */ +/**** + if (args[i].equals("--port")) { if (args.length - 1 < i + 1) usage("--port needs port number to listen on"); @@ -1099,6 +1101,7 @@ public class Snark System.out.println (" \tor (with --share) a file to share."); } +****/ /** * Aborts program abnormally. diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 73f273c6a..cb50fb6fb 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -600,10 +600,10 @@ public class SnarkManager implements CompleteListener { /** * Get all themes - * @return String[] -- Array of all the themes found. + * @return String[] -- Array of all the themes found, non-null, unsorted */ public String[] getThemes() { - String[] themes = null; + String[] themes; // "docs/themes/snark/" File dir = new File(_context.getBaseDir(), "docs/themes/snark"); FileFilter fileFilter = new FileFilter() { public boolean accept(File file) { return file.isDirectory(); } }; @@ -614,6 +614,8 @@ public class SnarkManager implements CompleteListener { for(int i = 0; i < dirnames.length; i++) { themes[i] = dirnames[i].getName(); } + } else { + themes = new String[0]; } // return the map. return themes; 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 a984f0ce8..f1c6658c9 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -61,7 +61,7 @@ public class I2PSnarkServlet extends BasicServlet { private static final String DEFAULT_NAME = "i2psnark"; public static final String PROP_CONFIG_FILE = "i2psnark.configFile"; - private static final String WARBASE = "/.icons/"; + private static final String WARBASE = "/.resources/"; private static final char HELLIP = '\u2026'; public I2PSnarkServlet() { @@ -191,31 +191,14 @@ public class I2PSnarkServlet extends BasicServlet { _themePath = "/themes/snark/" + _manager.getTheme() + '/'; _imgPath = _themePath + "images/"; - resp.setHeader("X-Frame-Options", "SAMEORIGIN"); - resp.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"); - resp.setHeader("X-XSS-Protection", "1; mode=block"); + req.setCharacterEncoding("UTF-8"); - String peerParam = req.getParameter("p"); - String stParam = req.getParameter("st"); - String peerString; - if (peerParam == null || (!_manager.util().connected()) || - peerParam.replaceAll("[a-zA-Z0-9~=-]", "").length() > 0) { // XSS - peerString = ""; - } else { - peerString = "?p=" + DataHelper.stripHTML(peerParam); - } - if (stParam != null && !stParam.equals("0")) { - stParam = DataHelper.stripHTML(stParam); - if (peerString.length() > 0) - peerString += "&st=" + stParam; - else - peerString = "?st="+ stParam; - } + String pOverride = _manager.util().connected() ? null : ""; + String peerString = getQueryString(req, pOverride, null, null); // AJAX for mainsection if ("/.ajax/xhr1.html".equals(path)) { - resp.setCharacterEncoding("UTF-8"); - resp.setContentType("text/html; charset=UTF-8"); + setHTMLHeaders(resp); PrintWriter out = resp.getWriter(); //if (_log.shouldLog(Log.DEBUG)) // _manager.addMessage((_context.clock().now() / 1000) + " xhr1 p=" + req.getParameter("p")); @@ -233,19 +216,18 @@ public class I2PSnarkServlet extends BasicServlet { // bypass the horrid Resource.getListHTML() String pathInfo = req.getPathInfo(); String pathInContext = addPaths(path, pathInfo); - req.setCharacterEncoding("UTF-8"); - resp.setCharacterEncoding("UTF-8"); - resp.setContentType("text/html; charset=UTF-8"); File resource = getResource(pathInContext); if (resource == null) { resp.sendError(404); } else { String base = addPaths(req.getRequestURI(), "/"); - String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null); + String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null, + req.getParameter("sort")); if (method.equals("POST")) { // P-R-G sendRedirect(req, resp, ""); } else if (listing != null) { + setHTMLHeaders(resp); resp.getWriter().write(listing); } else { // shouldn't happen resp.sendError(404); @@ -265,10 +247,6 @@ public class I2PSnarkServlet extends BasicServlet { // Either the main page or /configure - req.setCharacterEncoding("UTF-8"); - resp.setCharacterEncoding("UTF-8"); - resp.setContentType("text/html; charset=UTF-8"); - String nonce = req.getParameter("nonce"); if (nonce != null) { if (nonce.equals(String.valueOf(_nonce))) @@ -280,6 +258,7 @@ public class I2PSnarkServlet extends BasicServlet { return; } + setHTMLHeaders(resp); PrintWriter out = resp.getWriter(); out.write(DOCTYPE + "\n" + "\n" + @@ -293,6 +272,7 @@ public class I2PSnarkServlet extends BasicServlet { out.write(_("Configuration")); else out.write(_("Anonymous BitTorrent Client")); + String peerParam = req.getParameter("p"); if ("2".equals(peerParam)) out.write(" | Debug Mode"); out.write("\n"); @@ -324,7 +304,8 @@ public class I2PSnarkServlet extends BasicServlet { out.write("
"); - out.write("\"\"  "); + out.write(toThemeImg("arrow_refresh")); + out.write(">  "); if (_contextName.equals(DEFAULT_NAME)) out.write(_("I2PSnark")); else @@ -334,7 +315,8 @@ public class I2PSnarkServlet extends BasicServlet { out.write("
"); - out.write("\"\"  "); + out.write(toThemeImg("arrow_refresh")); + out.write(">  "); if (_contextName.equals(DEFAULT_NAME)) out.write(_("I2PSnark")); else @@ -378,6 +360,22 @@ public class I2PSnarkServlet extends BasicServlet { out.write(FOOTER); } + /** + * The standard HTTP headers for all HTML pages + * + * @since 0.9.16 moved from doGetAndPost() + */ + private static void setHTMLHeaders(HttpServletResponse resp) { + resp.setCharacterEncoding("UTF-8"); + resp.setContentType("text/html; charset=UTF-8"); + resp.setHeader("Cache-Control", "no-store, max-age=0, no-cache, must-revalidate"); + resp.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"); + resp.setDateHeader("Expires", 0); + resp.setHeader("Pragma", "no-cache"); + resp.setHeader("X-Frame-Options", "SAMEORIGIN"); + resp.setHeader("X-XSS-Protection", "1; mode=block"); + } + private void writeMessages(PrintWriter out, boolean isConfigure, String peerString) throws IOException { List msgs = _manager.getMessages(); if (!msgs.isEmpty()) { @@ -389,9 +387,10 @@ public class I2PSnarkServlet extends BasicServlet { out.write(peerString + "&"); else out.write("?"); - out.write("action=Clear&nonce=" + _nonce + "\">" + - "\""" + + out.write("action=Clear&nonce=" + _nonce + "\">"); + String tx = _("clear messages"); + out.write(toThemeImg("delete", tx, tx)); + out.write("" + "
    "); for (int i = msgs.size()-1; i >= 0; i--) { String msg = msgs.get(i); @@ -414,13 +413,7 @@ public class I2PSnarkServlet extends BasicServlet { boolean isForm = _manager.util().connected() || !snarks.isEmpty(); if (isForm) { out.write("
    \n"); - out.write("\n"); - // don't lose peer setting - if (peerParam != null) - out.write("\n"); - // ...or st setting - if (stParam != null) - out.write("\n"); + writeHiddenInputs(out, req, null); } out.write(TABLE_HEADER); @@ -442,90 +435,166 @@ public class I2PSnarkServlet extends BasicServlet { } int pageSize = Math.max(_manager.getPageSize(), 5); - out.write("\"");\n"); + String currentSort = req.getParameter("sort"); + boolean showSort = total > 1; + out.write(""); + String sort = ("2".equals(currentSort)) ? "-2" : "2"; + if (showSort) { + out.write(""); + } + String tx = _("Status"); + out.write(toThemeImg("status", tx, + showSort ? _("Sort by {0}", tx) + : tx)); + if (showSort) + out.write(""); + out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { out.write(" "); - out.write("\"");"); + tx = _("Hide Peers"); + out.write(toThemeImg("hidepeers", tx, tx)); } else { - out.write("?p=1"); - if (stParam != null) { - out.write("&st="); - out.write(stParam); - } - out.write("\">"); - out.write("\"");"); + tx = _("Show Peers"); + out.write(toThemeImg("showpeers", tx, tx)); } out.write("
    \n"); } out.write("\n"); - out.write("\"");\n"); + // cycle through sort by name or type + boolean isTypeSort = false; + if (showSort) { + if (currentSort == null || "0".equals(currentSort) || "1".equals(currentSort)) { + sort = "-1"; + } else if ("-1".equals(currentSort)) { + sort = "12"; + isTypeSort = true; + } else if ("12".equals(currentSort)) { + sort = "-12"; + isTypeSort = true; + } else { + sort = ""; + } + out.write(""); + } + tx = _("Torrent"); + out.write(toThemeImg("torrent", tx, + showSort ? _("Sort by {0}", (isTypeSort ? _("File type") : tx)) + : tx)); + if (showSort) + out.write(""); + out.write("\n"); if (total > 0 && (start > 0 || total > pageSize)) { - writePageNav(out, start, pageSize, total, peerParam, noThinsp); + writePageNav(out, req, start, pageSize, total, noThinsp); } out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { - out.write("\"");"); + } // Translators: Please keep short or translate as " " - out.write(_("ETA")); - out.write("\">"); + tx = _("ETA"); + out.write(toThemeImg("eta", tx, + showSort ? _("Sort by {0}", _("Estimated time remaining")) + : _("Estimated time remaining"))); + if (showSort) + out.write(""); } out.write("\n"); - out.write("\"");"); + } // Translators: Please keep short or translate as " " - out.write(_("RX")); - out.write("\">"); + tx = _("RX"); + out.write(toThemeImg("head_rx", tx, + showSort ? _("Sort by {0}", (isDlSort ? _("Downloaded") : _("Size"))) + : _("Downloaded"))); + if (showSort) + out.write(""); out.write("\n"); + boolean isRatSort = false; if (!snarks.isEmpty()) { - out.write("\"");"); + } // Translators: Please keep short or translate as " " - out.write(_("TX")); - out.write("\">"); + tx = _("TX"); + out.write(toThemeImg("head_tx", tx, + showSort ? _("Sort by {0}", (nextRatSort ? _("Upload ratio") : _("Uploaded"))) + : _("Uploaded"))); + if (showSort) + out.write(""); } out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { - out.write("\"");"); + } // Translators: Please keep short or translate as " " - out.write(_("RX Rate")); - out.write(" \">"); + tx = _("RX Rate"); + out.write(toThemeImg("head_rxspeed", tx, + showSort ? _("Sort by {0}", _("Down Rate")) + : _("Down Rate"))); + if (showSort) + out.write(""); } out.write("\n"); if (_manager.util().connected() && !snarks.isEmpty()) { - out.write("\"");"); + } // Translators: Please keep short or translate as " " - out.write(_("TX Rate")); - out.write(" \">"); + tx = _("TX Rate"); + out.write(toThemeImg("head_txspeed", tx, + showSort ? _("Sort by {0}", _("Up Rate")) + : _("Up Rate"))); + if (showSort) + out.write(""); } out.write("\n"); @@ -581,12 +650,11 @@ public class I2PSnarkServlet extends BasicServlet { String uri = _contextPath + '/'; boolean showDebug = "2".equals(peerParam); - String stParamStr = stParam == null ? "" : "&st=" + stParam; for (int i = 0; i < total; i++) { Snark snark = snarks.get(i); boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.getInfoHash()).equals(peerParam); boolean hide = i < start || i >= start + pageSize; - displaySnark(out, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug, hide, stParamStr); + displaySnark(out, req, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug, hide, isRatSort); } if (total == 0) { @@ -637,32 +705,118 @@ public class I2PSnarkServlet extends BasicServlet { return start == 0; } + /** + * hidden inputs for nonce and paramters p, st, and sort + * + * @param out writes to it + * @param action if non-null, add it as the action + * @since 0.9.16 + */ + private void writeHiddenInputs(PrintWriter out, HttpServletRequest req, String action) { + StringBuilder buf = new StringBuilder(256); + writeHiddenInputs(buf, req, action); + out.write(buf.toString()); + } + + /** + * hidden inputs for nonce and paramters p, st, and sort + * + * @param out appends to it + * @param action if non-null, add it as the action + * @since 0.9.16 + */ + private void writeHiddenInputs(StringBuilder buf, HttpServletRequest req, String action) { + buf.append("\n"); + String peerParam = req.getParameter("p"); + if (peerParam != null) { + buf.append("\n"); + } + String stParam = req.getParameter("st"); + if (stParam != null) { + buf.append("\n"); + } + String soParam = req.getParameter("sort"); + if (soParam != null) { + buf.append("\n"); + } + if (action != null) { + buf.append("\n"); + } + } + + /** + * Build HTML-escaped and stripped query string + * + * @param p override or "" for default or null to keep the same as in req + * @param st override or "" for default or null to keep the same as in req + * @param so override or "" for default or null to keep the same as in req + * @return non-null, possibly empty + * @since 0.9.16 + */ + private static String getQueryString(HttpServletRequest req, String p, String st, String so) { + StringBuilder buf = new StringBuilder(64); + if (p == null) { + p = req.getParameter("p"); + if (p != null) + p = DataHelper.stripHTML(p); + } + if (p != null && !p.equals("")) + buf.append("?p=").append(p); + if (so == null) { + so = req.getParameter("sort"); + if (so != null) + so = DataHelper.stripHTML(so); + } + if (so != null && !so.equals("")) { + if (buf.length() <= 0) + buf.append("?sort="); + else + buf.append("&sort="); + buf.append(so); + } + if (st == null) { + st = req.getParameter("st"); + if (st != null) + st = DataHelper.stripHTML(st); + } + if (st != null && !st.equals("")) { + if (buf.length() <= 0) + buf.append("?st="); + else + buf.append("&st="); + buf.append(st); + } + return buf.toString(); + } + /** * @since 0.9.6 */ - private void writePageNav(PrintWriter out, int start, int pageSize, int total, - String peerParam, boolean noThinsp) { + private void writePageNav(PrintWriter out, HttpServletRequest req, int start, int pageSize, int total, + boolean noThinsp) { // Page nav if (start > 0) { // First out.write("" + - "\""" + - " "); + out.write(getQueryString(req, null, "", null)); + out.write("\">"); + out.write(toThemeImg("control_rewind_blue", _("First"), _("First page"))); + out.write(" "); int prev = Math.max(0, start - pageSize); //if (prev > 0) { if (true) { // Back - out.write(" " + - "\""" + - " "); + out.write("  0) ? Integer.toString(prev) : ""; + out.write(getQueryString(req, null, sprev, null)); + out.write("\">"); + out.write(toThemeImg("control_back_blue", _("Prev"), _("Previous page"))); + out.write(" "); } } else { out.write( @@ -691,23 +845,19 @@ public class I2PSnarkServlet extends BasicServlet { //if (next + pageSize < total) { if (true) { // Next - out.write(" " + - "\""" + - " "); + out.write(" "); + out.write(toThemeImg("control_play_blue", _("Next"), _("Next page"))); + out.write(" "); } // Last int last = ((total - 1) / pageSize) * pageSize; - out.write(" " + - "\""" + - " "); + out.write(" "); + out.write(toThemeImg("control_fastforward_blue", _("Last"), _("Last page"))); + out.write(" "); } else { out.write(" " + "\"\" trackers = _manager.getTrackerMap(); trackers.put(name, new Tracker(name, aurl, hurl)); _manager.saveTrackerMap(); - // open trumps private - if (req.getParameter("_add_open_") != null) { + String type = req.getParameter("add_tracker_type"); + if ("1".equals(type)) { List newOpen = new ArrayList(_manager.util().getOpenTrackers()); newOpen.add(aurl); _manager.saveOpenTrackers(newOpen); - } else if (req.getParameter("_add_private_") != null) { + } else if ("2".equals(type)) { List newPriv = new ArrayList(_manager.getPrivateTrackers()); newPriv.add(aurl); _manager.savePrivateTrackers(newPriv); @@ -1191,34 +1344,22 @@ public class I2PSnarkServlet extends BasicServlet { return buf.toString(); } - /** - * Sort alphabetically in current locale, ignore case, ignore leading "the " - * (I guess this is worth it, a lot of torrents start with "The " - * @since 0.7.14 - */ - private static class TorrentNameComparator implements Comparator, Serializable { - - public int compare(Snark l, Snark r) { - // put downloads and magnets first - if (l.getStorage() == null && r.getStorage() != null) - return -1; - if (l.getStorage() != null && r.getStorage() == null) - return 1; - String ls = l.getBaseName(); - String llc = ls.toLowerCase(Locale.US); - if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_")) - ls = ls.substring(4); - String rs = r.getBaseName(); - String rlc = rs.toLowerCase(Locale.US); - if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_")) - rs = rs.substring(4); - return Collator.getInstance().compare(ls, rs); - } - } - private List getSortedSnarks(HttpServletRequest req) { ArrayList rv = new ArrayList(_manager.getTorrents()); - Collections.sort(rv, new TorrentNameComparator()); + if (rv.size() > 1) { + int sort = 0; + String ssort = req.getParameter("sort"); + if (ssort != null) { + try { + sort = Integer.parseInt(ssort); + } catch (NumberFormatException nfe) {} + } + try { + Collections.sort(rv, Sorters.getComparator(sort, this)); + } catch (IllegalArgumentException iae) { + // Java 7 TimSort - may be unstable + } + } return rv; } @@ -1230,11 +1371,11 @@ public class I2PSnarkServlet extends BasicServlet { * * @param stats in/out param (totals) * @param statsOnly if true, output nothing, update stats only - * @param stParam non null; empty or e.g. &st=10 */ - private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers, + private void displaySnark(PrintWriter out, HttpServletRequest req, + Snark snark, String uri, int row, long stats[], boolean showPeers, boolean isDegraded, boolean noThinsp, boolean showDebug, boolean statsOnly, - String stParam) throws IOException { + boolean showRatios) throws IOException { // stats long uploaded = snark.getUploaded(); stats[0] += snark.getDownloaded(); @@ -1288,10 +1429,10 @@ public class I2PSnarkServlet extends BasicServlet { String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd"); String statusString; if (snark.isChecking()) { - statusString = "\"\"" + + statusString = toThemeImg("stalled", "", _("Checking")) + "" + "" + _("Checking"); } else if (snark.isAllocating()) { - statusString = "\"\"" + + statusString = toThemeImg("stalled", "", _("Allocating")) + "" + "" + _("Allocating"); } else if (err != null && curPeers == 0) { // Also don't show if seeding... but then we won't see the not-registered error @@ -1305,7 +1446,7 @@ public class I2PSnarkServlet extends BasicServlet { // ngettext("1 peer", "{0} peers", knownPeers) + ""; //else if (isRunning) if (isRunning) - statusString = "\"\"" + + statusString = toThemeImg("trackererror", "", err) + "" + "" + _("Tracker Error") + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers); @@ -1314,11 +1455,11 @@ public class I2PSnarkServlet extends BasicServlet { err = DataHelper.escapeHTML(err.substring(0, MAX_DISPLAYED_ERROR_LENGTH)) + "…"; else err = DataHelper.escapeHTML(err); - statusString = "\"\"" + + statusString = toThemeImg("trackererror", "", err) + "" + "" + _("Tracker Error"); } } else if (snark.isStarting()) { - statusString = "\"\"" + + statusString = toThemeImg("stalled", "", _("Starting")) + "" + "" + _("Starting"); } else if (remaining == 0 || needed == 0) { // < 0 means no meta size yet // partial complete or seeding @@ -1334,52 +1475,52 @@ public class I2PSnarkServlet extends BasicServlet { txt = _("Complete"); } if (curPeers > 0 && !showPeers) - statusString = "\"\"" + + statusString = toThemeImg(img, "", txt) + "" + "" + txt + - ": " + + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + ""; else - statusString = "\"\"" + + statusString = toThemeImg(img, "", txt) + "" + "" + txt + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers); } else { - statusString = "\"\"" + + statusString = toThemeImg("complete", "", _("Complete")) + "" + "" + _("Complete"); } } else { if (isRunning && curPeers > 0 && downBps > 0 && !showPeers) - statusString = "\"\"" + + statusString = toThemeImg("downloading", "", _("OK")) + "" + "" + _("OK") + - ": " + + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + ""; else if (isRunning && curPeers > 0 && downBps > 0) - statusString = "\"\"" + + statusString = toThemeImg("downloading", "", _("OK")) + "" + "" + _("OK") + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers); else if (isRunning && curPeers > 0 && !showPeers) - statusString = "\"\"" + + statusString = toThemeImg("stalled", "", _("Stalled")) + "" + "" + _("Stalled") + - ": " + + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + ""; else if (isRunning && curPeers > 0) - statusString = "\"\"" + + statusString = toThemeImg("stalled", "", _("Stalled")) + "" + "" + _("Stalled") + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers); else if (isRunning && knownPeers > 0) - statusString = "\"\"" + + statusString = toThemeImg("nopeers", "", _("No Peers")) + "" + "" + _("No Peers") + ": 0" + thinsp(noThinsp) + knownPeers ; else if (isRunning) - statusString = "\"\"" + + statusString = toThemeImg("nopeers", "", _("No Peers")) + "" + "" + _("No Peers"); else - statusString = "\"\"" + + statusString = toThemeImg("stopped", "", _("Stopped")) + "" + "" + _("Stopped"); } @@ -1467,8 +1608,17 @@ public class I2PSnarkServlet extends BasicServlet { // out.write("??"); // no meta size yet out.write("\n\t"); out.write(""); - if (isValid && uploaded > 0) - out.write(formatSize(uploaded)); + if (isValid) { + if (showRatios) { + if (total > 0) { + double ratio = uploaded / ((double) total); + out.write((new DecimalFormat("0.000")).format(ratio)); + out.write(" x"); + } + } else if (uploaded > 0) { + out.write(formatSize(uploaded)); + } + } out.write("\n\t"); out.write(""); if (isRunning && needed > 0) @@ -1485,7 +1635,8 @@ public class I2PSnarkServlet extends BasicServlet { } else if (isRunning) { // Stop Button if (isDegraded) - out.write("\"").append(_("Info")).append("\""); + buf.append(linkUrl); + toThemeImg(buf, "details", _("Info"), ""); + buf.append(""); return buf.toString(); } return null; @@ -1843,14 +1997,10 @@ public class I2PSnarkServlet extends BasicServlet { out.write("
    \n"); // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file out.write("\n"); - out.write("\n"); - out.write("\n"); - // don't lose peer setting - String peerParam = req.getParameter("p"); - if (peerParam != null) - out.write("\n"); + writeHiddenInputs(out, req, "Add"); out.write("
    "); - out.write("\"\" "); + out.write(toThemeImg("add")); + out.write(' '); out.write(_("Add Torrent")); out.write("
    \n
    "); out.write(_("From URL")); @@ -1875,14 +2025,10 @@ public class I2PSnarkServlet extends BasicServlet { out.write("
    \n"); // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file out.write("\n"); - out.write("\n"); - out.write("\n"); - // don't lose peer setting - String peerParam = req.getParameter("p"); - if (peerParam != null) - out.write("\n"); + writeHiddenInputs(out, req, "Create"); out.write(""); - out.write("\"\" "); + out.write(toThemeImg("create")); + out.write(' '); out.write(_("Create Torrent")); out.write("
    \n
    "); //out.write("From file:
    \n"); @@ -1946,11 +2092,11 @@ public class I2PSnarkServlet extends BasicServlet { //int seedPct = 0; out.write("\n" + - "
    \n" + - "\n" + - "\n" + - "" + - "\"\" "); + "
    \n"); + writeHiddenInputs(out, req, "Save"); + out.write(""); + out.write(toThemeImg("config")); + out.write(' '); out.write(_("Configuration")); out.write("
    \n" + "\n"); - buf.append(""); } List> alist = meta.getAnnounceList(); if (alist != null) { - buf.append("\n"); @@ -2536,8 +2697,9 @@ public class I2PSnarkServlet extends BasicServlet { long dat = meta.getCreationDate(); if (dat > 0) { String date = (new SimpleDateFormat("yyyy-MM-dd HH:mm")).format(new Date(dat)); - buf.append("\n"); @@ -2546,8 +2708,9 @@ public class I2PSnarkServlet extends BasicServlet { if (cby != null) { if (cby.length() > 128) cby = com.substring(0, 128); - buf.append("\n"); @@ -2582,41 +2745,70 @@ public class I2PSnarkServlet extends BasicServlet { //buf.append(""); - buf.append("
    "); @@ -1979,6 +2125,7 @@ public class I2PSnarkServlet extends BasicServlet { out.write(": \n" + - "\n" + - "" + - "\"\" "); + "
    \n"); + writeHiddenInputs(buf, req, "Save2"); + buf.append(""); + toThemeImg(buf, "config"); + buf.append(' '); buf.append(_("Trackers")); buf.append("
    \n" + "" + "" + "" + - "" + - "" + + "" + + "" + + "" + "\n" + - "\n" + // spacer - "\n" + // spacer + "" + - "\n" + // spacer + "\n" + // spacer "
    ") @@ -2146,6 +2293,8 @@ public class I2PSnarkServlet extends BasicServlet { .append("") .append(_("Website URL")) .append("") + .append(_("Standard")) + .append("") .append(_("Open")) .append("") .append(_("Private")) @@ -2158,18 +2307,25 @@ public class I2PSnarkServlet extends BasicServlet { String name = t.name; String homeURL = t.baseURL; String announceURL = t.announceURL.replace("=", "="); + boolean isOpen = openTrackers.contains(t.announceURL); + boolean isPrivate = privateTrackers.contains(t.announceURL); buf.append("
    " + "").append(name) .append("").append(urlify(homeURL, 35)) - .append("" + - "" + + "
     
    \n" + + "
     
    \n" + "\n" + "\n" + + "\n" + "\n" + // "\n" + "\n" + - "\n" + "
     
     
    \n"); out.write(buf.toString()); } private void writeConfigLink(PrintWriter out) throws IOException { out.write("\n"); } @@ -2336,23 +2494,6 @@ public class I2PSnarkServlet extends BasicServlet { private static final String FOOTER = ""; - /** - * Sort alphabetically in current locale, ignore case, - * directories first - * @since 0.9.6 - */ - private static class ListingComparator implements Comparator, Serializable { - - public int compare(File l, File r) { - boolean ld = l.isDirectory(); - boolean rd = r.isDirectory(); - if (ld && !rd) - return -1; - if (rd && !ld) - return 1; - return Collator.getInstance().compare(l.getName(), r.getName()); - } - } /** * Modded heavily from the Jetty version in Resource.java, @@ -2381,10 +2522,11 @@ public class I2PSnarkServlet extends BasicServlet { * @param base The encoded base URL * @param parent True if the parent directory should be included * @param postParams map of POST parameters or null if not a POST + * @param sortParam may be null * @return String of HTML or null if postParams != null * @since 0.7.14 */ - private String getListHTML(File xxxr, String base, boolean parent, Map postParams) + private String getListHTML(File xxxr, String base, boolean parent, Map postParams, String sortParam) throws IOException { String decodedBase = decodePath(base); @@ -2436,6 +2578,10 @@ public class I2PSnarkServlet extends BasicServlet { // dummy r = new File(""); } + + boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() && + r.isDirectory(); + StringBuilder buf=new StringBuilder(4096); buf.append(DOCTYPE).append(""); if (title.endsWith("/")) @@ -2443,9 +2589,17 @@ public class I2PSnarkServlet extends BasicServlet { String directory = title; title = _("Torrent") + ": " + DataHelper.escapeHTML(title); buf.append(title); - buf.append("").append(HEADER_A).append(_themePath).append(HEADER_B).append("" + - "\n
    ") - .append("\"\" ") + buf.append("
    "); + toThemeImg(buf, "file"); + buf.append(" ") .append(_("Torrent file")) .append(": ") .append(DataHelper.escapeHTML(fullPath)) .append("
    ") - .append("\"\" ") + buf.append("
    "); + toThemeImg(buf, "file"); + buf.append(" ") .append(_("Data location")) .append(": ") .append(DataHelper.escapeHTML(snark.getStorage().getBase().getPath())) @@ -2495,17 +2653,19 @@ public class I2PSnarkServlet extends BasicServlet { buf.append("
    "); String trackerLink = getTrackerLink(announce, snark.getInfoHash()); if (trackerLink != null) - buf.append(trackerLink).append(' '); - buf.append("").append(_("Primary Tracker")).append(": "); + buf.append(trackerLink); + else + toThemeImg(buf, "details"); + buf.append(" ").append(_("Primary Tracker")).append(": "); buf.append(getShortTrackerLink(announce, snark.getInfoHash())); buf.append("
    " + - "\"\" "); - buf.append(_("Tracker List")).append(": "); + buf.append("
    "); + toThemeImg(buf, "details"); + buf.append(" ") + .append(_("Tracker List")).append(": "); for (List alist2 : alist) { buf.append('['); boolean more = false; @@ -2527,8 +2687,9 @@ public class I2PSnarkServlet extends BasicServlet { if (com != null) { if (com.length() > 1024) com = com.substring(0, 1024); - buf.append("
    \"\" ") + buf.append("
    "); + toThemeImg(buf, "details"); + buf.append(" ") .append(_("Comment")).append(": ") .append(DataHelper.stripHTML(com)) .append("
    \"\" ") + buf.append("
    "); + toThemeImg(buf, "details"); + buf.append(" ") .append(_("Created")).append(": ") .append(date).append(" UTC") .append("
    \"\" ") + buf.append("
    "); + toThemeImg(buf, "details"); + buf.append(" ") .append(_("Created By")).append(": ") .append(DataHelper.stripHTML(cby)) .append("
    ").append(_("Maggot link")).append(": ") // .append(MAGGOT).append(hex).append(':').append(hex).append("
    ") - .append("\"\" ") + buf.append("
    "); + toThemeImg(buf, "size"); + buf.append(" ") .append(_("Size")) .append(": ") .append(formatSize(snark.getTotalLength())); int pieces = snark.getPieces(); double completion = (pieces - snark.getNeeded()) / (double) pieces; + buf.append(" "); + toThemeImg(buf, "head_rx"); + buf.append(" "); if (completion < 1.0) - buf.append(" \"\" ") - .append(_("Completion")) + buf.append(_("Completion")) .append(": ") .append((new DecimalFormat("0.00%")).format(completion)); else - buf.append(" \"\" ") - .append(_("Complete")); - // else unknown + buf.append(_("Complete")).append(""); + // up ratio + buf.append(" "); + toThemeImg(buf, "head_tx"); + buf.append(" ") + .append(_("Upload ratio")) + .append(": "); + long uploaded = snark.getUploaded(); + if (uploaded > 0) { + double ratio = uploaded / ((double) snark.getTotalLength()); + buf.append((new DecimalFormat("0.000")).format(ratio)); + buf.append(" x"); + } else { + buf.append('0'); + } + // not including skipped files, but -1 when not running long needed = snark.getNeededLength(); - if (needed > 0) - buf.append(" \"\" ") + if (needed < 0) { + // including skipped files, valid when not running + needed = snark.getRemainingLength(); + } + if (needed > 0) { + buf.append(" "); + toThemeImg(buf, "head_rx"); + buf.append(" ") .append(_("Remaining")) .append(": ") .append(formatSize(needed)); + } if (meta != null) { List> files = meta.getFiles(); int fileCount = files != null ? files.size() : 1; - buf.append(" \"\" ") + buf.append(" "); + toThemeImg(buf, "file"); + buf.append(" ") .append(_("Files")) .append(": ") .append(fileCount); } - buf.append(" \"\" ") + buf.append(" "); + toThemeImg(buf, "file"); + buf.append(" ") .append(_("Pieces")) .append(": ") .append(pieces); - buf.append(" \"\" ") + buf.append(" "); + toThemeImg(buf, "file"); + buf.append(" ") .append(_("Piece size")) .append(": ") .append(formatSize(snark.getPieceLength(0))) @@ -2642,7 +2834,6 @@ public class I2PSnarkServlet extends BasicServlet { File[] ls = null; if (r.isDirectory()) { ls = r.listFiles(); - Arrays.sort(ls, new ListingComparator()); } // if r is not a directory, we are only showing torrent info section if (ls == null) { @@ -2651,40 +2842,92 @@ public class I2PSnarkServlet extends BasicServlet { return buf.toString(); } + Storage storage = snark != null ? snark.getStorage() : null; + List fileList = new ArrayList(ls.length); + for (int i = 0; i < ls.length; i++) { + fileList.add(new Sorters.FileAndIndex(ls[i], storage)); + } + + boolean showSort = fileList.size() > 1; + if (showSort) { + int sort = 0; + if (sortParam != null) { + try { + sort = Integer.parseInt(sortParam); + } catch (NumberFormatException nfe) {} + } + Collections.sort(fileList, Sorters.getFileComparator(sort, this)); + } + // second table - dir info buf.append("\n"); buf.append("\n") - .append("\n"); - buf.append("\n"); - buf.append("\n"); - if (showPriority) - buf.append("\n"); - buf.append("\n\n"); + .append("\n\n\n\n\n\n"); buf.append("\n"); @@ -2692,25 +2935,27 @@ public class I2PSnarkServlet extends BasicServlet { //DateFormat dfmt=DateFormat.getDateTimeInstance(DateFormat.MEDIUM, // DateFormat.MEDIUM); boolean showSaveButton = false; - for (int i=0 ; i< ls.length ; i++) + boolean rowEven = true; + for (Sorters.FileAndIndex fai : fileList) { //String encoded = encodePath(ls[i].getName()); // bugfix for I2P - Backport from Jetty 6 (zero file lengths and last-modified times) // http://jira.codehaus.org/browse/JETTY-361?page=com.atlassian.jira.plugin.system.issuetabpanels%3Achangehistory-tabpanel#issue-tabs // See resource.diff attachment //Resource item = addPath(encoded); - File item = ls[i]; + File item = fai.file; - String rowClass = (i % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd"); + String rowClass = (rowEven ? "snarkTorrentEven" : "snarkTorrentOdd"); + rowEven = !rowEven; buf.append(""); // Get completeness and status string boolean complete = false; String status = ""; long length = item.length(); - int fileIndex = -1; + int fileIndex = fai.index; int priority = 0; - if (item.isDirectory()) { + if (fai.isDirectory) { complete = true; //status = toImg("tick") + ' ' + _("Directory"); } else { @@ -2719,10 +2964,8 @@ public class I2PSnarkServlet extends BasicServlet { complete = true; status = toImg("cancel") + ' ' + _("Torrent not found?"); } else { - Storage storage = snark.getStorage(); - fileIndex = storage.indexOf(item); - long remaining = storage.remaining(fileIndex); + long remaining = fai.remaining; if (remaining < 0) { complete = true; status = toImg("cancel") + ' ' + _("File not found in torrent?"); @@ -2730,7 +2973,7 @@ public class I2PSnarkServlet extends BasicServlet { complete = true; status = toImg("tick") + ' ' + _("Complete"); } else { - priority = storage.getPriority(fileIndex); + priority = fai.priority; if (priority < 0) status = toImg("cancel"); else if (priority == 0) @@ -2745,7 +2988,7 @@ public class I2PSnarkServlet extends BasicServlet { } } - String path = addPaths(decodedBase, ls[i].getName()); + String path = addPaths(decodedBase, item.getName()); if (item.isDirectory() && !path.endsWith("/")) path=addPaths(path,"/"); path = encodePath(path); @@ -2782,19 +3025,19 @@ public class I2PSnarkServlet extends BasicServlet { if (showPriority) { buf.append("\n"); } if (showSaveButton) { - buf.append("\n"); + buf.append("\n"); } buf.append("
    ") - .append("\"")") - .append("\"")") - .append("\"")") - .append("\"")
    "); + String tx = _("Directory"); + // cycle through sort by name or type + String sort; + boolean isTypeSort = false; + if (showSort) { + if (sortParam == null || "0".equals(sortParam) || "1".equals(sortParam)) { + sort = "-1"; + } else if ("-1".equals(sortParam)) { + sort = "12"; + isTypeSort = true; + } else if ("12".equals(sortParam)) { + sort = "-12"; + isTypeSort = true; + } else { + sort = ""; + } + buf.append(""); + } + toThemeImg(buf, "file", tx, + showSort ? _("Sort by {0}", (isTypeSort ? _("File type") : _("Name"))) + : tx + ": " + directory); + if (showSort) + buf.append(""); + buf.append(""); + if (showSort) { + sort = ("5".equals(sortParam)) ? "-5" : "5"; + buf.append(""); + } + tx = _("Size"); + toThemeImg(buf, "size", tx, + showSort ? _("Sort by {0}", tx) : tx); + if (showSort) + buf.append(""); + buf.append(""); + if (showSort) { + sort = ("10".equals(sortParam)) ? "-10" : "10"; + buf.append(""); + } + tx = _("Status"); + toThemeImg(buf, "status", tx, + showSort ? _("Sort by {0}", _("Remaining")) : tx); + if (showSort) + buf.append(""); + if (showPriority) { + buf.append(""); + if (showSort) { + sort = ("13".equals(sortParam)) ? "-13" : "13"; + buf.append(""); + } + tx = _("Priority"); + toThemeImg(buf, "priority", tx, + showSort ? _("Sort by {0}", tx) : tx); + if (showSort) + buf.append(""); + } + buf.append("
    \"\" ") + buf.append("\">"); + toThemeImg(buf, "up"); + buf.append(' ') .append(_("Up to higher level directory")) .append("
    "); if ((!complete) && (!item.isDirectory())) { - buf.append(" 0) - buf.append("checked=\"true\""); + buf.append("checked=\"checked\""); buf.append('>').append(_("High")); - buf.append("').append(_("Normal")); - buf.append("').append(_("Skip")); showSaveButton = true; } @@ -2803,9 +3046,16 @@ public class I2PSnarkServlet extends BasicServlet { buf.append("
     
     " + + "") + .append(toImg("clock_red")).append(_("Set all high")).append("\n" + + "") + .append(toImg("clock")).append(_("Set all normal")).append("\n" + + "") + .append(toImg("cancel")).append(_("Skip all")).append("\n" + + "

    \n" + + "
    \n"); if (showPriority) @@ -2815,7 +3065,23 @@ public class I2PSnarkServlet extends BasicServlet { return buf.toString(); } - /** @since 0.7.14 */ + /** + * @param null ok + * @return query string or "" + * @since 0.9.16 + */ + private static String getQueryString(String so) { + if (so != null && !so.equals("")) + return "?sort=" + DataHelper.stripHTML(so); + return ""; + } + + /** + * Pick an icon; try to catch the common types in an i2p environment. + * + * @return file name not including ".png" + * @since 0.7.14 + */ private String toIcon(File item) { if (item.isDirectory()) return "folder"; @@ -2824,10 +3090,12 @@ public class I2PSnarkServlet extends BasicServlet { /** * Pick an icon; try to catch the common types in an i2p environment + * Pkg private for FileTypeSorter. + * * @return file name not including ".png" * @since 0.7.14 */ - private String toIcon(String path) { + String toIcon(String path) { String icon; // Note that for this to work well, our custom mime.properties file must be loaded. String plc = path.toLowerCase(Locale.US); @@ -2856,7 +3124,12 @@ public class I2PSnarkServlet extends BasicServlet { icon = "music"; else if (mime.startsWith("video/")) icon = "film"; - else if (mime.equals("application/zip") || mime.equals("application/x-gtar") || + else if (mime.equals("application/zip")) { + if (plc.endsWith(".su3") || plc.endsWith(".su2") || plc.endsWith(".sud")) + icon = "itoopie_xxsm"; + else + icon = "compress"; + } else if (mime.equals("application/x-gtar") || mime.equals("application/compress") || mime.equals("application/gzip") || mime.equals("application/x-7z-compressed") || mime.equals("application/x-rar-compressed") || mime.equals("application/x-tar") || mime.equals("application/x-bzip2")) @@ -2872,14 +3145,73 @@ public class I2PSnarkServlet extends BasicServlet { return icon; } - /** @since 0.7.14 */ + /** + * Icon file in the .war. Always 16x16. + * + * @param icon name without the ".png" + * @since 0.7.14 + */ private String toImg(String icon) { - return "\"\""; + return toImg(icon, ""); } - /** @since 0.8.2 */ + /** + * Icon file in the .war. Always 16x16. + * + * @param icon name without the ".png" + * @since 0.8.2 + */ private String toImg(String icon, String altText) { - return "\"""; + return "\"""; + } + + /** + * Image file in the theme. + * + * @param image name without the ".png" + * @since 0.9.16 + */ + private String toThemeImg(String image) { + return toThemeImg(image, "", ""); + } + + /** + * Image file in the theme. + * + * @param image name without the ".png" + * @since 0.9.16 + */ + private void toThemeImg(StringBuilder buf, String image) { + toThemeImg(buf, image, "", ""); + } + + /** + * Image file in the theme. + * + * @param image name without the ".png" + * @param altText non-null + * @param titleText non-null + * @since 0.9.16 + */ + private String toThemeImg(String image, String altText, String titleText) { + StringBuilder buf = new StringBuilder(128); + toThemeImg(buf, image, altText, titleText); + return buf.toString(); + } + + /** + * Image file in the theme. + * + * @param image name without the ".png" + * @param altText non-null + * @param titleText non-null + * @since 0.9.16 + */ + private void toThemeImg(StringBuilder buf, String image, String altText, String titleText) { + buf.append("\"").append(altText).append("\" 0) + buf.append(" title=\"").append(titleText).append('"'); + buf.append('>'); } /** @since 0.8.1 */ diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java b/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java new file mode 100644 index 000000000..8098520b1 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/web/Sorters.java @@ -0,0 +1,531 @@ +package org.klomp.snark.web; + +import java.io.File; +import java.io.Serializable; +import java.text.Collator; +import java.util.Collections; +import java.util.Comparator; +import java.util.Locale; + +import org.klomp.snark.MetaInfo; +import org.klomp.snark.Snark; +import org.klomp.snark.Storage; + +/** + * Comparators for various columns + * + * @since 0.9.16 from TorrentNameComparator, moved from I2PSnarkservlet + */ +class Sorters { + + /** + * Negative is reverse + * + *
      + *
    • 0, 1: Name + *
    • 2: Status + *
    • 3: Peers + *
    • 4: ETA + *
    • 5: Size + *
    • 6: Downloaded + *
    • 7: Uploaded + *
    • 8: Down rate + *
    • 9: Up rate + *
    • 10: Remaining (needed) + *
    • 11: Upload ratio + *
    • 12: File type + *
    + * + * @param servlet for file type callback only + */ + public static Comparator getComparator(int type, I2PSnarkServlet servlet) { + boolean rev = type < 0; + Comparator rv; + switch (type) { + + case -1: + case 0: + case 1: + default: + rv = new TorrentNameComparator(); + if (rev) + rv = Collections.reverseOrder(rv); + break; + + case -2: + case 2: + rv = new StatusComparator(rev); + break; + + case -3: + case 3: + rv = new PeersComparator(rev); + break; + + case -4: + case 4: + rv = new ETAComparator(rev); + break; + + case -5: + case 5: + rv = new SizeComparator(rev); + break; + + case -6: + case 6: + rv = new DownloadedComparator(rev); + break; + + case -7: + case 7: + rv = new UploadedComparator(rev); + break; + + case -8: + case 8: + rv = new DownRateComparator(rev); + break; + + case -9: + case 9: + rv = new UpRateComparator(rev); + break; + + case -10: + case 10: + rv = new RemainingComparator(rev); + break; + + case -11: + case 11: + rv = new RatioComparator(rev); + break; + + case -12: + case 12: + rv = new FileTypeComparator(rev, servlet); + break; + + } + return rv; + } + + + /** + * Sort alphabetically in current locale, ignore case, ignore leading "the " + * (I guess this is worth it, a lot of torrents start with "The " + * @since 0.7.14 + */ + private static class TorrentNameComparator implements Comparator, Serializable { + + public int compare(Snark l, Snark r) { + return comp(l, r); + } + + public static int comp(Snark l, Snark r) { + // put downloads and magnets first + if (l.getStorage() == null && r.getStorage() != null) + return -1; + if (l.getStorage() != null && r.getStorage() == null) + return 1; + String ls = l.getBaseName(); + String llc = ls.toLowerCase(Locale.US); + if (llc.startsWith("the ") || llc.startsWith("the.") || llc.startsWith("the_")) + ls = ls.substring(4); + String rs = r.getBaseName(); + String rlc = rs.toLowerCase(Locale.US); + if (rlc.startsWith("the ") || rlc.startsWith("the.") || rlc.startsWith("the_")) + rs = rs.substring(4); + return Collator.getInstance().compare(ls, rs); + } + } + + /** + * Forward or reverse sort, but the fallback is always forward + */ + private static abstract class Sort implements Comparator, Serializable { + + private final boolean _rev; + + public Sort(boolean rev) { + _rev = rev; + } + + public int compare(Snark l, Snark r) { + int rv = compareIt(l, r); + if (rv != 0) + return _rev ? 0 - rv : rv; + return TorrentNameComparator.comp(l, r); + } + + protected abstract int compareIt(Snark l, Snark r); + + protected static int compLong(long l, long r) { + if (l < r) + return -1; + if (l > r) + return 1; + return 0; + } + } + + + private static class StatusComparator extends Sort { + + private StatusComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + int rv = getStatus(l) - getStatus(r); + if (rv != 0) + return rv; + // use reverse remaining as first tie break + return compLong(r.getNeededLength(), l.getNeededLength()); + } + + private static int getStatus(Snark snark) { + long remaining = snark.getRemainingLength(); + long needed = snark.getNeededLength(); + if (snark.isStopped()) { + if (remaining < 0) + return 0; + if (remaining > 0) + return 5; + return 10; + } + if (snark.isStarting()) + return 15; + if (snark.isAllocating()) + return 20; + if (remaining < 0) + return 15; // magnet + if (remaining == 0) + return 100; + if (snark.isChecking()) + return 95; + if (snark.getNeededLength() <= 0) + return 90; + if (snark.getPeerCount() <= 0) + return 40; + if (snark.getDownloadRate() <= 0) + return 50; + return 60; + } + } + + private static class PeersComparator extends Sort { + + public PeersComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return l.getPeerCount() - r.getPeerCount(); + } + } + + private static class RemainingComparator extends Sort { + + public RemainingComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getNeededLength(), r.getNeededLength()); + } + } + + private static class ETAComparator extends Sort { + + public ETAComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(eta(l), eta(r)); + } + + private static long eta(Snark snark) { + long needed = snark.getNeededLength(); + if (needed <= 0) + return 0; + long total = snark.getTotalLength(); + if (needed > total) + needed = total; + long downBps = snark.getDownloadRate(); + if (downBps > 0) + return needed / downBps; + return Long.MAX_VALUE; + } + } + + private static class SizeComparator extends Sort { + + public SizeComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getTotalLength(), r.getTotalLength()); + } + } + + private static class DownloadedComparator extends Sort { + + public DownloadedComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + long ld = l.getTotalLength() - l.getRemainingLength(); + long rd = r.getTotalLength() - r.getRemainingLength(); + return compLong(ld, rd); + } + } + + private static class UploadedComparator extends Sort { + + public UploadedComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getUploaded(), r.getUploaded()); + } + } + + private static class DownRateComparator extends Sort { + + public DownRateComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getDownloadRate(), r.getDownloadRate()); + } + } + + private static class UpRateComparator extends Sort { + + public UpRateComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + return compLong(l.getUploadRate(), r.getUploadRate()); + } + } + + private static class RatioComparator extends Sort { + + private static final long M = 128 * 1024 * 1024; + + public RatioComparator(boolean rev) { super(rev); } + + public int compareIt(Snark l, Snark r) { + long lt = l.getTotalLength(); + long ld = lt > 0 ? ((M * l.getUploaded()) / lt) : 0; + long rt = r.getTotalLength(); + long rd = rt > 0 ? ((M * r.getUploaded()) / rt) : 0; + return compLong(ld, rd); + } + } + + private static class FileTypeComparator extends Sort { + + private final I2PSnarkServlet servlet; + + public FileTypeComparator(boolean rev, I2PSnarkServlet servlet) { + super(rev); + this.servlet = servlet; + } + + public int compareIt(Snark l, Snark r) { + String ls = toName(l); + String rs = toName(r); + return ls.compareTo(rs); + } + + private String toName(Snark snark) { + MetaInfo meta = snark.getMetaInfo(); + if (meta == null) + return "0"; + if (meta.getFiles() != null) + return "1"; + // arbitrary sort based on icon name + return servlet.toIcon(meta.getName()); + } + } + + ////////////// Comparators for details page below + + /** + * Class to precompute and efficiently sort data + * on a torrent file entry. + */ + public static class FileAndIndex { + public final File file; + public final boolean isDirectory; + public final long length; + public final long remaining; + public final int priority; + public final int index; + + /** + * @param storage may be null + */ + public FileAndIndex(File file, Storage storage) { + this.file = file; + index = storage != null ? storage.indexOf(file) : -1; + if (index >= 0) { + isDirectory = false; + remaining = storage.remaining(index); + priority = storage.getPriority(index); + } else { + isDirectory = file.isDirectory(); + remaining = -1; + priority = -999; + } + length = isDirectory ? 0 : file.length(); + } + } + + + /** + * Negative is reverse + * + *
      + *
    • 0, 1: Name + *
    • 5: Size + *
    • 10: Remaining (needed) + *
    • 12: File type + *
    • 13: Priority + *
    + * + * @param servlet for file type callback only + */ + public static Comparator getFileComparator(int type, I2PSnarkServlet servlet) { + boolean rev = type < 0; + Comparator rv; + + switch (type) { + + case -1: + case 0: + case 1: + default: + rv = new FileNameComparator(); + if (rev) + rv = Collections.reverseOrder(rv); + break; + + case -5: + case 5: + rv = new FAISizeComparator(rev); + break; + + case -10: + case 10: + rv = new FAIRemainingComparator(rev); + break; + + case -12: + case 12: + rv = new FAITypeComparator(rev, servlet); + break; + + case -13: + case 13: + rv = new FAIPriorityComparator(rev); + break; + + } + return rv; + } + + /** + * Sort alphabetically in current locale, ignore case, + * directories first + * @since 0.9.6 moved from I2PSnarkServlet in 0.9.16 + */ + private static class FileNameComparator implements Comparator, Serializable { + + public int compare(FileAndIndex l, FileAndIndex r) { + return comp(l, r); + } + + public static int comp(FileAndIndex l, FileAndIndex r) { + boolean ld = l.isDirectory; + boolean rd = r.isDirectory; + if (ld && !rd) + return -1; + if (rd && !ld) + return 1; + return Collator.getInstance().compare(l.file.getName(), r.file.getName()); + } + } + + /** + * Forward or reverse sort, but the fallback is always forward + */ + private static abstract class FAISort implements Comparator, Serializable { + + private final boolean _rev; + + public FAISort(boolean rev) { + _rev = rev; + } + + public int compare(FileAndIndex l, FileAndIndex r) { + int rv = compareIt(l, r); + if (rv != 0) + return _rev ? 0 - rv : rv; + return FileNameComparator.comp(l, r); + } + + protected abstract int compareIt(FileAndIndex l, FileAndIndex r); + + protected static int compLong(long l, long r) { + if (l < r) + return -1; + if (l > r) + return 1; + return 0; + } + } + + private static class FAIRemainingComparator extends FAISort { + + public FAIRemainingComparator(boolean rev) { super(rev); } + + public int compareIt(FileAndIndex l, FileAndIndex r) { + return compLong(l.remaining, r.remaining); + } + } + + private static class FAISizeComparator extends FAISort { + + public FAISizeComparator(boolean rev) { super(rev); } + + public int compareIt(FileAndIndex l, FileAndIndex r) { + return compLong(l.length, r.length); + } + } + + private static class FAITypeComparator extends FAISort { + + private final I2PSnarkServlet servlet; + + public FAITypeComparator(boolean rev, I2PSnarkServlet servlet) { + super(rev); + this.servlet = servlet; + } + + public int compareIt(FileAndIndex l, FileAndIndex r) { + String ls = toName(l); + String rs = toName(r); + return ls.compareTo(rs); + } + + private String toName(FileAndIndex fai) { + if (fai.isDirectory) + return "0"; + // arbitrary sort based on icon name + return servlet.toIcon(fai.file.getName()); + } + } + + private static class FAIPriorityComparator extends FAISort { + + public FAIPriorityComparator(boolean rev) { super(rev); } + + /** highest first */ + public int compareIt(FileAndIndex l, FileAndIndex r) { + return r.priority - l.priority; + } + } +} diff --git a/apps/i2psnark/mime.properties b/apps/i2psnark/mime.properties index b251fb72e..fff1a696e 100644 --- a/apps/i2psnark/mime.properties +++ b/apps/i2psnark/mime.properties @@ -8,6 +8,7 @@ epub = application/epub+zip flac = audio/flac flv = video/x-flv iso = application/x-iso9660-image +js = text/javascript m4a = audio/mp4a-latm m4v = video/x-m4v mkv = video/x-matroska diff --git a/apps/i2psnark/icons/application.png b/apps/i2psnark/resources/icons/application.png similarity index 100% rename from apps/i2psnark/icons/application.png rename to apps/i2psnark/resources/icons/application.png diff --git a/apps/i2psnark/icons/basket_put.png b/apps/i2psnark/resources/icons/basket_put.png similarity index 100% rename from apps/i2psnark/icons/basket_put.png rename to apps/i2psnark/resources/icons/basket_put.png diff --git a/apps/i2psnark/icons/cancel.png b/apps/i2psnark/resources/icons/cancel.png similarity index 100% rename from apps/i2psnark/icons/cancel.png rename to apps/i2psnark/resources/icons/cancel.png diff --git a/apps/i2psnark/icons/cd.png b/apps/i2psnark/resources/icons/cd.png similarity index 100% rename from apps/i2psnark/icons/cd.png rename to apps/i2psnark/resources/icons/cd.png diff --git a/apps/i2psnark/icons/clock.png b/apps/i2psnark/resources/icons/clock.png similarity index 100% rename from apps/i2psnark/icons/clock.png rename to apps/i2psnark/resources/icons/clock.png diff --git a/apps/i2psnark/icons/clock_red.png b/apps/i2psnark/resources/icons/clock_red.png similarity index 100% rename from apps/i2psnark/icons/clock_red.png rename to apps/i2psnark/resources/icons/clock_red.png diff --git a/apps/i2psnark/icons/compress.png b/apps/i2psnark/resources/icons/compress.png similarity index 100% rename from apps/i2psnark/icons/compress.png rename to apps/i2psnark/resources/icons/compress.png diff --git a/apps/i2psnark/icons/film.png b/apps/i2psnark/resources/icons/film.png similarity index 100% rename from apps/i2psnark/icons/film.png rename to apps/i2psnark/resources/icons/film.png diff --git a/apps/i2psnark/icons/folder.png b/apps/i2psnark/resources/icons/folder.png similarity index 100% rename from apps/i2psnark/icons/folder.png rename to apps/i2psnark/resources/icons/folder.png diff --git a/apps/i2psnark/icons/html.png b/apps/i2psnark/resources/icons/html.png similarity index 100% rename from apps/i2psnark/icons/html.png rename to apps/i2psnark/resources/icons/html.png diff --git a/apps/i2psnark/resources/icons/itoopie_xxsm.png b/apps/i2psnark/resources/icons/itoopie_xxsm.png new file mode 100644 index 000000000..0cec9e5c9 Binary files /dev/null and b/apps/i2psnark/resources/icons/itoopie_xxsm.png differ diff --git a/apps/i2psnark/icons/magnet.png b/apps/i2psnark/resources/icons/magnet.png similarity index 100% rename from apps/i2psnark/icons/magnet.png rename to apps/i2psnark/resources/icons/magnet.png diff --git a/apps/i2psnark/icons/music.png b/apps/i2psnark/resources/icons/music.png similarity index 100% rename from apps/i2psnark/icons/music.png rename to apps/i2psnark/resources/icons/music.png diff --git a/apps/i2psnark/icons/package.png b/apps/i2psnark/resources/icons/package.png similarity index 100% rename from apps/i2psnark/icons/package.png rename to apps/i2psnark/resources/icons/package.png diff --git a/apps/i2psnark/icons/page.png b/apps/i2psnark/resources/icons/page.png similarity index 100% rename from apps/i2psnark/icons/page.png rename to apps/i2psnark/resources/icons/page.png diff --git a/apps/i2psnark/icons/page_white.png b/apps/i2psnark/resources/icons/page_white.png similarity index 100% rename from apps/i2psnark/icons/page_white.png rename to apps/i2psnark/resources/icons/page_white.png diff --git a/apps/i2psnark/icons/page_white_acrobat.png b/apps/i2psnark/resources/icons/page_white_acrobat.png similarity index 100% rename from apps/i2psnark/icons/page_white_acrobat.png rename to apps/i2psnark/resources/icons/page_white_acrobat.png diff --git a/apps/i2psnark/icons/photo.png b/apps/i2psnark/resources/icons/photo.png similarity index 100% rename from apps/i2psnark/icons/photo.png rename to apps/i2psnark/resources/icons/photo.png diff --git a/apps/i2psnark/icons/plugin.png b/apps/i2psnark/resources/icons/plugin.png similarity index 100% rename from apps/i2psnark/icons/plugin.png rename to apps/i2psnark/resources/icons/plugin.png diff --git a/apps/i2psnark/icons/tick.png b/apps/i2psnark/resources/icons/tick.png similarity index 100% rename from apps/i2psnark/icons/tick.png rename to apps/i2psnark/resources/icons/tick.png diff --git a/apps/i2psnark/resources/js/folder.js b/apps/i2psnark/resources/js/folder.js new file mode 100644 index 000000000..fbc906bf9 --- /dev/null +++ b/apps/i2psnark/resources/js/folder.js @@ -0,0 +1,93 @@ +function setupbuttons() { + updatesetallbuttons(); + var form = document.forms[0]; + form.savepri.disabled = true; + form.savepri.className = 'disabled'; +} + +function priorityclicked() { + updatesetallbuttons(); + var form = document.forms[0]; + form.savepri.disabled = false; + form.savepri.className = 'accept'; +} + +function updatesetallbuttons() { + var notNorm = false; + var notHigh = false; + var notSkip = false; + var form = document.forms[0]; + for(i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type == 'radio') { + if (!elem.checked) { + if (elem.className == 'prinorm') + notNorm = true; + else if (elem.className == 'prihigh') + notHigh = true; + else + notSkip = true; + } + } + } + if (notNorm) + document.getElementById('setallnorm').className = 'control'; + else + document.getElementById('setallnorm').className = 'controld'; + if (notHigh) + document.getElementById('setallhigh').className = 'control'; + else + document.getElementById('setallhigh').className = 'controld'; + if (notSkip) + document.getElementById('setallskip').className = 'control'; + else + document.getElementById('setallskip').className = 'controld'; +} + +function setallnorm() { + var form = document.forms[0]; + for(i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type == 'radio') { + if (elem.className === 'prinorm') + elem.checked = true; + } + } + document.getElementById('setallnorm').className = 'controld'; + document.getElementById('setallhigh').className = 'control'; + document.getElementById('setallskip').className = 'control'; + form.savepri.disabled = false; + form.savepri.className = 'accept'; +} + +function setallhigh() { + var form = document.forms[0]; + for(i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type == 'radio') { + if (elem.className === 'prihigh') + elem.checked = true; + } + } + document.getElementById('setallnorm').className = 'control'; + document.getElementById('setallhigh').className = 'controld'; + document.getElementById('setallskip').className = 'control'; + form.savepri.disabled = false; + form.savepri.className = 'accept'; +} + +function setallskip() { + var form = document.forms[0]; + for(i = 0; i < form.elements.length; i++) { + var elem = form.elements[i]; + if (elem.type == 'radio') { + if (elem.className === 'priskip') + elem.checked = true; + } + } + document.getElementById('setallnorm').className = 'control'; + document.getElementById('setallhigh').className = 'control'; + document.getElementById('setallskip').className = 'controld'; + form.savepri.disabled = false; + form.savepri.className = 'accept'; +} diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java index 0e52f17a1..1bfbada56 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnel.java @@ -40,6 +40,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; +import java.lang.reflect.Constructor; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.ArrayList; @@ -256,7 +257,15 @@ public class I2PTunnel extends EventDispatcherImpl implements Logging { } if (gui) { - new I2PTunnelGUI(this); + // removed from source, now in i2p.scripts + //new I2PTunnelGUI(this); + try { + Class cls = Class.forName("net.i2p.i2ptunnel.I2PTunnelGUI"); + Constructor con = cls.getConstructor(I2PTunnel.class); + con.newInstance(this); + } catch (Throwable t) { + throw new UnsupportedOperationException("GUI is not available, try -cli", t); + } } else if (cli) { try { System.out.println("Enter 'help' for help."); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java deleted file mode 100644 index 7dd730845..000000000 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelGUI.java +++ /dev/null @@ -1,48 +0,0 @@ -/* I2PTunnel is GPL'ed (with the exception mentioned in I2PTunnel.java) - * (c) 2003 - 2004 mihi - */ -package net.i2p.i2ptunnel; - -import java.awt.BorderLayout; -import java.awt.Font; -import java.awt.Frame; -import java.awt.TextArea; -import java.awt.TextField; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -/** - * AWT gui since kaffe doesn't support swing yet - */ -public class I2PTunnelGUI extends Frame implements ActionListener, Logging { - - TextField input; - TextArea log; - I2PTunnel t; - - public I2PTunnelGUI(I2PTunnel t) { - super("I2PTunnel control panel"); - this.t = t; - setLayout(new BorderLayout()); - add("South", input = new TextField()); - input.addActionListener(this); - Font font = new Font("Monospaced", Font.PLAIN, 12); - add("Center", log = new TextArea("", 20, 80, TextArea.SCROLLBARS_VERTICAL_ONLY)); - log.setFont(font); - log.setEditable(false); - log("enter 'help' for help."); - pack(); - setVisible(true); - } - - public void log(String s) { - log.append(s + "\n"); - } - - public void actionPerformed(ActionEvent evt) { - log("I2PTunnel>" + input.getText()); - t.runCommand(input.getText(), this); - log("---"); - input.setText(""); - } -} \ No newline at end of file diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java index 4813fa2a1..858458e3f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClientBase.java @@ -601,9 +601,12 @@ public abstract class I2PTunnelHTTPClientBase extends I2PTunnelClientBase implem return; int status = ise != null ? ise.getStatus() : -1; String error; - //TODO MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION if (status == MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET) { + // We won't get this one unless it is treated as a hard failure + // in streaming. See PacketQueue.java error = usingWWWProxy ? "nolsp" : "nols"; + } else if (status == MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION) { + error = usingWWWProxy ? "encp" : "enc"; } else { error = usingWWWProxy ? "dnfp" : "dnf"; } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java index 0597db5f1..b49f51a07 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -188,8 +188,7 @@ public class EditBean extends IndexBean { /** @since 0.9.12 */ public boolean isSigTypeAvailable(int code) { - SigType type = SigType.getByCode(code); - return type != null && type.isAvailable(); + return SigType.isAvailable(code); } /** @since 0.8.9 */ diff --git a/apps/jetty/build.xml b/apps/jetty/build.xml index bb7de443d..fd9a9a45f 100644 --- a/apps/jetty/build.xml +++ b/apps/jetty/build.xml @@ -1,9 +1,9 @@ - + - + diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-continuation-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-continuation-8.1.16.v20140903.jar similarity index 79% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-continuation-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-continuation-8.1.16.v20140903.jar index 5dceaf49c..ce1acb18b 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-continuation-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-continuation-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-deploy-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-deploy-8.1.16.v20140903.jar similarity index 69% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-deploy-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-deploy-8.1.16.v20140903.jar index d83efa193..d3339b061 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-deploy-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-deploy-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-http-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-http-8.1.16.v20140903.jar similarity index 81% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-http-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-http-8.1.16.v20140903.jar index 330344363..30189c766 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-http-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-http-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-io-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-io-8.1.16.v20140903.jar similarity index 89% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-io-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-io-8.1.16.v20140903.jar index f2554fa39..a9afd7cc3 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-io-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-io-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-jmx-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-jmx-8.1.16.v20140903.jar similarity index 79% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-jmx-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-jmx-8.1.16.v20140903.jar index a00233fbf..f0eb0c51a 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-jmx-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-jmx-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-rewrite-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-rewrite-8.1.16.v20140903.jar similarity index 83% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-rewrite-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-rewrite-8.1.16.v20140903.jar index 5004187da..240c67052 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-rewrite-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-rewrite-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-security-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-security-8.1.16.v20140903.jar similarity index 88% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-security-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-security-8.1.16.v20140903.jar index bafcf7293..e5bde43b8 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-security-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-security-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-server-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-server-8.1.16.v20140903.jar similarity index 87% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-server-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-server-8.1.16.v20140903.jar index ded9cf489..ae8ac5577 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-server-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-server-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlet-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlet-8.1.16.v20140903.jar similarity index 91% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlet-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlet-8.1.16.v20140903.jar index 556dc68fe..eb2fa57db 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlet-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlet-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlets-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlets-8.1.16.v20140903.jar similarity index 90% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlets-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlets-8.1.16.v20140903.jar index d30991166..6fd7146ed 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-servlets-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-servlets-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-util-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-util-8.1.16.v20140903.jar similarity index 91% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-util-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-util-8.1.16.v20140903.jar index 170d66fc2..5c3c3460f 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-util-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-util-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-webapp-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-webapp-8.1.16.v20140903.jar similarity index 91% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-webapp-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-webapp-8.1.16.v20140903.jar index 17ebd18e7..85fd7e098 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-webapp-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-webapp-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-xml-8.1.15.v20140411.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-xml-8.1.16.v20140903.jar similarity index 88% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-xml-8.1.15.v20140411.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-xml-8.1.16.v20140903.jar index 5f922235d..1e485de6d 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jetty-xml-8.1.15.v20140411.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jetty-xml-8.1.16.v20140903.jar differ diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jsp/javax.servlet.jsp-2.2.0.v201112011158.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jsp/javax.servlet.jsp-2.2.0.v201112011158.jar similarity index 100% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/jsp/javax.servlet.jsp-2.2.0.v201112011158.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/jsp/javax.servlet.jsp-2.2.0.v201112011158.jar diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/lib/servlet-api-3.0.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/lib/servlet-api-3.0.jar similarity index 100% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/lib/servlet-api-3.0.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/lib/servlet-api-3.0.jar diff --git a/apps/jetty/jetty-distribution-8.1.15.v20140411/start.jar b/apps/jetty/jetty-distribution-8.1.16.v20140903/start.jar similarity index 90% rename from apps/jetty/jetty-distribution-8.1.15.v20140411/start.jar rename to apps/jetty/jetty-distribution-8.1.16.v20140903/start.jar index 9c1615291..8ea5baa13 100644 Binary files a/apps/jetty/jetty-distribution-8.1.15.v20140411/start.jar and b/apps/jetty/jetty-distribution-8.1.16.v20140903/start.jar differ diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java index efe9f4f81..7f13242d3 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigNetHelper.java @@ -5,7 +5,7 @@ import java.util.HashSet; import java.util.Set; import net.i2p.data.DataHelper; -import net.i2p.data.RouterAddress; +import net.i2p.data.router.RouterAddress; import net.i2p.router.CommSystemFacade; import net.i2p.router.Router; import net.i2p.router.transport.TransportManager; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java index 9fae3ab93..712830f59 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java @@ -29,8 +29,8 @@ import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.Lease; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.router.RouterContext; import net.i2p.router.TunnelPoolSettings; import net.i2p.router.util.HashDistance; // debug @@ -199,10 +199,10 @@ public class NetDbRenderer { FloodfillNetworkDatabaseFacade netdb = (FloodfillNetworkDatabaseFacade)_context.netDb(); buf.append("

    Total Leasesets: ").append(leases.size()); buf.append("

    Published (RAP) Leasesets: ").append(netdb.getKnownLeaseSets()); - buf.append("

    Mod Data: \"").append(DataHelper.getUTF8(_context.routingKeyGenerator().getModData())) - .append("\" Last Changed: ").append(new Date(_context.routingKeyGenerator().getLastChanged())); - buf.append("

    Next Mod Data: \"").append(DataHelper.getUTF8(_context.routingKeyGenerator().getNextModData())) - .append("\" Change in: ").append(DataHelper.formatDuration(_context.routingKeyGenerator().getTimeTillMidnight())); + buf.append("

    Mod Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData())) + .append("\" Last Changed: ").append(new Date(_context.routerKeyGenerator().getLastChanged())); + buf.append("

    Next Mod Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getNextModData())) + .append("\" Change in: ").append(DataHelper.formatDuration(_context.routerKeyGenerator().getTimeTillMidnight())); int ff = _context.peerManager().getPeersByCapability(FloodfillNetworkDatabaseFacade.CAPABILITY_FLOODFILL).size(); buf.append("

    Known Floodfills: ").append(ff); buf.append("

    Currently Floodfill? "); @@ -415,7 +415,9 @@ public class NetDbRenderer { // shouldnt happen buf.append("" + _("Published") + ": in ").append(DataHelper.formatDuration2(0-age)).append("???
    \n"); } - buf.append("" + _("Address(es)") + ": "); + buf.append("").append(_("Signing Key")).append(": ") + .append(info.getIdentity().getSigningPublicKey().getType().toString()); + buf.append("
    \n" + _("Address(es)") + ": "); String country = _context.commSystem().getCountry(info.getIdentity().getHash()); if(country != null) { buf.append("\"").append(country.toUpperCase(Locale.US)).append('\"');= 0) { // a retxed SYN succeeded before the first SYN failed diff --git a/build.xml b/build.xml index 873284998..9f3364fe4 100644 --- a/build.xml +++ b/build.xml @@ -548,7 +548,7 @@ windowtitle="I2P Anonymous Network - Java Documentation - Version ${release.number}"> - + diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java index 8357a485a..79c3108f7 100644 --- a/core/java/src/net/i2p/I2PAppContext.java +++ b/core/java/src/net/i2p/I2PAppContext.java @@ -86,7 +86,6 @@ public class I2PAppContext { private SHA256Generator _sha; protected Clock _clock; // overridden in RouterContext private DSAEngine _dsa; - private RoutingKeyGenerator _routingKeyGenerator; private RandomSource _random; private KeyGenerator _keyGenerator; protected KeyRing _keyRing; // overridden in RouterContext @@ -106,7 +105,6 @@ public class I2PAppContext { private volatile boolean _shaInitialized; protected volatile boolean _clockInitialized; // used in RouterContext private volatile boolean _dsaInitialized; - private volatile boolean _routingKeyGeneratorInitialized; private volatile boolean _randomInitialized; private volatile boolean _keyGeneratorInitialized; protected volatile boolean _keyRingInitialized; // used in RouterContext @@ -126,7 +124,7 @@ public class I2PAppContext { private final Object _lock1 = new Object(), _lock2 = new Object(), _lock3 = new Object(), _lock4 = new Object(), _lock5 = new Object(), _lock6 = new Object(), _lock7 = new Object(), _lock8 = new Object(), _lock9 = new Object(), _lock10 = new Object(), _lock11 = new Object(), _lock12 = new Object(), - _lock13 = new Object(), _lock14 = new Object(), _lock15 = new Object(), _lock16 = new Object(), + _lock13 = new Object(), _lock14 = new Object(), _lock16 = new Object(), _lock17 = new Object(), _lock18 = new Object(), _lock19 = new Object(), _lock20 = new Object(); /** @@ -851,19 +849,13 @@ public class I2PAppContext { * may want to test out how things react when peers don't agree on * how to skew. * + * As of 0.9.16, returns null in I2PAppContext. + * You must be in RouterContext to get a generator. + * + * @return null always */ public RoutingKeyGenerator routingKeyGenerator() { - if (!_routingKeyGeneratorInitialized) - initializeRoutingKeyGenerator(); - return _routingKeyGenerator; - } - - private void initializeRoutingKeyGenerator() { - synchronized (_lock15) { - if (_routingKeyGenerator == null) - _routingKeyGenerator = new RoutingKeyGenerator(this); - _routingKeyGeneratorInitialized = true; - } + return null; } /** diff --git a/core/java/src/net/i2p/crypto/ECUtil.java b/core/java/src/net/i2p/crypto/ECUtil.java new file mode 100644 index 000000000..8d2228480 --- /dev/null +++ b/core/java/src/net/i2p/crypto/ECUtil.java @@ -0,0 +1,135 @@ +package net.i2p.crypto; + +import java.math.BigInteger; +import java.security.spec.ECField; +import java.security.spec.ECFieldFp; +import java.security.spec.ECPoint; +import java.security.spec.EllipticCurve; + +import net.i2p.util.NativeBigInteger; + +/** + * Used by KeyGenerator.getSigningPublicKey() + * + * Modified from + * http://stackoverflow.com/questions/15727147/scalar-multiplication-of-point-over-elliptic-curve + * Apparently public domain. + * Supported P-192 only. + * Added curve parameters to support all curves. + * + * @since 0.9.16 + */ +class ECUtil { + + private static final BigInteger TWO = new BigInteger("2"); + private static final BigInteger THREE = new BigInteger("3"); + + public static ECPoint scalarMult(ECPoint p, BigInteger kin, EllipticCurve curve) { + ECPoint r = ECPoint.POINT_INFINITY; + BigInteger prime = ((ECFieldFp) curve.getField()).getP(); + BigInteger k = kin.mod(prime); + int length = k.bitLength(); + byte[] binarray = new byte[length]; + for (int i = 0; i <= length-1; i++) { + binarray[i] = k.mod(TWO).byteValue(); + k = k.divide(TWO); + } + + for (int i = length-1; i >= 0; i--) { + // i should start at length-1 not -2 because the MSB of binarry may not be 1 + r = doublePoint(r, curve); + if (binarray[i] == 1) + r = addPoint(r, p, curve); + } + return r; + } + + private static ECPoint addPoint(ECPoint r, ECPoint s, EllipticCurve curve) { + if (r.equals(s)) + return doublePoint(r, curve); + else if (r.equals(ECPoint.POINT_INFINITY)) + return s; + else if (s.equals(ECPoint.POINT_INFINITY)) + return r; + BigInteger prime = ((ECFieldFp) curve.getField()).getP(); + BigInteger slope = (r.getAffineY().subtract(s.getAffineY())).multiply(r.getAffineX().subtract(s.getAffineX()).modInverse(prime)).mod(prime); + slope = new NativeBigInteger(slope); + BigInteger xOut = (slope.modPow(TWO, prime).subtract(r.getAffineX())).subtract(s.getAffineX()).mod(prime); + BigInteger yOut = s.getAffineY().negate().mod(prime); + yOut = yOut.add(slope.multiply(s.getAffineX().subtract(xOut))).mod(prime); + ECPoint out = new ECPoint(xOut, yOut); + return out; + } + + private static ECPoint doublePoint(ECPoint r, EllipticCurve curve) { + if (r.equals(ECPoint.POINT_INFINITY)) + return r; + BigInteger slope = (r.getAffineX().pow(2)).multiply(THREE); + slope = slope.add(curve.getA()); + BigInteger prime = ((ECFieldFp) curve.getField()).getP(); + slope = slope.multiply((r.getAffineY().multiply(TWO)).modInverse(prime)); + BigInteger xOut = slope.pow(2).subtract(r.getAffineX().multiply(TWO)).mod(prime); + BigInteger yOut = (r.getAffineY().negate()).add(slope.multiply(r.getAffineX().subtract(xOut))).mod(prime); + ECPoint out = new ECPoint(xOut, yOut); + return out; + } + + /** + * P-192 test only. + * See KeyGenerator.main() for a test of all supported curves. + */ +/**** + public static void main(String[] args) { + EllipticCurve P192 = ECConstants.P192_SPEC.getCurve(); + BigInteger xs = new BigInteger("d458e7d127ae671b0c330266d246769353a012073e97acf8", 16); + BigInteger ys = new BigInteger("325930500d851f336bddc050cf7fb11b5673a1645086df3b", 16); + BigInteger xt = new BigInteger("f22c4395213e9ebe67ddecdd87fdbd01be16fb059b9753a4", 16); + BigInteger yt = new BigInteger("264424096af2b3597796db48f8dfb41fa9cecc97691a9c79", 16); + ECPoint S = new ECPoint(xs,ys); + ECPoint T = new ECPoint(xt,yt); + + // Verifying addition + ECPoint Rst = addPoint(S, T, P192); + BigInteger xst = new BigInteger("48e1e4096b9b8e5ca9d0f1f077b8abf58e843894de4d0290", 16); // Specified value of x of point R for addition in NIST Routine example + System.out.println("x-coordinate of point Rst is : " + Rst.getAffineX()); + System.out.println("y-coordinate of point Rst is : " + Rst.getAffineY()); + if (Rst.getAffineX().equals(xst)) + System.out.println("Adding is correct"); + else + System.out.println("Adding FAIL"); + + //Verifying Doubling + BigInteger xr = new BigInteger("30c5bc6b8c7da25354b373dc14dd8a0eba42d25a3f6e6962", 16); // Specified value of x of point R for doubling in NIST Routine example + BigInteger yr = new BigInteger("0dde14bc4249a721c407aedbf011e2ddbbcb2968c9d889cf", 16); + ECPoint R2s = new ECPoint(xr, yr); // Specified value of y of point R for doubling in NIST Routine example + System.out.println("x-coordinate of point R2s is : " + R2s.getAffineX()); + System.out.println("y-coordinate of point R2s is : " + R2s.getAffineY()); + System.out.println("x-coordinate of calculated point is : " + doublePoint(S, P192).getAffineX()); + System.out.println("y-coordinate of calculated point is : " + doublePoint(S, P192).getAffineY()); + if (R2s.getAffineX().equals(doublePoint(S, P192).getAffineX()) && + R2s.getAffineY().equals(doublePoint(S, P192).getAffineY())) + System.out.println("Doubling is correct"); + else + System.out.println("Doubling FAIL"); + + xr = new BigInteger("1faee4205a4f669d2d0a8f25e3bcec9a62a6952965bf6d31", 16); // Specified value of x of point R for scalar Multiplication in NIST Routine example + yr = new BigInteger("5ff2cdfa508a2581892367087c696f179e7a4d7e8260fb06", 16); // Specified value of y of point R for scalar Multiplication in NIST Routine example + ECPoint Rds = new ECPoint(xr, yr); + BigInteger d = new BigInteger("a78a236d60baec0c5dd41b33a542463a8255391af64c74ee", 16); + + ECPoint Rs = scalarMult(S, d, P192); + + System.out.println("x-coordinate of point Rds is : " + Rds.getAffineX()); + System.out.println("y-coordinate of point Rds is : " + Rds.getAffineY()); + System.out.println("x-coordinate of calculated point is : " + Rs.getAffineX()); + System.out.println("y-coordinate of calculated point is : " + Rs.getAffineY()); + + + if (Rds.getAffineX().equals(Rs.getAffineX()) && + Rds.getAffineY().equals(Rs.getAffineY())) + System.out.println("Scalar Multiplication is correct"); + else + System.out.println("Scalar Multiplication FAIL"); + } +****/ +} diff --git a/core/java/src/net/i2p/crypto/ElGamalEngine.java b/core/java/src/net/i2p/crypto/ElGamalEngine.java index e1ac37aad..a80e0a99e 100644 --- a/core/java/src/net/i2p/crypto/ElGamalEngine.java +++ b/core/java/src/net/i2p/crypto/ElGamalEngine.java @@ -56,6 +56,9 @@ public class ElGamalEngine { private final Log _log; private final I2PAppContext _context; private final YKGenerator _ykgen; + + private static final BigInteger ELGPM1 = CryptoConstants.elgp.subtract(BigInteger.ONE); + /** * The ElGamal engine should only be constructed and accessed through the @@ -171,10 +174,11 @@ public class ElGamalEngine { if (_log.shouldLog(Log.WARN)) _log.warn("Took too long to encrypt ElGamal block (" + diff + "ms)"); } - _context.statManager().addRateData("crypto.elGamal.encrypt", diff, 0); + _context.statManager().addRateData("crypto.elGamal.encrypt", diff); return out; } + /** Decrypt the data * @param encrypted encrypted data, must be exactly 514 bytes * Contains the two-part encrypted data starting at bytes 0 and 257. @@ -184,26 +188,26 @@ public class ElGamalEngine { * @return unencrypted data or null on failure */ public byte[] decrypt(byte encrypted[], PrivateKey privateKey) { - // actually it must be exactly 514 bytes or the arraycopy below will AIOOBE - if ((encrypted == null) || (encrypted.length > 514)) - throw new IllegalArgumentException("Data to decrypt must be <= 514 bytes at the moment"); + if ((encrypted == null) || (encrypted.length != 514)) + throw new IllegalArgumentException("Data to decrypt must be exactly 514 bytes"); long start = _context.clock().now(); - byte[] ybytes = new byte[257]; - byte[] dbytes = new byte[257]; - System.arraycopy(encrypted, 0, ybytes, 0, 257); - System.arraycopy(encrypted, 257, dbytes, 0, 257); - BigInteger y = new NativeBigInteger(1, ybytes); - BigInteger d = new NativeBigInteger(1, dbytes); BigInteger a = new NativeBigInteger(1, privateKey.getData()); - BigInteger y1p = CryptoConstants.elgp.subtract(BigInteger.ONE).subtract(a); + BigInteger y1p = ELGPM1.subtract(a); + // we use this buf first for Y, then for D, then for the hash + byte[] buf = SimpleByteCache.acquire(257); + System.arraycopy(encrypted, 0, buf, 0, 257); + BigInteger y = new NativeBigInteger(1, buf); BigInteger ya = y.modPow(y1p, CryptoConstants.elgp); + System.arraycopy(encrypted, 257, buf, 0, 257); + BigInteger d = new NativeBigInteger(1, buf); BigInteger m = ya.multiply(d); m = m.mod(CryptoConstants.elgp); byte val[] = m.toByteArray(); - int i = 0; - for (i = 0; i < val.length; i++) + int i; + for (i = 0; i < val.length; i++) { if (val[i] != (byte) 0x00) break; + } int payloadLen = val.length - i - 1 - Hash.HASH_LENGTH; if (payloadLen < 0) { @@ -220,10 +224,10 @@ public class ElGamalEngine { byte rv[] = new byte[payloadLen]; System.arraycopy(val, i + 1 + Hash.HASH_LENGTH, rv, 0, rv.length); - byte[] calcHash = SimpleByteCache.acquire(Hash.HASH_LENGTH); - _context.sha().calculateHash(rv, 0, payloadLen, calcHash, 0); - boolean ok = DataHelper.eq(calcHash, 0, val, i + 1, Hash.HASH_LENGTH); - SimpleByteCache.release(calcHash); + // we reuse buf here for the calculated hash + _context.sha().calculateHash(rv, 0, payloadLen, buf, 0); + boolean ok = DataHelper.eq(buf, 0, val, i + 1, Hash.HASH_LENGTH); + SimpleByteCache.release(buf); long end = _context.clock().now(); @@ -233,7 +237,7 @@ public class ElGamalEngine { _log.warn("Took too long to decrypt and verify ElGamal block (" + diff + "ms)"); } - _context.statManager().addRateData("crypto.elGamal.decrypt", diff, 0); + _context.statManager().addRateData("crypto.elGamal.decrypt", diff); if (ok) { //_log.debug("Hash matches: " + DataHelper.toString(hash.getData(), hash.getData().length)); diff --git a/core/java/src/net/i2p/crypto/KeyGenerator.java b/core/java/src/net/i2p/crypto/KeyGenerator.java index f078aaa5c..c23a21523 100644 --- a/core/java/src/net/i2p/crypto/KeyGenerator.java +++ b/core/java/src/net/i2p/crypto/KeyGenerator.java @@ -12,11 +12,25 @@ package net.i2p.crypto; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; +import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.ProviderException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.RSAKeyGenParameterSpec; +import java.security.spec.RSAPublicKeySpec; import net.i2p.I2PAppContext; +import net.i2p.crypto.eddsa.EdDSAPrivateKey; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; import net.i2p.data.Hash; import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; @@ -268,24 +282,56 @@ public class KeyGenerator { } /** Convert a SigningPrivateKey to a SigningPublicKey. - * DSA-SHA1 only. + * As of 0.9.16, supports all key types. * * @param priv a SigningPrivateKey object * @return a SigningPublicKey object - * @throws IllegalArgumentException on bad key + * @throws IllegalArgumentException on bad key or unknown type */ public static SigningPublicKey getSigningPublicKey(SigningPrivateKey priv) { - if (priv.getType() != SigType.DSA_SHA1) - throw new IllegalArgumentException(); - BigInteger x = new NativeBigInteger(1, priv.toByteArray()); - BigInteger y = CryptoConstants.dsag.modPow(x, CryptoConstants.dsap); - SigningPublicKey pub = new SigningPublicKey(); + SigType type = priv.getType(); + if (type == null) + throw new IllegalArgumentException("Unknown type"); try { - pub.setData(SigUtil.rectify(y, SigningPublicKey.KEYSIZE_BYTES)); - } catch (InvalidKeyException ike) { - throw new IllegalArgumentException(ike); + switch (type.getBaseAlgorithm()) { + case DSA: + BigInteger x = new NativeBigInteger(1, priv.toByteArray()); + BigInteger y = CryptoConstants.dsag.modPow(x, CryptoConstants.dsap); + SigningPublicKey pub = new SigningPublicKey(); + pub.setData(SigUtil.rectify(y, SigningPublicKey.KEYSIZE_BYTES)); + return pub; + + case EC: + ECPrivateKey ecpriv = SigUtil.toJavaECKey(priv); + BigInteger s = ecpriv.getS(); + ECParameterSpec spec = (ECParameterSpec) type.getParams(); + EllipticCurve curve = spec.getCurve(); + ECPoint g = spec.getGenerator(); + ECPoint w = ECUtil.scalarMult(g, s, curve); + ECPublicKeySpec ecks = new ECPublicKeySpec(w, ecpriv.getParams()); + KeyFactory eckf = KeyFactory.getInstance("EC"); + ECPublicKey ecpub = (ECPublicKey) eckf.generatePublic(ecks); + return SigUtil.fromJavaKey(ecpub, type); + + case RSA: + RSAPrivateKey rsapriv = SigUtil.toJavaRSAKey(priv); + BigInteger exp = ((RSAKeyGenParameterSpec)type.getParams()).getPublicExponent(); + RSAPublicKeySpec rsaks = new RSAPublicKeySpec(rsapriv.getModulus(), exp); + KeyFactory rsakf = KeyFactory.getInstance("RSA"); + RSAPublicKey rsapub = (RSAPublicKey) rsakf.generatePublic(rsaks); + return SigUtil.fromJavaKey(rsapub, type); + + case EdDSA: + EdDSAPrivateKey epriv = SigUtil.toJavaEdDSAKey(priv); + EdDSAPublicKey epub = new EdDSAPublicKey(new EdDSAPublicKeySpec(epriv.getA(), epriv.getParams())); + return SigUtil.fromJavaKey(epub, type); + + default: + throw new IllegalArgumentException("Unsupported algorithm"); + } + } catch (GeneralSecurityException gse) { + throw new IllegalArgumentException("Conversion failed", gse); } - return pub; } public static void main(String args[]) { @@ -322,14 +368,20 @@ public class KeyGenerator { long stime = 0; long vtime = 0; SimpleDataStructure keys[] = KeyGenerator.getInstance().generateSigningKeys(type); - //System.out.println("pubkey " + keys[0]); + SigningPublicKey pubkey = (SigningPublicKey) keys[0]; + SigningPrivateKey privkey = (SigningPrivateKey) keys[1]; + SigningPublicKey pubkey2 = getSigningPublicKey(privkey); + if (pubkey.equals(pubkey2)) + System.out.println(type + " private-to-public test PASSED"); + else + System.out.println(type + " private-to-public test FAILED"); //System.out.println("privkey " + keys[1]); for (int i = 0; i < runs; i++) { RandomSource.getInstance().nextBytes(src); long start = System.nanoTime(); - Signature sig = DSAEngine.getInstance().sign(src, (SigningPrivateKey) keys[1]); + Signature sig = DSAEngine.getInstance().sign(src, privkey); long mid = System.nanoTime(); - boolean ok = DSAEngine.getInstance().verifySignature(sig, src, (SigningPublicKey) keys[0]); + boolean ok = DSAEngine.getInstance().verifySignature(sig, src, pubkey); long end = System.nanoTime(); stime += mid - start; vtime += end - mid; diff --git a/core/java/src/net/i2p/crypto/SigUtil.java b/core/java/src/net/i2p/crypto/SigUtil.java index 09cc3ed78..c492dc663 100644 --- a/core/java/src/net/i2p/crypto/SigUtil.java +++ b/core/java/src/net/i2p/crypto/SigUtil.java @@ -345,7 +345,7 @@ public class SigUtil { } /** - * @deprecated unused + * */ public static RSAPrivateKey toJavaRSAKey(SigningPrivateKey pk) throws GeneralSecurityException { @@ -358,7 +358,7 @@ public class SigUtil { } /** - * @deprecated unused + * */ public static SigningPublicKey fromJavaKey(RSAPublicKey pk, SigType type) throws GeneralSecurityException { diff --git a/core/java/src/net/i2p/data/DataHelper.java b/core/java/src/net/i2p/data/DataHelper.java index d3dc36858..ac51f8128 100644 --- a/core/java/src/net/i2p/data/DataHelper.java +++ b/core/java/src/net/i2p/data/DataHelper.java @@ -24,20 +24,15 @@ import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; -import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.math.BigInteger; import java.security.MessageDigest; import java.text.DecimalFormat; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.Iterator; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; @@ -638,13 +633,17 @@ public class DataHelper { * Integers are a fixed number of bytes (numBytes), stored as unsigned integers in network byte order. * @param value value to write out, non-negative * @param rawStream stream to write to - * @param numBytes number of bytes to write the number into (padding as necessary) - * @throws DataFormatException if value is negative + * @param numBytes number of bytes to write the number into, 1-8 (padding as necessary) + * @throws DataFormatException if value is negative or if numBytes not 1-8 * @throws IOException if there is an IO error writing to the stream */ public static void writeLong(OutputStream rawStream, int numBytes, long value) throws DataFormatException, IOException { - if (value < 0) throw new DataFormatException("Value is negative (" + value + ")"); + if (numBytes <= 0 || numBytes > 8) + // probably got the args backwards + throw new DataFormatException("Bad byte count " + numBytes); + if (value < 0) + throw new DataFormatException("Value is negative (" + value + ")"); for (int i = (numBytes - 1) * 8; i >= 0; i -= 8) { byte cur = (byte) (value >> i); rawStream.write(cur); @@ -667,7 +666,7 @@ public class DataHelper { * @param value non-negative */ public static void toLong(byte target[], int offset, int numBytes, long value) throws IllegalArgumentException { - if (numBytes <= 0) throw new IllegalArgumentException("Invalid number of bytes"); + if (numBytes <= 0 || numBytes > 8) throw new IllegalArgumentException("Invalid number of bytes"); if (value < 0) throw new IllegalArgumentException("Negative value not allowed"); for (int i = offset + numBytes - 1; i >= offset; i--) { @@ -1425,58 +1424,6 @@ public class DataHelper { out.write(data); } - /** - * Sort based on the Hash of the DataStructure. - * Warning - relatively slow. - * WARNING - this sort order must be consistent network-wide, so while the order is arbitrary, - * it cannot be changed. - * Why? Just because it has to be consistent so signing will work. - * How to spec as returning the same type as the param? - * DEPRECATED - Only used by RouterInfo. - * - * @return a new list - */ - public static List sortStructures(Collection dataStructures) { - if (dataStructures == null) return Collections.emptyList(); - - // This used to use Hash.toString(), which is insane, since a change to toString() - // would break the whole network. Now use Hash.toBase64(). - // Note that the Base64 sort order is NOT the same as the raw byte sort order, - // despite what you may read elsewhere. - - //ArrayList rv = new ArrayList(dataStructures.size()); - //TreeMap tm = new TreeMap(); - //for (DataStructure struct : dataStructures) { - // tm.put(struct.calculateHash().toString(), struct); - //} - //for (DataStructure struct : tm.values()) { - // rv.add(struct); - //} - ArrayList rv = new ArrayList(dataStructures); - sortStructureList(rv); - return rv; - } - - /** - * See above. - * DEPRECATED - Only used by RouterInfo. - * - * @since 0.9 - */ - static void sortStructureList(List dataStructures) { - Collections.sort(dataStructures, new DataStructureComparator()); - } - - /** - * See sortStructures() comments. - * @since 0.8.3 - */ - private static class DataStructureComparator implements Comparator, Serializable { - public int compare(DataStructure l, DataStructure r) { - return l.calculateHash().toBase64().compareTo(r.calculateHash().toBase64()); - } - } - /** * NOTE: formatDuration2() recommended in most cases for readability */ diff --git a/core/java/src/net/i2p/data/DatabaseEntry.java b/core/java/src/net/i2p/data/DatabaseEntry.java index 4b84ed106..2adc066bf 100644 --- a/core/java/src/net/i2p/data/DatabaseEntry.java +++ b/core/java/src/net/i2p/data/DatabaseEntry.java @@ -11,6 +11,7 @@ package net.i2p.data; import java.util.Arrays; +import net.i2p.I2PAppContext; import net.i2p.crypto.DSAEngine; /** @@ -47,7 +48,7 @@ public abstract class DatabaseEntry extends DataStructureImpl { protected volatile Signature _signature; protected volatile Hash _currentRoutingKey; - protected volatile byte[] _routingKeyGenMod; + protected volatile long _routingKeyGenMod; /** * A common interface to the timestamp of the two subclasses. @@ -106,11 +107,15 @@ public abstract class DatabaseEntry extends DataStructureImpl { * Get the routing key for the structure using the current modifier in the RoutingKeyGenerator. * This only calculates a new one when necessary though (if the generator's key modifier changes) * + * @throws IllegalStateException if not in RouterContext */ public Hash getRoutingKey() { - RoutingKeyGenerator gen = RoutingKeyGenerator.getInstance(); - byte[] mod = gen.getModData(); - if (!Arrays.equals(mod, _routingKeyGenMod)) { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + if (!ctx.isRouterContext()) + throw new IllegalStateException("Not in router context"); + RoutingKeyGenerator gen = ctx.routingKeyGenerator(); + long mod = gen.getLastChanged(); + if (mod != _routingKeyGenMod) { _currentRoutingKey = gen.getRoutingKey(getHash()); _routingKeyGenMod = mod; } @@ -124,9 +129,16 @@ public abstract class DatabaseEntry extends DataStructureImpl { _currentRoutingKey = key; } + /** + * @throws IllegalStateException if not in RouterContext + */ public boolean validateRoutingKey() { + I2PAppContext ctx = I2PAppContext.getGlobalContext(); + if (!ctx.isRouterContext()) + throw new IllegalStateException("Not in router context"); + RoutingKeyGenerator gen = ctx.routingKeyGenerator(); Hash destKey = getHash(); - Hash rk = RoutingKeyGenerator.getInstance().getRoutingKey(destKey); + Hash rk = gen.getRoutingKey(destKey); return rk.equals(getRoutingKey()); } diff --git a/core/java/src/net/i2p/data/KeysAndCert.java b/core/java/src/net/i2p/data/KeysAndCert.java index b0a8a845b..309799a01 100644 --- a/core/java/src/net/i2p/data/KeysAndCert.java +++ b/core/java/src/net/i2p/data/KeysAndCert.java @@ -77,6 +77,13 @@ public class KeysAndCert extends DataStructureImpl { _signingKey = key; } + /** + * @since 0.9.16 + */ + public byte[] getPadding() { + return _padding; + } + /** * @throws IllegalStateException if was already set * @since 0.9.12 @@ -114,6 +121,8 @@ public class KeysAndCert extends DataStructureImpl { _publicKey.writeBytes(out); if (_padding != null) out.write(_padding); + else if (_signingKey.length() < SigningPublicKey.KEYSIZE_BYTES) + throw new DataFormatException("No padding set"); _signingKey.writeTruncatedBytes(out); _certificate.writeBytes(out); } diff --git a/core/java/src/net/i2p/data/PrivateKey.java b/core/java/src/net/i2p/data/PrivateKey.java index f10248189..163edcc3f 100644 --- a/core/java/src/net/i2p/data/PrivateKey.java +++ b/core/java/src/net/i2p/data/PrivateKey.java @@ -50,6 +50,7 @@ public class PrivateKey extends SimpleDataStructure { /** derives a new PublicKey object derived from the secret contents * of this PrivateKey * @return a PublicKey object + * @throws IllegalArgumentException on bad key */ public PublicKey toPublic() { return KeyGenerator.getPublicKey(this); diff --git a/core/java/src/net/i2p/data/PrivateKeyFile.java b/core/java/src/net/i2p/data/PrivateKeyFile.java index 012310b9f..42a26ef36 100644 --- a/core/java/src/net/i2p/data/PrivateKeyFile.java +++ b/core/java/src/net/i2p/data/PrivateKeyFile.java @@ -1,11 +1,13 @@ package net.i2p.data; +import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; -import java.io.FileOutputStream; +import java.io.InputStream; import java.io.IOException; +import java.io.OutputStream; import java.security.GeneralSecurityException; import java.util.Locale; import java.util.Map; @@ -24,6 +26,7 @@ import net.i2p.crypto.DSAEngine; import net.i2p.crypto.KeyGenerator; import net.i2p.crypto.SigType; import net.i2p.util.RandomSource; +import net.i2p.util.SecureFileOutputStream; /** * This helper class reads and writes files in the @@ -48,11 +51,11 @@ public class PrivateKeyFile { private static final int HASH_EFFORT = VerifiedDestination.MIN_HASHCASH_EFFORT; - private final File file; + protected final File file; private final I2PClient client; - private Destination dest; - private PrivateKey privKey; - private SigningPrivateKey signingPrivKey; + protected Destination dest; + protected PrivateKey privKey; + protected SigningPrivateKey signingPrivKey; /** * Create a new PrivateKeyFile, or modify an existing one, with various @@ -224,6 +227,16 @@ public class PrivateKeyFile { */ public PrivateKeyFile(File file, PublicKey pubkey, SigningPublicKey spubkey, Certificate cert, PrivateKey pk, SigningPrivateKey spk) { + this(file, pubkey, spubkey, cert, pk, spk, null); + } + + /** + * @param padding null OK, must be non-null if spubkey length < 128 + * @throws IllegalArgumentException on mismatch of spubkey and spk types + * @since 0.9.16 + */ + public PrivateKeyFile(File file, PublicKey pubkey, SigningPublicKey spubkey, Certificate cert, + PrivateKey pk, SigningPrivateKey spk, byte[] padding) { if (spubkey.getType() != spk.getType()) throw new IllegalArgumentException("Signing key type mismatch"); this.file = file; @@ -232,6 +245,8 @@ public class PrivateKeyFile { this.dest.setPublicKey(pubkey); this.dest.setSigningPublicKey(spubkey); this.dest.setCertificate(cert); + if (padding != null) + this.dest.setPadding(padding); this.privKey = pk; this.signingPrivKey = spk; } @@ -241,9 +256,9 @@ public class PrivateKeyFile { */ public Destination createIfAbsent() throws I2PException, IOException, DataFormatException { if(!this.file.exists()) { - FileOutputStream out = null; + OutputStream out = null; try { - out = new FileOutputStream(this.file); + out = new SecureFileOutputStream(this.file); if (this.client != null) this.client.createDestination(out); else @@ -257,7 +272,10 @@ public class PrivateKeyFile { return getDestination(); } - /** Also sets the local privKey and signingPrivKey */ + /** + * If the destination is not set, read it in from the file. + * Also sets the local privKey and signingPrivKey. + */ public Destination getDestination() throws I2PSessionException, IOException, DataFormatException { if (dest == null) { I2PSession s = open(); @@ -408,9 +426,9 @@ public class PrivateKeyFile { } public I2PSession open(Properties opts) throws I2PSessionException, IOException { - FileInputStream in = null; + InputStream in = null; try { - in = new FileInputStream(this.file); + in = new BufferedInputStream(new FileInputStream(this.file)); I2PSession s = this.client.createSession(in, opts); return s; } finally { @@ -424,13 +442,12 @@ public class PrivateKeyFile { * Copied from I2PClientImpl.createDestination() */ public void write() throws IOException, DataFormatException { - FileOutputStream out = null; + OutputStream out = null; try { - out = new FileOutputStream(this.file); + out = new SecureFileOutputStream(this.file); this.dest.writeBytes(out); this.privKey.writeBytes(out); this.signingPrivKey.writeBytes(out); - out.flush(); } finally { if (out != null) { try { out.close(); } catch (IOException ioe) {} @@ -438,6 +455,23 @@ public class PrivateKeyFile { } } + /** + * Verify that the PublicKey matches the PrivateKey, and + * the SigningPublicKey matches the SigningPrivateKey. + * + * @return success + * @since 0.9.16 + */ + public boolean validateKeyPairs() { + try { + if (!dest.getPublicKey().equals(KeyGenerator.getPublicKey(privKey))) + return false; + return dest.getSigningPublicKey().equals(KeyGenerator.getSigningPublicKey(signingPrivKey)); + } catch (IllegalArgumentException iae) { + return false; + } + } + @Override public String toString() { StringBuilder s = new StringBuilder(128); diff --git a/core/java/src/net/i2p/data/RoutingKeyGenerator.java b/core/java/src/net/i2p/data/RoutingKeyGenerator.java index 367089442..20e2218ee 100644 --- a/core/java/src/net/i2p/data/RoutingKeyGenerator.java +++ b/core/java/src/net/i2p/data/RoutingKeyGenerator.java @@ -9,215 +9,40 @@ package net.i2p.data; * */ -import java.text.SimpleDateFormat; -import java.util.Calendar; -import java.util.Date; -import java.util.Arrays; -import java.util.GregorianCalendar; -import java.util.Locale; -import java.util.TimeZone; - import net.i2p.I2PAppContext; -import net.i2p.crypto.SHA256Generator; -import net.i2p.util.HexDump; -import net.i2p.util.Log; /** * Component to manage the munging of hashes into routing keys - given a hash, * perform some consistent transformation against it and return the result. * This transformation is fed by the current "mod data". * - * Right now the mod data is the current date (GMT) as a string: "yyyyMMdd", - * and the transformation takes the original hash, appends the bytes of that mod data, - * then returns the SHA256 of that concatenation. - * - * Do we want this to simply do the XOR of the SHA256 of the current mod data and - * the key? does that provide the randomization we need? It'd save an SHA256 op. - * Bah, too much effort to think about for so little gain. Other algorithms may come - * into play layer on about making periodic updates to the routing key for data elements - * to mess with Sybil. This may be good enough though. - * - * Also - the method generateDateBasedModData() should be called after midnight GMT - * once per day to generate the correct routing keys! - * - * Warning - API subject to change. Not for use outside the router. + * As of 0.9.16, this is essentially just an interface. + * Implementation moved to net.i2p.data.router.RouterKeyGenerator. + * No generator is available in I2PAppContext; you must be in RouterContext. * */ -public class RoutingKeyGenerator { - private final Log _log; - private final I2PAppContext _context; - - public RoutingKeyGenerator(I2PAppContext context) { - _log = context.logManager().getLog(RoutingKeyGenerator.class); - _context = context; - // ensure non-null mod data - generateDateBasedModData(); - } +public abstract class RoutingKeyGenerator { + /** + * Get the generator for this context. + * + * @return null in I2PAppContext; non-null in RouterContext. + */ public static RoutingKeyGenerator getInstance() { return I2PAppContext.getGlobalContext().routingKeyGenerator(); } - - private volatile byte _currentModData[]; - private volatile byte _nextModData[]; - private volatile long _nextMidnight; - private volatile long _lastChanged; - - private final static Calendar _cal = GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT")); - private static final String FORMAT = "yyyyMMdd"; - private static final int LENGTH = FORMAT.length(); - private final static SimpleDateFormat _fmt = new SimpleDateFormat(FORMAT, Locale.US); - static { - // make sure GMT is set, azi2phelper Vuze plugin is disabling static JVM TZ setting in Router.java - _fmt.setCalendar(_cal); - } /** - * The current (today's) mod data. - * Warning - not a copy, do not corrupt. - * - * @return non-null, 8 bytes + * The version of the current (today's) mod data. + * Use to determine if the routing key should be regenerated. */ - public byte[] getModData() { - return _currentModData; - } + public abstract long getLastChanged(); /** - * Tomorrow's mod data. - * Warning - not a copy, do not corrupt. - * For debugging use only. - * - * @return non-null, 8 bytes - * @since 0.9.10 - */ - public byte[] getNextModData() { - return _nextModData; - } - - public long getLastChanged() { - return _lastChanged; - } - - /** - * How long until midnight (ms) - * - * @return could be slightly negative - * @since 0.9.10 moved from UpdateRoutingKeyModifierJob - */ - public long getTimeTillMidnight() { - return _nextMidnight - _context.clock().now(); - } - - /** - * Set _cal to midnight for the time given. - * Caller must synch. - * @since 0.9.10 - */ - private void setCalToPreviousMidnight(long now) { - _cal.setTime(new Date(now)); - _cal.set(Calendar.YEAR, _cal.get(Calendar.YEAR)); // gcj <= 4.0 workaround - _cal.set(Calendar.DAY_OF_YEAR, _cal.get(Calendar.DAY_OF_YEAR)); // gcj <= 4.0 workaround - _cal.set(Calendar.HOUR_OF_DAY, 0); - _cal.set(Calendar.MINUTE, 0); - _cal.set(Calendar.SECOND, 0); - _cal.set(Calendar.MILLISECOND, 0); - } - - /** - * Generate mod data from _cal. - * Caller must synch. - * @since 0.9.10 - */ - private byte[] generateModDataFromCal() { - Date today = _cal.getTime(); - - String modVal = _fmt.format(today); - if (modVal.length() != LENGTH) - throw new IllegalStateException(); - byte[] mod = new byte[LENGTH]; - for (int i = 0; i < LENGTH; i++) - mod[i] = (byte)(modVal.charAt(i) & 0xFF); - return mod; - } - - /** - * Update the current modifier data with some bytes derived from the current - * date (yyyyMMdd in GMT) - * - * @return true if changed - */ - public synchronized boolean generateDateBasedModData() { - long now = _context.clock().now(); - setCalToPreviousMidnight(now); - byte[] mod = generateModDataFromCal(); - boolean changed = !Arrays.equals(_currentModData, mod); - if (changed) { - // add a day and store next midnight and mod data for convenience - _cal.add(Calendar.DATE, 1); - _nextMidnight = _cal.getTime().getTime(); - byte[] next = generateModDataFromCal(); - _currentModData = mod; - _nextModData = next; - _lastChanged = now; - if (_log.shouldLog(Log.INFO)) - _log.info("Routing modifier generated: " + HexDump.dump(mod)); - } - return changed; - } - - /** - * Generate a modified (yet consistent) hash from the origKey by generating the - * SHA256 of the targetKey with the current modData appended to it - * - * This makes Sybil's job a lot harder, as she needs to essentially take over the - * whole keyspace. + * Get the routing key for a key. * * @throws IllegalArgumentException if origKey is null */ - public Hash getRoutingKey(Hash origKey) { - return getKey(origKey, _currentModData); - } - - /** - * Get the routing key using tomorrow's modData, not today's - * - * @since 0.9.10 - */ - public Hash getNextRoutingKey(Hash origKey) { - return getKey(origKey, _nextModData); - } - - /** - * Generate a modified (yet consistent) hash from the origKey by generating the - * SHA256 of the targetKey with the specified modData appended to it - * - * @throws IllegalArgumentException if origKey is null - */ - private static Hash getKey(Hash origKey, byte[] modData) { - if (origKey == null) throw new IllegalArgumentException("Original key is null"); - byte modVal[] = new byte[Hash.HASH_LENGTH + LENGTH]; - System.arraycopy(origKey.getData(), 0, modVal, 0, Hash.HASH_LENGTH); - System.arraycopy(modData, 0, modVal, Hash.HASH_LENGTH, LENGTH); - return SHA256Generator.getInstance().calculateHash(modVal); - } + public abstract Hash getRoutingKey(Hash origKey); -/**** - public static void main(String args[]) { - Hash k1 = new Hash(); - byte k1d[] = new byte[Hash.HASH_LENGTH]; - RandomSource.getInstance().nextBytes(k1d); - k1.setData(k1d); - - for (int i = 0; i < 10; i++) { - System.out.println("K1: " + k1); - Hash k1m = RoutingKeyGenerator.getInstance().getRoutingKey(k1); - System.out.println("MOD: " + new String(RoutingKeyGenerator.getInstance().getModData())); - System.out.println("K1M: " + k1m); - } - try { - Thread.sleep(2000); - } catch (Throwable t) { // nop - } - } -****/ } diff --git a/core/java/src/net/i2p/data/SigningPrivateKey.java b/core/java/src/net/i2p/data/SigningPrivateKey.java index a8fcbb208..07b8969e3 100644 --- a/core/java/src/net/i2p/data/SigningPrivateKey.java +++ b/core/java/src/net/i2p/data/SigningPrivateKey.java @@ -75,8 +75,12 @@ public class SigningPrivateKey extends SimpleDataStructure { return _type; } - /** converts this signing private key to its public equivalent - * @return a SigningPublicKey object derived from this private key + /** + * Converts this signing private key to its public equivalent. + * As of 0.9.16, supports all key types. + * + * @return a SigningPublicKey object derived from this private key + * @throws IllegalArgumentException on bad key or unknown or unsupported type */ public SigningPublicKey toPublic() { return KeyGenerator.getSigningPublicKey(this); diff --git a/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java index afa22de9d..331ee3dad 100644 --- a/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java +++ b/core/java/src/net/i2p/data/i2cp/MessagePayloadMessage.java @@ -70,9 +70,12 @@ public class MessagePayloadMessage extends I2CPMessageImpl { } } + /** + * @throws UnsupportedOperationException always + */ @Override protected byte[] doWriteMessage() throws I2CPMessageException, IOException { - throw new RuntimeException("go away, we dont want any"); + throw new UnsupportedOperationException(); } /** diff --git a/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java index ef0e94194..67a67ffa4 100644 --- a/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java +++ b/core/java/src/net/i2p/data/i2cp/SendMessageMessage.java @@ -101,9 +101,12 @@ public class SendMessageMessage extends I2CPMessageImpl { } } + /** + * @throws UnsupportedOperationException always + */ @Override protected byte[] doWriteMessage() throws I2CPMessageException, IOException { - throw new RuntimeException("wtf, dont run me"); + throw new UnsupportedOperationException(); } /** diff --git a/installer/resources/proxy/enc-header.ht b/installer/resources/proxy/enc-header.ht new file mode 100644 index 000000000..e5c271e23 --- /dev/null +++ b/installer/resources/proxy/enc-header.ht @@ -0,0 +1,24 @@ +HTTP/1.1 504 Gateway Timeout +Content-Type: text/html; charset=UTF-8 +Cache-control: no-cache +Connection: close +Proxy-Connection: close + + + +_("Warning: Eepsite Unreachable") + + + + +

    +
    +

    _("Warning: Eepsite Unreachable")

    +

    +_("The eepsite was not reachable, because it uses encryption options that are not supported by your I2P or Java version.") +


    +

    _("Could not connect to the following destination:") +

    diff --git a/installer/resources/proxy/encp-header.ht b/installer/resources/proxy/encp-header.ht new file mode 100644 index 000000000..9d53fb707 --- /dev/null +++ b/installer/resources/proxy/encp-header.ht @@ -0,0 +1,25 @@ +HTTP/1.1 504 Gateway Timeout +Content-Type: text/html; charset=UTF-8 +Cache-control: no-cache +Connection: close +Proxy-Connection: close + + + +_("Warning: Outproxy Unreachable") + + + + + +
    +

    _("Warning: Outproxy Unreachable")

    +

    +_("The HTTP outproxy was not reachable, because it uses encryption options that are not supported by your I2P or Java version.") +_("You may want to {0}retry{1} as this will randomly reselect an outproxy from the pool you have defined {2}here{3} (if you have more than one configured).", "", "", "", "") +_("If you continue to have trouble you may want to edit your outproxy list {0}here{1}.", "", "") +

    +

    _("Could not connect to the following destination:")

    diff --git a/installer/resources/themes/snark/light/snark.css b/installer/resources/themes/snark/light/snark.css index cf61bf102..c195775c0 100644 --- a/installer/resources/themes/snark/light/snark.css +++ b/installer/resources/themes/snark/light/snark.css @@ -513,6 +513,40 @@ a:active { color: #77b; } +a.control, a.controld { + background: #fff; + border: 1px inset #191; + border-radius: 4px; + color: #359; + font-weight: bold; + margin: 2px 4px; + padding: 3px 4px; + text-shadow: 0px 0px #410; + white-space: nowrap; +} + +a.controld { + color: #459; + font-weight: normal; +} + +a.control img, a.controld img { + display: none; +} + +a.control:hover { + background-color: #559; + border: 1px outset #559; + color: #fff; + text-shadow: 0px 1px 5px #410; +} + +a.control:active { + background: #f60 !important; + color: #fff !important; + text-shadow: 0 !important; +} + input { font-size: 9pt; font-weight: bold; @@ -594,6 +628,14 @@ input[type=radio] { input.default { width: 1px; height: 1px; visibility: hidden; } +input.disabled, input.disabled:hover { + background-color: #fff; + border: 1px inset #191; + color: #459; + font-weight: normal; + text-shadow: 0px 0px 0px #410; +} + select { background: #fff !important; color: #22f; diff --git a/installer/resources/themes/snark/ubergine/snark.css b/installer/resources/themes/snark/ubergine/snark.css index 4a57291f5..f2e09b4f9 100644 --- a/installer/resources/themes/snark/ubergine/snark.css +++ b/installer/resources/themes/snark/ubergine/snark.css @@ -366,6 +366,10 @@ table.snarkTorrents tbody tr:hover, table.snarkDirInfo tbody tr:hover { width: 16px; } +td.snarkFileIcon:first-child { + text-align: center; +} + .snarkFileName { padding: 4px 0px !important; text-align: left !important; @@ -526,6 +530,40 @@ a:hover { font-weight: bold; } +a.control, a.controld { + background: #989; + border: 1px inset #bbb; + border-radius: 4px; + color: #000; + font-weight: bold; + margin: 2px 4px; + padding: 3px 4px; + text-shadow: 0px 0px #410; + white-space: nowrap; +} + +a.controld { + color: #444; + font-weight: normal; +} + +a.controld img { + display: none; +} + +a.control:hover { + background-color: #f60; + border: 1px outset #bbb; + color: #fff; + text-shadow: 0px 1px 5px #f00; +} + +a.control:active { + background: #000 !important; + color: #f60 !important; + text-shadow: 0 !important; +} + input { font-size: 8.5pt; font-weight: bold; @@ -598,6 +636,14 @@ input[type=radio] { input.default { width: 1px; height: 1px; visibility: hidden; } +input.disabled, input.disabled:hover { + background-color: #989; + border: 1px inset #bbb; + color: #444; + font-weight: normal; + text-shadow: 0px 0px 0px #444; +} + input.accept { background: #989 url('../../console/images/accept.png') no-repeat 2px center; padding: 2px 3px 2px 20px !important; diff --git a/installer/resources/themes/snark/vanilla/snark.css b/installer/resources/themes/snark/vanilla/snark.css index 6afd7c428..ddbcd8e8c 100644 --- a/installer/resources/themes/snark/vanilla/snark.css +++ b/installer/resources/themes/snark/vanilla/snark.css @@ -373,6 +373,10 @@ td:first-child { width: 16px; } +td.snarkFileIcon:first-child { + text-align: center; +} + .snarkFileName { padding: 4px 0px !important; text-align: left !important; @@ -543,6 +547,40 @@ a:hover { font-weight: bold; } +a.control, a.controld { + background: #fef url('images/bling.png') repeat-x scroll center center; + border: 1px inset #bbb; + border-radius: 4px; + color: #f30; + font-weight: bold; + margin: 2px 4px; + padding: 3px 4px; + text-shadow: 0px 0px #410; + white-space: nowrap; +} + +a.controld { + color: #f60; + font-weight: normal; +} + +a.controld img { + display: none; +} + +a.control:hover { + background-color: #fef; + border: 1px outset #bbb; + color: #f60; + text-shadow: 0px 1px 5px #fdf; +} + +a.control:active { + background: #000 !important; + color: #f60 !important; + text-shadow: 0 !important; +} + input { font-size: 9pt; font-weight: bold; @@ -612,6 +650,14 @@ input[type=radio] { input.default { width: 1px; height: 1px; visibility: hidden; } +input.disabled, input.disabled:hover { + background-color: #989; + border: 1px inset #bbb; + color: #f60; + font-weight: normal; + text-shadow: 0px 0px 0px #410; +} + input.accept { background: #f3efc7 url('../../console/images/accept.png') no-repeat 2px center; padding: 2px 3px 2px 20px !important; diff --git a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java index 10533cfac..805c8cc4d 100644 --- a/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java +++ b/router/java/src/net/i2p/data/i2np/BuildRequestRecord.java @@ -146,10 +146,10 @@ public class BuildRequestRecord { return (_data.getData()[_data.getOffset() + OFF_FLAG] & FLAG_OUTBOUND_ENDPOINT) != 0; } /** - * Time that the request was sent, truncated to the nearest hour + * Time that the request was sent (ms), truncated to the nearest hour */ public long readRequestTime() { - return DataHelper.fromLong(_data.getData(), _data.getOffset() + OFF_REQ_TIME, 4) * 60l * 60l * 1000l; + return DataHelper.fromLong(_data.getData(), _data.getOffset() + OFF_REQ_TIME, 4) * (60 * 60 * 1000L); } /** * What message ID should we send the request to the next hop with. If this is the outbound tunnel endpoint, @@ -250,6 +250,8 @@ public class BuildRequestRecord { else if (isOutEndpoint) buf[OFF_FLAG] |= FLAG_OUTBOUND_ENDPOINT; long truncatedHour = ctx.clock().now(); + // prevent hop identification at top of the hour + truncatedHour -= ctx.random().nextInt(90*1000); truncatedHour /= (60l*60l*1000l); DataHelper.toLong(buf, OFF_REQ_TIME, 4, truncatedHour); DataHelper.toLong(buf, OFF_SEND_MSG_ID, 4, nextMsgId); diff --git a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java index 37ff20186..790e37622 100644 --- a/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java +++ b/router/java/src/net/i2p/data/i2np/DatabaseLookupMessage.java @@ -17,7 +17,7 @@ import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.data.TunnelId; diff --git a/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java b/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java index 8e48a7439..b34912e8d 100644 --- a/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java +++ b/router/java/src/net/i2p/data/i2np/DatabaseStoreMessage.java @@ -18,7 +18,7 @@ import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.TunnelId; /** diff --git a/core/java/src/net/i2p/data/RouterAddress.java b/router/java/src/net/i2p/data/router/RouterAddress.java similarity index 98% rename from core/java/src/net/i2p/data/RouterAddress.java rename to router/java/src/net/i2p/data/router/RouterAddress.java index 960e495fa..ca0f94d08 100644 --- a/core/java/src/net/i2p/data/RouterAddress.java +++ b/router/java/src/net/i2p/data/router/RouterAddress.java @@ -1,4 +1,4 @@ -package net.i2p.data; +package net.i2p.data.router; /* * free (adj.): unencumbered; not under the control of others @@ -17,6 +17,9 @@ import java.util.Date; import java.util.Map; import java.util.Properties; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.DataStructureImpl; import net.i2p.util.Addresses; import net.i2p.util.OrderedProperties; @@ -36,6 +39,7 @@ import net.i2p.util.OrderedProperties; * several releases for the change to propagate as it is backwards-incompatible. * Restored as of 0.9.12. * + * @since 0.9.16 moved from net.i2p.data * @author jrandom */ public class RouterAddress extends DataStructureImpl { diff --git a/core/java/src/net/i2p/data/RouterIdentity.java b/router/java/src/net/i2p/data/router/RouterIdentity.java similarity index 89% rename from core/java/src/net/i2p/data/RouterIdentity.java rename to router/java/src/net/i2p/data/router/RouterIdentity.java index 346bb5f8d..6dc7ca2d7 100644 --- a/core/java/src/net/i2p/data/RouterIdentity.java +++ b/router/java/src/net/i2p/data/router/RouterIdentity.java @@ -1,4 +1,7 @@ -package net.i2p.data; +package net.i2p.data.router; + +import net.i2p.data.Certificate; +import net.i2p.data.KeysAndCert; /* * free (adj.): unencumbered; not under the control of others @@ -16,6 +19,7 @@ package net.i2p.data; * As of 0.9.9 this data structure is immutable after the two keys and the certificate * are set; attempts to change them will throw an IllegalStateException. * + * @since 0.9.16 moved from net.i2p.data * @author jrandom */ public class RouterIdentity extends KeysAndCert { diff --git a/core/java/src/net/i2p/data/RouterInfo.java b/router/java/src/net/i2p/data/router/RouterInfo.java similarity index 98% rename from core/java/src/net/i2p/data/RouterInfo.java rename to router/java/src/net/i2p/data/router/RouterInfo.java index 22f9ce5dd..390b013ec 100644 --- a/core/java/src/net/i2p/data/RouterInfo.java +++ b/router/java/src/net/i2p/data/router/RouterInfo.java @@ -1,4 +1,4 @@ -package net.i2p.data; +package net.i2p.data.router; /* * free (adj.): unencumbered; not under the control of others @@ -31,6 +31,13 @@ import net.i2p.crypto.SHA1; import net.i2p.crypto.SHA1Hash; import net.i2p.crypto.SHA256Generator; import net.i2p.crypto.SigType; +import net.i2p.data.DatabaseEntry; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Hash; +import net.i2p.data.KeysAndCert; +import net.i2p.data.Signature; +import net.i2p.data.SimpleDataStructure; import net.i2p.util.Clock; import net.i2p.util.Log; import net.i2p.util.OrderedProperties; @@ -47,6 +54,7 @@ import net.i2p.util.SystemVersion; * To ensure integrity of the RouterInfo, methods that change an element of the * RouterInfo will throw an IllegalStateException after the RouterInfo is signed. * + * @since 0.9.16 moved from net.i2p.data * @author jrandom */ public class RouterInfo extends DatabaseEntry { @@ -190,7 +198,7 @@ public class RouterInfo extends DatabaseEntry { // WARNING this sort algorithm cannot be changed, as it must be consistent // network-wide. The signature is not checked at readin time, but only // later, and the addresses are stored in a Set, not a List. - DataHelper.sortStructureList(_addresses); + SortHelper.sortStructureList(_addresses); } } } @@ -308,7 +316,7 @@ public class RouterInfo extends DatabaseEntry { // WARNING this sort algorithm cannot be changed, as it must be consistent // network-wide. The signature is not checked at readin time, but only // later, and the hashes are stored in a Set, not a List. - peers = (Collection) DataHelper.sortStructures(peers); + peers = (Collection) SortHelper.sortStructures(peers); for (Hash peerHash : peers) { peerHash.writeBytes(out); } diff --git a/router/java/src/net/i2p/data/router/RouterKeyGenerator.java b/router/java/src/net/i2p/data/router/RouterKeyGenerator.java new file mode 100644 index 000000000..69e6aacec --- /dev/null +++ b/router/java/src/net/i2p/data/router/RouterKeyGenerator.java @@ -0,0 +1,224 @@ +package net.i2p.data.router; + +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Arrays; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +import net.i2p.I2PAppContext; +import net.i2p.crypto.SHA256Generator; +import net.i2p.data.Hash; +import net.i2p.data.RoutingKeyGenerator; +import net.i2p.util.HexDump; +import net.i2p.util.Log; + +/** + * Component to manage the munging of hashes into routing keys - given a hash, + * perform some consistent transformation against it and return the result. + * This transformation is fed by the current "mod data". + * + * Right now the mod data is the current date (GMT) as a string: "yyyyMMdd", + * and the transformation takes the original hash, appends the bytes of that mod data, + * then returns the SHA256 of that concatenation. + * + * Do we want this to simply do the XOR of the SHA256 of the current mod data and + * the key? does that provide the randomization we need? It'd save an SHA256 op. + * Bah, too much effort to think about for so little gain. Other algorithms may come + * into play layer on about making periodic updates to the routing key for data elements + * to mess with Sybil. This may be good enough though. + * + * Also - the method generateDateBasedModData() should be called after midnight GMT + * once per day to generate the correct routing keys! + * + * @since 0.9.16 moved from net.i2p.data.RoutingKeyGenerator.. + * + */ +public class RouterKeyGenerator extends RoutingKeyGenerator { + private final Log _log; + private final I2PAppContext _context; + + public RouterKeyGenerator(I2PAppContext context) { + _log = context.logManager().getLog(RoutingKeyGenerator.class); + _context = context; + // ensure non-null mod data + generateDateBasedModData(); + } + + private volatile byte _currentModData[]; + private volatile byte _nextModData[]; + private volatile long _nextMidnight; + private volatile long _lastChanged; + + private final static Calendar _cal = GregorianCalendar.getInstance(TimeZone.getTimeZone("GMT")); + private static final String FORMAT = "yyyyMMdd"; + private static final int LENGTH = FORMAT.length(); + private final static SimpleDateFormat _fmt = new SimpleDateFormat(FORMAT, Locale.US); + static { + // make sure GMT is set, azi2phelper Vuze plugin is disabling static JVM TZ setting in Router.java + _fmt.setCalendar(_cal); + } + + /** + * The current (today's) mod data. + * Warning - not a copy, do not corrupt. + * + * @return non-null, 8 bytes + */ + public byte[] getModData() { + return _currentModData; + } + + /** + * Tomorrow's mod data. + * Warning - not a copy, do not corrupt. + * For debugging use only. + * + * @return non-null, 8 bytes + * @since 0.9.10 + */ + public byte[] getNextModData() { + return _nextModData; + } + + public long getLastChanged() { + return _lastChanged; + } + + /** + * How long until midnight (ms) + * + * @return could be slightly negative + * @since 0.9.10 moved from UpdateRoutingKeyModifierJob + */ + public long getTimeTillMidnight() { + return _nextMidnight - _context.clock().now(); + } + + /** + * Set _cal to midnight for the time given. + * Caller must synch. + * @since 0.9.10 + */ + private void setCalToPreviousMidnight(long now) { + _cal.setTime(new Date(now)); + _cal.set(Calendar.YEAR, _cal.get(Calendar.YEAR)); // gcj <= 4.0 workaround + _cal.set(Calendar.DAY_OF_YEAR, _cal.get(Calendar.DAY_OF_YEAR)); // gcj <= 4.0 workaround + _cal.set(Calendar.HOUR_OF_DAY, 0); + _cal.set(Calendar.MINUTE, 0); + _cal.set(Calendar.SECOND, 0); + _cal.set(Calendar.MILLISECOND, 0); + } + + /** + * Generate mod data from _cal. + * Caller must synch. + * @since 0.9.10 + */ + private byte[] generateModDataFromCal() { + Date today = _cal.getTime(); + + String modVal = _fmt.format(today); + if (modVal.length() != LENGTH) + throw new IllegalStateException(); + byte[] mod = new byte[LENGTH]; + for (int i = 0; i < LENGTH; i++) + mod[i] = (byte)(modVal.charAt(i) & 0xFF); + return mod; + } + + /** + * Update the current modifier data with some bytes derived from the current + * date (yyyyMMdd in GMT) + * + * @return true if changed + */ + public synchronized boolean generateDateBasedModData() { + long now = _context.clock().now(); + setCalToPreviousMidnight(now); + byte[] mod = generateModDataFromCal(); + boolean changed = !Arrays.equals(_currentModData, mod); + if (changed) { + // add a day and store next midnight and mod data for convenience + _cal.add(Calendar.DATE, 1); + _nextMidnight = _cal.getTime().getTime(); + byte[] next = generateModDataFromCal(); + _currentModData = mod; + _nextModData = next; + // ensure version is bumped + if (_lastChanged == now) + now++; + _lastChanged = now; + if (_log.shouldLog(Log.INFO)) + _log.info("Routing modifier generated: " + HexDump.dump(mod)); + } + return changed; + } + + /** + * Generate a modified (yet consistent) hash from the origKey by generating the + * SHA256 of the targetKey with the current modData appended to it + * + * This makes Sybil's job a lot harder, as she needs to essentially take over the + * whole keyspace. + * + * @throws IllegalArgumentException if origKey is null + */ + public Hash getRoutingKey(Hash origKey) { + return getKey(origKey, _currentModData); + } + + /** + * Get the routing key using tomorrow's modData, not today's + * + * @since 0.9.10 + */ + public Hash getNextRoutingKey(Hash origKey) { + return getKey(origKey, _nextModData); + } + + /** + * Generate a modified (yet consistent) hash from the origKey by generating the + * SHA256 of the targetKey with the specified modData appended to it + * + * @throws IllegalArgumentException if origKey is null + */ + private static Hash getKey(Hash origKey, byte[] modData) { + if (origKey == null) throw new IllegalArgumentException("Original key is null"); + byte modVal[] = new byte[Hash.HASH_LENGTH + LENGTH]; + System.arraycopy(origKey.getData(), 0, modVal, 0, Hash.HASH_LENGTH); + System.arraycopy(modData, 0, modVal, Hash.HASH_LENGTH, LENGTH); + return SHA256Generator.getInstance().calculateHash(modVal); + } + +/**** + public static void main(String args[]) { + Hash k1 = new Hash(); + byte k1d[] = new byte[Hash.HASH_LENGTH]; + RandomSource.getInstance().nextBytes(k1d); + k1.setData(k1d); + + for (int i = 0; i < 10; i++) { + System.out.println("K1: " + k1); + Hash k1m = RoutingKeyGenerator.getInstance().getRoutingKey(k1); + System.out.println("MOD: " + new String(RoutingKeyGenerator.getInstance().getModData())); + System.out.println("K1M: " + k1m); + } + try { + Thread.sleep(2000); + } catch (Throwable t) { // nop + } + } +****/ +} diff --git a/router/java/src/net/i2p/data/router/RouterPrivateKeyFile.java b/router/java/src/net/i2p/data/router/RouterPrivateKeyFile.java new file mode 100644 index 000000000..2f466ca05 --- /dev/null +++ b/router/java/src/net/i2p/data/router/RouterPrivateKeyFile.java @@ -0,0 +1,62 @@ +package net.i2p.data.router; + + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.IOException; + +import net.i2p.crypto.SigType; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKey; +import net.i2p.data.PrivateKeyFile; +import net.i2p.data.SigningPrivateKey; + +/** + * Same format as super, simply adds a method to + * treat it as a RouterIdentity instead of a Destination. + * + * @since 0.9.16 + */ +public class RouterPrivateKeyFile extends PrivateKeyFile { + + public RouterPrivateKeyFile(File file) { + super(file); + } + + /** + * Read it in from the file. + * Also sets the local privKey and signingPrivKey. + */ + public RouterIdentity getRouterIdentity() throws IOException, DataFormatException { + InputStream in = null; + try { + in = new BufferedInputStream(new FileInputStream(this.file)); + RouterIdentity ri = new RouterIdentity(); + ri.readBytes(in); + privKey = new PrivateKey(); + privKey.readBytes(in); + SigType type = ri.getSigningPublicKey().getType(); + if (type == null) + throw new DataFormatException("Unknown sig type"); + signingPrivKey = new SigningPrivateKey(type); + signingPrivKey.readBytes(in); + + // set it a Destination, so we may call validateKeyPairs() + // or other methods + dest = new Destination(); + dest.setPublicKey(ri.getPublicKey()); + dest.setSigningPublicKey(ri.getSigningPublicKey()); + dest.setCertificate(ri.getCertificate()); + dest.setPadding(ri.getPadding()); + + return ri; + } finally { + if (in != null) { + try { in.close(); } catch (IOException ioe) {} + } + } + } +} diff --git a/router/java/src/net/i2p/data/router/SortHelper.java b/router/java/src/net/i2p/data/router/SortHelper.java new file mode 100644 index 000000000..0c40fa3ed --- /dev/null +++ b/router/java/src/net/i2p/data/router/SortHelper.java @@ -0,0 +1,79 @@ +package net.i2p.data.router; + +/* + * free (adj.): unencumbered; not under the control of others + * Written by jrandom in 2003 and released into the public domain + * with no warranty of any kind, either expressed or implied. + * It probably won't make your computer catch on fire, or eat + * your children, but it might. Use at your own risk. + * + */ + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import net.i2p.data.DataStructure; + +/** + * The sorting of addresses in RIs + * + * @since 0.9.16 moved from DataHelper + */ +class SortHelper { + + /** + * Sort based on the Hash of the DataStructure. + * Warning - relatively slow. + * WARNING - this sort order must be consistent network-wide, so while the order is arbitrary, + * it cannot be changed. + * Why? Just because it has to be consistent so signing will work. + * How to spec as returning the same type as the param? + * DEPRECATED - Only used by RouterInfo. + * + * @return a new list + */ + public static List sortStructures(Collection dataStructures) { + if (dataStructures == null) return Collections.emptyList(); + + // This used to use Hash.toString(), which is insane, since a change to toString() + // would break the whole network. Now use Hash.toBase64(). + // Note that the Base64 sort order is NOT the same as the raw byte sort order, + // despite what you may read elsewhere. + + //ArrayList rv = new ArrayList(dataStructures.size()); + //TreeMap tm = new TreeMap(); + //for (DataStructure struct : dataStructures) { + // tm.put(struct.calculateHash().toString(), struct); + //} + //for (DataStructure struct : tm.values()) { + // rv.add(struct); + //} + ArrayList rv = new ArrayList(dataStructures); + sortStructureList(rv); + return rv; + } + + /** + * See above. + * DEPRECATED - Only used by RouterInfo. + * + * @since 0.9 + */ + static void sortStructureList(List dataStructures) { + Collections.sort(dataStructures, new DataStructureComparator()); + } + + /** + * See sortStructures() comments. + * @since 0.8.3 + */ + private static class DataStructureComparator implements Comparator, Serializable { + public int compare(DataStructure l, DataStructure r) { + return l.calculateHash().toBase64().compareTo(r.calculateHash().toBase64()); + } + } +} diff --git a/router/java/src/net/i2p/data/router/package.html b/router/java/src/net/i2p/data/router/package.html new file mode 100644 index 000000000..fac87d498 --- /dev/null +++ b/router/java/src/net/i2p/data/router/package.html @@ -0,0 +1,7 @@ + + +

    +Classes formerly in net.i2p.data but moved here as they are only used by the router. +

    + + diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java index 093a6fdc5..a6fe1d209 100644 --- a/router/java/src/net/i2p/router/Blocklist.java +++ b/router/java/src/net/i2p/router/Blocklist.java @@ -28,8 +28,8 @@ import java.util.TreeSet; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.util.Addresses; import net.i2p.util.ConcurrentHashSet; diff --git a/router/java/src/net/i2p/router/CommSystemFacade.java b/router/java/src/net/i2p/router/CommSystemFacade.java index 9c7d92338..59a22eb9c 100644 --- a/router/java/src/net/i2p/router/CommSystemFacade.java +++ b/router/java/src/net/i2p/router/CommSystemFacade.java @@ -13,7 +13,7 @@ import java.io.Writer; import java.util.Collections; import java.util.List; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; +import net.i2p.data.router.RouterAddress; /** * Manages the communication subsystem between peers, including connections, diff --git a/router/java/src/net/i2p/router/HandlerJobBuilder.java b/router/java/src/net/i2p/router/HandlerJobBuilder.java index c1c9832cd..62e2074a5 100644 --- a/router/java/src/net/i2p/router/HandlerJobBuilder.java +++ b/router/java/src/net/i2p/router/HandlerJobBuilder.java @@ -9,7 +9,7 @@ package net.i2p.router; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.I2NPMessage; /** diff --git a/router/java/src/net/i2p/router/InNetMessagePool.java b/router/java/src/net/i2p/router/InNetMessagePool.java index 28296cebb..fb4c2a632 100644 --- a/router/java/src/net/i2p/router/InNetMessagePool.java +++ b/router/java/src/net/i2p/router/InNetMessagePool.java @@ -14,7 +14,7 @@ import java.util.Date; import java.util.List; import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DeliveryStatusMessage; diff --git a/router/java/src/net/i2p/router/KeyManager.java b/router/java/src/net/i2p/router/KeyManager.java index ce4992f8b..2807e8fe7 100644 --- a/router/java/src/net/i2p/router/KeyManager.java +++ b/router/java/src/net/i2p/router/KeyManager.java @@ -18,6 +18,7 @@ import java.io.OutputStream; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import net.i2p.crypto.SigType; import net.i2p.data.DataFormatException; import net.i2p.data.DataStructure; import net.i2p.data.Destination; @@ -26,6 +27,7 @@ import net.i2p.data.PrivateKey; import net.i2p.data.PublicKey; import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPublicKey; +import net.i2p.router.startup.CreateRouterInfoJob; import net.i2p.util.Log; import net.i2p.util.SecureDirectory; import net.i2p.util.SecureFileOutputStream; @@ -47,10 +49,10 @@ public class KeyManager { public final static String PROP_KEYDIR = "router.keyBackupDir"; public final static String DEFAULT_KEYDIR = "keyBackup"; - private final static String KEYFILE_PRIVATE_ENC = "privateEncryption.key"; - private final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key"; - private final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key"; - private final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key"; + public final static String KEYFILE_PRIVATE_ENC = "privateEncryption.key"; + public final static String KEYFILE_PUBLIC_ENC = "publicEncryption.key"; + public final static String KEYFILE_PRIVATE_SIGNING = "privateSigning.key"; + public final static String KEYFILE_PUBLIC_SIGNING = "publicSigning.key"; public KeyManager(RouterContext context) { _context = context; @@ -151,8 +153,9 @@ public class KeyManager { private void syncKeys(File keyDir) { syncPrivateKey(keyDir); syncPublicKey(keyDir); - syncSigningKey(keyDir); - syncVerificationKey(keyDir); + SigType type = CreateRouterInfoJob.getSigTypeConfig(getContext()); + syncSigningKey(keyDir, type); + syncVerificationKey(keyDir, type); } private void syncPrivateKey(File keyDir) { @@ -181,27 +184,33 @@ public class KeyManager { _publicKey = (PublicKey) readin; } - private void syncSigningKey(File keyDir) { + /** + * @param type the SigType to expect on read-in, ignored on write + */ + private void syncSigningKey(File keyDir, SigType type) { DataStructure ds; File keyFile = new File(keyDir, KEYFILE_PRIVATE_SIGNING); boolean exists = (_signingPrivateKey != null); if (exists) ds = _signingPrivateKey; else - ds = new SigningPrivateKey(); + ds = new SigningPrivateKey(type); DataStructure readin = syncKey(keyFile, ds, exists); if (readin != null && !exists) _signingPrivateKey = (SigningPrivateKey) readin; } - private void syncVerificationKey(File keyDir) { + /** + * @param type the SigType to expect on read-in, ignored on write + */ + private void syncVerificationKey(File keyDir, SigType type) { DataStructure ds; File keyFile = new File(keyDir, KEYFILE_PUBLIC_SIGNING); boolean exists = (_signingPublicKey != null); if (exists) ds = _signingPublicKey; else - ds = new SigningPublicKey(); + ds = new SigningPublicKey(type); DataStructure readin = syncKey(keyFile, ds, exists); if (readin != null && !exists) _signingPublicKey = (SigningPublicKey) readin; diff --git a/router/java/src/net/i2p/router/LeaseSetKeys.java b/router/java/src/net/i2p/router/LeaseSetKeys.java index abfc566df..849e54f47 100644 --- a/router/java/src/net/i2p/router/LeaseSetKeys.java +++ b/router/java/src/net/i2p/router/LeaseSetKeys.java @@ -40,7 +40,7 @@ public class LeaseSetKeys { /** * Key with which a LeaseSet can be revoked (by republishing it with no Leases) * - * @deprecated unused + * Deprecated, unused */ public SigningPrivateKey getRevocationKey() { return _revocationKey; } diff --git a/router/java/src/net/i2p/router/MultiRouter.java b/router/java/src/net/i2p/router/MultiRouter.java index 5e8cb4760..abcd32e3d 100644 --- a/router/java/src/net/i2p/router/MultiRouter.java +++ b/router/java/src/net/i2p/router/MultiRouter.java @@ -10,7 +10,7 @@ import java.util.Scanner; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.Router; /** diff --git a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java index 7fc3c5082..fdd1fd30f 100644 --- a/router/java/src/net/i2p/router/NetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/NetworkDatabaseFacade.java @@ -14,9 +14,10 @@ import java.util.Collections; import java.util.Set; import net.i2p.data.DatabaseEntry; +import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.networkdb.reseed.ReseedChecker; /** @@ -51,18 +52,51 @@ public abstract class NetworkDatabaseFacade implements Service { public abstract LeaseSet lookupLeaseSetLocally(Hash key); public abstract void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs); public abstract RouterInfo lookupRouterInfoLocally(Hash key); + + /** + * Lookup using the client's tunnels + * Succeeds even if LS validation fails due to unsupported sig type + * + * @param fromLocalDest use these tunnels for the lookup, or null for exploratory + * @since 0.9.16 + */ + public abstract void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest); + + /** + * Lookup locally in netDB and in badDest cache + * Succeeds even if LS validation failed due to unsupported sig type + * + * @since 0.9.16 + */ + public abstract Destination lookupDestinationLocally(Hash key); + /** - * return the leaseSet if another leaseSet already existed at that key + * @return the leaseSet if another leaseSet already existed at that key * * @throws IllegalArgumentException if the data is not valid */ public abstract LeaseSet store(Hash key, LeaseSet leaseSet) throws IllegalArgumentException; + /** - * return the routerInfo if another router already existed at that key + * @return the routerInfo if another router already existed at that key * * @throws IllegalArgumentException if the data is not valid */ public abstract RouterInfo store(Hash key, RouterInfo routerInfo) throws IllegalArgumentException; + + /** + * @return the old entry if it already existed at that key + * @throws IllegalArgumentException if the data is not valid + * @since 0.9.16 + */ + public DatabaseEntry store(Hash key, DatabaseEntry entry) throws IllegalArgumentException { + if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) + return store(key, (RouterInfo) entry); + if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) + return store(key, (LeaseSet) entry); + throw new IllegalArgumentException("unknown type"); + } + /** * @throws IllegalArgumentException if the local router is not valid */ @@ -101,4 +135,12 @@ public abstract class NetworkDatabaseFacade implements Service { * @since IPv6 */ public boolean floodfillEnabled() { return false; }; + + /** + * Is it permanently negative cached? + * + * @param key only for Destinations; for RouterIdentities, see Banlist + * @since 0.9.16 + */ + public boolean isNegativeCachedForever(Hash key) { return false; } } diff --git a/router/java/src/net/i2p/router/OutNetMessage.java b/router/java/src/net/i2p/router/OutNetMessage.java index 633416640..54434ac4b 100644 --- a/router/java/src/net/i2p/router/OutNetMessage.java +++ b/router/java/src/net/i2p/router/OutNetMessage.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; import java.util.Set; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.util.CDPQEntry; import net.i2p.util.Log; diff --git a/router/java/src/net/i2p/router/PersistentKeyRing.java b/router/java/src/net/i2p/router/PersistentKeyRing.java index a3e71ee8e..920eec7d2 100644 --- a/router/java/src/net/i2p/router/PersistentKeyRing.java +++ b/router/java/src/net/i2p/router/PersistentKeyRing.java @@ -70,9 +70,8 @@ public class PersistentKeyRing extends KeyRing { Hash h = e.getKey(); buf.append(h.toBase64().substring(0, 6)).append("…"); buf.append("
    "); - LeaseSet ls = _ctx.netDb().lookupLeaseSetLocally(h); - if (ls != null) { - Destination dest = ls.getDestination(); + Destination dest = _ctx.netDb().lookupDestinationLocally(h); + if (dest != null) { if (_ctx.clientManager().isLocal(dest)) { TunnelPoolSettings in = _ctx.tunnelManager().getInboundSettings(h); if (in != null && in.getDestinationNickname() != null) diff --git a/router/java/src/net/i2p/router/Router.java b/router/java/src/net/i2p/router/Router.java index 106efe00d..8fd08eb79 100644 --- a/router/java/src/net/i2p/router/Router.java +++ b/router/java/src/net/i2p/router/Router.java @@ -29,11 +29,12 @@ import net.i2p.data.Certificate; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SigningPrivateKey; import net.i2p.data.i2np.GarlicMessage; import net.i2p.router.message.GarlicMessageHandler; import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; +import net.i2p.router.startup.CreateRouterInfoJob; import net.i2p.router.startup.StartupJob; import net.i2p.router.startup.WorkingDir; import net.i2p.router.tasks.*; @@ -98,10 +99,6 @@ public class Router implements RouterClock.ClockShiftListener { /** this does not put an 'H' in your routerInfo **/ public final static String PROP_HIDDEN_HIDDEN = "router.isHidden"; public final static String PROP_DYNAMIC_KEYS = "router.dynamicKeys"; - public final static String PROP_INFO_FILENAME = "router.info.location"; - public final static String PROP_INFO_FILENAME_DEFAULT = "router.info"; - public final static String PROP_KEYS_FILENAME = "router.keys.location"; - public final static String PROP_KEYS_FILENAME_DEFAULT = "router.keys"; public final static String PROP_SHUTDOWN_IN_PROGRESS = "__shutdownInProgress"; public final static String DNS_CACHE_TIME = "" + (5*60); private static final String EVENTLOG = "eventlog.txt"; @@ -672,20 +669,6 @@ public class Router implements RouterClock.ClockShiftListener { return Boolean.parseBoolean(h); return _context.commSystem().isInBadCountry(); } - - /** - * Only called at startup via LoadRouterInfoJob and RebuildRouterInfoJob. - * Not called by periodic RepublishLocalRouterInfoJob. - * We don't want to change the cert on the fly as it changes the router hash. - * RouterInfo.isHidden() checks the capability, but RouterIdentity.isHidden() checks the cert. - * There's no reason to ever add a hidden cert? - * @return the certificate for a new RouterInfo - probably a null cert. - */ - public Certificate createCertificate() { - if (_context.getBooleanProperty(PROP_HIDDEN)) - return new Certificate(Certificate.CERTIFICATE_TYPE_HIDDEN, null); - return Certificate.NULL_CERT; - } /** * @since 0.9.3 @@ -698,16 +681,18 @@ public class Router implements RouterClock.ClockShiftListener { * Ugly list of files that we need to kill if we are building a new identity * */ - private static final String _rebuildFiles[] = new String[] { "router.info", - "router.keys", - "netDb/my.info", // no longer used - "connectionTag.keys", // never used? - "keyBackup/privateEncryption.key", - "keyBackup/privateSigning.key", - "keyBackup/publicEncryption.key", - "keyBackup/publicSigning.key", - "sessionKeys.dat" // no longer used - }; + private static final String _rebuildFiles[] = new String[] { + CreateRouterInfoJob.INFO_FILENAME, + CreateRouterInfoJob.KEYS_FILENAME, + CreateRouterInfoJob.KEYS2_FILENAME, + "netDb/my.info", // no longer used + "connectionTag.keys", // never used? + KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PRIVATE_ENC, + KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PUBLIC_ENC, + KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PRIVATE_SIGNING, + KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PUBLIC_SIGNING, + "sessionKeys.dat" // no longer used + }; public void killKeys() { //new Exception("Clearing identity files").printStackTrace(); @@ -1085,7 +1070,7 @@ public class Router implements RouterClock.ClockShiftListener { return; _eventLog.addEvent(EventLog.CLOCK_SHIFT, Long.toString(delta)); // update the routing key modifier - _context.routingKeyGenerator().generateDateBasedModData(); + _context.routerKeyGenerator().generateDateBasedModData(); if (_context.commSystem().countActivePeers() <= 0) return; if (delta > 0) diff --git a/router/java/src/net/i2p/router/RouterContext.java b/router/java/src/net/i2p/router/RouterContext.java index 1f8a38af6..fd5c65775 100644 --- a/router/java/src/net/i2p/router/RouterContext.java +++ b/router/java/src/net/i2p/router/RouterContext.java @@ -10,7 +10,9 @@ import java.util.concurrent.CopyOnWriteArrayList; import net.i2p.I2PAppContext; import net.i2p.app.ClientAppManager; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.RoutingKeyGenerator; +import net.i2p.data.router.RouterInfo; +import net.i2p.data.router.RouterKeyGenerator; import net.i2p.internal.InternalClientManager; import net.i2p.router.client.ClientManagerFacadeImpl; import net.i2p.router.crypto.TransientSessionKeyManager; @@ -65,6 +67,7 @@ public class RouterContext extends I2PAppContext { //private MessageStateMonitor _messageStateMonitor; private RouterThrottle _throttle; private RouterAppManager _appManager; + private RouterKeyGenerator _routingKeyGenerator; private final Set _finalShutdownTasks; // split up big lock on this to avoid deadlocks private volatile boolean _initialized; @@ -183,6 +186,7 @@ public class RouterContext extends I2PAppContext { _messageHistory = new MessageHistory(this); _messageRegistry = new OutboundMessageRegistry(this); //_messageStateMonitor = new MessageStateMonitor(this); + _routingKeyGenerator = new RouterKeyGenerator(this); if (!getBooleanProperty("i2p.dummyNetDb")) _netDb = new FloodfillNetworkDatabaseFacade(this); // new KademliaNetworkDatabaseFacade(this); else @@ -582,4 +586,35 @@ public class RouterContext extends I2PAppContext { _sessionKeyManagerInitialized = true; } } + + /** + * Determine how much do we want to mess with the keys to turn them + * into something we can route. This is context specific because we + * may want to test out how things react when peers don't agree on + * how to skew. + * + * Returns same thing as routerKeyGenerator() + * + * @return non-null + * @since 0.9.16 Overrides I2PAppContext. Returns non-null in RouterContext and null in I2PAppcontext. + */ + @Override + public RoutingKeyGenerator routingKeyGenerator() { + return _routingKeyGenerator; + } + + /** + * Determine how much do we want to mess with the keys to turn them + * into something we can route. This is context specific because we + * may want to test out how things react when peers don't agree on + * how to skew. + * + * Returns same thing as routingKeyGenerator() + * + * @return non-null + * @since 0.9.16 + */ + public RouterKeyGenerator routerKeyGenerator() { + return _routingKeyGenerator; + } } diff --git a/router/java/src/net/i2p/router/RouterThrottleImpl.java b/router/java/src/net/i2p/router/RouterThrottleImpl.java index 805e7bbbc..275e4d85a 100644 --- a/router/java/src/net/i2p/router/RouterThrottleImpl.java +++ b/router/java/src/net/i2p/router/RouterThrottleImpl.java @@ -1,7 +1,7 @@ package net.i2p.router; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.peermanager.TunnelHistory; import net.i2p.stat.Rate; import net.i2p.stat.RateAverages; diff --git a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java index cdcc0008c..382552ce6 100644 --- a/router/java/src/net/i2p/router/client/ClientMessageEventListener.java +++ b/router/java/src/net/i2p/router/client/ClientMessageEventListener.java @@ -12,8 +12,10 @@ import java.util.Properties; import net.i2p.CoreVersion; import net.i2p.crypto.SigType; +import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.Payload; +import net.i2p.data.PublicKey; import net.i2p.data.i2cp.BandwidthLimitsMessage; import net.i2p.data.i2cp.CreateLeaseSetMessage; import net.i2p.data.i2cp.CreateSessionMessage; @@ -37,6 +39,7 @@ import net.i2p.data.i2cp.SessionId; import net.i2p.data.i2cp.SessionStatusMessage; import net.i2p.data.i2cp.SetDateMessage; import net.i2p.router.ClientTunnelSettings; +import net.i2p.router.LeaseSetKeys; import net.i2p.router.RouterContext; import net.i2p.util.Log; import net.i2p.util.PasswordManager; @@ -81,8 +84,8 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi _log.debug("Message received: \n" + message); int type = message.getType(); if (!_authorized) { - // TODO change to default true - boolean strict = _context.getBooleanProperty(PROP_AUTH_STRICT); + // Default true as of 0.9.16 + boolean strict = _context.getBooleanPropertyDefaultTrue(PROP_AUTH_STRICT); if ((strict && type != GetDateMessage.MESSAGE_TYPE) || (type != CreateSessionMessage.MESSAGE_TYPE && type != GetDateMessage.MESSAGE_TYPE && @@ -367,8 +370,41 @@ class ClientMessageEventListener implements I2CPMessageReader.I2CPMessageEventLi _runner.disconnectClient("Invalid CreateLeaseSetMessage"); return; } - - _context.keyManager().registerKeys(message.getLeaseSet().getDestination(), message.getSigningPrivateKey(), message.getPrivateKey()); + Destination dest = _runner.getConfig().getDestination(); + Destination ndest = message.getLeaseSet().getDestination(); + if (!dest.equals(ndest)) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Different destination in LS"); + _runner.disconnectClient("Different destination in LS"); + return; + } + LeaseSetKeys keys = _context.keyManager().getKeys(dest); + if (keys == null || + !message.getPrivateKey().equals(keys.getDecryptionKey())) { + // Verify and register crypto keys if new or if changed + // Private crypto key should never change, and if it does, + // one of the checks below will fail + PublicKey pk; + try { + pk = message.getPrivateKey().toPublic(); + } catch (IllegalArgumentException iae) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Bad private key in LS"); + _runner.disconnectClient("Bad private key in LS"); + return; + } + if (!pk.equals(message.getLeaseSet().getEncryptionKey())) { + if (_log.shouldLog(Log.ERROR)) + _log.error("Private/public crypto key mismatch in LS"); + _runner.disconnectClient("Private/public crypto key mismatch in LS"); + return; + } + // just register new SPK, don't verify, unused + _context.keyManager().registerKeys(dest, message.getSigningPrivateKey(), message.getPrivateKey()); + } else if (!message.getSigningPrivateKey().equals(keys.getRevocationKey())) { + // just register new SPK, don't verify, unused + _context.keyManager().registerKeys(dest, message.getSigningPrivateKey(), message.getPrivateKey()); + } try { _context.netDb().publish(message.getLeaseSet()); } catch (IllegalArgumentException iae) { diff --git a/router/java/src/net/i2p/router/client/LookupDestJob.java b/router/java/src/net/i2p/router/client/LookupDestJob.java index be08388ba..911365b8e 100644 --- a/router/java/src/net/i2p/router/client/LookupDestJob.java +++ b/router/java/src/net/i2p/router/client/LookupDestJob.java @@ -38,7 +38,11 @@ class LookupDestJob extends JobImpl { } /** - * One of h or name non-null + * One of h or name non-null. + * + * For hash or b32 name, the dest will be returned if the LS can be found, + * even if the dest uses unsupported crypto. + * * @param reqID must be >= 0 if name != null * @param sessID must non-null if reqID >= 0 * @param fromLocalDest use these tunnels for the lookup, or null for exploratory @@ -88,7 +92,7 @@ class LookupDestJob extends JobImpl { returnFail(); } else { DoneJob done = new DoneJob(getContext()); - getContext().netDb().lookupLeaseSet(_hash, done, done, _timeout, _fromLocalDest); + getContext().netDb().lookupDestination(_hash, done, _timeout, _fromLocalDest); } } @@ -98,9 +102,9 @@ class LookupDestJob extends JobImpl { } public String getName() { return "LeaseSet Lookup Reply to Client"; } public void runJob() { - LeaseSet ls = getContext().netDb().lookupLeaseSetLocally(_hash); - if (ls != null) - returnDest(ls.getDestination()); + Destination dest = getContext().netDb().lookupDestinationLocally(_hash); + if (dest != null) + returnDest(dest); else returnFail(); } diff --git a/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java index 11b9419f3..460d7a712 100644 --- a/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/dummy/DummyNetworkDatabaseFacade.java @@ -15,16 +15,17 @@ import java.util.Map; import java.util.Set; import net.i2p.data.DatabaseEntry; +import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.Job; import net.i2p.router.NetworkDatabaseFacade; import net.i2p.router.RouterContext; public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade { - private Map _routers; - private RouterContext _context; + private final Map _routers; + private final RouterContext _context; public DummyNetworkDatabaseFacade(RouterContext ctx) { _routers = Collections.synchronizedMap(new HashMap()); @@ -42,6 +43,11 @@ public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade { public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) {} public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs, Hash fromLocalDest) {} public LeaseSet lookupLeaseSetLocally(Hash key) { return null; } + + public void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest) {} + + public Destination lookupDestinationLocally(Hash key) { return null; } + public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) { RouterInfo info = lookupRouterInfoLocally(key); if (info == null) @@ -50,13 +56,16 @@ public class DummyNetworkDatabaseFacade extends NetworkDatabaseFacade { _context.jobQueue().addJob(onFindJob); } public RouterInfo lookupRouterInfoLocally(Hash key) { return _routers.get(key); } + public void publish(LeaseSet localLeaseSet) {} public void publish(RouterInfo localRouterInfo) {} + public LeaseSet store(Hash key, LeaseSet leaseSet) { return leaseSet; } public RouterInfo store(Hash key, RouterInfo routerInfo) { RouterInfo rv = _routers.put(key, routerInfo); return rv; } + public void unpublish(LeaseSet localLeaseSet) {} public void fail(Hash dbEntry) { _routers.remove(dbEntry); diff --git a/router/java/src/net/i2p/router/message/GarlicConfig.java b/router/java/src/net/i2p/router/message/GarlicConfig.java index 9fe299e0c..4970a589f 100644 --- a/router/java/src/net/i2p/router/message/GarlicConfig.java +++ b/router/java/src/net/i2p/router/message/GarlicConfig.java @@ -14,7 +14,7 @@ import java.util.List; import net.i2p.data.Certificate; import net.i2p.data.PublicKey; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DeliveryInstructions; /** diff --git a/router/java/src/net/i2p/router/message/GarlicMessageHandler.java b/router/java/src/net/i2p/router/message/GarlicMessageHandler.java index c44ec2f05..3a762b6fd 100644 --- a/router/java/src/net/i2p/router/message/GarlicMessageHandler.java +++ b/router/java/src/net/i2p/router/message/GarlicMessageHandler.java @@ -9,7 +9,7 @@ package net.i2p.router.message; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.HandlerJobBuilder; diff --git a/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java index 9392526b3..9a22ec6e8 100644 --- a/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java +++ b/router/java/src/net/i2p/router/message/HandleGarlicMessageJob.java @@ -9,7 +9,7 @@ package net.i2p.router.message; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.DeliveryInstructions; import net.i2p.data.i2np.GarlicMessage; import net.i2p.data.i2np.I2NPMessage; diff --git a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java index 5040606e8..d9f7a0a51 100644 --- a/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java +++ b/router/java/src/net/i2p/router/message/OutboundClientMessageOneShotJob.java @@ -18,7 +18,7 @@ import net.i2p.data.Lease; import net.i2p.data.LeaseSet; import net.i2p.data.Payload; import net.i2p.data.PublicKey; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.data.i2cp.MessageId; @@ -425,12 +425,19 @@ public class OutboundClientMessageOneShotJob extends JobImpl { getContext().statManager().addRateData("client.leaseSetFailedRemoteTime", lookupTime); } - //if (_finished == Result.NONE) { + + int cause; + if (getContext().netDb().isNegativeCachedForever(_to.calculateHash())) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Unable to send to " + _toString + " because the sig type is unsupported"); + cause = MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION; + } else { if (_log.shouldLog(Log.WARN)) _log.warn("Unable to send to " + _toString + " because we couldn't find their leaseSet"); - //} + cause = MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET; + } - dieFatal(MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET); + dieFatal(cause); } } diff --git a/router/java/src/net/i2p/router/message/SendMessageDirectJob.java b/router/java/src/net/i2p/router/message/SendMessageDirectJob.java index 6d0cfdbcf..cc7a337cc 100644 --- a/router/java/src/net/i2p/router/message/SendMessageDirectJob.java +++ b/router/java/src/net/i2p/router/message/SendMessageDirectJob.java @@ -11,7 +11,7 @@ package net.i2p.router.message; import java.util.Date; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.Job; import net.i2p.router.JobImpl; diff --git a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java index 685e18512..9931444aa 100644 --- a/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/HandleDatabaseLookupMessageJob.java @@ -14,8 +14,8 @@ import java.util.Set; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterIdentity; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; diff --git a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java index f6a29355b..8b235260e 100644 --- a/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java +++ b/router/java/src/net/i2p/router/networkdb/PublishLocalRouterInfoJob.java @@ -12,7 +12,7 @@ import java.util.Date; import java.util.Properties; import net.i2p.data.DataFormatException; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SigningPrivateKey; import net.i2p.router.JobImpl; import net.i2p.router.Router; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java b/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java index ebcde49c6..f916e976f 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/BundleRouterInfos.java @@ -25,8 +25,8 @@ import gnu.getopt.Getopt; import net.i2p.I2PAppContext; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.router.transport.BadCountries; import net.i2p.router.transport.GeoIP; import net.i2p.util.FileUtil; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java index c2ea790a5..b52df8e17 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExpireRoutersJob.java @@ -12,7 +12,7 @@ import java.util.Set; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.CommSystemFacade; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java index 88122c0e2..aff802b77 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/ExploreJob.java @@ -15,6 +15,8 @@ import java.util.Set; import net.i2p.data.Hash; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.router.RouterContext; import net.i2p.util.Log; @@ -71,15 +73,15 @@ class ExploreJob extends SearchJob { * and PeerSelector doesn't include the floodfill peers, * so we add the ff peers ourselves and then use the regular PeerSelector. * - * TODO should we encrypt this also like we do for normal lookups? - * Could the OBEP capture it and reply with a reference to a hostile peer? - * * @param replyTunnelId tunnel to receive replies through * @param replyGateway gateway for the reply tunnel * @param expiration when the search should stop + * @param peer the peer to send it to + * + * @return a DatabaseLookupMessage or GarlicMessage */ @Override - protected DatabaseLookupMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration) { + protected I2NPMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration, RouterInfo peer) { DatabaseLookupMessage msg = new DatabaseLookupMessage(getContext(), true); msg.setSearchKey(getState().getTarget()); msg.setFrom(replyGateway); @@ -127,7 +129,27 @@ class ExploreJob extends SearchJob { _log.debug("Peers we don't want to hear about: " + dontIncludePeers); msg.setDontIncludePeers(dontIncludePeers); - return msg; + + // Now encrypt if we can + I2NPMessage outMsg; + if (getContext().getProperty(IterativeSearchJob.PROP_ENCRYPT_RI, IterativeSearchJob.DEFAULT_ENCRYPT_RI)) { + // request encrypted reply? + if (DatabaseLookupMessage.supportsEncryptedReplies(peer)) { + MessageWrapper.OneTimeSession sess; + sess = MessageWrapper.generateSession(getContext()); + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Requesting encrypted reply from " + peer.getIdentity().calculateHash() + + ' ' + sess.key + ' ' + sess.tag); + msg.setReplySession(sess.key, sess.tag); + } + outMsg = MessageWrapper.wrap(getContext(), msg, peer); + if (_log.shouldLog(Log.DEBUG)) + _log.debug(getJobId() + ": Encrypted exploratory DLM for " + getState().getTarget() + " to " + + peer.getIdentity().calculateHash()); + } else { + outMsg = msg; + } + return outMsg; } /** max # of concurrent searches */ diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java index 8f3a193a5..46f985a91 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodOnlyLookupMatchJob.java @@ -2,10 +2,10 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.DatabaseEntry; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.ReplyJob; import net.i2p.router.RouterContext; @@ -62,6 +62,9 @@ class FloodOnlyLookupMatchJob extends JobImpl implements ReplyJob { } else { getContext().netDb().store(dsm.getKey(), (RouterInfo) dsm.getEntry()); } + } catch (UnsupportedCryptoException uce) { + _search.failed(); + return; } catch (IllegalArgumentException iae) { if (_log.shouldLog(Log.WARN)) _log.warn(_search.getJobId() + ": Received an invalid store reply", iae); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java index 8ef121d06..a07ac8c70 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseLookupMessageHandler.java @@ -9,7 +9,7 @@ package net.i2p.router.networkdb.kademlia; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.HandlerJobBuilder; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java index e4c6ce71a..99d8c6265 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillDatabaseStoreMessageHandler.java @@ -9,7 +9,7 @@ package net.i2p.router.networkdb.kademlia; */ import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; +import net.i2p.data.router.RouterIdentity; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.HandlerJobBuilder; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java index 99c4beda2..d94475ff2 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillMonitorJob.java @@ -3,14 +3,17 @@ package net.i2p.router.networkdb.kademlia; import java.util.List; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.Router; import net.i2p.router.RouterContext; import net.i2p.router.peermanager.PeerProfile; import net.i2p.router.util.EventLog; +import net.i2p.stat.Rate; +import net.i2p.stat.RateStat; import net.i2p.util.Log; +import net.i2p.util.SystemVersion; /** * Simple job to monitor the floodfill pool. @@ -78,6 +81,10 @@ class FloodfillMonitorJob extends JobImpl { // auto from here down + // ARM ElG decrypt is too slow + if (SystemVersion.isARM()) + return false; + // Only if up a while... if (getContext().router().getUptime() < MIN_UPTIME) return false; @@ -148,12 +155,22 @@ class FloodfillMonitorJob extends JobImpl { happy = false; } } - + + double elG = 0; + RateStat stat = getContext().statManager().getRate("crypto.elGamal.decrypt"); + if (stat != null) { + Rate rate = stat.getRate(60*60*1000L); + if (rate != null) { + elG = rate.getAvgOrLifetimeAvg(); + happy = happy && elG <= 40.0d; + } + } + if (_log.shouldLog(Log.DEBUG)) { final RouterContext rc = getContext(); final String log = String.format( "FF criteria breakdown: happy=%b, capabilities=%s, maxLag=%d, known=%d, " + - "active=%d, participating=%d, offset=%d, ssuAddr=%s", + "active=%d, participating=%d, offset=%d, ssuAddr=%s ElG=%f", happy, rc.router().getRouterInfo().getCapabilities(), rc.jobQueue().getMaxLag(), @@ -161,7 +178,8 @@ class FloodfillMonitorJob extends JobImpl { rc.commSystem().countActivePeers(), rc.tunnelManager().getParticipatingCount(), Math.abs(rc.clock().getOffset()), - rc.router().getRouterInfo().getTargetAddress("SSU").toString() + rc.router().getRouterInfo().getTargetAddress("SSU").toString(), + elG ); _log.debug(log); } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java index 24a7bc40a..935809226 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillNetworkDatabaseFacade.java @@ -7,11 +7,13 @@ import java.util.Map; import java.util.Set; import net.i2p.data.DatabaseEntry; +import net.i2p.data.Destination; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.router.RouterInfo; +import net.i2p.data.router.RouterKeyGenerator; import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; @@ -31,7 +33,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad private final Set _verifiesInProgress; private FloodThrottler _floodThrottler; private LookupThrottler _lookupThrottler; - private NegativeLookupCache _negativeCache; /** * This is the flood redundancy. Entries are @@ -65,7 +66,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad _context.statManager().createRateStat("netDb.searchReplyNotValidated", "How many search replies we get that we are NOT able to validate (fetch)", "NetworkDatabase", new long[] { 5*60*1000l, 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l }); _context.statManager().createRateStat("netDb.searchReplyValidationSkipped", "How many search replies we get from unreliable peers that we skip?", "NetworkDatabase", new long[] { 5*60*1000l, 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l }); _context.statManager().createRateStat("netDb.republishQuantity", "How many peers do we need to send a found leaseSet to?", "NetworkDatabase", new long[] { 10*60*1000l, 60*60*1000l, 3*60*60*1000l, 24*60*60*1000l }); - _context.statManager().createRateStat("netDb.negativeCache", "Aborted lookup, already cached", "NetworkDatabase", new long[] { 60*60*1000l }); } @Override @@ -73,7 +73,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad super.startup(); _context.jobQueue().addJob(new FloodfillMonitorJob(_context, this)); _lookupThrottler = new LookupThrottler(); - _negativeCache = new NegativeLookupCache(); // refresh old routers Job rrj = new RefreshRoutersJob(_context, this); @@ -171,25 +170,6 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad return _lookupThrottler.shouldThrottle(from, id); } - /** - * Increment in the negative lookup cache - * @since 0.9.4 - */ - void lookupFailed(Hash key) { - _negativeCache.lookupFailed(key); - } - - /** - * Is the key in the negative lookup cache? - * @since 0.9.4 - */ - boolean isNegativeCached(Hash key) { - boolean rv = _negativeCache.isCached(key); - if (rv) - _context.statManager().addRateData("netDb.negativeCache", 1); - return rv; - } - /** * Send to a subset of all floodfill peers. * We do this to implement Kademlia within the floodfills, i.e. @@ -197,16 +177,17 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad */ public void flood(DatabaseEntry ds) { Hash key = ds.getHash(); - Hash rkey = _context.routingKeyGenerator().getRoutingKey(key); + RouterKeyGenerator gen = _context.routerKeyGenerator(); + Hash rkey = gen.getRoutingKey(key); FloodfillPeerSelector sel = (FloodfillPeerSelector)getPeerSelector(); List peers = sel.selectFloodfillParticipants(rkey, MAX_TO_FLOOD, getKBuckets()); // todo key cert skip? - long until = _context.routingKeyGenerator().getTimeTillMidnight(); + long until = gen.getTimeTillMidnight(); if (until < NEXT_RKEY_LS_ADVANCE_TIME || (ds.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO && until < NEXT_RKEY_RI_ADVANCE_TIME)) { - // to avoid lookup failures after midnight, also flood to some closest to the + // to avoid lookup faulures after midnight, also flood to some closest to the // next routing key for a period of time before midnight. - Hash nkey = _context.routingKeyGenerator().getNextRoutingKey(key); + Hash nkey = gen.getNextRoutingKey(key); List nextPeers = sel.selectFloodfillParticipants(nkey, NEXT_FLOOD_QTY, getKBuckets()); int i = 0; for (Hash h : nextPeers) { @@ -301,7 +282,9 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad } /** - * Lookup using exploratory tunnels + * Lookup using exploratory tunnels. + * + * Caller should check negative cache and/or banlist before calling. * * Begin a kademlia style search for the key specified, which can take up to timeoutMs and * will fire the appropriate jobs on success or timeout (or if the kademlia search completes @@ -315,7 +298,10 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad } /** - * Lookup using the client's tunnels + * Lookup using the client's tunnels. + * + * Caller should check negative cache and/or banlist before calling. + * * @param fromLocalDest use these tunnels for the lookup, or null for exploratory * @return null always * @since 0.9.10 @@ -473,6 +459,7 @@ public class FloodfillNetworkDatabaseFacade extends KademliaNetworkDatabaseFacad // should we skip the search? if (_floodfillEnabled || _context.jobQueue().getMaxLag() > 500 || + _context.banlist().isBanlistedForever(peer) || getKBucketSetSize() > MAX_DB_BEFORE_SKIPPING_SEARCH) { // don't try to overload ourselves (e.g. failing 3000 router refs at // once, and then firing off 3000 netDb lookup tasks) diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java index fdf47bf3c..6870edcd5 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillPeerSelector.java @@ -18,8 +18,8 @@ import java.util.Set; import java.util.TreeSet; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.kademlia.SelectionCollector; import net.i2p.kademlia.XORComparator; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java index f66b50020..085c0c921 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/FloodfillVerifyStoreJob.java @@ -6,9 +6,10 @@ import java.util.Set; import net.i2p.data.Certificate; import net.i2p.data.DatabaseEntry; +import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; @@ -173,9 +174,9 @@ class FloodfillVerifyStoreJob extends JobImpl { FloodfillPeerSelector sel = (FloodfillPeerSelector)_facade.getPeerSelector(); Certificate keyCert = null; if (!_isRouterInfo) { - LeaseSet ls = _facade.lookupLeaseSetLocally(_key); - if (ls != null) { - Certificate cert = ls.getDestination().getCertificate(); + Destination dest = _facade.lookupDestinationLocally(_key); + if (dest != null) { + Certificate cert = dest.getCertificate(); if (cert.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) keyCert = cert; } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java index 0d03d7d4f..eceb4b576 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseLookupMessageJob.java @@ -11,8 +11,8 @@ package net.i2p.router.networkdb.kademlia; import java.util.Set; import net.i2p.data.Hash; -import net.i2p.data.RouterIdentity; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.DatabaseLookupMessage; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java index f623022ca..ffb6d77f0 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/HandleFloodfillDatabaseStoreMessageJob.java @@ -14,9 +14,9 @@ import java.util.Date; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterIdentity; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.DeliveryStatusMessage; import net.i2p.router.JobImpl; @@ -51,6 +51,8 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { long recvBegin = System.currentTimeMillis(); String invalidMessage = null; + // set if invalid store but not his fault + boolean dontBlamePeer = false; boolean wasNew = false; RouterInfo prevNetDb = null; Hash key = _message.getKey(); @@ -72,6 +74,7 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { if (getContext().clientManager().isLocal(key)) { //getContext().statManager().addRateData("netDb.storeLocalLeaseSetAttempt", 1, 0); // throw rather than return, so that we send the ack below (prevent easy attack) + dontBlamePeer = true; throw new IllegalArgumentException("Peer attempted to store local leaseSet: " + key.toBase64().substring(0, 4)); } @@ -114,6 +117,9 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { //if (!ls.getReceivedAsReply()) // match.setReceivedAsPublished(true); } + } catch (UnsupportedCryptoException uce) { + invalidMessage = uce.getMessage(); + dontBlamePeer = true; } catch (IllegalArgumentException iae) { invalidMessage = iae.getMessage(); } @@ -131,8 +137,10 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { if (getContext().routerHash().equals(key)) { //getContext().statManager().addRateData("netDb.storeLocalRouterInfoAttempt", 1, 0); // throw rather than return, so that we send the ack below (prevent easy attack) + dontBlamePeer = true; throw new IllegalArgumentException("Peer attempted to store our RouterInfo"); } + getContext().profileManager().heardAbout(key); prevNetDb = getContext().netDb().store(key, ri); wasNew = ((null == prevNetDb) || (prevNetDb.getPublished() < ri.getPublished())); // Check new routerinfo address against blocklist @@ -152,7 +160,9 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { _log.warn("New address received, Blocklisting old peer " + key + ' ' + ri); } } - getContext().profileManager().heardAbout(key); + } catch (UnsupportedCryptoException uce) { + invalidMessage = uce.getMessage(); + dontBlamePeer = true; } catch (IllegalArgumentException iae) { invalidMessage = iae.getMessage(); } @@ -165,14 +175,16 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { long recvEnd = System.currentTimeMillis(); getContext().statManager().addRateData("netDb.storeRecvTime", recvEnd-recvBegin); - if (_message.getReplyToken() > 0) + // ack even if invalid or unsupported + // TODO any cases where we shouldn't? + if (_message.getReplyToken() > 0) sendAck(); long ackEnd = System.currentTimeMillis(); if (_from != null) _fromHash = _from.getHash(); if (_fromHash != null) { - if (invalidMessage == null) { + if (invalidMessage == null || dontBlamePeer) { getContext().profileManager().dbStoreReceived(_fromHash, wasNew); getContext().statManager().addRateData("netDb.storeHandled", ackEnd-recvEnd); } else { @@ -180,7 +192,7 @@ public class HandleFloodfillDatabaseStoreMessageJob extends JobImpl { if (_log.shouldLog(Log.WARN)) _log.warn("Peer " + _fromHash.toBase64() + " sent bad data: " + invalidMessage); } - } else if (invalidMessage != null) { + } else if (invalidMessage != null && !dontBlamePeer) { if (_log.shouldLog(Log.WARN)) _log.warn("Unknown peer sent bad data: " + invalidMessage); } diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java index a69a2bc90..6bd687185 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/HarvesterJob.java @@ -7,7 +7,7 @@ import java.util.Set; import java.util.TreeMap; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java index 13d763688..ac283109c 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeLookupJob.java @@ -1,7 +1,7 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.util.Log; import net.i2p.router.JobImpl; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java index 7a2fa7770..490a45a21 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/IterativeSearchJob.java @@ -13,9 +13,9 @@ import java.util.concurrent.ConcurrentHashMap; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.kademlia.XORComparator; import net.i2p.router.CommSystemFacade; @@ -28,6 +28,8 @@ import net.i2p.router.TunnelInfo; import net.i2p.router.TunnelManagerFacade; import net.i2p.router.util.RandomIterator; import net.i2p.util.Log; +import net.i2p.util.NativeBigInteger; +import net.i2p.util.SystemVersion; /** * A traditional Kademlia search that continues to search @@ -88,8 +90,13 @@ class IterativeSearchJob extends FloodSearchJob { */ private static final int MAX_CONCURRENT = 1; - /** testing */ - private static final String PROP_ENCRYPT_RI = "router.encryptRouterLookups"; + public static final String PROP_ENCRYPT_RI = "router.encryptRouterLookups"; + + /** only on fast boxes, for now */ + public static final boolean DEFAULT_ENCRYPT_RI = + SystemVersion.isX86() && SystemVersion.is64Bit() && + !SystemVersion.isApache() && !SystemVersion.isGNU() && + NativeBigInteger.isNative(); /** * Lookup using exploratory tunnels @@ -315,7 +322,7 @@ class IterativeSearchJob extends FloodSearchJob { _sentTime.put(peer, Long.valueOf(now)); I2NPMessage outMsg = null; - if (_isLease || getContext().getBooleanProperty(PROP_ENCRYPT_RI)) { + if (_isLease || getContext().getProperty(PROP_ENCRYPT_RI, DEFAULT_ENCRYPT_RI)) { // Full ElG is fairly expensive so only do it for LS lookups // if we have the ff RI, garlic encrypt it RouterInfo ri = getContext().netDb().lookupRouterInfoLocally(peer); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java index b4d108d02..5507710cd 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/KademliaNetworkDatabaseFacade.java @@ -19,14 +19,20 @@ import java.util.Iterator; import java.util.Map; import java.util.Set; +import net.i2p.crypto.SigType; +import net.i2p.data.Certificate; import net.i2p.data.DatabaseEntry; +import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; +import net.i2p.data.Destination; import net.i2p.data.Hash; +import net.i2p.data.KeyCertificate; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.kademlia.RejectTrimmer; import net.i2p.kademlia.SelectionCollector; @@ -63,6 +69,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { protected final RouterContext _context; private final ReseedChecker _reseedChecker; private volatile long _lastRIPublishTime; + private NegativeLookupCache _negativeCache; /** * Map of Hash to RepublishLeaseSetJob for leases we'realready managing. @@ -155,6 +162,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _reseedChecker = new ReseedChecker(context); context.statManager().createRateStat("netDb.lookupDeferred", "how many lookups are deferred?", "NetworkDatabase", new long[] { 60*60*1000 }); context.statManager().createRateStat("netDb.exploreKeySet", "how many keys are queued for exploration?", "NetworkDatabase", new long[] { 60*60*1000 }); + context.statManager().createRateStat("netDb.negativeCache", "Aborted lookup, already cached", "NetworkDatabase", new long[] { 60*60*1000l }); // following are for StoreJob context.statManager().createRateStat("netDb.storeRouterInfoSent", "How many routerInfo store messages have we sent?", "NetworkDatabase", new long[] { 60*60*1000l }); context.statManager().createRateStat("netDb.storeLeaseSetSent", "How many leaseSet store messages have we sent?", "NetworkDatabase", new long[] { 60*60*1000l }); @@ -223,6 +231,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { //_ds = null; _exploreKeys.clear(); // hope this doesn't cause an explosion, it shouldn't. // _exploreKeys = null; + _negativeCache.clear(); } public synchronized void restart() { @@ -262,6 +271,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { //_ds = new TransientDataStore(); // _exploreKeys = new HashSet(64); _dbDir = dbDir; + _negativeCache = new NegativeLookupCache(); createHandlers(); @@ -480,7 +490,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } /** - * Lookup using exploratory tunnels + * Lookup using exploratory tunnels. + * Use lookupDestination() if you don't need the LS or need it validated. */ public void lookupLeaseSet(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) { lookupLeaseSet(key, onFindJob, onFailedLookupJob, timeoutMs, null); @@ -488,6 +499,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { /** * Lookup using the client's tunnels + * Use lookupDestination() if you don't need the LS or need it validated. + * * @param fromLocalDest use these tunnels for the lookup, or null for exploratory * @since 0.9.10 */ @@ -500,6 +513,11 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _log.debug("leaseSet found locally, firing " + onFindJob); if (onFindJob != null) _context.jobQueue().addJob(onFindJob); + } else if (isNegativeCached(key)) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Negative cached, not searching: " + key); + if (onFailedLookupJob != null) + _context.jobQueue().addJob(onFailedLookupJob); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("leaseSet not found locally, running search"); @@ -509,6 +527,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _log.debug("after lookupLeaseSet"); } + /** + * Use lookupDestination() if you don't need the LS or need it validated. + */ public LeaseSet lookupLeaseSetLocally(Hash key) { if (!_initialized) return null; DatabaseEntry ds = _ds.get(key); @@ -531,6 +552,47 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { return null; } } + + /** + * Lookup using the client's tunnels + * Succeeds even if LS validation and store fails due to unsupported sig type, expired, etc. + * + * Note that there are not separate success and fail jobs. Caller must call + * lookupDestinationLocally() in the job to determine success. + * + * @param onFinishedJob non-null + * @param fromLocalDest use these tunnels for the lookup, or null for exploratory + * @since 0.9.16 + */ + public void lookupDestination(Hash key, Job onFinishedJob, long timeoutMs, Hash fromLocalDest) { + if (!_initialized) return; + Destination d = lookupDestinationLocally(key); + if (d != null) { + _context.jobQueue().addJob(onFinishedJob); + } else { + search(key, onFinishedJob, onFinishedJob, timeoutMs, true, fromLocalDest); + } + } + + /** + * Lookup locally in netDB and in badDest cache + * Succeeds even if LS validation fails due to unsupported sig type, expired, etc. + * + * @since 0.9.16 + */ + public Destination lookupDestinationLocally(Hash key) { + if (!_initialized) return null; + DatabaseEntry ds = _ds.get(key); + if (ds != null) { + if (ds.getType() == DatabaseEntry.KEY_TYPE_LEASESET) { + LeaseSet ls = (LeaseSet)ds; + return ls.getDestination(); + } + } else { + return _negativeCache.getBadDest(key); + } + return null; + } public void lookupRouterInfo(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs) { if (!_initialized) return; @@ -538,6 +600,9 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { if (ri != null) { if (onFindJob != null) _context.jobQueue().addJob(onFindJob); + } else if (_context.banlist().isBanlistedForever(key)) { + if (onFailedLookupJob != null) + _context.jobQueue().addJob(onFailedLookupJob); } else { search(key, onFindJob, onFailedLookupJob, timeoutMs, false); } @@ -694,9 +759,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { * Unlike for RouterInfos, this is only called once, when stored. * After that, LeaseSet.isCurrent() is used. * + * @throws UnsupportedCryptoException if that's why it failed. * @return reason why the entry is not valid, or null if it is valid */ - private String validate(Hash key, LeaseSet leaseSet) { + private String validate(Hash key, LeaseSet leaseSet) throws UnsupportedCryptoException { if (!key.equals(leaseSet.getDestination().calculateHash())) { if (_log.shouldLog(Log.WARN)) _log.warn("Invalid store attempt! key does not match leaseSet.destination! key = " @@ -704,9 +770,11 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { return "Key does not match leaseSet.destination - " + key.toBase64(); } if (!leaseSet.verifySignature()) { + // throws UnsupportedCryptoException + processStoreFailure(key, leaseSet); if (_log.shouldLog(Log.WARN)) - _log.warn("Invalid leaseSet signature! leaseSet = " + leaseSet); - return "Invalid leaseSet signature on " + leaseSet.getDestination().calculateHash().toBase64(); + _log.warn("Invalid leaseSet signature! " + leaseSet); + return "Invalid leaseSet signature on " + key; } long earliest = leaseSet.getEarliestLeaseDate(); long latest = leaseSet.getLatestLeaseDate(); @@ -722,7 +790,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { + " first exp. " + new Date(earliest) + " last exp. " + new Date(latest), new Exception("Rejecting store")); - return "Expired leaseSet for " + leaseSet.getDestination().calculateHash().toBase64() + return "Expired leaseSet for " + leaseSet.getDestination().calculateHash() + " expired " + DataHelper.formatDuration(age) + " ago"; } if (latest > now + (Router.CLOCK_FUDGE_FACTOR + MAX_LEASE_FUTURE)) { @@ -739,9 +807,13 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } /** - * Store the leaseSet + * Store the leaseSet. + * + * If the store fails due to unsupported crypto, it will negative cache + * the hash until restart. * * @throws IllegalArgumentException if the leaseSet is not valid + * @throws UnsupportedCryptoException if that's why it failed. * @return previous entry or null */ public LeaseSet store(Hash key, LeaseSet leaseSet) throws IllegalArgumentException { @@ -798,6 +870,10 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { * * Call this only on first store, to check the key and signature once * + * If the store fails due to unsupported crypto, it will banlist + * the router hash until restart and then throw UnsupportedCrytpoException. + * + * @throws UnsupportedCryptoException if that's why it failed. * @return reason why the entry is not valid, or null if it is valid */ private String validate(Hash key, RouterInfo routerInfo) throws IllegalArgumentException { @@ -807,6 +883,8 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { return "Key does not match routerInfo.identity"; } if (!routerInfo.isValid()) { + // throws UnsupportedCryptoException + processStoreFailure(key, routerInfo); if (_log.shouldLog(Log.WARN)) _log.warn("Invalid routerInfo signature! forged router structure! router = " + routerInfo); return "Invalid routerInfo signature"; @@ -892,15 +970,29 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { } /** - * store the routerInfo + * Store the routerInfo. + * + * If the store fails due to unsupported crypto, it will banlist + * the router hash until restart and then throw UnsupportedCrytpoException. * * @throws IllegalArgumentException if the routerInfo is not valid + * @throws UnsupportedCryptoException if that's why it failed. * @return previous entry or null */ public RouterInfo store(Hash key, RouterInfo routerInfo) throws IllegalArgumentException { return store(key, routerInfo, true); } + /** + * Store the routerInfo. + * + * If the store fails due to unsupported crypto, it will banlist + * the router hash until restart and then throw UnsupportedCrytpoException. + * + * @throws IllegalArgumentException if the routerInfo is not valid + * @throws UnsupportedCryptoException if that's why it failed. + * @return previous entry or null + */ RouterInfo store(Hash key, RouterInfo routerInfo, boolean persist) throws IllegalArgumentException { if (!_initialized) return null; @@ -934,6 +1026,59 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _kb.add(key); return rv; } + + /** + * If the validate fails, call this + * to determine if it was because of unsupported crypto. + * + * If so, this will banlist-forever the router hash or permanently negative cache the dest hash, + * and then throw the exception. Otherwise it does nothing. + * + * @throws UnsupportedCryptoException if that's why it failed. + * @since 0.9.16 + */ + private void processStoreFailure(Hash h, DatabaseEntry entry) throws UnsupportedCryptoException { + if (entry.getHash().equals(h)) { + if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) { + LeaseSet ls = (LeaseSet) entry; + Destination d = ls.getDestination(); + Certificate c = d.getCertificate(); + if (c.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) { + try { + KeyCertificate kc = c.toKeyCertificate(); + SigType type = kc.getSigType(); + if (type == null || !type.isAvailable()) { + failPermanently(d); + String stype = (type != null) ? type.toString() : Integer.toString(kc.getSigTypeCode()); + if (_log.shouldLog(Log.WARN)) + _log.warn("Unsupported sig type " + stype + " for destination " + h); + throw new UnsupportedCryptoException("Sig type " + stype); + } + } catch (DataFormatException dfe) {} + } + } else if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) { + RouterInfo ri = (RouterInfo) entry; + RouterIdentity id = ri.getIdentity(); + Certificate c = id.getCertificate(); + if (c.getCertificateType() == Certificate.CERTIFICATE_TYPE_KEY) { + try { + KeyCertificate kc = c.toKeyCertificate(); + SigType type = kc.getSigType(); + if (type == null || !type.isAvailable()) { + String stype = (type != null) ? type.toString() : Integer.toString(kc.getSigTypeCode()); + _context.banlist().banlistRouterForever(h, "Unsupported signature type " + stype); + if (_log.shouldLog(Log.WARN)) + _log.warn("Unsupported sig type " + stype + " for router " + h); + throw new UnsupportedCryptoException("Sig type " + stype); + } + } catch (DataFormatException dfe) {} + } + } + } + if (_log.shouldLog(Log.WARN)) + _log.warn("Verify fail, cause unknown: " + entry); + } + /** * Final remove for a leaseset. @@ -1005,8 +1150,12 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { * without any match) * * Unused - called only by FNDF.searchFull() from FloodSearchJob which is overridden - don't use this. + * + * @throws UnsupportedOperationException always */ SearchJob search(Hash key, Job onFindJob, Job onFailedLookupJob, long timeoutMs, boolean isLease) { + throw new UnsupportedOperationException(); +/**** if (!_initialized) return null; boolean isNew = true; SearchJob searchJob = null; @@ -1031,6 +1180,7 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _context.statManager().addRateData("netDb.lookupDeferred", deferred, searchJob.getExpiration()-_context.clock().now()); } return searchJob; +****/ } /** @@ -1102,6 +1252,47 @@ public class KademliaNetworkDatabaseFacade extends NetworkDatabaseFacade { _context.jobQueue().addJob(new StoreJob(_context, this, key, ds, onSuccess, onFailure, sendTimeout, toIgnore)); } + /** + * Increment in the negative lookup cache + * + * @param key for Destinations or RouterIdentities + * @since 0.9.4 moved from FNDF to KNDF in 0.9.16 + */ + void lookupFailed(Hash key) { + _negativeCache.lookupFailed(key); + } + + /** + * Is the key in the negative lookup cache? + *& + * @param key for Destinations or RouterIdentities + * @since 0.9.4 moved from FNDF to KNDF in 0.9.16 + */ + boolean isNegativeCached(Hash key) { + boolean rv = _negativeCache.isCached(key); + if (rv) + _context.statManager().addRateData("netDb.negativeCache", 1); + return rv; + } + + /** + * Negative cache until restart + * @since 0.9.16 + */ + void failPermanently(Destination dest) { + _negativeCache.failPermanently(dest); + } + + /** + * Is it permanently negative cached? + * + * @param key only for Destinations; for RouterIdentities, see Banlist + * @since 0.9.16 + */ + public boolean isNegativeCachedForever(Hash key) { + return _negativeCache.getBadDest(key) != null; + } + /** * Debug info, HTML formatted * @since 0.9.10 diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java index 15ae87a8b..ff9d3d501 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/MessageWrapper.java @@ -8,7 +8,7 @@ import net.i2p.crypto.TagSetHandle; import net.i2p.data.Certificate; import net.i2p.data.Hash; import net.i2p.data.PublicKey; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SessionKey; import net.i2p.data.SessionTag; import net.i2p.data.i2np.DeliveryInstructions; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java b/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java index abbb67ed6..1784f9434 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/NegativeLookupCache.java @@ -1,6 +1,9 @@ package net.i2p.router.networkdb.kademlia; +import java.util.Map; +import net.i2p.data.Destination; import net.i2p.data.Hash; +import net.i2p.util.LHMCache; import net.i2p.util.ObjectCounter; import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; @@ -12,11 +15,15 @@ import net.i2p.util.SimpleTimer; */ class NegativeLookupCache { private final ObjectCounter counter; + private final Map badDests; + private static final int MAX_FAILS = 3; + private static final int MAX_BAD_DESTS = 128; private static final long CLEAN_TIME = 2*60*1000; public NegativeLookupCache() { this.counter = new ObjectCounter(); + this.badDests = new LHMCache(MAX_BAD_DESTS); SimpleScheduler.getInstance().addPeriodicEvent(new Cleaner(), CLEAN_TIME); } @@ -25,7 +32,46 @@ class NegativeLookupCache { } public boolean isCached(Hash h) { - return this.counter.count(h) >= MAX_FAILS; + if (counter.count(h) >= MAX_FAILS) + return true; + synchronized(badDests) { + return badDests.get(h) != null; + } + } + + /** + * Negative cache the hash until restart, + * but cache the destination. + * + * @since 0.9.16 + */ + public void failPermanently(Destination dest) { + Hash h = dest.calculateHash(); + synchronized(badDests) { + badDests.put(h, dest); + } + } + + /** + * Get an unsupported but cached Destination + * + * @return dest or null if not cached + * @since 0.9.16 + */ + public Destination getBadDest(Hash h) { + synchronized(badDests) { + return badDests.get(h); + } + } + + /** + * @since 0.9.16 + */ + public void clear() { + counter.clear(); + synchronized(badDests) { + badDests.clear(); + } } private class Cleaner implements SimpleTimer.TimedEvent { diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java index f73eb00fe..4eb23e13c 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/PeerSelector.java @@ -16,7 +16,7 @@ import java.util.Set; import java.util.TreeMap; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.kademlia.KBucketSet; import net.i2p.kademlia.SelectionCollector; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java index 620679b7e..6c60e3a41 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/PersistentDataStore.java @@ -29,7 +29,7 @@ import net.i2p.data.Base64; import net.i2p.data.DatabaseEntry; import net.i2p.data.DataFormatException; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.Router; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java index 852a5c792..20dd2a075 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/RefreshRoutersJob.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Set; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; import net.i2p.util.Log; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java index 54a938319..f0edded8b 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchJob.java @@ -17,11 +17,12 @@ import net.i2p.data.DatabaseEntry; import net.i2p.data.DataHelper; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseLookupMessage; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; +import net.i2p.data.i2np.I2NPMessage; +import net.i2p.data.router.RouterInfo; import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.OutNetMessage; @@ -201,22 +202,29 @@ class SearchJob extends JobImpl { _log.debug(getJobId() + ": Already completed"); return; } + if (_state.isAborted()) { + if (_log.shouldLog(Log.INFO)) + _log.info(getJobId() + ": Search aborted"); + _state.complete(); + fail(); + return; + } if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Searching: " + _state); if (isLocal()) { if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Key found locally"); - _state.complete(true); + _state.complete(); succeed(); } else if (isExpired()) { if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Key search expired"); - _state.complete(true); + _state.complete(); fail(); } else if (_state.getAttempted().size() > MAX_PEERS_QUERIED) { if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Too many peers quried"); - _state.complete(true); + _state.complete(); fail(); } else { //_log.debug("Continuing search"); @@ -424,7 +432,7 @@ class SearchJob extends JobImpl { int timeout = getPerPeerTimeoutMs(to); long expiration = getContext().clock().now() + timeout; - DatabaseLookupMessage msg = buildMessage(inTunnelId, inTunnel.getPeer(0), expiration); + I2NPMessage msg = buildMessage(inTunnelId, inTunnel.getPeer(0), expiration, router); TunnelInfo outTunnel = getContext().tunnelManager().selectOutboundExploratoryTunnel(to); if (outTunnel == null) { @@ -437,9 +445,9 @@ class SearchJob extends JobImpl { if (_log.shouldLog(Log.DEBUG)) _log.debug(getJobId() + ": Sending search to " + to - + " for " + msg.getSearchKey().toBase64() + " w/ replies through [" - + msg.getFrom().toBase64() + "] via tunnel [" - + msg.getReplyTunnel() + "]"); + + " for " + getState().getTarget() + " w/ replies through " + + inTunnel.getPeer(0) + " via tunnel " + + inTunnelId); SearchMessageSelector sel = new SearchMessageSelector(getContext(), router, _expiration, _state); SearchUpdateReplyFoundJob reply = new SearchUpdateReplyFoundJob(getContext(), router, _state, _facade, @@ -482,8 +490,11 @@ class SearchJob extends JobImpl { * @param replyTunnelId tunnel to receive replies through * @param replyGateway gateway for the reply tunnel * @param expiration when the search should stop + * @param peer unused here; see ExploreJob extension + * + * @return a DatabaseLookupMessage */ - protected DatabaseLookupMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration) { + protected I2NPMessage buildMessage(TunnelId replyTunnelId, Hash replyGateway, long expiration, RouterInfo peer) { DatabaseLookupMessage msg = new DatabaseLookupMessage(getContext(), true); msg.setSearchKey(_state.getTarget()); //msg.setFrom(replyGateway.getIdentity().getHash()); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java index 3d756529d..30e8e4b18 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchMessageSelector.java @@ -3,7 +3,7 @@ package net.i2p.router.networkdb.kademlia; import java.util.concurrent.atomic.AtomicInteger; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java index 8d8e5f19d..a8354b096 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchReplyJob.java @@ -2,7 +2,7 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.Hash; import net.i2p.data.i2np.DatabaseSearchReplyMessage; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; import net.i2p.util.Log; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java index 106c8ad4d..61bd1b645 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchState.java @@ -19,15 +19,16 @@ import net.i2p.router.RouterContext; */ class SearchState { private final RouterContext _context; - private final HashSet _pendingPeers; + private final Set _pendingPeers; private final Map _pendingPeerTimes; - private final HashSet _attemptedPeers; - private final HashSet _failedPeers; - private final HashSet _successfulPeers; - private final HashSet _repliedPeers; + private final Set _attemptedPeers; + private final Set _failedPeers; + private final Set _successfulPeers; + private final Set _repliedPeers; private final Hash _searchKey; private volatile long _completed; private volatile long _started; + private volatile boolean _aborted; public SearchState(RouterContext context, Hash key) { _context = context; @@ -87,10 +88,19 @@ class SearchState { return new HashSet(_failedPeers); } } + public boolean completed() { return _completed != -1; } - public void complete(boolean completed) { - if (completed) - _completed = _context.clock().now(); + + public void complete() { + _completed = _context.clock().now(); + } + + /** @since 0.9.16 */ + public boolean isAborted() { return _aborted; } + + /** @since 0.9.16 */ + public void abort() { + _aborted = true; } public long getWhenStarted() { return _started; } @@ -177,6 +187,8 @@ class SearchState { buf.append(" completed? false "); else buf.append(" completed on ").append(new Date(_completed)); + if (_aborted) + buf.append(" (Aborted)"); buf.append("\n\tAttempted: "); synchronized (_attemptedPeers) { buf.append(_attemptedPeers.size()).append(' '); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java index 22602b497..63cc1c18f 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SearchUpdateReplyFoundJob.java @@ -5,7 +5,7 @@ import java.util.Date; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; @@ -18,24 +18,26 @@ import net.i2p.util.Log; /** * Called after a match to a db search is found * + * Used only by SearchJob which is only used by ExploreJob */ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob { - private Log _log; + private final Log _log; private I2NPMessage _message; - private Hash _peer; - private SearchState _state; - private KademliaNetworkDatabaseFacade _facade; - private SearchJob _job; - private TunnelInfo _outTunnel; - private TunnelInfo _replyTunnel; - private boolean _isFloodfillPeer; - private long _sentOn; + private final Hash _peer; + private final SearchState _state; + private final KademliaNetworkDatabaseFacade _facade; + private final SearchJob _job; + private final TunnelInfo _outTunnel; + private final TunnelInfo _replyTunnel; + private final boolean _isFloodfillPeer; + private final long _sentOn; public SearchUpdateReplyFoundJob(RouterContext context, RouterInfo peer, SearchState state, KademliaNetworkDatabaseFacade facade, SearchJob job) { this(context, peer, state, facade, job, null, null); } + public SearchUpdateReplyFoundJob(RouterContext context, RouterInfo peer, SearchState state, KademliaNetworkDatabaseFacade facade, SearchJob job, TunnelInfo outTunnel, TunnelInfo replyTunnel) { @@ -52,6 +54,7 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob { } public String getName() { return "Update Reply Found for Kademlia Search"; } + public void runJob() { if (_isFloodfillPeer) _job.decrementOutstandingFloodfillSearches(); @@ -59,7 +62,7 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob { I2NPMessage message = _message; if (_log.shouldLog(Log.INFO)) _log.info(getJobId() + ": Reply from " + _peer.toBase64() - + " with message " + message.getClass().getName()); + + " with message " + message.getClass().getSimpleName()); long howLong = System.currentTimeMillis() - _sentOn; // assume requests are 1KB (they're almost always much smaller, but tunnels have a fixed size) @@ -78,34 +81,21 @@ class SearchUpdateReplyFoundJob extends JobImpl implements ReplyJob { if (message instanceof DatabaseStoreMessage) { long timeToReply = _state.dataFound(_peer); - DatabaseStoreMessage msg = (DatabaseStoreMessage)message; DatabaseEntry entry = msg.getEntry(); - if (entry.getType() == DatabaseEntry.KEY_TYPE_LEASESET) { - try { - _facade.store(msg.getKey(), (LeaseSet) entry); - getContext().profileManager().dbLookupSuccessful(_peer, timeToReply); - } catch (IllegalArgumentException iae) { - if (_log.shouldLog(Log.ERROR)) - _log.warn("Peer " + _peer + " sent us an invalid leaseSet: " + iae.getMessage()); - getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply); - } - } else if (entry.getType() == DatabaseEntry.KEY_TYPE_ROUTERINFO) { - if (_log.shouldLog(Log.INFO)) - _log.info(getJobId() + ": dbStore received on search containing router " - + msg.getKey() + " with publishDate of " - + new Date(entry.getDate())); - try { - _facade.store(msg.getKey(), (RouterInfo) entry); - getContext().profileManager().dbLookupSuccessful(_peer, timeToReply); - } catch (IllegalArgumentException iae) { - if (_log.shouldLog(Log.ERROR)) - _log.warn("Peer " + _peer + " sent us an invalid routerInfo: " + iae.getMessage()); - getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply); - } - } else { - if (_log.shouldLog(Log.ERROR)) - _log.error(getJobId() + ": Unknown db store type?!@ " + entry.getType()); + try { + _facade.store(msg.getKey(), entry); + getContext().profileManager().dbLookupSuccessful(_peer, timeToReply); + } catch (UnsupportedCryptoException iae) { + // don't blame the peer + getContext().profileManager().dbLookupSuccessful(_peer, timeToReply); + _state.abort(); + // searchNext() will call fail() + } catch (IllegalArgumentException iae) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Peer " + _peer + " sent us invalid data: ", iae); + // blame the peer + getContext().profileManager().dbLookupReply(_peer, 0, 0, 1, 0, timeToReply); } } else if (message instanceof DatabaseSearchReplyMessage) { _job.replyFound((DatabaseSearchReplyMessage)message, _peer); diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java index 711510fb4..62a878d6d 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/SingleLookupJob.java @@ -1,7 +1,7 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DatabaseSearchReplyMessage; import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java index cced2f546..7938ab072 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StartExplorersJob.java @@ -12,7 +12,7 @@ import java.util.HashSet; import java.util.Set; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.JobImpl; import net.i2p.router.Router; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java index 311f2ba8f..81bef6950 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreJob.java @@ -18,7 +18,7 @@ import net.i2p.data.DatabaseEntry; import net.i2p.data.DataFormatException; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.I2NPMessage; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java b/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java index 6901f0afa..352c19cd7 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/StoreMessageSelector.java @@ -1,7 +1,7 @@ package net.i2p.router.networkdb.kademlia; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.i2np.DeliveryStatusMessage; import net.i2p.data.i2np.I2NPMessage; import net.i2p.router.MessageSelector; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java index 2a7318d25..11603efb9 100644 --- a/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java +++ b/router/java/src/net/i2p/router/networkdb/kademlia/TransientDataStore.java @@ -18,7 +18,7 @@ import java.util.Set; import net.i2p.data.DatabaseEntry; import net.i2p.data.Hash; import net.i2p.data.LeaseSet; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.RouterContext; import net.i2p.util.Log; diff --git a/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java b/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java new file mode 100644 index 000000000..0159eb020 --- /dev/null +++ b/router/java/src/net/i2p/router/networkdb/kademlia/UnsupportedCryptoException.java @@ -0,0 +1,18 @@ +package net.i2p.router.networkdb.kademlia; + +/** + * Signature verification failed because the + * sig type is unknown or unavailable. + * + * @since 0.9.16 + */ +public class UnsupportedCryptoException extends IllegalArgumentException { + + public UnsupportedCryptoException(String msg) { + super(msg); + } + + public UnsupportedCryptoException(String msg, Throwable t) { + super(msg, t); + } +} diff --git a/router/java/src/net/i2p/router/peermanager/PeerManager.java b/router/java/src/net/i2p/router/peermanager/PeerManager.java index ef7b1de34..d2de16d9b 100644 --- a/router/java/src/net/i2p/router/peermanager/PeerManager.java +++ b/router/java/src/net/i2p/router/peermanager/PeerManager.java @@ -19,7 +19,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.router.PeerSelectionCriteria; import net.i2p.router.Router; import net.i2p.router.RouterContext; diff --git a/router/java/src/net/i2p/router/peermanager/PeerTestJob.java b/router/java/src/net/i2p/router/peermanager/PeerTestJob.java index 24176a208..466c63ec1 100644 --- a/router/java/src/net/i2p/router/peermanager/PeerTestJob.java +++ b/router/java/src/net/i2p/router/peermanager/PeerTestJob.java @@ -5,7 +5,7 @@ import java.util.List; import java.util.Set; import net.i2p.data.Hash; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterInfo; import net.i2p.data.TunnelId; import net.i2p.data.i2np.DatabaseStoreMessage; import net.i2p.data.i2np.DeliveryStatusMessage; diff --git a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java index a224f576a..7e89cab25 100644 --- a/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java +++ b/router/java/src/net/i2p/router/peermanager/ProfileOrganizer.java @@ -19,8 +19,8 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import net.i2p.crypto.SHA256Generator; import net.i2p.data.Hash; -import net.i2p.data.RouterAddress; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterAddress; +import net.i2p.data.router.RouterInfo; import net.i2p.router.NetworkDatabaseFacade; import net.i2p.router.RouterContext; import net.i2p.router.tunnel.pool.TunnelPeerSelector; diff --git a/router/java/src/net/i2p/router/startup/BootCommSystemJob.java b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java index 3af4164b5..305869bad 100644 --- a/router/java/src/net/i2p/router/startup/BootCommSystemJob.java +++ b/router/java/src/net/i2p/router/startup/BootCommSystemJob.java @@ -16,7 +16,7 @@ import net.i2p.router.tasks.ReadConfigJob; import net.i2p.util.Log; /** This actually boots almost everything */ -public class BootCommSystemJob extends JobImpl { +class BootCommSystemJob extends JobImpl { private Log _log; public static final String PROP_USE_TRUSTED_LINKS = "router.trustedLinks"; diff --git a/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java b/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java index bf8d36a77..e512f9ea3 100644 --- a/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java +++ b/router/java/src/net/i2p/router/startup/BootNetworkDbJob.java @@ -12,7 +12,7 @@ import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; /** start up the network database */ -public class BootNetworkDbJob extends JobImpl { +class BootNetworkDbJob extends JobImpl { public BootNetworkDbJob(RouterContext ctx) { super(ctx); diff --git a/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java b/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java index 7ac5254f0..33f401023 100644 --- a/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java +++ b/router/java/src/net/i2p/router/startup/BootPeerManagerJob.java @@ -12,7 +12,7 @@ import net.i2p.router.JobImpl; import net.i2p.router.RouterContext; /** start up the peer manager */ -public class BootPeerManagerJob extends JobImpl { +class BootPeerManagerJob extends JobImpl { public BootPeerManagerJob(RouterContext ctx) { super(ctx); diff --git a/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java b/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java index 3b88a2d9c..566204068 100644 --- a/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java +++ b/router/java/src/net/i2p/router/startup/BuildTrustedLinksJob.java @@ -15,7 +15,7 @@ import net.i2p.router.RouterContext; /** * For future restricted routes. Does nothing now. */ -public class BuildTrustedLinksJob extends JobImpl { +class BuildTrustedLinksJob extends JobImpl { private final Job _next; public BuildTrustedLinksJob(RouterContext context, Job next) { diff --git a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java index 498cbe665..83f2c7a7e 100644 --- a/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java +++ b/router/java/src/net/i2p/router/startup/CreateRouterInfoJob.java @@ -12,16 +12,22 @@ import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.security.GeneralSecurityException; import java.util.Properties; +import net.i2p.crypto.SigType; import net.i2p.data.Certificate; import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.KeyCertificate; import net.i2p.data.PrivateKey; +import net.i2p.data.PrivateKeyFile; import net.i2p.data.PublicKey; -import net.i2p.data.RouterIdentity; -import net.i2p.data.RouterInfo; +import net.i2p.data.router.RouterIdentity; +import net.i2p.data.router.RouterInfo; import net.i2p.data.SigningPrivateKey; import net.i2p.data.SigningPublicKey; +import net.i2p.data.SimpleDataStructure; import net.i2p.router.Job; import net.i2p.router.JobImpl; import net.i2p.router.Router; @@ -40,7 +46,14 @@ public class CreateRouterInfoJob extends JobImpl { private final Log _log; private final Job _next; - public CreateRouterInfoJob(RouterContext ctx, Job next) { + public static final String INFO_FILENAME = "router.info"; + public static final String KEYS_FILENAME = "router.keys"; + public static final String KEYS2_FILENAME = "router.keys.dat"; + private static final String PROP_ROUTER_SIGTYPE = "router.sigType"; + /** TODO when changing, check isAvailable() and fallback to DSA_SHA1 */ + private static final SigType DEFAULT_SIGTYPE = SigType.DSA_SHA1; + + CreateRouterInfoJob(RouterContext ctx, Job next) { super(ctx); _next = next; _log = ctx.logManager().getLog(CreateRouterInfoJob.class); @@ -59,9 +72,13 @@ public class CreateRouterInfoJob extends JobImpl { /** * Writes 6 files: router.info (standard RI format), - * router,keys, and 4 individual key files under keyBackup/ + * router.keys2, and 4 individual key files under keyBackup/ * - * router.keys file format: Note that this is NOT the + * router.keys2 file format: This is the + * same "eepPriv.dat" format used by the client code, + * as documented in PrivateKeyFile. + * + * Old router.keys file format: Note that this is NOT the * same "eepPriv.dat" format used by the client code. *
          *   - Private key (256 bytes)
    @@ -74,9 +91,9 @@ public class CreateRouterInfoJob extends JobImpl {
          *  Caller must hold Router.routerInfoFileLock.
          */
         RouterInfo createRouterInfo() {
    +        SigType type = getSigTypeConfig(getContext());
             RouterInfo info = new RouterInfo();
             OutputStream fos1 = null;
    -        OutputStream fos2 = null;
             try {
                 info.setAddresses(getContext().commSystem().createAddresses());
                 Properties stats = getContext().statPublisher().publishStatistics();
    @@ -86,21 +103,26 @@ public class CreateRouterInfoJob extends JobImpl {
                 // not necessary, in constructor
                 //info.setPeers(new HashSet());
                 info.setPublished(getCurrentPublishDate(getContext()));
    -            RouterIdentity ident = new RouterIdentity();
    -            Certificate cert = getContext().router().createCertificate();
    -            ident.setCertificate(cert);
    -            PublicKey pubkey = null;
    -            PrivateKey privkey = null;
    -            SigningPublicKey signingPubKey = null;
    -            SigningPrivateKey signingPrivKey = null;
                 Object keypair[] = getContext().keyGenerator().generatePKIKeypair();
    -            pubkey = (PublicKey)keypair[0];
    -            privkey = (PrivateKey)keypair[1];
    -            Object signingKeypair[] = getContext().keyGenerator().generateSigningKeypair();
    -            signingPubKey = (SigningPublicKey)signingKeypair[0];
    -            signingPrivKey = (SigningPrivateKey)signingKeypair[1];
    +            PublicKey pubkey = (PublicKey)keypair[0];
    +            PrivateKey privkey = (PrivateKey)keypair[1];
    +            SimpleDataStructure signingKeypair[] = getContext().keyGenerator().generateSigningKeys(type);
    +            SigningPublicKey signingPubKey = (SigningPublicKey)signingKeypair[0];
    +            SigningPrivateKey signingPrivKey = (SigningPrivateKey)signingKeypair[1];
    +            RouterIdentity ident = new RouterIdentity();
    +            Certificate cert = createCertificate(getContext(), signingPubKey);
    +            ident.setCertificate(cert);
                 ident.setPublicKey(pubkey);
                 ident.setSigningPublicKey(signingPubKey);
    +            byte[] padding;
    +            int padLen = SigningPublicKey.KEYSIZE_BYTES - signingPubKey.length();
    +            if (padLen > 0) {
    +                padding = new byte[padLen];
    +                getContext().random().nextBytes(padding);
    +                ident.setPadding(padding);
    +            } else {
    +                padding = null;
    +            }
                 info.setIdentity(ident);
                 
                 info.sign(signingPrivKey);
    @@ -108,34 +130,54 @@ public class CreateRouterInfoJob extends JobImpl {
                 if (!info.isValid())
                     throw new DataFormatException("RouterInfo we just built is invalid: " + info);
                 
    -            String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
    -            File ifile = new File(getContext().getRouterDir(), infoFilename);
    +            // remove router.keys
    +            (new File(getContext().getRouterDir(), KEYS_FILENAME)).delete();
    +
    +            // write router.info
    +            File ifile = new File(getContext().getRouterDir(), INFO_FILENAME);
                 fos1 = new BufferedOutputStream(new SecureFileOutputStream(ifile));
                 info.writeBytes(fos1);
                 
    -            String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
    -            File kfile = new File(getContext().getRouterDir(), keyFilename);
    -            fos2 = new BufferedOutputStream(new SecureFileOutputStream(kfile));
    -            privkey.writeBytes(fos2);
    -            signingPrivKey.writeBytes(fos2);
    -            pubkey.writeBytes(fos2);
    -            signingPubKey.writeBytes(fos2);
    +            // write router.keys.dat
    +            File kfile = new File(getContext().getRouterDir(), KEYS2_FILENAME);
    +            PrivateKeyFile pkf = new PrivateKeyFile(kfile, pubkey, signingPubKey, cert,
    +                                                    privkey, signingPrivKey, padding);
    +            pkf.write();
                 
                 getContext().keyManager().setKeys(pubkey, privkey, signingPubKey, signingPrivKey);
                 
    -            _log.info("Router info created and stored at " + ifile.getAbsolutePath() + " with private keys stored at " + kfile.getAbsolutePath() + " [" + info + "]");
    +            if (_log.shouldLog(Log.INFO))
    +                _log.info("Router info created and stored at " + ifile.getAbsolutePath() + " with private keys stored at " + kfile.getAbsolutePath() + " [" + info + "]");
                 getContext().router().eventLog().addEvent(EventLog.REKEYED, ident.calculateHash().toBase64());
    +        } catch (GeneralSecurityException gse) {
    +            _log.log(Log.CRIT, "Error building the new router information", gse);
             } catch (DataFormatException dfe) {
                 _log.log(Log.CRIT, "Error building the new router information", dfe);
             } catch (IOException ioe) {
                 _log.log(Log.CRIT, "Error writing out the new router information", ioe);
             } finally {
                 if (fos1 != null) try { fos1.close(); } catch (IOException ioe) {}
    -            if (fos2 != null) try { fos2.close(); } catch (IOException ioe) {}
             }
             return info;
         }
         
    +    /**
    +     *  The configured SigType to expect on read-in
    +     *  @since 0.9.16
    +     */
    +    public static SigType getSigTypeConfig(RouterContext ctx) {
    +        SigType cstype = CreateRouterInfoJob.DEFAULT_SIGTYPE;
    +        String sstype = ctx.getProperty(PROP_ROUTER_SIGTYPE);
    +        if (sstype != null) {
    +            SigType ntype = SigType.parseSigType(sstype);
    +            if (ntype != null)
    +                cstype = ntype;
    +        }
    +        // fallback?
    +        if (cstype != SigType.DSA_SHA1 && !cstype.isAvailable())
    +            cstype = SigType.DSA_SHA1;
    +        return cstype;
    +    }
         
         /**
          * We probably don't want to expose the exact time at which a router published its info.
    @@ -146,4 +188,22 @@ public class CreateRouterInfoJob extends JobImpl {
             //_log.info("Setting published date to /now/");
             return context.clock().now();
         }
    +
    +    /**
    +     *  Only called at startup via LoadRouterInfoJob and RebuildRouterInfoJob.
    +     *  Not called by periodic RepublishLocalRouterInfoJob.
    +     *  We don't want to change the cert on the fly as it changes the router hash.
    +     *  RouterInfo.isHidden() checks the capability, but RouterIdentity.isHidden() checks the cert.
    +     *  There's no reason to ever add a hidden cert?
    +     *
    +     *  @return the certificate for a new RouterInfo - probably a null cert.
    +     *  @since 0.9.16 moved from Router
    +     */
    +    static Certificate createCertificate(RouterContext ctx, SigningPublicKey spk) {
    +        if (spk.getType() != SigType.DSA_SHA1)
    +            return new KeyCertificate(spk);
    +        if (ctx.getBooleanProperty(Router.PROP_HIDDEN))
    +            return new Certificate(Certificate.CERTIFICATE_TYPE_HIDDEN, null);
    +        return Certificate.NULL_CERT;
    +    }
     }
    diff --git a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
    index 9a560da5f..e02ef04aa 100644
    --- a/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
    +++ b/router/java/src/net/i2p/router/startup/LoadRouterInfoJob.java
    @@ -15,18 +15,28 @@ import java.io.InputStream;
     import java.io.IOException;
     import java.util.concurrent.atomic.AtomicBoolean;
     
    +import net.i2p.crypto.KeyGenerator;
    +import net.i2p.crypto.SigType;
    +import net.i2p.data.Certificate;
     import net.i2p.data.DataFormatException;
    +import net.i2p.data.DataHelper;
     import net.i2p.data.PrivateKey;
     import net.i2p.data.PublicKey;
    -import net.i2p.data.RouterInfo;
     import net.i2p.data.SigningPrivateKey;
     import net.i2p.data.SigningPublicKey;
    +import net.i2p.data.router.RouterIdentity;
    +import net.i2p.data.router.RouterInfo;
    +import net.i2p.data.router.RouterPrivateKeyFile;
     import net.i2p.router.JobImpl;
     import net.i2p.router.Router;
     import net.i2p.router.RouterContext;
     import net.i2p.util.Log;
     
    -public class LoadRouterInfoJob extends JobImpl {
    +/**
    + *  Run once or twice at startup by StartupJob,
    + *  and then runs BootCommSystemJob
    + */
    +class LoadRouterInfoJob extends JobImpl {
         private final Log _log;
         private RouterInfo _us;
         private static final AtomicBoolean _keyLengthChecked = new AtomicBoolean();
    @@ -45,6 +55,7 @@ public class LoadRouterInfoJob extends JobImpl {
             if (_us == null) {
                 RebuildRouterInfoJob r = new RebuildRouterInfoJob(getContext());
                 r.rebuildRouterInfo(false);
    +            // run a second time
                 getContext().jobQueue().addJob(this);
                 return;
             } else {
    @@ -54,18 +65,21 @@ public class LoadRouterInfoJob extends JobImpl {
             }
         }
         
    +    /**
    +     *  Loads router.info and router.keys2 or router.keys.
    +     *
    +     *  See CreateRouterInfoJob for file formats
    +     */
         private void loadRouterInfo() {
    -        String routerInfoFile = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
             RouterInfo info = null;
    -        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
    -        
    -        File rif = new File(getContext().getRouterDir(), routerInfoFile);
    +        File rif = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
             boolean infoExists = rif.exists();
    -        File rkf = new File(getContext().getRouterDir(), keyFilename);
    +        File rkf = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS_FILENAME);
             boolean keysExist = rkf.exists();
    +        File rkf2 = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS2_FILENAME);
    +        boolean keys2Exist = rkf2.exists();
             
             InputStream fis1 = null;
    -        InputStream fis2 = null;
             try {
                 // if we have a routerinfo but no keys, things go bad in a hurry:
                 // CRIT   ...rkdb.PublishLocalRouterInfoJob: Internal error - signing private key not known?  rescheduling publish for 30s
    @@ -73,7 +87,7 @@ public class LoadRouterInfoJob extends JobImpl {
                 // CRIT   ...sport.udp.EstablishmentManager: Error in the establisher java.lang.NullPointerException
                 // at net.i2p.router.transport.udp.PacketBuilder.buildSessionConfirmedPacket(PacketBuilder.java:574)
                 // so pretend the RI isn't there if there is no keyfile
    -            if (infoExists && keysExist) {
    +            if (infoExists && (keys2Exist || keysExist)) {
                     fis1 = new BufferedInputStream(new FileInputStream(rif));
                     info = new RouterInfo();
                     info.readBytes(fis1);
    @@ -85,29 +99,32 @@ public class LoadRouterInfoJob extends JobImpl {
                     _us = info;
                 }
                 
    -            if (keysExist) {
    -                fis2 = new BufferedInputStream(new FileInputStream(rkf));
    -                PrivateKey privkey = new PrivateKey();
    -                privkey.readBytes(fis2);
    -                if (shouldRebuild(privkey)) {
    +            if (keys2Exist || keysExist) {
    +                KeyData kd = readKeyData(rkf, rkf2);
    +                PublicKey pubkey = kd.routerIdentity.getPublicKey();
    +                SigningPublicKey signingPubKey = kd.routerIdentity.getSigningPublicKey();
    +                PrivateKey privkey = kd.privateKey;
    +                SigningPrivateKey signingPrivKey = kd.signingPrivateKey;
    +                SigType stype = signingPubKey.getType();
    +
    +                // check if the sigtype config changed
    +                SigType cstype = CreateRouterInfoJob.getSigTypeConfig(getContext());
    +                boolean sigTypeChanged = stype != cstype;
    +
    +                if (sigTypeChanged || shouldRebuild(privkey)) {
    +                    if (sigTypeChanged)
    +                        _log.logAlways(Log.WARN, "Rebuilding RouterInfo with new signature type " + cstype);
                         _us = null;
                         // windows... close before deleting
                         if (fis1 != null) {
                             try { fis1.close(); } catch (IOException ioe) {}
                             fis1 = null;
                         }
    -                    try { fis2.close(); } catch (IOException ioe) {}
    -                    fis2 = null;
                         rif.delete();
                         rkf.delete();
    +                    rkf2.delete();
                         return;
                     }
    -                SigningPrivateKey signingPrivKey = new SigningPrivateKey();
    -                signingPrivKey.readBytes(fis2);
    -                PublicKey pubkey = new PublicKey();
    -                pubkey.readBytes(fis2);
    -                SigningPublicKey signingPubKey = new SigningPublicKey();
    -                signingPubKey.readBytes(fis2);
                     
                     getContext().keyManager().setKeys(pubkey, privkey, signingPubKey, signingPrivKey);
                 }
    @@ -119,12 +136,9 @@ public class LoadRouterInfoJob extends JobImpl {
                     try { fis1.close(); } catch (IOException ioe2) {}
                     fis1 = null;
                 }
    -            if (fis2 != null) {
    -                try { fis2.close(); } catch (IOException ioe2) {}
    -                fis2 = null;
    -            }
                 rif.delete();
                 rkf.delete();
    +            rkf2.delete();
             } catch (DataFormatException dfe) {
                 _log.log(Log.CRIT, "Corrupt router info or keys at " + rif.getAbsolutePath() + " / " + rkf.getAbsolutePath(), dfe);
                 _us = null;
    @@ -133,15 +147,11 @@ public class LoadRouterInfoJob extends JobImpl {
                     try { fis1.close(); } catch (IOException ioe) {}
                     fis1 = null;
                 }
    -            if (fis2 != null) {
    -                try { fis2.close(); } catch (IOException ioe) {}
    -                fis2 = null;
    -            }
                 rif.delete();
                 rkf.delete();
    +            rkf2.delete();
             } finally {
                 if (fis1 != null) try { fis1.close(); } catch (IOException ioe) {}
    -            if (fis2 != null) try { fis2.close(); } catch (IOException ioe) {}
             }
         }
     
    @@ -174,4 +184,68 @@ public class LoadRouterInfoJob extends JobImpl {
                 _log.logAlways(Log.WARN, "Rebuilding RouterInfo with faster key");
             return uselong != haslong;
         }
    +
    +    /** @since 0.9.16 */
    +    public static class KeyData {
    +        public final RouterIdentity routerIdentity;
    +        public final PrivateKey privateKey;
    +        public final SigningPrivateKey signingPrivateKey;
    +
    +        public KeyData(RouterIdentity ri, PrivateKey pk, SigningPrivateKey spk) {
    +            routerIdentity = ri;
    +            privateKey = pk;
    +            signingPrivateKey = spk;
    +        }
    +    }
    +
    +    /**
    +     *  @param rkf1 in router.keys format, tried second
    +     *  @param rkf2 in eepPriv.dat format, tried first
    +     *  @return non-null, throws IOE if neither exisits
    +     *  @since 0.9.16
    +     */
    +    public static KeyData readKeyData(File rkf1, File rkf2) throws DataFormatException, IOException {
    +        RouterIdentity ri;
    +        PrivateKey privkey;
    +        SigningPrivateKey signingPrivKey;
    +        if (rkf2.exists()) {
    +            RouterPrivateKeyFile pkf = new RouterPrivateKeyFile(rkf2);
    +            ri = pkf.getRouterIdentity();
    +            if (!pkf.validateKeyPairs())
    +                throw new DataFormatException("Key pairs invalid");
    +            privkey = pkf.getPrivKey();
    +            signingPrivKey = pkf.getSigningPrivKey();
    +        } else {
    +            InputStream fis = null;
    +            try {
    +                fis = new BufferedInputStream(new FileInputStream(rkf1));
    +                privkey = new PrivateKey();
    +                privkey.readBytes(fis);
    +                signingPrivKey = new SigningPrivateKey();
    +                signingPrivKey.readBytes(fis);
    +                PublicKey pubkey = new PublicKey();
    +                pubkey.readBytes(fis);
    +                SigningPublicKey signingPubKey = new SigningPublicKey();
    +                signingPubKey.readBytes(fis);
    +
    +                // validate
    +                try {
    +                    if (!pubkey.equals(KeyGenerator.getPublicKey(privkey)))
    +                        throw new DataFormatException("Key pairs invalid");
    +                    if (!signingPubKey.equals(KeyGenerator.getSigningPublicKey(signingPrivKey)))
    +                        throw new DataFormatException("Key pairs invalid");
    +                } catch (IllegalArgumentException iae) {
    +                    throw new DataFormatException("Key pairs invalid", iae);
    +                }
    +
    +                ri = new RouterIdentity();
    +                ri.setPublicKey(pubkey);
    +                ri.setSigningPublicKey(signingPubKey);
    +                ri.setCertificate(Certificate.NULL_CERT);
    +            } finally {
    +                if (fis != null) try { fis.close(); } catch (IOException ioe) {}
    +            }
    +        }
    +        return new KeyData(ri, privkey, signingPrivKey);
    +    }
     }
    diff --git a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
    index b611114d0..ef0826cf5 100644
    --- a/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
    +++ b/router/java/src/net/i2p/router/startup/RebuildRouterInfoJob.java
    @@ -9,22 +9,24 @@ package net.i2p.router.startup;
      */
     
     import java.io.File;
    -import java.io.FileInputStream;
     import java.io.FileOutputStream;
     import java.io.IOException;
     import java.util.Properties;
     
    +import net.i2p.crypto.SigType;
     import net.i2p.data.Certificate;
     import net.i2p.data.DataFormatException;
    +import net.i2p.data.DataHelper;
     import net.i2p.data.PrivateKey;
     import net.i2p.data.PublicKey;
    -import net.i2p.data.RouterIdentity;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterIdentity;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.SigningPrivateKey;
     import net.i2p.data.SigningPublicKey;
     import net.i2p.router.JobImpl;
     import net.i2p.router.Router;
     import net.i2p.router.RouterContext;
    +import net.i2p.router.startup.LoadRouterInfoJob.KeyData;
     import net.i2p.util.Log;
     import net.i2p.util.SecureFileOutputStream;
     
    @@ -44,7 +46,7 @@ import net.i2p.util.SecureFileOutputStream;
      * router.info.rebuild file is deleted
      *
      */
    -public class RebuildRouterInfoJob extends JobImpl {
    +class RebuildRouterInfoJob extends JobImpl {
         private final Log _log;
         
         private final static long REBUILD_DELAY = 45*1000; // every 30 seconds
    @@ -57,11 +59,11 @@ public class RebuildRouterInfoJob extends JobImpl {
         public String getName() { return "Rebuild Router Info"; }
         
         public void runJob() {
    +        throw new UnsupportedOperationException();
    +/****
             _log.debug("Testing to rebuild router info");
    -        String infoFile = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
    -        File info = new File(getContext().getRouterDir(), infoFile);
    -        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
    -        File keyFile = new File(getContext().getRouterDir(), keyFilename);
    +        File info = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
    +        File keyFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS2_FILENAME);
             
             if (!info.exists() || !keyFile.exists()) {
                 _log.info("Router info file [" + info.getAbsolutePath() + "] or private key file [" + keyFile.getAbsolutePath() + "] deleted, rebuilding");
    @@ -71,51 +73,37 @@ public class RebuildRouterInfoJob extends JobImpl {
             }
             getTiming().setStartAfter(getContext().clock().now() + REBUILD_DELAY);
             getContext().jobQueue().addJob(this);
    +****/
         }
         
         void rebuildRouterInfo() {
             rebuildRouterInfo(true);
         }
     
    +    /**
    +     *  @param alreadyRunning unused
    +     */
         void rebuildRouterInfo(boolean alreadyRunning) {
             _log.debug("Rebuilding the new router info");
             RouterInfo info = null;
    -        String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
    -        File infoFile = new File(getContext().getRouterDir(), infoFilename);
    -        String keyFilename = getContext().getProperty(Router.PROP_KEYS_FILENAME, Router.PROP_KEYS_FILENAME_DEFAULT);
    -        File keyFile = new File(getContext().getRouterDir(), keyFilename);
    +        File infoFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
    +        File keyFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS_FILENAME);
    +        File keyFile2 = new File(getContext().getRouterDir(), CreateRouterInfoJob.KEYS2_FILENAME);
             
    -        if (keyFile.exists()) {
    +        if (keyFile2.exists() || keyFile.exists()) {
                 // ok, no need to rebuild a brand new identity, just update what we can
                 RouterInfo oldinfo = getContext().router().getRouterInfo();
                 if (oldinfo == null) {
    -                info = new RouterInfo();
    -                FileInputStream fis = null;
                     try {
    -                    fis = new FileInputStream(keyFile);
    -                    PrivateKey privkey = new PrivateKey();
    -                    privkey.readBytes(fis);
    -                    SigningPrivateKey signingPrivKey = new SigningPrivateKey();
    -                    signingPrivKey.readBytes(fis);
    -                    PublicKey pubkey = new PublicKey();
    -                    pubkey.readBytes(fis);
    -                    SigningPublicKey signingPubKey = new SigningPublicKey();
    -                    signingPubKey.readBytes(fis);
    -                    RouterIdentity ident = new RouterIdentity();
    -                    Certificate cert = getContext().router().createCertificate();
    -                    ident.setCertificate(cert);
    -                    ident.setPublicKey(pubkey);
    -                    ident.setSigningPublicKey(signingPubKey);
    -                    info.setIdentity(ident);
    +                    KeyData kd = LoadRouterInfoJob.readKeyData(keyFile, keyFile2);
    +                    info = new RouterInfo();
    +                    info.setIdentity(kd.routerIdentity);
                     } catch (Exception e) {
                         _log.log(Log.CRIT, "Error reading in the key data from " + keyFile.getAbsolutePath(), e);
    -                    if (fis != null) try { fis.close(); } catch (IOException ioe) {}
    -                    fis = null;
                         keyFile.delete();
    +                    keyFile2.delete();
                         rebuildRouterInfo(alreadyRunning);
                         return;
    -                } finally {
    -                    if (fis != null) try { fis.close(); } catch (IOException ioe) {}
                     }
                 } else {
                     // Make a new RI from the old identity, or else info.setAddresses() will throw an ISE
    @@ -160,12 +148,14 @@ public class RebuildRouterInfoJob extends JobImpl {
                 _log.warn("Private key file " + keyFile.getAbsolutePath() + " deleted!  Rebuilding a brand new router identity!");
                 // this proc writes the keys and info to the file as well as builds the latest and greatest info
                 CreateRouterInfoJob j = new CreateRouterInfoJob(getContext(), null);
    -            info = j.createRouterInfo();
    +            synchronized (getContext().router().routerInfoFileLock) {
    +                info = j.createRouterInfo();
    +            }
             }
             
             //MessageHistory.initialize();
             getContext().router().setRouterInfo(info);
    -        _log.info("Router info rebuilt and stored at " + infoFilename + " [" + info + "]");
    +        _log.info("Router info rebuilt and stored at " + infoFile + " [" + info + "]");
         }
         
     }
    diff --git a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
    index 50d840d43..672bf702a 100644
    --- a/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
    +++ b/router/java/src/net/i2p/router/startup/StartAcceptingClientsJob.java
    @@ -12,7 +12,7 @@ import net.i2p.router.JobImpl;
     import net.i2p.router.RouterContext;
     
     /** start I2CP interface */
    -public class StartAcceptingClientsJob extends JobImpl {
    +class StartAcceptingClientsJob extends JobImpl {
         
         public StartAcceptingClientsJob(RouterContext context) {
             super(context);
    diff --git a/router/java/src/net/i2p/router/startup/WorkingDir.java b/router/java/src/net/i2p/router/startup/WorkingDir.java
    index d44b2a539..23149c759 100644
    --- a/router/java/src/net/i2p/router/startup/WorkingDir.java
    +++ b/router/java/src/net/i2p/router/startup/WorkingDir.java
    @@ -147,7 +147,7 @@ public class WorkingDir {
             // Check for a router.keys file or logs dir, if either exists it's an old install,
             // and only migrate the data files if told to do so
             // (router.keys could be deleted later by a killkeys())
    -        test = new File(oldDirf, "router.keys");
    +        test = new File(oldDirf, CreateRouterInfoJob.KEYS_FILENAME);
             boolean oldInstall = test.exists();
             if (!oldInstall) {
                 test = new File(oldDirf, "logs");
    diff --git a/router/java/src/net/i2p/router/tasks/GracefulShutdown.java b/router/java/src/net/i2p/router/tasks/GracefulShutdown.java
    index c2ded9b49..5d86a291b 100644
    --- a/router/java/src/net/i2p/router/tasks/GracefulShutdown.java
    +++ b/router/java/src/net/i2p/router/tasks/GracefulShutdown.java
    @@ -31,7 +31,7 @@ public class GracefulShutdown implements Runnable {
                         else if (gracefulExitCode == Router.EXIT_HARD_RESTART)
                             log.log(Log.CRIT, "Restarting after a brief delay");
                         else
    -                        log.log(Log.CRIT, "Graceful shutdown progress - no more tunnels, safe to die");
    +                        log.log(Log.CRIT, "Graceful shutdown progress: No more tunnels, starting final shutdown");
                         // Allow time for a UI reponse
                         try {
                             synchronized (Thread.currentThread()) {
    diff --git a/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java b/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java
    index c29dd2577..6a14a5134 100644
    --- a/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java
    +++ b/router/java/src/net/i2p/router/tasks/PersistRouterInfoJob.java
    @@ -13,10 +13,10 @@ import java.io.FileOutputStream;
     import java.io.IOException;
     
     import net.i2p.data.DataFormatException;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.router.JobImpl;
    -import net.i2p.router.Router;
     import net.i2p.router.RouterContext;
    +import net.i2p.router.startup.CreateRouterInfoJob;
     import net.i2p.util.Log;
     import net.i2p.util.SecureFileOutputStream;
     
    @@ -37,8 +37,7 @@ public class PersistRouterInfoJob extends JobImpl {
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug("Persisting updated router info");
     
    -        String infoFilename = getContext().getProperty(Router.PROP_INFO_FILENAME, Router.PROP_INFO_FILENAME_DEFAULT);
    -        File infoFile = new File(getContext().getRouterDir(), infoFilename);
    +        File infoFile = new File(getContext().getRouterDir(), CreateRouterInfoJob.INFO_FILENAME);
     
             RouterInfo info = getContext().router().getRouterInfo();
     
    diff --git a/router/java/src/net/i2p/router/tasks/UpdateRoutingKeyModifierJob.java b/router/java/src/net/i2p/router/tasks/UpdateRoutingKeyModifierJob.java
    index 2952c2eda..5b367620b 100644
    --- a/router/java/src/net/i2p/router/tasks/UpdateRoutingKeyModifierJob.java
    +++ b/router/java/src/net/i2p/router/tasks/UpdateRoutingKeyModifierJob.java
    @@ -8,7 +8,7 @@ package net.i2p.router.tasks;
      *
      */
     
    -import net.i2p.data.RoutingKeyGenerator;
    +import net.i2p.data.router.RouterKeyGenerator;
     import net.i2p.router.JobImpl;
     import net.i2p.router.RouterContext;
     import net.i2p.util.Log;
    @@ -33,7 +33,7 @@ public class UpdateRoutingKeyModifierJob extends JobImpl {
         public String getName() { return "Update Routing Key Modifier"; }
     
         public void runJob() {
    -        RoutingKeyGenerator gen = getContext().routingKeyGenerator();
    +        RouterKeyGenerator gen = getContext().routerKeyGenerator();
             // make sure we requeue quickly if just before midnight
             long delay = Math.max(5, Math.min(MAX_DELAY_FAILSAFE, gen.getTimeTillMidnight()));
             // TODO tell netdb if mod data changed?
    diff --git a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
    index bc3ad9543..b9e38f9da 100644
    --- a/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
    +++ b/router/java/src/net/i2p/router/transport/CommSystemFacadeImpl.java
    @@ -17,8 +17,8 @@ import java.util.Locale;
     import java.util.Vector;
     
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.router.CommSystemFacade;
     import net.i2p.router.OutNetMessage;
     import net.i2p.router.RouterContext;
    diff --git a/router/java/src/net/i2p/router/transport/Transport.java b/router/java/src/net/i2p/router/transport/Transport.java
    index e232fa7e4..459c84680 100644
    --- a/router/java/src/net/i2p/router/transport/Transport.java
    +++ b/router/java/src/net/i2p/router/transport/Transport.java
    @@ -14,8 +14,8 @@ import java.util.List;
     import java.util.Vector;
     
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.router.OutNetMessage;
     
     /**
    diff --git a/router/java/src/net/i2p/router/transport/TransportEventListener.java b/router/java/src/net/i2p/router/transport/TransportEventListener.java
    index 9a6d80d1c..b8437efd1 100644
    --- a/router/java/src/net/i2p/router/transport/TransportEventListener.java
    +++ b/router/java/src/net/i2p/router/transport/TransportEventListener.java
    @@ -9,7 +9,7 @@ package net.i2p.router.transport;
      */
     
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterIdentity;
    +import net.i2p.data.router.RouterIdentity;
     import net.i2p.data.i2np.I2NPMessage;
     
     public interface TransportEventListener {
    diff --git a/router/java/src/net/i2p/router/transport/TransportImpl.java b/router/java/src/net/i2p/router/transport/TransportImpl.java
    index 194ae3016..6dd93b4c5 100644
    --- a/router/java/src/net/i2p/router/transport/TransportImpl.java
    +++ b/router/java/src/net/i2p/router/transport/TransportImpl.java
    @@ -30,9 +30,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
     
     import net.i2p.data.DataHelper;
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterIdentity;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterIdentity;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.i2np.I2NPMessage;
     import net.i2p.router.CommSystemFacade;
     import net.i2p.router.Job;
    diff --git a/router/java/src/net/i2p/router/transport/TransportManager.java b/router/java/src/net/i2p/router/transport/TransportManager.java
    index d9e0c3798..75e843707 100644
    --- a/router/java/src/net/i2p/router/transport/TransportManager.java
    +++ b/router/java/src/net/i2p/router/transport/TransportManager.java
    @@ -22,8 +22,8 @@ import java.util.Vector;
     import java.util.concurrent.ConcurrentHashMap;
     
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterIdentity;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterIdentity;
     import net.i2p.data.i2np.I2NPMessage;
     import net.i2p.router.CommSystemFacade;
     import net.i2p.router.OutNetMessage;
    @@ -59,6 +59,7 @@ public class TransportManager implements TransportEventListener {
             _context = context;
             _log = _context.logManager().getLog(TransportManager.class);
             _context.statManager().createRateStat("transport.banlistOnUnreachable", "Add a peer to the banlist since none of the transports can reach them", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
    +        _context.statManager().createRateStat("transport.banlistOnUsupportedSigType", "Add a peer to the banlist since signature type is unsupported", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
             _context.statManager().createRateStat("transport.noBidsYetNotAllUnreachable", "Add a peer to the banlist since none of the transports can reach them", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
             _context.statManager().createRateStat("transport.bidFailBanlisted", "Could not attempt to bid on message, as they were banlisted", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
             _context.statManager().createRateStat("transport.bidFailSelf", "Could not attempt to bid on message, as it targeted ourselves", "Transport", new long[] { 60*1000, 10*60*1000, 60*60*1000 });
    @@ -499,8 +500,11 @@ public class TransportManager implements TransportEventListener {
                 }
             }
             if (unreachableTransports >= _transports.size()) {
    -            // Don't banlist if we aren't talking to anybody, as we may have a network connection issue
    -            if (unreachableTransports >= _transports.size() && countActivePeers() > 0) {
    +            if (msg.getTarget().getIdentity().getSigningPublicKey().getType() == null) {
    +                _context.statManager().addRateData("transport.banlistOnUnsupportedSigType", 1);
    +                _context.banlist().banlistRouterForever(peer, _x("Unsupported signature type"));
    +            } else if (unreachableTransports >= _transports.size() && countActivePeers() > 0) {
    +                // Don't banlist if we aren't talking to anybody, as we may have a network connection issue
                     _context.statManager().addRateData("transport.banlistOnUnreachable", msg.getLifetime(), msg.getLifetime());
                     _context.banlist().banlistRouter(peer, _x("Unreachable on any transport"));
                 }
    diff --git a/router/java/src/net/i2p/router/transport/TransportUtil.java b/router/java/src/net/i2p/router/transport/TransportUtil.java
    index 1682abd91..648119f91 100644
    --- a/router/java/src/net/i2p/router/transport/TransportUtil.java
    +++ b/router/java/src/net/i2p/router/transport/TransportUtil.java
    @@ -13,7 +13,7 @@ import java.net.UnknownHostException;
     import java.util.HashMap;
     import java.util.Map;
     
    -import net.i2p.data.RouterAddress;
    +import net.i2p.data.router.RouterAddress;
     import net.i2p.router.RouterContext;
     
     /**
    diff --git a/router/java/src/net/i2p/router/transport/crypto/DHSessionKeyBuilder.java b/router/java/src/net/i2p/router/transport/crypto/DHSessionKeyBuilder.java
    index 0d49a656a..061bbc09e 100644
    --- a/router/java/src/net/i2p/router/transport/crypto/DHSessionKeyBuilder.java
    +++ b/router/java/src/net/i2p/router/transport/crypto/DHSessionKeyBuilder.java
    @@ -124,14 +124,6 @@ public class DHSessionKeyBuilder {
             if (read != 256) {
                 return null;
             }
    -        if (1 == (Y[0] & 0x80)) {
    -            // high bit set, need to inject an additional byte to keep 2s complement
    -            if (_log.shouldLog(Log.DEBUG))
    -                _log.debug("High bit set");
    -            byte Y2[] = new byte[257];
    -            System.arraycopy(Y, 0, Y2, 1, 256);
    -            Y = Y2;
    -        }
             return new NativeBigInteger(1, Y);
         }
     ****/
    @@ -217,17 +209,7 @@ public class DHSessionKeyBuilder {
         public void setPeerPublicValue(byte val[]) throws InvalidPublicParameterException {
             if (val.length != 256)
                 throw new IllegalArgumentException("Peer public value must be exactly 256 bytes");
    -
    -        if (1 == (val[0] & 0x80)) {
    -            // high bit set, need to inject an additional byte to keep 2s complement
    -            //if (_log.shouldLog(Log.DEBUG))
    -            //    _log.debug("High bit set");
    -            byte val2[] = new byte[257];
    -            System.arraycopy(val, 0, val2, 1, 256);
    -            val = val2;
    -        }
             setPeerPublicValue(new NativeBigInteger(1, val));
    -        //_peerValue = new NativeBigInteger(val);
         }
     
         public synchronized BigInteger getPeerPublicValue() {
    @@ -283,7 +265,7 @@ public class DHSessionKeyBuilder {
          * Side effect - sets extraExchangedBytes to the next 32 bytes.
          */
         private final SessionKey calculateSessionKey(BigInteger myPrivateValue, BigInteger publicPeerValue) {
    -        //long start = System.currentTimeMillis();
    +        long start = System.currentTimeMillis();
             SessionKey key = new SessionKey();
             BigInteger exchangedKey = publicPeerValue.modPow(myPrivateValue, CryptoConstants.elgp);
             // surprise! leading zero byte half the time!
    @@ -312,10 +294,10 @@ public class DHSessionKeyBuilder {
                 //    _log.debug("Storing " + remaining.length + " bytes from the end of the DH exchange");
             }
             key.setData(val);
    -        //long end = System.currentTimeMillis();
    -        //long diff = end - start;
    +        long end = System.currentTimeMillis();
    +        long diff = end - start;
             
    -        //_context.statManager().addRateData("crypto.dhCalculateSessionTime", diff, diff);
    +        I2PAppContext.getGlobalContext().statManager().addRateData("crypto.dhCalculateSessionTime", diff);
             //if (diff > 1000) {
             //    if (_log.shouldLog(Log.WARN)) _log.warn("Generating session key took too long (" + diff + " ms");
             //} else {
    @@ -436,6 +418,15 @@ public class DHSessionKeyBuilder {
              * or pulls a prebuilt one from the queue.
              */
             public DHSessionKeyBuilder getBuilder();
    +
    +        /**
    +         * Return an unused DH key builder
    +         * to be put back onto the queue for reuse.
    +         *
    +         * @param builder must not have a peerPublicValue set
    +         * @since 0.9.16
    +         */
    +        public void returnUnused(DHSessionKeyBuilder builder);
         }
     
         public static class PrecalcRunner extends I2PThread implements Factory {
    @@ -455,8 +446,9 @@ public class DHSessionKeyBuilder {
                 _context = ctx;
                 _log = ctx.logManager().getLog(DHSessionKeyBuilder.class);
                 ctx.statManager().createRateStat("crypto.dhGeneratePublicTime", "How long it takes to create x and X", "Encryption", new long[] { 60*60*1000 });
    -            //ctx.statManager().createRateStat("crypto.dhCalculateSessionTime", "How long it takes to create the session key", "Encryption", new long[] { 60*60*1000 });        
    +            ctx.statManager().createRateStat("crypto.dhCalculateSessionTime", "How long it takes to create the session key", "Encryption", new long[] { 60*60*1000 });        
                 ctx.statManager().createRateStat("crypto.DHUsed", "Need a DH from the queue", "Encryption", new long[] { 60*60*1000 });
    +            ctx.statManager().createRateStat("crypto.DHReused", "Unused DH requeued", "Encryption", new long[] { 60*60*1000 });
                 ctx.statManager().createRateStat("crypto.DHEmpty", "DH queue empty", "Encryption", new long[] { 60*60*1000 });
     
                 // add to the defaults for every 128MB of RAM, up to 512MB
    @@ -536,11 +528,11 @@ public class DHSessionKeyBuilder {
              * @since 0.9 moved from DHSKB
              */
             public DHSessionKeyBuilder getBuilder() {
    -            _context.statManager().addRateData("crypto.DHUsed", 1, 0);
    +            _context.statManager().addRateData("crypto.DHUsed", 1);
                 DHSessionKeyBuilder builder = _builders.poll();
                 if (builder == null) {
                     if (_log.shouldLog(Log.INFO)) _log.info("No more builders, creating one now");
    -                _context.statManager().addRateData("crypto.DHEmpty", 1, 0);
    +                _context.statManager().addRateData("crypto.DHEmpty", 1);
                     builder = precalc();
                 }
                 return builder;
    @@ -551,7 +543,7 @@ public class DHSessionKeyBuilder {
                 DHSessionKeyBuilder builder = new DHSessionKeyBuilder(_context);
                 long end = System.currentTimeMillis();
                 long diff = end - start;
    -            _context.statManager().addRateData("crypto.dhGeneratePublicTime", diff, diff);
    +            _context.statManager().addRateData("crypto.dhGeneratePublicTime", diff);
                 if (diff > 1000) {
                     if (_log.shouldLog(Log.WARN))
                         _log.warn("Took more than a second (" + diff + "ms) to generate local DH value");
    @@ -561,6 +553,23 @@ public class DHSessionKeyBuilder {
                 return builder;
             }
     
    +        /**
    +         * Return an unused DH key builder
    +         * to be put back onto the queue for reuse.
    +         *
    +         * @param builder must not have a peerPublicValue set
    +         * @since 0.9.16
    +         */
    +        public void returnUnused(DHSessionKeyBuilder builder) {
    +            if (builder.getPeerPublicValue() != null) {
    +                if (_log.shouldLog(Log.WARN))
    +                    _log.warn("builder returned used");
    +                return;
    +            }
    +            _context.statManager().addRateData("crypto.DHReused", 1);
    +            _builders.offer(builder);
    +        }
    +
             /** @return true if successful, false if full */
             private final boolean addBuilder(DHSessionKeyBuilder builder) {
                 return _builders.offer(builder);
    diff --git a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
    index bc6e37bc4..4697e15f6 100644
    --- a/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
    +++ b/router/java/src/net/i2p/router/transport/ntcp/EstablishState.java
    @@ -1,5 +1,6 @@
     package net.i2p.router.transport.ntcp;
     
    +import java.io.ByteArrayInputStream;
     import java.io.ByteArrayOutputStream;
     import java.io.IOException;
     import java.net.InetAddress;
    @@ -7,11 +8,12 @@ import java.net.UnknownHostException;
     import java.nio.ByteBuffer;
     
     import net.i2p.I2PAppContext;
    +import net.i2p.crypto.SigType;
     import net.i2p.data.Base64;
     import net.i2p.data.DataFormatException;
     import net.i2p.data.DataHelper;
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterIdentity;
    +import net.i2p.data.router.RouterIdentity;
     import net.i2p.data.Signature;
     import net.i2p.router.Router;
     import net.i2p.router.RouterContext;
    @@ -62,6 +64,7 @@ import net.i2p.util.SimpleByteCache;
     class EstablishState {
         
         public static final VerifiedEstablishState VERIFIED = new VerifiedEstablishState();
    +    public static final FailedEstablishState FAILED = new FailedEstablishState();
         
         private final RouterContext _context;
         private final Log _log;
    @@ -70,13 +73,14 @@ class EstablishState {
         private final byte _X[];
         private final byte _hX_xor_bobIdentHash[];
         private int _aliceIdentSize;
    +    private RouterIdentity _aliceIdent;
         /** contains the decrypted aliceIndexSize + aliceIdent + tsA + padding + aliceSig */
         private ByteArrayOutputStream _sz_aliceIdent_tsA_padding_aliceSig;
         /** how long we expect _sz_aliceIdent_tsA_padding_aliceSig to be when its full */
         private int _sz_aliceIdent_tsA_padding_aliceSigSize;
         // alice receives (and bob sends)
    -    private byte _Y[];
    -    private transient byte _e_hXY_tsB[];
    +    private final byte _Y[];
    +    private final byte _e_hXY_tsB[];
         /** Bob's Timestamp in seconds */
         private transient long _tsB;
         /** Alice's Timestamp in seconds */
    @@ -85,7 +89,7 @@ class EstablishState {
     
         /** previously received encrypted block (or the IV) */
         private byte _prevEncrypted[];
    -    /** current encrypted block we are reading */
    +    /** current encrypted block we are reading (IB only) or an IV buf used at the end for OB */
         private byte _curEncrypted[];
         /**
          * next index in _curEncrypted to write to (equals _curEncrypted length if the block is
    @@ -103,24 +107,63 @@ class EstablishState {
     
         private final NTCPTransport _transport;
         private final NTCPConnection _con;
    -    private boolean _corrupt;
         /** error causing the corruption */
         private String _err;
         /** exception causing the error */
         private Exception _e;
    -    private boolean _verified;
    -    private boolean _confirmWritten;
         private boolean _failedBySkew;
         
    +    private static final int MIN_RI_SIZE = 387;
    +    private static final int MAX_RI_SIZE = 2048;
    +
    +    private static final int AES_SIZE = 16;
    +    private static final int XY_SIZE = 256;
    +    private static final int HXY_SIZE = 32;  //Hash.HASH_LENGTH;
    +    private static final int HXY_TSB_PAD_SIZE = HXY_SIZE + 4 + 12;  // 48
    +
    +    protected State _state;
    +
    +    private enum State {
    +        OB_INIT,
    +        /** sent 1 */
    +        OB_SENT_X,
    +        /** sent 1, got 2 partial */
    +        OB_GOT_Y,
    +        /** sent 1, got 2 */
    +        OB_GOT_HXY,
    +        /** sent 1, got 2, sent 3 */
    +        OB_SENT_RI,
    +        /** sent 1, got 2, sent 3, got 4 */
    +        OB_GOT_SIG,
    +
    +        IB_INIT,
    +        /** got 1 partial */
    +        IB_GOT_X,
    +        /** got 1 */
    +        IB_GOT_HX,
    +        /** got 1, sent 2 */
    +        IB_SENT_Y,
    +        /** got 1, sent 2, got partial 3 */
    +        IB_GOT_RI_SIZE,
    +        /** got 1, sent 2, got 3 */
    +        IB_GOT_RI,
    +
    +        /** OB: got and verified 4; IB: got and verified 3 and sent 4 */
    +        VERIFIED,
    +        CORRUPT
    +    }
    +
         private EstablishState() {
             _context = null;
             _log = null;
             _X = null;
    +        _Y = null;
             _hX_xor_bobIdentHash = null;
             _curDecrypted = null;
             _dh = null;
             _transport = null;
             _con = null;
    +        _e_hXY_tsB = null;
         }
     
         public EstablishState(RouterContext ctx, NTCPTransport transport, NTCPConnection con) {
    @@ -129,20 +172,25 @@ class EstablishState {
             _transport = transport;
             _con = con;
             _dh = _transport.getDHBuilder();
    -        _hX_xor_bobIdentHash = new byte[Hash.HASH_LENGTH];
    +        _hX_xor_bobIdentHash = SimpleByteCache.acquire(HXY_SIZE);
             if (_con.isInbound()) {
    -            _X = new byte[256];
    +            _X = SimpleByteCache.acquire(XY_SIZE);
    +            _Y = _dh.getMyPublicValueBytes();
                 _sz_aliceIdent_tsA_padding_aliceSig = new ByteArrayOutputStream(512);
    +            _prevEncrypted = SimpleByteCache.acquire(AES_SIZE);
    +            _state = State.IB_INIT;
             } else {
                 _X = _dh.getMyPublicValueBytes();
    -            _Y = new byte[256];
    -            ctx.sha().calculateHash(_X, 0, _X.length, _hX_xor_bobIdentHash, 0);
    +            _Y = SimpleByteCache.acquire(XY_SIZE);
    +            ctx.sha().calculateHash(_X, 0, XY_SIZE, _hX_xor_bobIdentHash, 0);
                 xor32(con.getRemotePeer().calculateHash().getData(), _hX_xor_bobIdentHash);
    +            // _prevEncrypted will be created later
    +            _state = State.OB_INIT;
             }
     
    -        _prevEncrypted = new byte[16];
    -        _curEncrypted = new byte[16];
    -        _curDecrypted = new byte[16];
    +        _e_hXY_tsB = new byte[HXY_TSB_PAD_SIZE];
    +        _curEncrypted = SimpleByteCache.acquire(AES_SIZE);
    +        _curDecrypted = SimpleByteCache.acquire(AES_SIZE);
         }
     
         /**
    @@ -154,14 +202,14 @@ class EstablishState {
          * All data must be copied out of the buffer as Reader.processRead()
          * will return it to the pool.
          */
    -    public void receive(ByteBuffer src) {
    -        if (_corrupt || _verified)
    -            throw new IllegalStateException(prefix() + "received after completion [corrupt?" + _corrupt + " verified? " + _verified + "] on " + _con);
    +    public synchronized void receive(ByteBuffer src) {
    +        if (_state == State.VERIFIED || _state == State.CORRUPT)
    +            throw new IllegalStateException(prefix() + "received unexpected data on " + _con);
             if (!src.hasRemaining())
                 return; // nothing to receive
     
             if (_log.shouldLog(Log.DEBUG))
    -            _log.debug(prefix()+"receive " + src);
    +            _log.debug(prefix() + "Receiving: " + src.remaining() + " Received: " + _received);
             if (_con.isInbound())
                 receiveInbound(src);
             else
    @@ -169,12 +217,9 @@ class EstablishState {
         }
     
         /**
    -     * we have written all of the data required to confirm the connection
    -     * establishment
    +     *  Was this connection failed because of clock skew?
          */
    -    public boolean confirmWritten() { return _confirmWritten; }
    -
    -    public boolean getFailedBySkew() { return _failedBySkew; }
    +    public synchronized boolean getFailedBySkew() { return _failedBySkew; }
     
         /**
          *  we are Bob, so receive these bytes as part of an inbound connection
    @@ -182,11 +227,11 @@ class EstablishState {
          *
          *  All data must be copied out of the buffer as Reader.processRead()
          *  will return it to the pool.
    +     *
    +     *  Caller must synch.
          */
         private void receiveInbound(ByteBuffer src) {
    -        if (_log.shouldLog(Log.DEBUG))
    -            _log.debug(prefix()+"Receiving inbound: prev received=" + _received + " src.remaining=" + src.remaining());
    -        while (_received < _X.length && src.hasRemaining()) {
    +        while (_state == State.IB_INIT && src.hasRemaining()) {
                 byte c = src.get();
                 _X[_received++] = c;
                 //if (_log.shouldLog(Log.DEBUG)) _log.debug("recv x" + (int)c + " received=" + _received);
    @@ -197,24 +242,28 @@ class EstablishState {
                 //        return;
                 //    }
                 //}
    +            if (_received >= XY_SIZE)
    +                _state = State.IB_GOT_X;
             }
    -        while (_received < _X.length + _hX_xor_bobIdentHash.length && src.hasRemaining()) {
    -            int i = _received-_X.length;
    +        while (_state == State.IB_GOT_X && src.hasRemaining()) {
    +            int i = _received - XY_SIZE;
                 _received++;
                 byte c = src.get();
                 _hX_xor_bobIdentHash[i] = c;
                 //if (_log.shouldLog(Log.DEBUG)) _log.debug("recv bih" + (int)c + " received=" + _received);
    +            if (i >= HXY_SIZE - 1)
    +                _state = State.IB_GOT_HX;
             }
     
    -        if (_received >= _X.length + _hX_xor_bobIdentHash.length) {
    -            if (_dh.getSessionKey() == null) {
    +        if (_state == State.IB_GOT_HX) {
    +
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug(prefix()+"Enough data for a DH received");
     
                     // first verify that Alice knows who she is trying to talk with and that the X
                     // isn't corrupt
    -                byte[] realXor = SimpleByteCache.acquire(Hash.HASH_LENGTH);
    -                _context.sha().calculateHash(_X, 0, _X.length, realXor, 0);
    +                byte[] realXor = SimpleByteCache.acquire(HXY_SIZE);
    +                _context.sha().calculateHash(_X, 0, XY_SIZE, realXor, 0);
                     xor32(_context.routerHash().getData(), realXor);
                     //if (_log.shouldLog(Log.DEBUG)) {
                         //_log.debug(prefix()+"_X = " + Base64.encode(_X));
    @@ -240,43 +289,43 @@ class EstablishState {
                         // ok, they're actually trying to talk to us, and we got their (unauthenticated) X
                         _dh.setPeerPublicValue(_X);
                         _dh.getSessionKey(); // force the calc
    -                    System.arraycopy(realXor, 16, _prevEncrypted, 0, _prevEncrypted.length);
    +                    System.arraycopy(_hX_xor_bobIdentHash, AES_SIZE, _prevEncrypted, 0, AES_SIZE);
                         if (_log.shouldLog(Log.DEBUG))
                             _log.debug(prefix()+"DH session key calculated (" + _dh.getSessionKey().toBase64() + ")");
     
                         // now prepare our response: Y+E(H(X+Y)+tsB+padding, sk, Y[239:255])
    -                    _Y = _dh.getMyPublicValueBytes();
    -                    byte xy[] = new byte[_X.length+_Y.length];
    -                    System.arraycopy(_X, 0, xy, 0, _X.length);
    -                    System.arraycopy(_Y, 0, xy, _X.length, _Y.length);
    -                    byte[] hxy = SimpleByteCache.acquire(Hash.HASH_LENGTH);
    -                    _context.sha().calculateHash(xy, 0, xy.length, hxy, 0);
    +                    byte xy[] = new byte[XY_SIZE + XY_SIZE];
    +                    System.arraycopy(_X, 0, xy, 0, XY_SIZE);
    +                    System.arraycopy(_Y, 0, xy, XY_SIZE, XY_SIZE);
    +                    byte[] hxy = SimpleByteCache.acquire(HXY_SIZE);
    +                    _context.sha().calculateHash(xy, 0, XY_SIZE + XY_SIZE, hxy, 0);
                         _tsB = (_context.clock().now() + 500) / 1000l; // our (Bob's) timestamp in seconds
    -                    byte toEncrypt[] = new byte[hxy.length + (4 + 12)];  // 48
    -                    System.arraycopy(hxy, 0, toEncrypt, 0, hxy.length);
    +                    byte toEncrypt[] = new byte[HXY_TSB_PAD_SIZE];  // 48
    +                    System.arraycopy(hxy, 0, toEncrypt, 0, HXY_SIZE);
                         byte tsB[] = DataHelper.toLong(4, _tsB);
    -                    System.arraycopy(tsB, 0, toEncrypt, hxy.length, tsB.length);
    +                    System.arraycopy(tsB, 0, toEncrypt, HXY_SIZE, tsB.length);
                         //DataHelper.toLong(toEncrypt, hxy.getData().length, 4, _tsB);
    -                    _context.random().nextBytes(toEncrypt, hxy.length + 4, 12);
    +                    _context.random().nextBytes(toEncrypt, HXY_SIZE + 4, 12);
                         if (_log.shouldLog(Log.DEBUG)) {
                             //_log.debug(prefix()+"Y="+Base64.encode(_Y));
                             //_log.debug(prefix()+"x+y="+Base64.encode(xy));
                             _log.debug(prefix()+"h(x+y)="+Base64.encode(hxy));
    -                        _log.debug(prefix()+"tsb="+Base64.encode(tsB));
    +                        _log.debug(prefix() + "tsb = " + _tsB);
                             _log.debug(prefix()+"unencrypted H(X+Y)+tsB+padding: " + Base64.encode(toEncrypt));
    -                        _log.debug(prefix()+"encryption iv= " + Base64.encode(_Y, _Y.length-16, 16));
    +                        _log.debug(prefix()+"encryption iv= " + Base64.encode(_Y, XY_SIZE-AES_SIZE, AES_SIZE));
                             _log.debug(prefix()+"encryption key= " + _dh.getSessionKey().toBase64());
                         }
                         SimpleByteCache.release(hxy);
    -                    _e_hXY_tsB = new byte[toEncrypt.length];
    -                    _context.aes().encrypt(toEncrypt, 0, _e_hXY_tsB, 0, _dh.getSessionKey(), _Y, _Y.length-16, toEncrypt.length);
    +                    _context.aes().encrypt(toEncrypt, 0, _e_hXY_tsB, 0, _dh.getSessionKey(),
    +                                           _Y, XY_SIZE-AES_SIZE, HXY_TSB_PAD_SIZE);
                         if (_log.shouldLog(Log.DEBUG))
                             _log.debug(prefix()+"encrypted H(X+Y)+tsB+padding: " + Base64.encode(_e_hXY_tsB));
    -                    byte write[] = new byte[_Y.length + _e_hXY_tsB.length];
    -                    System.arraycopy(_Y, 0, write, 0, _Y.length);
    -                    System.arraycopy(_e_hXY_tsB, 0, write, _Y.length, _e_hXY_tsB.length);
    +                    byte write[] = new byte[XY_SIZE + HXY_TSB_PAD_SIZE];
    +                    System.arraycopy(_Y, 0, write, 0, XY_SIZE);
    +                    System.arraycopy(_e_hXY_tsB, 0, write, XY_SIZE, HXY_TSB_PAD_SIZE);
     
                         // ok, now that is prepared, we want to actually send it, so make sure we are up for writing
    +                    _state = State.IB_SENT_Y;
                         _transport.getPumper().wantsWrite(_con, write);
                         if (!src.hasRemaining()) return;
                     } catch (DHSessionKeyBuilder.InvalidPublicParameterException e) {
    @@ -284,73 +333,116 @@ class EstablishState {
                         fail("Invalid X", e);
                         return;
                     }
    -            }
     
    -            // ok, we are onto the encrypted area
    -            while (src.hasRemaining() && !_corrupt) {
    +        }
    +
    +        // ok, we are onto the encrypted area, i.e. Message #3
    +        while ((_state == State.IB_SENT_Y ||
    +                _state == State.IB_GOT_RI_SIZE ||
    +                _state == State.IB_GOT_RI) && src.hasRemaining()) {
    +
                     //if (_log.shouldLog(Log.DEBUG))
                     //    _log.debug(prefix()+"Encrypted bytes available (" + src.hasRemaining() + ")");
    -                while (_curEncryptedOffset < _curEncrypted.length && src.hasRemaining()) {
    +                // Collect a 16-byte block
    +                while (_curEncryptedOffset < AES_SIZE && src.hasRemaining()) {
                         _curEncrypted[_curEncryptedOffset++] = src.get();
                         _received++;
                     }
    -                if (_curEncryptedOffset >= _curEncrypted.length) {
    -                    _context.aes().decrypt(_curEncrypted, 0, _curDecrypted, 0, _dh.getSessionKey(), _prevEncrypted, 0, _curEncrypted.length);
    +                // Decrypt the 16-byte block
    +                if (_curEncryptedOffset >= AES_SIZE) {
    +                    _context.aes().decrypt(_curEncrypted, 0, _curDecrypted, 0, _dh.getSessionKey(),
    +                                           _prevEncrypted, 0, AES_SIZE);
                         //if (_log.shouldLog(Log.DEBUG))
    -                    //    _log.debug(prefix()+"full block read and decrypted: " + Base64.encode(_curDecrypted));
    +                    //    _log.debug(prefix() + "full block read and decrypted: ");
     
    -                    byte swap[] = new byte[16];
    +                    byte swap[] = _prevEncrypted;
                         _prevEncrypted = _curEncrypted;
                         _curEncrypted = swap;
                         _curEncryptedOffset = 0;
     
    -                    if (_aliceIdentSize <= 0) { // we are on the first decrypted block
    -                        _aliceIdentSize = (int)DataHelper.fromLong(_curDecrypted, 0, 2);
    -                        _sz_aliceIdent_tsA_padding_aliceSigSize = 2 + _aliceIdentSize + 4 + Signature.SIGNATURE_BYTES;
    -                        int rem = (_sz_aliceIdent_tsA_padding_aliceSigSize % 16);
    -                        int padding = 0;
    -                        if (rem > 0)
    -                            padding = 16-rem;
    -                        _sz_aliceIdent_tsA_padding_aliceSigSize += padding;
    -                        try {
    -                            _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
    -                        } catch (IOException ioe) {
    -                            if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
    +                    if (_state == State.IB_SENT_Y) { // we are on the first decrypted block
    +                        int sz = (int)DataHelper.fromLong(_curDecrypted, 0, 2);
    +                        if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE) {
    +                            _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
    +                            fail("size is invalid", new Exception("size is " + sz));
    +                            return;
                             }
                             if (_log.shouldLog(Log.DEBUG))
    -                            _log.debug(prefix()+"alice ident size decrypted as " + _aliceIdentSize + ", making the padding at " + padding + " and total size at " + _sz_aliceIdent_tsA_padding_aliceSigSize);
    -                    } else {
    -                        // subsequent block...
    -                        try {
    -                            _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
    -                        } catch (IOException ioe) {
    -                            if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
    -                        }
    -                        //if (_log.shouldLog(Log.DEBUG))
    -                        //    _log.debug(prefix()+"subsequent block decrypted (" + _sz_aliceIdent_tsA_padding_aliceSig.size() + ")");
    +                            _log.debug(prefix() + "got the RI size: " + sz);
    +                        _aliceIdentSize  = sz;
    +                        _state = State.IB_GOT_RI_SIZE;
     
    -                        if (_sz_aliceIdent_tsA_padding_aliceSig.size() >= _sz_aliceIdent_tsA_padding_aliceSigSize) {
    +                        // We must defer the calculations for total size of the message until
    +                        //  we get the full alice ident so
    +                        // we can determine how long the signature is.
    +                        // See below
    +
    +                    }
    +                    try {
    +                        _sz_aliceIdent_tsA_padding_aliceSig.write(_curDecrypted);
    +                    } catch (IOException ioe) {
    +                        if (_log.shouldLog(Log.ERROR)) _log.error(prefix()+"Error writing to the baos?", ioe);
    +                    }
    +                    //if (_log.shouldLog(Log.DEBUG))
    +                    //    _log.debug(prefix()+"subsequent block decrypted (" + _sz_aliceIdent_tsA_padding_aliceSig.size() + ")");
    +
    +                    if (_state == State.IB_GOT_RI_SIZE &&
    +                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= 2 + _aliceIdentSize) {
    +                        // we have enough to get Alice's RI and determine the sig+padding length
    +                        readAliceRouterIdentity();
    +                        if (_log.shouldLog(Log.DEBUG))
    +                            _log.debug(prefix() + "got the RI");
    +                        if (_aliceIdent == null) {
    +                            // readAliceRouterIdentity already called fail
    +                            return;
    +                        }
    +                        SigType type = _aliceIdent.getSigningPublicKey().getType();
    +                        if (type == null) {
    +                            fail("Unsupported sig type");
    +                            return;
    +                        }
    +                        _state = State.IB_GOT_RI;
    +                        // handle variable signature size
    +                        _sz_aliceIdent_tsA_padding_aliceSigSize = 2 + _aliceIdentSize + 4 + type.getSigLen();
    +                        int rem = (_sz_aliceIdent_tsA_padding_aliceSigSize % AES_SIZE);
    +                        int padding = 0;
    +                        if (rem > 0)
    +                            padding = AES_SIZE-rem;
    +                        _sz_aliceIdent_tsA_padding_aliceSigSize += padding;
    +                        if (_log.shouldLog(Log.DEBUG))
    +                            _log.debug(prefix() + "alice ident size decrypted as " + _aliceIdentSize +
    +                                       ", making the padding at " + padding + " and total size at " +
    +                                       _sz_aliceIdent_tsA_padding_aliceSigSize);
    +                    }
    +
    +                    if (_state == State.IB_GOT_RI &&
    +                        _sz_aliceIdent_tsA_padding_aliceSig.size() >= _sz_aliceIdent_tsA_padding_aliceSigSize) {
    +                        // we have the remainder of Message #3, i.e. the padding+signature
    +                        // Time to verify.
    +
    +                            if (_log.shouldLog(Log.DEBUG))
    +                                _log.debug(prefix() + "got the sig");
                                 verifyInbound();
    -                            if (!_corrupt && _verified && src.hasRemaining())
    +                            if (_state == State.VERIFIED && src.hasRemaining())
                                     prepareExtra(src);
                                 if (_log.shouldLog(Log.DEBUG))
                                     _log.debug(prefix()+"verifying size (sz=" + _sz_aliceIdent_tsA_padding_aliceSig.size()
                                                + " expected=" + _sz_aliceIdent_tsA_padding_aliceSigSize
    -                                           + " corrupt=" + _corrupt
    -                                           + " verified=" + _verified + " extra=" + (_extra != null ? _extra.length : 0) + ")");
    +                                           + ' ' + _state
    +                                           + " extra=" + (_extra != null ? _extra.length : 0) + ")");
                                 return;
    -                        }
                         }
                     } else {
                         // no more bytes available in the buffer, and only a partial
                         // block was read, so we can't decrypt it.
                         if (_log.shouldLog(Log.DEBUG))
    -                        _log.debug(prefix()+"end of available data with only a partial block read (" + _curEncryptedOffset + ", " + _received + ")");
    +                        _log.debug(prefix() + "end of available data with only a partial block read (" +
    +                                   _curEncryptedOffset + ", " + _received + ")");
                     }
    -            }
    -            if (_log.shouldLog(Log.DEBUG))
    -                _log.debug(prefix()+"done with the data, not yet complete or corrupt");
             }
    +
    +        if (_log.shouldLog(Log.DEBUG))
    +            _log.debug(prefix()+"done with the data, not yet complete or corrupt");
         }
     
         /**
    @@ -359,22 +451,22 @@ class EstablishState {
          *
          *  All data must be copied out of the buffer as Reader.processRead()
          *  will return it to the pool.
    +     *
    +     *  Caller must synch.
          */
         private void receiveOutbound(ByteBuffer src) {
    -        if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix()+"Receive outbound " + src + " received=" + _received);
    -
             // recv Y+E(H(X+Y)+tsB, sk, Y[239:255])
    -        while (_received < _Y.length && src.hasRemaining()) {
    +        while (_state == State.OB_SENT_X && src.hasRemaining()) {
                 byte c = src.get();
                 _Y[_received++] = c;
                 //if (_log.shouldLog(Log.DEBUG)) _log.debug("recv x" + (int)c + " received=" + _received);
    -            if (_received >= _Y.length) {
    +            if (_received >= XY_SIZE) {
                     try {
                         _dh.setPeerPublicValue(_Y);
                         _dh.getSessionKey(); // force the calc
                         if (_log.shouldLog(Log.DEBUG))
                             _log.debug(prefix()+"DH session key calculated (" + _dh.getSessionKey().toBase64() + ")");
    -                    _e_hXY_tsB = new byte[Hash.HASH_LENGTH+4+12];
    +                    _state = State.OB_GOT_Y;
                     } catch (DHSessionKeyBuilder.InvalidPublicParameterException e) {
                         _context.statManager().addRateData("ntcp.invalidDH", 1);
                         fail("Invalid X", e);
    @@ -382,34 +474,34 @@ class EstablishState {
                     }
                 }
             }
    -        if (_e_hXY_tsB == null) return; // !src.hasRemaining
     
    -        while (_received < _Y.length + _e_hXY_tsB.length && src.hasRemaining()) {
    -            int i = _received-_Y.length;
    +        while (_state == State.OB_GOT_Y && src.hasRemaining()) {
    +            int i = _received-XY_SIZE;
                 _received++;
                 byte c = src.get();
                 _e_hXY_tsB[i] = c;
    -            if (_log.shouldLog(Log.DEBUG))
    -                _log.debug(prefix() + "recv _e_hXY_tsB " + (int)c + " received=" + _received);
    -            if (i+1 >= _e_hXY_tsB.length) {
    +            //if (_log.shouldLog(Log.DEBUG))
    +            //    _log.debug(prefix() + "recv _e_hXY_tsB " + (int)c + " received=" + _received);
    +            if (i+1 >= HXY_TSB_PAD_SIZE) {
                     if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix() + "received _e_hXY_tsB fully");
    -                byte hXY_tsB[] = new byte[_e_hXY_tsB.length];
    -                _context.aes().decrypt(_e_hXY_tsB, 0, hXY_tsB, 0, _dh.getSessionKey(), _Y, _Y.length-16, _e_hXY_tsB.length);
    -                byte XY[] = new byte[_X.length + _Y.length];
    -                System.arraycopy(_X, 0, XY, 0, _X.length);
    -                System.arraycopy(_Y, 0, XY, _X.length, _Y.length);
    -                byte[] h = SimpleByteCache.acquire(Hash.HASH_LENGTH);
    -                _context.sha().calculateHash(XY, 0, XY.length, h, 0);
    +                byte hXY_tsB[] = new byte[HXY_TSB_PAD_SIZE];
    +                _context.aes().decrypt(_e_hXY_tsB, 0, hXY_tsB, 0, _dh.getSessionKey(), _Y, XY_SIZE-AES_SIZE, HXY_TSB_PAD_SIZE);
    +                byte XY[] = new byte[XY_SIZE + XY_SIZE];
    +                System.arraycopy(_X, 0, XY, 0, XY_SIZE);
    +                System.arraycopy(_Y, 0, XY, XY_SIZE, XY_SIZE);
    +                byte[] h = SimpleByteCache.acquire(HXY_SIZE);
    +                _context.sha().calculateHash(XY, 0, XY_SIZE + XY_SIZE, h, 0);
                     //if (_log.shouldLog(Log.DEBUG))
                     //    _log.debug(prefix() + "h(XY)=" + h.toBase64());
    -                if (!DataHelper.eq(h, 0, hXY_tsB, 0, Hash.HASH_LENGTH)) {
    +                if (!DataHelper.eq(h, 0, hXY_tsB, 0, HXY_SIZE)) {
                         SimpleByteCache.release(h);
                         _context.statManager().addRateData("ntcp.invalidHXY", 1);
                         fail("Invalid H(X+Y) - mitm attack attempted?");
                         return;
                     }
                     SimpleByteCache.release(h);
    -                _tsB = DataHelper.fromLong(hXY_tsB, Hash.HASH_LENGTH, 4); // their (Bob's) timestamp in seconds
    +                _state = State.OB_GOT_HXY;
    +                _tsB = DataHelper.fromLong(hXY_tsB, HXY_SIZE, 4); // their (Bob's) timestamp in seconds
                     _tsA = (_context.clock().now() + 500) / 1000; // our (Alice's) timestamp in seconds
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug(prefix()+"h(X+Y) is correct, tsA-tsB=" + (_tsA-_tsB));
    @@ -439,13 +531,13 @@ class EstablishState {
     
                     // now prepare and send our response
                     // send E(#+Alice.identity+tsA+padding+S(X+Y+Bob.identHash+tsA+tsB), sk, hX_xor_Bob.identHash[16:31])
    -                int sigSize = _X.length+_Y.length+Hash.HASH_LENGTH+4+4;//+12;
    +                int sigSize = XY_SIZE + XY_SIZE + HXY_SIZE + 4+4;//+12;
                     byte preSign[] = new byte[sigSize];
    -                System.arraycopy(_X, 0, preSign, 0, _X.length);
    -                System.arraycopy(_Y, 0, preSign, _X.length, _Y.length);
    -                System.arraycopy(_con.getRemotePeer().calculateHash().getData(), 0, preSign, _X.length+_Y.length, Hash.HASH_LENGTH);
    -                DataHelper.toLong(preSign, _X.length+_Y.length+Hash.HASH_LENGTH, 4, _tsA);
    -                DataHelper.toLong(preSign, _X.length+_Y.length+Hash.HASH_LENGTH+4, 4, _tsB);
    +                System.arraycopy(_X, 0, preSign, 0, XY_SIZE);
    +                System.arraycopy(_Y, 0, preSign, XY_SIZE, XY_SIZE);
    +                System.arraycopy(_con.getRemotePeer().calculateHash().getData(), 0, preSign, XY_SIZE + XY_SIZE, HXY_SIZE);
    +                DataHelper.toLong(preSign, XY_SIZE + XY_SIZE + HXY_SIZE, 4, _tsA);
    +                DataHelper.toLong(preSign, XY_SIZE + XY_SIZE + HXY_SIZE + 4, 4, _tsB);
                     // hXY_tsB has 12 bytes of padding (size=48, tsB=4 + hXY=32)
                     //System.arraycopy(hXY_tsB, hXY_tsB.length-12, preSign, _X.length+_Y.length+Hash.HASH_LENGTH+4+4, 12);
                     //byte sigPad[] = new byte[padSig];
    @@ -458,80 +550,99 @@ class EstablishState {
                     //}
     
                     byte ident[] = _context.router().getRouterInfo().getIdentity().toByteArray();
    -                int min = 2+ident.length+4+Signature.SIGNATURE_BYTES;
    -                int rem = min % 16;
    +                // handle variable signature size
    +                int min = 2 + ident.length + 4 + sig.length();
    +                int rem = min % AES_SIZE;
                     int padding = 0;
                     if (rem > 0)
    -                    padding = 16 - rem;
    +                    padding = AES_SIZE - rem;
                     byte preEncrypt[] = new byte[min+padding];
                     DataHelper.toLong(preEncrypt, 0, 2, ident.length);
                     System.arraycopy(ident, 0, preEncrypt, 2, ident.length);
                     DataHelper.toLong(preEncrypt, 2+ident.length, 4, _tsA);
                     if (padding > 0)
                         _context.random().nextBytes(preEncrypt, 2 + ident.length + 4, padding);
    -                System.arraycopy(sig.getData(), 0, preEncrypt, 2+ident.length+4+padding, Signature.SIGNATURE_BYTES);
    +                System.arraycopy(sig.getData(), 0, preEncrypt, 2+ident.length+4+padding, sig.length());
     
                     _prevEncrypted = new byte[preEncrypt.length];
    -                _context.aes().encrypt(preEncrypt, 0, _prevEncrypted, 0, _dh.getSessionKey(), _hX_xor_bobIdentHash, _hX_xor_bobIdentHash.length-16, preEncrypt.length);
    +                _context.aes().encrypt(preEncrypt, 0, _prevEncrypted, 0, _dh.getSessionKey(),
    +                                       _hX_xor_bobIdentHash, _hX_xor_bobIdentHash.length-AES_SIZE, preEncrypt.length);
     
                     //if (_log.shouldLog(Log.DEBUG)) {
                         //_log.debug(prefix() + "unencrypted response to Bob: " + Base64.encode(preEncrypt));
                         //_log.debug(prefix() + "encrypted response to Bob: " + Base64.encode(_prevEncrypted));
                     //}
                     // send 'er off (when the bw limiter says, etc)
    +                _state = State.OB_SENT_RI;
                     _transport.getPumper().wantsWrite(_con, _prevEncrypted);
                 }
             }
    -        if (_received >= _Y.length + _e_hXY_tsB.length && src.hasRemaining()) {
    +        if (_state == State.OB_SENT_RI && src.hasRemaining()) {
                 // we are receiving their confirmation
     
                 // recv E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev)
                 int off = 0;
                 if (_e_bobSig == null) {
    -                _e_bobSig = new byte[48];
    +                // handle variable signature size
    +                int siglen = _con.getRemotePeer().getSigningPublicKey().getType().getSigLen();
    +                int rem = siglen % AES_SIZE;
    +                int padding;
    +                if (rem > 0)
    +                    padding = AES_SIZE - rem;
    +                else
    +                    padding = 0;
    +                _e_bobSig = new byte[siglen + padding];
                     if (_log.shouldLog(Log.DEBUG))
    -                    _log.debug(prefix() + "receiving E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " + src.hasRemaining() + ")");
    +                    _log.debug(prefix() + "receiving E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
    +                               src.hasRemaining() + ")");
                 } else {
    -                off = _received - _Y.length - _e_hXY_tsB.length;
    +                off = _received - XY_SIZE - HXY_TSB_PAD_SIZE;
                     if (_log.shouldLog(Log.DEBUG))
    -                    _log.debug(prefix() + "continuing to receive E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " + src.hasRemaining() + " off=" + off + " recv=" + _received + ")");
    +                    _log.debug(prefix() + "continuing to receive E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev) (remaining? " +
    +                               src.hasRemaining() + " off=" + off + " recv=" + _received + ")");
                 }
    -            while (src.hasRemaining() && off < _e_bobSig.length) {
    -                if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix()+"recv bobSig received=" + _received);
    +            while (_state == State.OB_SENT_RI && src.hasRemaining()) {
    +                //if (_log.shouldLog(Log.DEBUG)) _log.debug(prefix()+"recv bobSig received=" + _received);
                     _e_bobSig[off++] = src.get();
                     _received++;
     
                     if (off >= _e_bobSig.length) {
    +                    _state = State.OB_GOT_SIG;
                         //if (_log.shouldLog(Log.DEBUG))
                         //    _log.debug(prefix() + "received E(S(X+Y+Alice.identHash+tsA+tsB)+padding, sk, prev): " + Base64.encode(_e_bobSig));
                         byte bobSig[] = new byte[_e_bobSig.length];
    -                    _context.aes().decrypt(_e_bobSig, 0, bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, _e_hXY_tsB.length-16, _e_bobSig.length);
    +                    _context.aes().decrypt(_e_bobSig, 0, bobSig, 0, _dh.getSessionKey(),
    +                                           _e_hXY_tsB, HXY_TSB_PAD_SIZE - AES_SIZE, _e_bobSig.length);
                         // ignore the padding
    -                    byte bobSigData[] = new byte[Signature.SIGNATURE_BYTES];
    -                    System.arraycopy(bobSig, 0, bobSigData, 0, Signature.SIGNATURE_BYTES);
    -                    Signature sig = new Signature(bobSigData);
    +                    // handle variable signature size
    +                    SigType type = _con.getRemotePeer().getSigningPublicKey().getType();
    +                    int siglen = type.getSigLen();
    +                    byte bobSigData[] = new byte[siglen];
    +                    System.arraycopy(bobSig, 0, bobSigData, 0, siglen);
    +                    Signature sig = new Signature(type, bobSigData);
     
    -                    byte toVerify[] = new byte[_X.length+_Y.length+Hash.HASH_LENGTH+4+4];
    +                    byte toVerify[] = new byte[XY_SIZE + XY_SIZE + HXY_SIZE +4+4];
                         int voff = 0;
    -                    System.arraycopy(_X, 0, toVerify, voff, _X.length); voff += _X.length;
    -                    System.arraycopy(_Y, 0, toVerify, voff, _Y.length); voff += _Y.length;
    -                    System.arraycopy(_context.routerHash().getData(), 0, toVerify, voff, Hash.HASH_LENGTH); voff += Hash.HASH_LENGTH;
    +                    System.arraycopy(_X, 0, toVerify, voff, XY_SIZE); voff += XY_SIZE;
    +                    System.arraycopy(_Y, 0, toVerify, voff, XY_SIZE); voff += XY_SIZE;
    +                    System.arraycopy(_context.routerHash().getData(), 0, toVerify, voff, HXY_SIZE); voff += HXY_SIZE;
                         DataHelper.toLong(toVerify, voff, 4, _tsA); voff += 4;
                         DataHelper.toLong(toVerify, voff, 4, _tsB); voff += 4;
     
    -                    _verified = _context.dsa().verifySignature(sig, toVerify, _con.getRemotePeer().getSigningPublicKey());
    -                    if (!_verified) {
    +                    boolean ok = _context.dsa().verifySignature(sig, toVerify, _con.getRemotePeer().getSigningPublicKey());
    +                    if (!ok) {
                             _context.statManager().addRateData("ntcp.invalidSignature", 1);
                             fail("Signature was invalid - attempt to spoof " + _con.getRemotePeer().calculateHash().toBase64() + "?");
                         } else {
    +                        _state = State.VERIFIED;
                             if (_log.shouldLog(Log.DEBUG))
                                 _log.debug(prefix() + "signature verified from Bob.  done!");
                             prepareExtra(src);
    -                        byte nextWriteIV[] = new byte[16];
    -                        System.arraycopy(_prevEncrypted, _prevEncrypted.length-16, nextWriteIV, 0, 16);
    -                        byte nextReadIV[] = new byte[16];
    -                        System.arraycopy(_e_bobSig, _e_bobSig.length-16, nextReadIV, 0, nextReadIV.length);
    -                        _con.finishOutboundEstablishment(_dh.getSessionKey(), (_tsA-_tsB), nextWriteIV, nextReadIV); // skew in seconds
    +                        byte nextWriteIV[] = _curEncrypted; // reuse buf
    +                        System.arraycopy(_prevEncrypted, _prevEncrypted.length-AES_SIZE, nextWriteIV, 0, AES_SIZE);
    +                        // this does not copy the nextWriteIV, do not release to cache
    +                        _con.finishOutboundEstablishment(_dh.getSessionKey(), (_tsA-_tsB), nextWriteIV, _e_bobSig); // skew in seconds
    +                        releaseBufs();
                             // if socket gets closed this will be null - prevent NPE
                             InetAddress ia = _con.getChannel().socket().getInetAddress();
                             if (ia != null)
    @@ -544,9 +655,10 @@ class EstablishState {
         }
     
         /** did the handshake fail for some reason? */
    -    public boolean isCorrupt() { return _err != null; }
    +    public synchronized boolean isCorrupt() { return _state == State.CORRUPT; }
    +
         /** @return is the handshake complete and valid? */
    -    public boolean isComplete() { return _verified; }
    +    public synchronized boolean isComplete() { return _state == State.VERIFIED; }
     
         /**
          * We are Alice.
    @@ -554,47 +666,90 @@ class EstablishState {
          * queueing up the write of the first part of the handshake
          * This method sends message #1 to Bob.
          */
    -    public void prepareOutbound() {
    -        if (_received <= 0) {
    +    public synchronized void prepareOutbound() {
    +        if (_state == State.OB_INIT) {
                 if (_log.shouldLog(Log.DEBUG))
    -                _log.debug(prefix() + "write out the first part of our handshake");
    -            byte toWrite[] = new byte[_X.length + _hX_xor_bobIdentHash.length];
    -            System.arraycopy(_X, 0, toWrite, 0, _X.length);
    -            System.arraycopy(_hX_xor_bobIdentHash, 0, toWrite, _X.length, _hX_xor_bobIdentHash.length);
    +                _log.debug(prefix() + "send X");
    +            byte toWrite[] = new byte[XY_SIZE + _hX_xor_bobIdentHash.length];
    +            System.arraycopy(_X, 0, toWrite, 0, XY_SIZE);
    +            System.arraycopy(_hX_xor_bobIdentHash, 0, toWrite, XY_SIZE, _hX_xor_bobIdentHash.length);
    +            _state = State.OB_SENT_X;
                 _transport.getPumper().wantsWrite(_con, toWrite);
             } else {
    -            if (_log.shouldLog(Log.DEBUG))
    -                _log.debug(prefix()+"prepare outbound with received=" + _received);
    +            if (_log.shouldLog(Log.WARN))
    +                _log.warn(prefix() + "unexpected prepareOutbound()");
             }
         }
     
         /**
    -     * We are Bob. Verify message #3 from Alice, then send message #4 to Alice.
    +     * We are Bob. We have received enough of message #3 from Alice
    +     * to get Alice's RouterIdentity.
          *
    -     * Make sure the signatures are correct, and if they are, update the
    -     * NIOConnection with the session key / peer ident / clock skew / iv.
    -     * The NIOConnection itself is responsible for registering with the
    -     * transport
    +     * _aliceIdentSize must be set.
    +     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least 2 + _aliceIdentSize bytes.
    +     *
    +     * Sets _aliceIdent so that we
    +     * may determine the signature and padding sizes.
    +     *
    +     * After all of message #3 is received including the signature and
    +     * padding, verifyIdentity() must be called.
    +     *
    +     *  State must be IB_GOT_RI_SIZE.
    +     *  Caller must synch.
    +     *
    +     * @since 0.9.16 pulled out of verifyInbound()
          */
    -    private void verifyInbound() {
    -        if (_corrupt) return;
    +    private void readAliceRouterIdentity() {
             byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
             //if (_log.shouldLog(Log.DEBUG))
             //    _log.debug(prefix()+"decrypted sz(etc) data: " + Base64.encode(b));
     
             try {
    -            RouterIdentity alice = new RouterIdentity();
    -            int sz = (int)DataHelper.fromLong(b, 0, 2); // TO-DO: Hey zzz... Throws an NPE for me... see below, for my "quick fix", need to find out the real reason
    -            if ( (sz <= 0) || (sz > b.length-2-4-Signature.SIGNATURE_BYTES) ) {
    +            int sz = _aliceIdentSize;
    +            if (sz < MIN_RI_SIZE || sz > MAX_RI_SIZE ||
    +                sz > b.length-2) {
                     _context.statManager().addRateData("ntcp.invalidInboundSize", sz);
                     fail("size is invalid", new Exception("size is " + sz));
                     return;
                 }
    -            byte aliceData[] = new byte[sz];
    -            System.arraycopy(b, 2, aliceData, 0, sz);
    -            alice.fromByteArray(aliceData);
    -            long tsA = DataHelper.fromLong(b, 2+sz, 4);
    +            RouterIdentity alice = new RouterIdentity();
    +            ByteArrayInputStream bais = new ByteArrayInputStream(b, 2, sz);
    +            alice.readBytes(bais);
    +            _aliceIdent = alice;
    +        } catch (IOException ioe) {
    +            _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
    +            fail("Error verifying peer", ioe);
    +        } catch (DataFormatException dfe) {
    +            _context.statManager().addRateData("ntcp.invalidInboundDFE", 1);
    +            fail("Error verifying peer", dfe);
    +        }
    +    }
     
    +
    +    /**
    +     * We are Bob. Verify message #3 from Alice, then send message #4 to Alice.
    +     *
    +     * _aliceIdentSize and _aliceIdent must be set.
    +     * _sz_aliceIdent_tsA_padding_aliceSig must contain at least
    +     *  (2 + _aliceIdentSize + 4 + padding + sig) bytes.
    +     *
    +     * Sets _aliceIdent so that we
    +     *
    +     * readAliceRouterIdentity() must have been called previously
    +     *
    +     * Make sure the signatures are correct, and if they are, update the
    +     * NIOConnection with the session key / peer ident / clock skew / iv.
    +     * The NIOConnection itself is responsible for registering with the
    +     * transport
    +     *
    +     *  State must be IB_GOT_RI.
    +     *  Caller must synch.
    +     */
    +    private void verifyInbound() {
    +        byte b[] = _sz_aliceIdent_tsA_padding_aliceSig.toByteArray();
    +        try {
    +            int sz = _aliceIdentSize;
    +            long tsA = DataHelper.fromLong(b, 2+sz, 4);
                 ByteArrayOutputStream baos = new ByteArrayOutputStream(768);
                 baos.write(_X);
                 baos.write(_Y);
    @@ -604,31 +759,37 @@ class EstablishState {
                 //baos.write(b, 2+sz+4, b.length-2-sz-4-Signature.SIGNATURE_BYTES);
     
                 byte toVerify[] = baos.toByteArray();
    -            if (_log.shouldLog(Log.DEBUG)) {
    -                _log.debug(prefix()+"checking " + Base64.encode(toVerify, 0, 16));
    -                //_log.debug(prefix()+"check pad " + Base64.encode(b, 2+sz+4, 12));
    -            }
    +            //if (_log.shouldLog(Log.DEBUG)) {
    +            //    _log.debug(prefix()+"checking " + Base64.encode(toVerify, 0, AES_SIZE));
    +            //    //_log.debug(prefix()+"check pad " + Base64.encode(b, 2+sz+4, 12));
    +            //}
     
    -            byte s[] = new byte[Signature.SIGNATURE_BYTES];
    +            // handle variable signature size
    +            SigType type = _aliceIdent.getSigningPublicKey().getType();
    +            if (type == null) {
    +                fail("unsupported sig type");
    +                return;
    +            }
    +            byte s[] = new byte[type.getSigLen()];
                 System.arraycopy(b, b.length-s.length, s, 0, s.length);
    -            Signature sig = new Signature(s);
    -            _verified = _context.dsa().verifySignature(sig, toVerify, alice.getSigningPublicKey());
    -            if (_verified) {
    +            Signature sig = new Signature(type, s);
    +            boolean ok = _context.dsa().verifySignature(sig, toVerify, _aliceIdent.getSigningPublicKey());
    +            if (ok) {
                     // get inet-addr
                     InetAddress addr = this._con.getChannel().socket().getInetAddress();
                     byte[] ip = (addr == null) ? null : addr.getAddress();
    -                if (_context.banlist().isBanlistedForever(alice.calculateHash())) {
    +                if (_context.banlist().isBanlistedForever(_aliceIdent.calculateHash())) {
                         if (_log.shouldLog(Log.WARN))
    -                        _log.warn("Dropping inbound connection from permanently banlisted peer: " + alice.calculateHash().toBase64());
    +                        _log.warn("Dropping inbound connection from permanently banlisted peer: " + _aliceIdent.calculateHash());
                         // So next time we will not accept the con from this IP,
                         // rather than doing the whole handshake
                         if(ip != null)
                            _context.blocklist().add(ip);
    -                    fail("Peer is banlisted forever: " + alice.calculateHash().toBase64());
    +                    fail("Peer is banlisted forever: " + _aliceIdent.calculateHash());
                         return;
                     }
                     if(ip != null)
    -                   _transport.setIP(alice.calculateHash(), ip);
    +                   _transport.setIP(_aliceIdent.calculateHash(), ip);
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug(prefix() + "verification successful for " + _con);
     
    @@ -642,10 +803,10 @@ class EstablishState {
                             _log.logAlways(Log.WARN, "NTP failure, NTCP adjusting clock by " + DataHelper.formatDuration(diff));
                     } else if (diff >= Router.CLOCK_FUDGE_FACTOR) {
                         _context.statManager().addRateData("ntcp.invalidInboundSkew", diff);
    -                    _transport.markReachable(alice.calculateHash(), true);
    +                    _transport.markReachable(_aliceIdent.calculateHash(), true);
                         // Only banlist if we know what time it is
                         _context.banlist().banlistRouter(DataHelper.formatDuration(diff),
    -                                                       alice.calculateHash(),
    +                                                       _aliceIdent.calculateHash(),
                                                            _x("Excessive clock skew: {0}"));
                         _transport.setLastBadSkew(tsA- _tsB);
                         fail("Clocks too skewed (" + diff + " ms)", null, true);
    @@ -654,50 +815,60 @@ class EstablishState {
                         _log.debug(prefix()+"Clock skew: " + diff + " ms");
                     }
     
    -                sendInboundConfirm(alice, tsA);
    -                _con.setRemotePeer(alice);
    +                _state = State.VERIFIED;
    +                sendInboundConfirm(_aliceIdent, tsA);
    +                _con.setRemotePeer(_aliceIdent);
                     if (_log.shouldLog(Log.DEBUG))
                         _log.debug(prefix()+"e_bobSig is " + _e_bobSig.length + " bytes long");
    -                byte iv[] = new byte[16];
    -                System.arraycopy(_e_bobSig, _e_bobSig.length-16, iv, 0, 16);
    +                byte iv[] = _curEncrypted;  // reuse buf
    +                System.arraycopy(_e_bobSig, _e_bobSig.length-AES_SIZE, iv, 0, AES_SIZE);
    +                // this does not copy the IV, do not release to cache
                     _con.finishInboundEstablishment(_dh.getSessionKey(), (tsA-_tsB), iv, _prevEncrypted); // skew in seconds
    +                releaseBufs();
                     if (_log.shouldLog(Log.INFO))
    -                    _log.info(prefix()+"Verified remote peer as " + alice.calculateHash().toBase64());
    +                    _log.info(prefix()+"Verified remote peer as " + _aliceIdent.calculateHash());
                 } else {
                     _context.statManager().addRateData("ntcp.invalidInboundSignature", 1);
    -                fail("Peer verification failed - spoof of " + alice.calculateHash().toBase64() + "?");
    +                fail("Peer verification failed - spoof of " + _aliceIdent.calculateHash() + "?");
                 }
             } catch (IOException ioe) {
                 _context.statManager().addRateData("ntcp.invalidInboundIOE", 1);
                 fail("Error verifying peer", ioe);
    -        } catch (DataFormatException dfe) {
    -            _context.statManager().addRateData("ntcp.invalidInboundDFE", 1);
    -            fail("Error verifying peer", dfe);
    -        } catch(NullPointerException npe) {
    -            fail("Error verifying peer", npe); // TO-DO: zzz This is that quick-fix. -- Sponge
             }
         }
     
         /**
          *  We are Bob. Send message #4 to Alice.
    +     *
    +     *  State must be VERIFIED.
    +     *  Caller must synch.
          */
         private void sendInboundConfirm(RouterIdentity alice, long tsA) {
             // send Alice E(S(X+Y+Alice.identHash+tsA+tsB), sk, prev)
    -        byte toSign[] = new byte[256+256+32+4+4];
    +        byte toSign[] = new byte[XY_SIZE + XY_SIZE + 32+4+4];
             int off = 0;
    -        System.arraycopy(_X, 0, toSign, off, 256); off += 256;
    -        System.arraycopy(_Y, 0, toSign, off, 256); off += 256;
    +        System.arraycopy(_X, 0, toSign, off, XY_SIZE); off += XY_SIZE;
    +        System.arraycopy(_Y, 0, toSign, off, XY_SIZE); off += XY_SIZE;
             Hash h = alice.calculateHash();
             System.arraycopy(h.getData(), 0, toSign, off, 32); off += 32;
             DataHelper.toLong(toSign, off, 4, tsA); off += 4;
             DataHelper.toLong(toSign, off, 4, _tsB); off += 4;
     
    +        // handle variable signature size
             Signature sig = _context.dsa().sign(toSign, _context.keyManager().getSigningPrivateKey());
    -        byte preSig[] = new byte[Signature.SIGNATURE_BYTES+8];
    -        System.arraycopy(sig.getData(), 0, preSig, 0, Signature.SIGNATURE_BYTES);
    -        _context.random().nextBytes(preSig, Signature.SIGNATURE_BYTES, 8);
    +        int siglen = sig.length();
    +        int rem = siglen % AES_SIZE;
    +        int padding;
    +        if (rem > 0)
    +            padding = AES_SIZE - rem;
    +        else
    +            padding = 0;
    +        byte preSig[] = new byte[siglen + padding];
    +        System.arraycopy(sig.getData(), 0, preSig, 0, siglen);
    +        if (padding > 0)
    +            _context.random().nextBytes(preSig, siglen, padding);
             _e_bobSig = new byte[preSig.length];
    -        _context.aes().encrypt(preSig, 0, _e_bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, _e_hXY_tsB.length-16, _e_bobSig.length);
    +        _context.aes().encrypt(preSig, 0, _e_bobSig, 0, _dh.getSessionKey(), _e_hXY_tsB, HXY_TSB_PAD_SIZE - AES_SIZE, _e_bobSig.length);
     
             if (_log.shouldLog(Log.DEBUG))
                 _log.debug(prefix() + "Sending encrypted inbound confirmation");
    @@ -708,6 +879,9 @@ class EstablishState {
          *
          *  All data must be copied out of the buffer as Reader.processRead()
          *  will return it to the pool.
    +     *
    +     *  State must be VERIFIED.
    +     *  Caller must synch.
          */
         private void prepareExtra(ByteBuffer buf) {
             int remaining = buf.remaining();
    @@ -724,21 +898,61 @@ class EstablishState {
          * if complete, this will contain any bytes received as part of the
          * handshake that were after the actual handshake.  This may return null.
          */
    -    public byte[] getExtraBytes() { return _extra; }
    +    public synchronized byte[] getExtraBytes() { return _extra; }
     
    +    /**
    +     *  Release resources on timeout.
    +     *  @param e may be null
    +     *  @since 0.9.16
    +     */
    +    public synchronized void close(String reason, Exception e) {
    +        fail(reason, e);
    +    }
    +
    +    /** Caller must synch. */
         private void fail(String reason) { fail(reason, null); }
    +
    +    /** Caller must synch. */
         private void fail(String reason, Exception e) { fail(reason, e, false); }
    +
    +    /** Caller must synch. */
         private void fail(String reason, Exception e, boolean bySkew) {
    -        _corrupt = true;
    +        if (_state == State.CORRUPT || _state == State.VERIFIED)
    +            return;
    +        _state = State.CORRUPT;
             _failedBySkew = bySkew;
             _err = reason;
             _e = e;
             if (_log.shouldLog(Log.WARN))
                 _log.warn(prefix()+"Failed to establish: " + _err, e);
    +        releaseBufs();
         }
     
    -    public String getError() { return _err; }
    -    public Exception getException() { return _e; }
    +    /**
    +     *  Only call once. Caller must synch.
    +     *  @since 0.9.16
    +     */
    +    private void releaseBufs() {
    +        // null or longer for OB
    +        if (_prevEncrypted != null && _prevEncrypted.length == AES_SIZE)
    +            SimpleByteCache.release(_prevEncrypted);
    +        // Do not release _curEncrypted if verified, it is passed to
    +        // NTCPConnection to use as the IV
    +        if (_state != State.VERIFIED)
    +            SimpleByteCache.release(_curEncrypted);
    +        SimpleByteCache.release(_curDecrypted);
    +        SimpleByteCache.release(_hX_xor_bobIdentHash);
    +        if (_dh.getPeerPublicValue() == null)
    +            _transport.returnUnused(_dh);
    +        if (_con.isInbound())
    +            SimpleByteCache.release(_X);
    +        else
    +            SimpleByteCache.release(_Y);
    +    }
    +
    +    public synchronized String getError() { return _err; }
    +
    +    public synchronized Exception getException() { return _e; }
         
         /**
          *  XOR a into b. Modifies b. a is unmodified.
    @@ -757,11 +971,12 @@ class EstablishState {
         @Override
         public String toString() {
             StringBuilder buf = new StringBuilder(64);
    -        buf.append("est").append(System.identityHashCode(this));
    -        if (_con.isInbound()) buf.append(" inbound");
    -        else buf.append(" outbound");
    -        if (_corrupt) buf.append(" corrupt");
    -        if (_verified) buf.append(" verified");
    +        if (_con.isInbound())
    +            buf.append("IBES ");
    +        else
    +            buf.append("OBES ");
    +        buf.append(System.identityHashCode(this));
    +        buf.append(' ').append(_state);
             if (_con.isEstablished()) buf.append(" established");
             buf.append(": ");
             return buf.toString();
    @@ -828,12 +1043,36 @@ class EstablishState {
          *  @since 0.9.8
          */
         private static class VerifiedEstablishState extends EstablishState {
    -        @Override public boolean isComplete() { return true; }
    +
    +        public VerifiedEstablishState() {
    +            super();
    +            _state = State.VERIFIED;
    +        }
    +
             @Override public void prepareOutbound() {
                 Log log =RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
                 log.warn("prepareOutbound() on verified state, doing nothing!");
             }
    -        @Override public String toString() { return "VerfiedEstablishState";}
    +
    +        @Override public String toString() { return "VerifiedEstablishState";}
    +    }
    +
    +    /**
    +     *  @since 0.9.16
    +     */
    +    private static class FailedEstablishState extends EstablishState {
    +
    +        public FailedEstablishState() {
    +            super();
    +            _state = State.CORRUPT;
    +        }
    +
    +        @Override public void prepareOutbound() {
    +            Log log =RouterContext.getCurrentContext().logManager().getLog(VerifiedEstablishState.class);
    +            log.warn("prepareOutbound() on verified state, doing nothing!");
    +        }
    +
    +        @Override public String toString() { return "FailedEstablishState";}
         }
     
         /** @deprecated unused */
    diff --git a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
    index d4fd7521a..b6f539c96 100644
    --- a/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
    +++ b/router/java/src/net/i2p/router/transport/ntcp/EventPumper.java
    @@ -20,8 +20,8 @@ import java.util.concurrent.ConcurrentLinkedQueue;
     import java.util.concurrent.LinkedBlockingQueue;
     
     import net.i2p.I2PAppContext;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterIdentity;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterIdentity;
     import net.i2p.router.CommSystemFacade;
     import net.i2p.router.RouterContext;
     import net.i2p.router.transport.FIFOBandwidthLimiter;
    @@ -532,14 +532,14 @@ class EventPumper implements Runnable {
                     con.outboundConnected();
                     _context.statManager().addRateData("ntcp.connectSuccessful", 1);
                 } else {
    -                con.close();
    +                con.closeOnTimeout("connect failed", null);
                     _transport.markUnreachable(con.getRemotePeer().calculateHash());
                     _context.statManager().addRateData("ntcp.connectFailedTimeout", 1);
                 }
             } catch (IOException ioe) {   // this is the usual failure path for a timeout or connect refused
                 if (_log.shouldLog(Log.INFO))
                     _log.info("Failed outbound " + con, ioe);
    -            con.close();
    +            con.closeOnTimeout("connect failed", ioe);
                 //_context.banlist().banlistRouter(con.getRemotePeer().calculateHash(), "Error connecting", NTCPTransport.STYLE);
                 _transport.markUnreachable(con.getRemotePeer().calculateHash());
                 _context.statManager().addRateData("ntcp.connectFailedTimeoutIOE", 1);
    diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
    index 2b2ae6a44..a9752a286 100644
    --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
    +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPConnection.java
    @@ -17,9 +17,9 @@ import java.util.zip.Adler32;
     import net.i2p.data.Base64;
     import net.i2p.data.ByteArray;
     import net.i2p.data.DataHelper;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterIdentity;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterIdentity;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.SessionKey;
     import net.i2p.data.i2np.DatabaseStoreMessage;
     import net.i2p.data.i2np.I2NPMessage;
    @@ -266,6 +266,8 @@ class NTCPConnection {
         /** 
          * @param clockSkew alice's clock minus bob's clock in seconds (may be negative, obviously, but |val| should
          *                  be under 1 minute)
    +     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt
    +     * @param prevReadEnd 16 or more bytes, last 16 bytes copied
          */
         public void finishInboundEstablishment(SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
             NTCPConnection toClose = locked_finishInboundEstablishment(key, clockSkew, prevWriteEnd, prevReadEnd);
    @@ -278,6 +280,12 @@ class NTCPConnection {
             enqueueInfoMessage();
         }
         
    +    /** 
    +     * @param clockSkew alice's clock minus bob's clock in seconds (may be negative, obviously, but |val| should
    +     *                  be under 1 minute)
    +     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt
    +     * @param prevReadEnd 16 or more bytes, last 16 bytes copied
    +     */
         private synchronized NTCPConnection locked_finishInboundEstablishment(
                 SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
             _sessionKey = key;
    @@ -371,10 +379,21 @@ class NTCPConnection {
             }
         }
         
    +    /**
    +     *  Close and release EstablishState resources.
    +     *  @param e may be null
    +     *  @since 0.9.16
    +     */
    +    public void closeOnTimeout(String cause, Exception e) {
    +        EstablishState es = _establishState;
    +        close();
    +        es.close(cause, e);
    +    }
    +
         private synchronized NTCPConnection locked_close(boolean allowRequeue) {
             if (_chan != null) try { _chan.close(); } catch (IOException ioe) { }
             if (_conKey != null) _conKey.cancel();
    -        _establishState = EstablishState.VERIFIED;
    +        _establishState = EstablishState.FAILED;
             NTCPConnection old = _transport.removeCon(this);
             _transport.getReader().connectionClosed(this);
             _transport.getWriter().connectionClosed(this);
    @@ -571,6 +590,8 @@ class NTCPConnection {
         /** 
          * @param clockSkew alice's clock minus bob's clock in seconds (may be negative, obviously, but |val| should
          *                  be under 1 minute)
    +     * @param prevWriteEnd exactly 16 bytes, not copied, do not corrupt
    +     * @param prevReadEnd 16 or more bytes, last 16 bytes copied
          */
         public synchronized void finishOutboundEstablishment(SessionKey key, long clockSkew, byte prevWriteEnd[], byte prevReadEnd[]) {
             if (_log.shouldLog(Log.DEBUG))
    diff --git a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
    index 0ad8d88a9..f1269c772 100644
    --- a/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
    +++ b/router/java/src/net/i2p/router/transport/ntcp/NTCPTransport.java
    @@ -25,9 +25,9 @@ import java.util.concurrent.ConcurrentHashMap;
     
     import net.i2p.data.DataHelper;
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterIdentity;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterIdentity;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.i2np.DatabaseStoreMessage;
     import net.i2p.data.i2np.I2NPMessage;
     import net.i2p.router.CommSystemFacade;
    @@ -362,6 +362,12 @@ public class NTCPTransport extends TransportImpl {
                 return null;
             }
     
    +        // Check for supported sig type
    +        if (toAddress.getIdentity().getSigningPublicKey().getType() == null) {
    +            markUnreachable(peer);
    +            return null;
    +        }
    +
             if (!allowConnection()) {
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("no bid when trying to send to " + peer + ", max connection limit reached");
    @@ -769,6 +775,17 @@ public class NTCPTransport extends TransportImpl {
             return _dhFactory.getBuilder();
         }
     
    +    /**
    +     * Return an unused DH key builder
    +     * to be put back onto the queue for reuse.
    +     *
    +     * @param builder must not have a peerPublicValue set
    +     * @since 0.9.16
    +     */
    +    void returnUnused(DHSessionKeyBuilder builder) {
    +        _dhFactory.returnUnused(builder);
    +    }
    +
         /**
          * how long from initial connection attempt (accept() or connect()) until
          * the con must be established to avoid premature close()ing
    diff --git a/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java b/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java
    index c8b7c9630..b8c1a5f7e 100644
    --- a/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java
    +++ b/router/java/src/net/i2p/router/transport/udp/ACKBitfield.java
    @@ -5,12 +5,30 @@ package net.i2p.router.transport.udp;
      * received messages
      */
     interface ACKBitfield {
    +
         /** what message is this partially ACKing? */
         public long getMessageId(); 
    +
         /** how many fragments are covered in this bitfield? */
         public int fragmentCount();
    +
         /** has the given fragment been received? */
         public boolean received(int fragmentNum);
    +
         /** has the entire message been received completely? */
         public boolean receivedComplete();
    +
    +    /**
    +     *  Number of fragments acked in this bitfield.
    +     *  Faster than looping through received()
    +     *  @since 0.9.16
    +     */
    +    public int ackCount();
    +
    +    /**
    +     *  Highest fragment number acked in this bitfield.
    +     *  @return highest fragment number acked, or -1 if none
    +     *  @since 0.9.16
    +     */
    +    public int highestReceived();
     }
    diff --git a/router/java/src/net/i2p/router/transport/udp/ACKSender.java b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
    index 7d9355302..b7cb7d6cb 100644
    --- a/router/java/src/net/i2p/router/transport/udp/ACKSender.java
    +++ b/router/java/src/net/i2p/router/transport/udp/ACKSender.java
    @@ -177,7 +177,7 @@ class ACKSender implements Runnable {
                         ack.setMessageType(PacketBuilder.TYPE_ACK);
                         
                         if (_log.shouldLog(Log.INFO))
    -                        _log.info("Sending ACK for " + ackBitfields);
    +                        _log.info("Sending " + ackBitfields + " to " + peer);
                         // locking issues, we ignore the result, and acks are small,
                         // so don't even bother allocating
                         //peer.allocateSendingBytes(ack.getPacket().getLength(), true);
    diff --git a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
    index 267c4fc2a..96c4282c8 100644
    --- a/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
    +++ b/router/java/src/net/i2p/router/transport/udp/EstablishmentManager.java
    @@ -10,9 +10,9 @@ import java.util.Map;
     import java.util.concurrent.ConcurrentHashMap;
     
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterIdentity;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterIdentity;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.SessionKey;
     import net.i2p.data.i2np.DatabaseStoreMessage;
     import net.i2p.data.i2np.DeliveryStatusMessage;
    @@ -1363,6 +1363,7 @@ class EstablishmentManager {
                 _transport.markUnreachable(peer);
                 _transport.dropPeer(peer, false, err);
                 //_context.profileManager().commErrorOccurred(peer);
    +            outboundState.fail();
             } else {
                 OutNetMessage msg;
                 while ((msg = outboundState.getNextQueuedMessage()) != null) {
    diff --git a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
    index abdd59e15..6d1f17755 100644
    --- a/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
    +++ b/router/java/src/net/i2p/router/transport/udp/InboundEstablishState.java
    @@ -5,11 +5,12 @@ import java.io.IOException;
     import java.util.Queue;
     import java.util.concurrent.LinkedBlockingQueue;
     
    +import net.i2p.crypto.SigType;
     import net.i2p.data.Base64;
     import net.i2p.data.ByteArray;
     import net.i2p.data.DataFormatException;
     import net.i2p.data.DataHelper;
    -import net.i2p.data.RouterIdentity;
    +import net.i2p.data.router.RouterIdentity;
     import net.i2p.data.SessionKey;
     import net.i2p.data.Signature;
     import net.i2p.router.OutNetMessage;
    @@ -47,6 +48,9 @@ class InboundEstablishState {
         private long _receivedSignedOnTime;
         private byte _receivedSignature[];
         private boolean _verificationAttempted;
    +    // sig not verified
    +    private RouterIdentity _receivedUnconfirmedIdentity;
    +    // identical to uncomfirmed, but sig now verified
         private RouterIdentity _receivedConfirmedIdentity;
         // general status 
         private final long _establishBegin;
    @@ -295,9 +299,28 @@ class InboundEstablishState {
             
             if (cur == _receivedIdentity.length-1) {
                 _receivedSignedOnTime = conf.readFinalFragmentSignedOnTime();
    -            if (_receivedSignature == null)
    -                _receivedSignature = new byte[Signature.SIGNATURE_BYTES];
    -            conf.readFinalSignature(_receivedSignature, 0);
    +            // TODO verify time to prevent replay attacks
    +            buildIdentity();
    +            if (_receivedUnconfirmedIdentity != null) {
    +                SigType type = _receivedUnconfirmedIdentity.getSigningPublicKey().getType();
    +                if (type != null) {
    +                    int sigLen = type.getSigLen();
    +                    if (_receivedSignature == null)
    +                        _receivedSignature = new byte[sigLen];
    +                    conf.readFinalSignature(_receivedSignature, 0, sigLen);
    +                } else {
    +                    if (_log.shouldLog(Log.WARN))
    +                        _log.warn("Unsupported sig type from: " + toString());
    +                    // _x() in UDPTransport
    +                    _context.banlist().banlistRouterForever(_receivedUnconfirmedIdentity.calculateHash(),
    +                                                            "Unsupported signature type");
    +                    fail();
    +                }
    +            } else {
    +                if (_log.shouldLog(Log.WARN))
    +                    _log.warn("Bad ident from: " + toString());
    +                fail();
    +            }
             }
             
             if ( (_currentState == InboundState.IB_STATE_UNKNOWN) || 
    @@ -318,9 +341,10 @@ class InboundEstablishState {
          */
         private boolean confirmedFullyReceived() {
             if (_receivedIdentity != null) {
    -            for (int i = 0; i < _receivedIdentity.length; i++)
    +            for (int i = 0; i < _receivedIdentity.length; i++) {
                     if (_receivedIdentity[i] == null)
                         return false;
    +            }
                 return true;
             } else {
                 return false;
    @@ -339,7 +363,51 @@ class InboundEstablishState {
             }
             return _receivedConfirmedIdentity;
         }
    -    
    +
    +    /**
    +     *  Construct Alice's RouterIdentity.
    +     *  Must have received all fragments.
    +     *  Sets _receivedUnconfirmedIdentity, unless invalid.
    +     *
    +     *  Caller must synch on this.
    +     *
    +     *  @since 0.9.16 was in verifyIdentity()
    +     */
    +    private void buildIdentity() {
    +        if (_receivedUnconfirmedIdentity != null)
    +            return;   // dup pkt?
    +        int frags = _receivedIdentity.length;
    +        byte[] ident;
    +        if (frags > 1) {
    +            int identSize = 0;
    +            for (int i = 0; i < _receivedIdentity.length; i++)
    +                identSize += _receivedIdentity[i].length;
    +            ident = new byte[identSize];
    +            int off = 0;
    +            for (int i = 0; i < _receivedIdentity.length; i++) {
    +                int len = _receivedIdentity[i].length;
    +                System.arraycopy(_receivedIdentity[i], 0, ident, off, len);
    +                off += len;
    +            }
    +        } else {
    +            // no need to copy
    +            ident = _receivedIdentity[0];
    +        }
    +        ByteArrayInputStream in = new ByteArrayInputStream(ident); 
    +        RouterIdentity peer = new RouterIdentity();
    +        try {
    +            peer.readBytes(in);
    +            _receivedUnconfirmedIdentity = peer;
    +        } catch (DataFormatException dfe) {
    +            if (_log.shouldLog(Log.WARN))
    +                _log.warn("Improperly formatted yet fully received ident", dfe);
    +        } catch (IOException ioe) {
    +            if (_log.shouldLog(Log.WARN))
    +                _log.warn("Improperly formatted yet fully received ident", ioe);
    +        }
    +    }
    +            
    +
         /**
          * Determine if Alice sent us a valid confirmation packet.  The 
          * identity signs: Alice's IP + Alice's port + Bob's IP + Bob's port
    @@ -351,21 +419,11 @@ class InboundEstablishState {
          * Caller must synch on this.
          */
         private void verifyIdentity() {
    -        int identSize = 0;
    -        for (int i = 0; i < _receivedIdentity.length; i++)
    -            identSize += _receivedIdentity[i].length;
    -        byte ident[] = new byte[identSize];
    -        int off = 0;
    -        for (int i = 0; i < _receivedIdentity.length; i++) {
    -            int len = _receivedIdentity[i].length;
    -            System.arraycopy(_receivedIdentity[i], 0, ident, off, len);
    -            off += len;
    -        }
    -        ByteArrayInputStream in = new ByteArrayInputStream(ident); 
    -        RouterIdentity peer = new RouterIdentity();
    -        try {
    -            peer.readBytes(in);
    -            
    +            if (_receivedUnconfirmedIdentity == null)
    +                return;   // either not yet recvd or bad ident
    +            if (_receivedSignature == null)
    +                return;   // either not yet recvd or bad sig
    +
                 byte signed[] = new byte[256+256 // X + Y
                                          + _aliceIP.length + 2
                                          + _bobIP.length + 2
    @@ -373,7 +431,7 @@ class InboundEstablishState {
                                          + 4 // signed on time
                                          ];
     
    -            off = 0;
    +            int off = 0;
                 System.arraycopy(_receivedX, 0, signed, off, _receivedX.length);
                 off += _receivedX.length;
                 getSentY();
    @@ -391,22 +449,15 @@ class InboundEstablishState {
                 off += 4;
                 DataHelper.toLong(signed, off, 4, _receivedSignedOnTime);
                 Signature sig = new Signature(_receivedSignature);
    -            boolean ok = _context.dsa().verifySignature(sig, signed, peer.getSigningPublicKey());
    +            boolean ok = _context.dsa().verifySignature(sig, signed, _receivedUnconfirmedIdentity.getSigningPublicKey());
                 if (ok) {
                     // todo partial spoof detection - get peer.calculateHash(),
                     // lookup in netdb locally, if not equal, fail?
    -                _receivedConfirmedIdentity = peer;
    +                _receivedConfirmedIdentity = _receivedUnconfirmedIdentity;
                 } else {
                     if (_log.shouldLog(Log.WARN))
    -                    _log.warn("Signature failed from " + peer);
    +                    _log.warn("Signature failed from " + _receivedUnconfirmedIdentity);
                 }
    -        } catch (DataFormatException dfe) {
    -            if (_log.shouldLog(Log.WARN))
    -                _log.warn("Improperly formatted yet fully received ident", dfe);
    -        } catch (IOException ioe) {
    -            if (_log.shouldLog(Log.WARN))
    -                _log.warn("Improperly formatted yet fully received ident", ioe);
    -        }
         }
         
         private void packetReceived() {
    diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
    index 78e41112f..f69ecb0c5 100644
    --- a/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
    +++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageFragments.java
    @@ -74,6 +74,21 @@ class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
          * Pull the fragments and ACKs out of the authenticated data packet
          */
         public void receiveData(PeerState from, UDPPacketReader.DataReader data) {
    +        try {
    +            rcvData(from, data);
    +        } catch (DataFormatException dfe) {
    +            if (_log.shouldLog(Log.WARN))
    +                _log.warn("Bad pkt from: " + from, dfe);
    +        } catch (IndexOutOfBoundsException ioobe) {
    +            if (_log.shouldLog(Log.WARN))
    +                _log.warn("Bad pkt from: " + from, ioobe);
    +        }
    +    }
    +
    +    /**
    +     * Pull the fragments and ACKs out of the authenticated data packet
    +     */
    +    private void rcvData(PeerState from, UDPPacketReader.DataReader data) throws DataFormatException {
             //long beforeMsgs = _context.clock().now();
             int fragmentsIncluded = receiveMessages(from, data);
             //long afterMsgs = _context.clock().now();
    @@ -95,7 +110,7 @@ class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
          *
          * @return number of data fragments included
          */
    -    private int receiveMessages(PeerState from, UDPPacketReader.DataReader data) {
    +    private int receiveMessages(PeerState from, UDPPacketReader.DataReader data) throws DataFormatException {
             int fragments = data.readFragmentCount();
             if (fragments <= 0) return fragments;
             Hash fromPeer = from.getRemotePeer();
    @@ -132,11 +147,7 @@ class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
                     boolean isNew = false;
                     state = messages.get(messageId);
                     if (state == null) {
    -                    try {
    -                        state = new InboundMessageState(_context, mid, fromPeer, data, i);
    -                    } catch (DataFormatException dfe) {
    -                        break;
    -                    }
    +                    state = new InboundMessageState(_context, mid, fromPeer, data, i);
                         isNew = true;
                         fragmentOK = true;
                         // we will add to messages shortly if it isn't complete
    @@ -199,7 +210,7 @@ class InboundMessageFragments /*implements UDPTransport.PartialACKSource */{
         /**
          *  @return the number of bitfields in the ack? why?
          */
    -    private int receiveACKs(PeerState from, UDPPacketReader.DataReader data) {
    +    private int receiveACKs(PeerState from, UDPPacketReader.DataReader data) throws DataFormatException {
             int rv = 0;
             boolean newAck = false;
             if (data.readACKsIncluded()) {
    diff --git a/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
    index 4c11aa0f4..fcaae36dd 100644
    --- a/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
    +++ b/router/java/src/net/i2p/router/transport/udp/InboundMessageState.java
    @@ -39,6 +39,9 @@ class InboundMessageState implements CDQEntry {
         private static final long MAX_RECEIVE_TIME = 10*1000;
         public static final int MAX_FRAGMENTS = 64;
         
    +    /** 10 */
    +    public static final int MAX_PARTIAL_BITFIELD_BYTES = (MAX_FRAGMENTS / 7) + 1;
    +
         private static final int MAX_FRAGMENT_SIZE = UDPPacket.MAX_PACKET_SIZE;
         private static final ByteCache _fragmentCache = ByteCache.getInstance(64, MAX_FRAGMENT_SIZE);
         
    @@ -86,7 +89,7 @@ class InboundMessageState implements CDQEntry {
          *
          * @return true if the data was ok, false if it was corrupt
          */
    -    public boolean receiveFragment(UDPPacketReader.DataReader data, int dataFragment) {
    +    public boolean receiveFragment(UDPPacketReader.DataReader data, int dataFragment) throws DataFormatException {
             int fragmentNum = data.readMessageFragmentNum(dataFragment);
             if ( (fragmentNum < 0) || (fragmentNum >= _fragments.length)) {
                 if (_log.shouldLog(Log.WARN))
    @@ -220,57 +223,79 @@ class InboundMessageState implements CDQEntry {
         }
     
         public ACKBitfield createACKBitfield() {
    -        return new PartialBitfield(_messageId, _fragments);
    +        int sz = (_lastFragment >= 0) ? _lastFragment + 1 : _fragments.length;
    +        return new PartialBitfield(_messageId, _fragments, sz);
         }
         
         /**
    -     *  A true partial bitfield that is not complete.
    +     *  A true partial bitfield that is probably not complete.
    +     *  fragmentCount() will return 64 if unknown.
          */
         private static final class PartialBitfield implements ACKBitfield {
             private final long _bitfieldMessageId;
    -        private final boolean _fragmentsReceived[];
    +        private final int _ackCount;
    +        private final int _highestReceived;
    +        // bitfield, 1 for acked
    +        private final long _fragmentAcks;
             
             /**
              *  @param data each element is non-null or null for received or not
    +         *  @param size size of data to use
              */
    -        public PartialBitfield(long messageId, Object data[]) {
    +        public PartialBitfield(long messageId, Object data[], int size) {
    +            if (size > MAX_FRAGMENTS)
    +                throw new IllegalArgumentException();
                 _bitfieldMessageId = messageId;
    -            boolean fragmentsRcvd[] = null;
    -            for (int i = data.length - 1; i >= 0; i--) {
    +            int ackCount = 0;
    +            int highestReceived = -1;
    +            long acks = 0;
    +            for (int i = 0; i < size; i++) {
                     if (data[i] != null) {
    -                    if (fragmentsRcvd == null)
    -                        fragmentsRcvd = new boolean[i+1];
    -                    fragmentsRcvd[i] = true;
    +                    acks |= mask(i);
    +                    ackCount++;
    +                    highestReceived = i;
                     }
                 }
    -            if (fragmentsRcvd == null)
    -                _fragmentsReceived = new boolean[0];
    -            else
    -                _fragmentsReceived = fragmentsRcvd;
    +            _fragmentAcks = acks;
    +            _ackCount = ackCount;
    +            _highestReceived = highestReceived;
             }
     
    -        public int fragmentCount() { return _fragmentsReceived.length; }
    +        /**
    +         *  @param fragment 0-63
    +         */
    +        private static long mask(int fragment) {
    +            return 1L << fragment;
    +        }
    +
    +        public int fragmentCount() { return _highestReceived + 1; }
    +
    +        public int ackCount() { return _ackCount; }
    +
    +        public int highestReceived() { return _highestReceived; }
     
             public long getMessageId() { return _bitfieldMessageId; }
     
             public boolean received(int fragmentNum) { 
    -            if ( (fragmentNum < 0) || (fragmentNum >= _fragmentsReceived.length) )
    +            if (fragmentNum < 0 || fragmentNum > _highestReceived)
                     return false;
    -            return _fragmentsReceived[fragmentNum];
    +            return (_fragmentAcks & mask(fragmentNum)) != 0;
             }
     
    -        /** @return false always */
    -        public boolean receivedComplete() { return false; }
    +        public boolean receivedComplete() { return _ackCount == _highestReceived + 1; }
             
             @Override
             public String toString() { 
                 StringBuilder buf = new StringBuilder(64);
    -            buf.append("Partial ACK of ");
    +            buf.append("OB Partial ACK of ");
                 buf.append(_bitfieldMessageId);
    -            buf.append(" with ACKs for: ");
    -            for (int i = 0; i < _fragmentsReceived.length; i++)
    -                if (_fragmentsReceived[i])
    -                    buf.append(i).append(" ");
    +            buf.append(" highest: ").append(_highestReceived);
    +            buf.append(" with ").append(_ackCount).append(" ACKs for: [");
    +            for (int i = 0; i <= _highestReceived; i++) {
    +                if (received(i))
    +                    buf.append(i).append(' ');
    +            }
    +            buf.append("] / ").append(_highestReceived + 1);
                 return buf.toString();
             }
         }
    diff --git a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
    index 37db45880..7e55cf1ce 100644
    --- a/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
    +++ b/router/java/src/net/i2p/router/transport/udp/IntroductionManager.java
    @@ -11,8 +11,8 @@ import java.util.Set;
     import java.util.concurrent.ConcurrentHashMap;
     
     import net.i2p.data.Base64;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.SessionKey;
     import net.i2p.router.RouterContext;
     import net.i2p.util.Addresses;
    diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
    index ba4375dbd..9f519f5ae 100644
    --- a/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
    +++ b/router/java/src/net/i2p/router/transport/udp/OutboundEstablishState.java
    @@ -3,10 +3,11 @@ package net.i2p.router.transport.udp;
     import java.util.Queue;
     import java.util.concurrent.LinkedBlockingQueue;
     
    +import net.i2p.crypto.SigType;
     import net.i2p.data.Base64;
     import net.i2p.data.ByteArray;
     import net.i2p.data.DataHelper;
    -import net.i2p.data.RouterIdentity;
    +import net.i2p.data.router.RouterIdentity;
     import net.i2p.data.SessionKey;
     import net.i2p.data.Signature;
     import net.i2p.data.i2np.DatabaseStoreMessage;
    @@ -41,6 +42,7 @@ class OutboundEstablishState {
         private SessionKey _sessionKey;
         private SessionKey _macKey;
         private Signature _receivedSignature;
    +    // includes trailing padding to mod 16
         private byte[] _receivedEncryptedSignature;
         private byte[] _receivedIV;
         // SessionConfirmed messages
    @@ -104,6 +106,7 @@ class OutboundEstablishState {
         /**
          *  @param claimedAddress an IP/port based RemoteHostId, or null if unknown
          *  @param remoteHostId non-null, == claimedAddress if direct, or a hash-based one if indirect
    +     *  @param remotePeer must have supported sig type
          *  @param introKey Bob's introduction key, as published in the netdb
          *  @param addr non-null
          */
    @@ -198,14 +201,16 @@ class OutboundEstablishState {
         /** caller must synch - only call once */
         private void prepareSessionRequest() {
             _keyBuilder = _keyFactory.getBuilder();
    -        _sentX = new byte[UDPPacketReader.SessionRequestReader.X_LENGTH];
             byte X[] = _keyBuilder.getMyPublicValue().toByteArray();
    -        if (X.length == 257)
    +        if (X.length == 257) {
    +            _sentX = new byte[256];
                 System.arraycopy(X, 1, _sentX, 0, _sentX.length);
    -        else if (X.length == 256)
    -            System.arraycopy(X, 0, _sentX, 0, _sentX.length);
    -        else
    +        } else if (X.length == 256) {
    +            _sentX = X;
    +        } else {
    +            _sentX = new byte[256];
                 System.arraycopy(X, 0, _sentX, _sentX.length - X.length, X.length);
    +        }
         }
     
         public synchronized byte[] getSentX() {
    @@ -216,7 +221,7 @@ class OutboundEstablishState {
             return _sentX;
         }
     
    -    /**
    +    /**x
          * The remote side (Bob) - note that in some places he's called Charlie.
          * Warning - may change after introduction. May be null before introduction.
          */
    @@ -247,8 +252,20 @@ class OutboundEstablishState {
             _alicePort = reader.readPort();
             _receivedRelayTag = reader.readRelayTag();
             _receivedSignedOnTime = reader.readSignedOnTime();
    -        _receivedEncryptedSignature = new byte[Signature.SIGNATURE_BYTES + 8];
    -        reader.readEncryptedSignature(_receivedEncryptedSignature, 0);
    +        // handle variable signature size
    +        SigType type = _remotePeer.getSigningPublicKey().getType();
    +        if (type == null) {
    +            // shouldn't happen, we only connect to supported peers
    +            fail();
    +            packetReceived();
    +            return;
    +        }
    +        int sigLen = type.getSigLen();
    +        int mod = sigLen % 16;
    +        int pad = (mod == 0) ? 0 : (16 - mod);
    +        int esigLen = sigLen + pad;
    +        _receivedEncryptedSignature = new byte[esigLen];
    +        reader.readEncryptedSignature(_receivedEncryptedSignature, 0, esigLen);
             _receivedIV = new byte[UDPPacket.IV_SIZE];
             reader.readIV(_receivedIV, 0);
             
    @@ -324,6 +341,11 @@ class OutboundEstablishState {
             _receivedEncryptedSignature = null;
             _receivedIV = null;
             _receivedSignature = null;
    +        if (_keyBuilder != null) {
    +            if (_keyBuilder.getPeerPublicValue() == null)
    +                _keyFactory.returnUnused(_keyBuilder);
    +            _keyBuilder = null;
    +        }
             // sure, there's a chance the packet was corrupted, but in practice
             // this means that Bob doesn't know his external port, so give up.
             _currentState = OutboundState.OB_STATE_VALIDATION_FAILED;
    @@ -353,7 +375,9 @@ class OutboundEstablishState {
          * decrypt the signature (and subsequent pad bytes) with the 
          * additional layer of encryption using the negotiated key along side
          * the packet's IV
    +     *
          *  Caller must synch on this.
    +     *  Only call this once! Decrypts in-place.
          */
         private void decryptSignature() {
             if (_receivedEncryptedSignature == null) throw new NullPointerException("encrypted signature is null! this=" + this.toString());
    @@ -361,11 +385,20 @@ class OutboundEstablishState {
             if (_receivedIV == null) throw new NullPointerException("IV is null!");
             _context.aes().decrypt(_receivedEncryptedSignature, 0, _receivedEncryptedSignature, 0, 
                                    _sessionKey, _receivedIV, _receivedEncryptedSignature.length);
    -        byte signatureBytes[] = new byte[Signature.SIGNATURE_BYTES];
    -        System.arraycopy(_receivedEncryptedSignature, 0, signatureBytes, 0, Signature.SIGNATURE_BYTES);
    -        _receivedSignature = new Signature(signatureBytes);
    +        // handle variable signature size
    +        SigType type = _remotePeer.getSigningPublicKey().getType();
    +        // if type == null throws NPE
    +        int sigLen = type.getSigLen();
    +        int mod = sigLen % 16;
    +        if (mod != 0) {
    +            byte signatureBytes[] = new byte[sigLen];
    +            System.arraycopy(_receivedEncryptedSignature, 0, signatureBytes, 0, sigLen);
    +            _receivedSignature = new Signature(type, signatureBytes);
    +        } else {
    +            _receivedSignature = new Signature(type, _receivedEncryptedSignature);
    +        }
             if (_log.shouldLog(Log.DEBUG))
    -            _log.debug("Decrypted received signature: " + Base64.encode(signatureBytes));
    +            _log.debug("Decrypted received signature: " + Base64.encode(_receivedSignature.getData()));
         }
     
         /**
    diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
    index 034f3e605..ab91478f1 100644
    --- a/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
    +++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageFragments.java
    @@ -1,14 +1,19 @@
     package net.i2p.router.transport.udp;
     
    +import java.io.Serializable;
     import java.util.ArrayList;
    +import java.util.Collections;
    +import java.util.Comparator;
     import java.util.Iterator;
     import java.util.List;
     import java.util.Set;
     
    +import net.i2p.data.DataHelper;
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.router.OutNetMessage;
     import net.i2p.router.RouterContext;
    +import net.i2p.router.transport.udp.PacketBuilder.Fragment;
     import net.i2p.util.ConcurrentHashSet;
     import net.i2p.util.Log;
     
    @@ -74,6 +79,7 @@ class OutboundMessageFragments {
             _context.statManager().createRateStat("udp.sendVolleyTime", "Long it takes to send a full volley", "udp", UDPTransport.RATES);
             _context.statManager().createRateStat("udp.sendConfirmTime", "How long it takes to send a message and get the ACK", "udp", UDPTransport.RATES);
             _context.statManager().createRateStat("udp.sendConfirmFragments", "How many fragments are included in a fully ACKed message", "udp", UDPTransport.RATES);
    +        _context.statManager().createRateStat("udp.sendFragmentsPerPacket", "How many fragments are sent in a data packet", "udp", UDPTransport.RATES);
             _context.statManager().createRateStat("udp.sendConfirmVolley", "How many times did fragments need to be sent before ACK", "udp", UDPTransport.RATES);
             _context.statManager().createRateStat("udp.sendFailed", "How many sends a failed message was pushed", "udp", UDPTransport.RATES);
             _context.statManager().createRateStat("udp.sendAggressiveFailed", "How many volleys was a packet sent before we gave up", "udp", UDPTransport.RATES);
    @@ -81,7 +87,7 @@ class OutboundMessageFragments {
             _context.statManager().createRateStat("udp.outboundActivePeers", "How many peers we are actively sending to", "udp", UDPTransport.RATES);
             _context.statManager().createRateStat("udp.sendRejected", "What volley are we on when the peer was throttled", "udp", UDPTransport.RATES);
             _context.statManager().createRateStat("udp.partialACKReceived", "How many fragments were partially ACKed", "udp", UDPTransport.RATES);
    -        _context.statManager().createRateStat("udp.sendSparse", "How many fragments were partially ACKed and hence not resent (time == message lifetime)", "udp", UDPTransport.RATES);
    +        //_context.statManager().createRateStat("udp.sendSparse", "How many fragments were partially ACKed and hence not resent (time == message lifetime)", "udp", UDPTransport.RATES);
             _context.statManager().createRateStat("udp.sendPiggyback", "How many acks were piggybacked on a data packet (time == message lifetime)", "udp", UDPTransport.RATES);
             _context.statManager().createRateStat("udp.sendPiggybackPartial", "How many partial acks were piggybacked on a data packet (time == message lifetime)", "udp", UDPTransport.RATES);
             _context.statManager().createRequiredRateStat("udp.packetsRetransmitted", "Lifetime of packets during retransmission (ms)", "udp", UDPTransport.RATES);
    @@ -236,19 +242,17 @@ class OutboundMessageFragments {
         /**
          * Fetch all the packets for a message volley, blocking until there is a
          * message which can be fully transmitted (or the transport is shut down).
    -     * The returned array may be sparse, with null packets taking the place of
    -     * already ACKed fragments.
          *
          * NOT thread-safe. Called by the PacketPusher thread only.
          *
          * @return null only on shutdown
          */
    -    public UDPPacket[] getNextVolley() {
    +    public List getNextVolley() {
             PeerState peer = null;
    -        OutboundMessageState state = null;
    +        List states = null;
             // Keep track of how many we've looked at, since we don't start the iterator at the beginning.
             int peersProcessed = 0;
    -        while (_alive && (state == null) ) {
    +        while (_alive && (states == null) ) {
                 int nextSendDelay = Integer.MAX_VALUE;
                 // no, not every time - O(n**2) - do just before waiting below
                 //finishMessages();
    @@ -275,8 +279,8 @@ class OutboundMessageFragments {
                                 continue;
                             }
                             peersProcessed++;
    -                        state = peer.allocateSend();
    -                        if (state != null) {
    +                        states = peer.allocateSend();
    +                        if (states != null) {
                                 // we have something to send and we will be returning it
                                 break;
                             } else if (peersProcessed >= _activePeers.size()) {
    @@ -292,13 +296,13 @@ class OutboundMessageFragments {
                             }
                         }
     
    -                    if (peer != null && _log.shouldLog(Log.DEBUG))
    -                        _log.debug("Done looping, next peer we are sending for: " +
    -                                   peer.getRemotePeer());
    +                    //if (peer != null && _log.shouldLog(Log.DEBUG))
    +                    //    _log.debug("Done looping, next peer we are sending for: " +
    +                    //               peer.getRemotePeer());
     
                         // if we've gone all the way through the loop, wait
                         // ... unless nextSendDelay says we have more ready now
    -                    if (state == null && peersProcessed >= _activePeers.size() && nextSendDelay > 0) {
    +                    if (states == null && peersProcessed >= _activePeers.size() && nextSendDelay > 0) {
                             _isWaiting = true;
                             peersProcessed = 0;
                             // why? we do this in the loop one at a time
    @@ -328,9 +332,9 @@ class OutboundMessageFragments {
             } // while alive && state == null
     
             if (_log.shouldLog(Log.DEBUG))
    -            _log.debug("Sending " + state);
    +            _log.debug("Sending " + DataHelper.toString(states));
     
    -        UDPPacket packets[] = preparePackets(state, peer);
    +        List packets = preparePackets(states, peer);
     
           /****
             if ( (state != null) && (state.getMessage() != null) ) {
    @@ -352,58 +356,108 @@ class OutboundMessageFragments {
         /**
          *  @return null if state or peer is null
          */
    -    private UDPPacket[] preparePackets(OutboundMessageState state, PeerState peer) {
    -        if ( (state != null) && (peer != null) ) {
    -            int fragments = state.getFragmentCount();
    -            if (fragments < 0)
    -                return null;
    +    private List preparePackets(List states, PeerState peer) {
    +        if (states == null || peer == null)
    +            return null;
     
    -            // ok, simplest possible thing is to always tack on the bitfields if
    -            List msgIds = peer.getCurrentFullACKs();
    -            int newFullAckCount = msgIds.size();
    -            msgIds.addAll(peer.getCurrentResendACKs());
    -            List partialACKBitfields = new ArrayList();
    -            peer.fetchPartialACKs(partialACKBitfields);
    -            int piggybackedPartialACK = partialACKBitfields.size();
    -            // getCurrentFullACKs() already makes a copy, do we need to copy again?
    -            // YES because buildPacket() now removes them (maybe)
    -            List remaining = new ArrayList(msgIds);
    -            int sparseCount = 0;
    -            UDPPacket rv[] = new UDPPacket[fragments]; //sparse
    +        // ok, simplest possible thing is to always tack on the bitfields if
    +        List msgIds = peer.getCurrentFullACKs();
    +        int newFullAckCount = msgIds.size();
    +        msgIds.addAll(peer.getCurrentResendACKs());
    +        List partialACKBitfields = new ArrayList();
    +        peer.fetchPartialACKs(partialACKBitfields);
    +        int piggybackedPartialACK = partialACKBitfields.size();
    +        // getCurrentFullACKs() already makes a copy, do we need to copy again?
    +        // YES because buildPacket() now removes them (maybe)
    +        List remaining = new ArrayList(msgIds);
    +
    +        // build the list of fragments to send
    +        List toSend = new ArrayList(8);
    +        for (OutboundMessageState state : states) {
    +            int fragments = state.getFragmentCount();
    +            int queued = 0;
                 for (int i = 0; i < fragments; i++) {
                     if (state.needsSending(i)) {
    -                    int before = remaining.size();
    -                    try {
    -                        rv[i] = _builder.buildPacket(state, i, peer, remaining, newFullAckCount, partialACKBitfields);
    -                    } catch (ArrayIndexOutOfBoundsException aioobe) {
    -                        _log.log(Log.CRIT, "Corrupt trying to build a packet - please tell jrandom: " +
    -                                 partialACKBitfields + " / " + remaining + " / " + msgIds);
    -                        sparseCount++;
    -                        continue;
    -                    }
    -                    int after = remaining.size();
    -                    newFullAckCount = Math.max(0, newFullAckCount - (before - after));
    -                    if (rv[i] == null) {
    -                        sparseCount++;
    -                        continue;
    -                    }
    -                    rv[i].setFragmentCount(fragments);
    -                    OutNetMessage msg = state.getMessage();
    -                    if (msg != null)
    -                        rv[i].setMessageType(msg.getMessageTypeId());
    -                    else
    -                        rv[i].setMessageType(-1);
    -                } else {
    -                    sparseCount++;
    +                    toSend.add(new Fragment(state, i));
    +                    queued++;
                     }
                 }
    -            if (sparseCount > 0)
    -                remaining.clear();
    +            // per-state stats
    +            if (queued > 0 && state.getPushCount() > 1) {
    +                peer.messageRetransmitted(queued);
    +                // _packetsRetransmitted += toSend; // lifetime for the transport
    +                _context.statManager().addRateData("udp.peerPacketsRetransmitted", peer.getPacketsRetransmitted(), peer.getPacketsTransmitted());
    +                _context.statManager().addRateData("udp.packetsRetransmitted", state.getLifetime(), peer.getPacketsTransmitted());
    +                if (_log.shouldLog(Log.INFO))
    +                    _log.info("Retransmitting " + state + " to " + peer);
    +                _context.statManager().addRateData("udp.sendVolleyTime", state.getLifetime(), queued);
    +            }
    +        }
    +
    +        if (toSend.isEmpty())
    +            return null;
    +
    +        int fragmentsToSend = toSend.size();
    +        // sort by size, biggest first
    +        // don't bother unless more than one state (fragments are already sorted within a state)
    +        if (fragmentsToSend > 1 && states.size() > 1)
    +            Collections.sort(toSend, new FragmentComparator());
    +
    +        List sendNext = new ArrayList(Math.min(toSend.size(), 4));
    +        List rv = new ArrayList(toSend.size());
    +        for (int i = 0; i < toSend.size(); i++) {
    +            Fragment next = toSend.get(i);
    +            sendNext.add(next);
    +            OutboundMessageState state = next.state;
    +            OutNetMessage msg = state.getMessage();
    +            int msgType = (msg != null) ? msg.getMessageTypeId() : -1;
    +            if (_log.shouldLog(Log.INFO))
    +                _log.info("Building packet for " + next + " to " + peer);
    +            int curTotalDataSize = state.fragmentSize(next.num);
    +            // now stuff in more fragments if they fit
    +            if (i +1 < toSend.size()) {
    +                int maxAvail = PacketBuilder.getMaxAdditionalFragmentSize(peer, sendNext.size(), curTotalDataSize);
    +                for (int j = i + 1; j < toSend.size(); j++) {
    +                    next = toSend.get(j);
    +                    int nextDataSize = next.state.fragmentSize(next.num);
    +                    //if (PacketBuilder.canFitAnotherFragment(peer, sendNext.size(), curTotalDataSize, nextDataSize)) {
    +                    //if (_builder.canFitAnotherFragment(peer, sendNext.size(), curTotalDataSize, nextDataSize)) {
    +                    if (nextDataSize <= maxAvail) {
    +                        // add it
    +                        toSend.remove(j);
    +                        j--;
    +                        sendNext.add(next);
    +                        curTotalDataSize += nextDataSize;
    +                        maxAvail = PacketBuilder.getMaxAdditionalFragmentSize(peer, sendNext.size(), curTotalDataSize);
    +                        if (_log.shouldLog(Log.INFO))
    +                            _log.info("Adding in additional " + next + " to " + peer);
    +                    }  // else too big
    +                }
    +            }
    +
    +            int before = remaining.size();
    +            UDPPacket pkt = _builder.buildPacket(sendNext, peer, remaining, newFullAckCount, partialACKBitfields);
    +            if (pkt != null) {
    +                if (_log.shouldLog(Log.INFO))
    +                    _log.info("Built packet with " + sendNext.size() + " fragments totalling " + curTotalDataSize +
    +                              " data bytes to " + peer);
    +                _context.statManager().addRateData("udp.sendFragmentsPerPacket", sendNext.size());
    +            }
    +            sendNext.clear();
    +            if (pkt == null) {
    +                if (_log.shouldLog(Log.WARN))
    +                    _log.info("Build packet FAIL for " + DataHelper.toString(sendNext) + " to " + peer);
    +                continue;
    +            }
    +            rv.add(pkt);
    +
    +            int after = remaining.size();
    +            newFullAckCount = Math.max(0, newFullAckCount - (before - after));
     
                 int piggybackedAck = 0;
                 if (msgIds.size() != remaining.size()) {
    -                for (int i = 0; i < msgIds.size(); i++) {
    -                    Long id = msgIds.get(i);
    +                for (int j = 0; j < msgIds.size(); j++) {
    +                    Long id = msgIds.get(j);
                         if (!remaining.contains(id)) {
                             peer.removeACKMessage(id);
                             piggybackedAck++;
    @@ -411,29 +465,36 @@ class OutboundMessageFragments {
                     }
                 }
     
    -            if (sparseCount > 0)
    -                _context.statManager().addRateData("udp.sendSparse", sparseCount, state.getLifetime());
                 if (piggybackedAck > 0)
    -                _context.statManager().addRateData("udp.sendPiggyback", piggybackedAck, state.getLifetime());
    +                _context.statManager().addRateData("udp.sendPiggyback", piggybackedAck);
                 if (piggybackedPartialACK - partialACKBitfields.size() > 0)
                     _context.statManager().addRateData("udp.sendPiggybackPartial", piggybackedPartialACK - partialACKBitfields.size(), state.getLifetime());
    -            if (_log.shouldLog(Log.INFO))
    -                _log.info("Building packet for " + state + " to " + peer + " with sparse count: " + sparseCount);
    -            peer.packetsTransmitted(fragments - sparseCount);
    -            if (state.getPushCount() > 1) {
    -                int toSend = fragments-sparseCount;
    -                peer.messageRetransmitted(toSend);
    -                // _packetsRetransmitted += toSend; // lifetime for the transport
    -                _context.statManager().addRateData("udp.peerPacketsRetransmitted", peer.getPacketsRetransmitted(), peer.getPacketsTransmitted());
    -                _context.statManager().addRateData("udp.packetsRetransmitted", state.getLifetime(), peer.getPacketsTransmitted());
    -                if (_log.shouldLog(Log.INFO))
    -                    _log.info("Retransmitting " + state + " to " + peer);
    -                _context.statManager().addRateData("udp.sendVolleyTime", state.getLifetime(), toSend);
    -            }
    -            return rv;
    -        } else {
    -            // !alive
    -            return null;
    +
    +            // following for debugging and stats
    +            pkt.setFragmentCount(sendNext.size());
    +            pkt.setMessageType(msgType);  //type of first fragment
    +        }
    +
    +
    +
    +        int sent = rv.size();
    +        peer.packetsTransmitted(sent);
    +        if (_log.shouldLog(Log.INFO))
    +            _log.info("Sent " + fragmentsToSend + " fragments of " + states.size() +
    +                      " messages in " + sent + " packets to " + peer);
    +
    +        return rv;
    +    }
    +
    +    /**
    +     *  Biggest first
    +     *  @since 0.9.16
    +     */
    +    private static class FragmentComparator implements Comparator, Serializable {
    +
    +        public int compare(Fragment l, Fragment r) {
    +            // reverse
    +            return r.state.fragmentSize(r.num) - l.state.fragmentSize(l.num);
             }
         }
     
    diff --git a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
    index 39746e737..bc625bea4 100644
    --- a/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
    +++ b/router/java/src/net/i2p/router/transport/udp/OutboundMessageState.java
    @@ -4,16 +4,16 @@ import java.util.Date;
     
     import net.i2p.I2PAppContext;
     import net.i2p.data.Base64;
    -import net.i2p.data.ByteArray;
     import net.i2p.data.i2np.I2NPMessage;
     import net.i2p.router.OutNetMessage;
     import net.i2p.router.util.CDPQEntry;
    -import net.i2p.util.ByteCache;
     import net.i2p.util.Log;
     
     /**
      * Maintain the outbound fragmentation for resending, for a single message.
      *
    + * All methods are thread-safe.
    + *
      */
     class OutboundMessageState implements CDPQEntry {
         private final I2PAppContext _context;
    @@ -21,50 +21,32 @@ class OutboundMessageState implements CDPQEntry {
         /** may be null if we are part of the establishment */
         private final OutNetMessage _message;
         private final I2NPMessage _i2npMessage;
    -    private final long _messageId;
         /** will be null, unless we are part of the establishment */
         private final PeerState _peer;
         private final long _expiration;
    -    private ByteArray _messageBuf;
    +    private final byte[] _messageBuf;
         /** fixed fragment size across the message */
    -    private int _fragmentSize;
    -    /** size of the I2NP message */
    -    private int _totalSize;
    -    /** sends[i] is how many times the fragment has been sent, or -1 if ACKed */
    -    private short _fragmentSends[];
    +    private final int _fragmentSize;
    +    /** bitmask, 0 if acked, all 0 = complete */
    +    private long _fragmentAcks;
    +    private final int _numFragments;
         private final long _startedOn;
         private long _nextSendTime;
         private int _pushCount;
    -    private short _maxSends;
    -    // private int _nextSendFragment;
    -    /** for tracking use-after-free bugs */
    -    private boolean _released;
    -    private Exception _releasedBy;
    +    private int _maxSends;
         // we can't use the ones in _message since it is null for injections
         private long _enqueueTime;
         private long _seqNum;
         
         public static final int MAX_MSG_SIZE = 32 * 1024;
    -    private static final int CACHE4_BYTES = MAX_MSG_SIZE;
    -    private static final int CACHE3_BYTES = CACHE4_BYTES / 4;
    -    private static final int CACHE2_BYTES = CACHE3_BYTES / 4;
    -    private static final int CACHE1_BYTES = CACHE2_BYTES / 4;
    -
    -    private static final int CACHE1_MAX = 256;
    -    private static final int CACHE2_MAX = CACHE1_MAX / 4;
    -    private static final int CACHE3_MAX = CACHE2_MAX / 4;
    -    private static final int CACHE4_MAX = CACHE3_MAX / 4;
    -
    -    private static final ByteCache _cache1 = ByteCache.getInstance(CACHE1_MAX, CACHE1_BYTES);
    -    private static final ByteCache _cache2 = ByteCache.getInstance(CACHE2_MAX, CACHE2_BYTES);
    -    private static final ByteCache _cache3 = ByteCache.getInstance(CACHE3_MAX, CACHE3_BYTES);
    -    private static final ByteCache _cache4 = ByteCache.getInstance(CACHE4_MAX, CACHE4_BYTES);
     
         private static final long EXPIRATION = 10*1000;
         
     
         /**
    -     *  Called from UDPTransport
    +     *  "injected" message from the establisher.
    +     *
    +     *  Called from UDPTransport.
          *  @throws IAE if too big or if msg or peer is null
          */
         public OutboundMessageState(I2PAppContext context, I2NPMessage msg, PeerState peer) {
    @@ -72,7 +54,9 @@ class OutboundMessageState implements CDPQEntry {
         }
         
         /**
    -     *  Called from OutboundMessageFragments
    +     *  Normal constructor.
    +     *
    +     *  Called from OutboundMessageFragments.
          *  @throws IAE if too big or if msg or peer is null
          */
         public OutboundMessageState(I2PAppContext context, OutNetMessage m, PeerState peer) {
    @@ -92,160 +76,87 @@ class OutboundMessageState implements CDPQEntry {
             _message = m;
             _i2npMessage = msg;
             _peer = peer;
    -        _messageId = msg.getUniqueId();
             _startedOn = _context.clock().now();
             _nextSendTime = _startedOn;
             _expiration = _startedOn + EXPIRATION;
             //_expiration = msg.getExpiration();
     
    -        //if (_log.shouldLog(Log.DEBUG))
    -        //    _log.debug("Raw byte array for " + _messageId + ": " + Base64.encode(_messageBuf.getData(), 0, len));
    +        // now "fragment" it
    +        int totalSize = _i2npMessage.getRawMessageSize();
    +        if (totalSize > MAX_MSG_SIZE)
    +            throw new IllegalArgumentException("Size too large! " + totalSize);
    +        _messageBuf = new byte[totalSize];
    +        _i2npMessage.toRawByteArray(_messageBuf);
    +        _fragmentSize = _peer.fragmentSize();
    +        int numFragments = totalSize / _fragmentSize;
    +        if (numFragments * _fragmentSize < totalSize)
    +            numFragments++;
    +        // This should never happen, as 534 bytes * 64 fragments > 32KB, and we won't bid on > 32KB
    +        if (numFragments > InboundMessageState.MAX_FRAGMENTS)
    +            throw new IllegalArgumentException("Fragmenting a " + totalSize + " message into " + numFragments + " fragments - too many!");
    +        _numFragments = numFragments;
    +        // all 1's where we care
    +        _fragmentAcks = _numFragments < 64 ? mask(_numFragments) - 1L : -1L;
         }
         
         /**
    -     * lazily inits the message buffer unless already inited
    +     *  @param fragment 0-63
          */
    -    private synchronized void initBuf() {
    -        if (_messageBuf != null)
    -            return;
    -        final int size = _i2npMessage.getRawMessageSize();
    -        acquireBuf(size);
    -        _totalSize = _i2npMessage.toRawByteArray(_messageBuf.getData());
    -        _messageBuf.setValid(_totalSize);
    -    }
    -    
    -    /**
    -     *  @throws IAE if too big
    -     *  @since 0.9.3
    -     */
    -    private void acquireBuf(int size) {
    -        if (_messageBuf != null)
    -            releaseBuf();
    -        if (size <= CACHE1_BYTES)
    -            _messageBuf =  _cache1.acquire();
    -        else if (size <= CACHE2_BYTES)
    -            _messageBuf = _cache2.acquire();
    -        else if (size <= CACHE3_BYTES)
    -            _messageBuf = _cache3.acquire();
    -        else if (size <= CACHE4_BYTES)
    -            _messageBuf = _cache4.acquire();
    -        else
    -            throw new IllegalArgumentException("Size too large! " + size);
    -    }
    -    
    -    /**
    -     *  @since 0.9.3
    -     */
    -    private void releaseBuf() {
    -        if (_messageBuf == null)
    -            return;
    -        int size = _messageBuf.getData().length;
    -        if (size == CACHE1_BYTES)
    -            _cache1.release(_messageBuf);
    -        else if (size == CACHE2_BYTES)
    -            _cache2.release(_messageBuf);
    -        else if (size == CACHE3_BYTES)
    -            _cache3.release(_messageBuf);
    -        else if (size == CACHE4_BYTES)
    -            _cache4.release(_messageBuf);
    -        _messageBuf = null;
    -        _released = true;
    +    private static long mask(int fragment) {
    +        return 1L << fragment;
         }
     
    -    /**
    -     *  This is synchronized with writeFragment(),
    -     *  so we do not release (probably due to an ack) while we are retransmitting.
    -     *  Also prevent double-free
    -     */
    -    public synchronized void releaseResources() { 
    -        if (_messageBuf != null && !_released) {
    -            releaseBuf();
    -            if (_log.shouldLog(Log.WARN))
    -                _releasedBy = new Exception ("Released on " + new Date() + " by:");
    -        }
    -        //_messageBuf = null;
    -    }
    -    
         public OutNetMessage getMessage() { return _message; }
    -    public long getMessageId() { return _messageId; }
    +
    +    public long getMessageId() { return _i2npMessage.getUniqueId(); }
    +
         public PeerState getPeer() { return _peer; }
     
         public boolean isExpired() {
             return _expiration < _context.clock().now(); 
         }
     
    -    public boolean isComplete() {
    -        short sends[] = _fragmentSends;
    -        if (sends == null) return false;
    -        for (int i = 0; i < sends.length; i++)
    -            if (sends[i] >= 0)
    -                return false;
    -        // nothing else pending ack
    -        return true;
    +    public synchronized boolean isComplete() {
    +        return _fragmentAcks == 0;
         }
     
         public synchronized int getUnackedSize() {
    -        short fragmentSends[] = _fragmentSends;
    -        ByteArray messageBuf = _messageBuf;
             int rv = 0;
    -        if ( (messageBuf != null) && (fragmentSends != null) ) {
    -            int lastSize = _totalSize % _fragmentSize;
    -            if (lastSize == 0)
    -                lastSize = _fragmentSize;
    -            for (int i = 0; i < fragmentSends.length; i++) {
    -                if (fragmentSends[i] >= (short)0) {
    -                    if (i + 1 == fragmentSends.length)
    -                        rv += lastSize;
    -                    else
    -                        rv += _fragmentSize;
    -                }
    +        if (isComplete())
    +            return rv;
    +        int lastSize = _messageBuf.length % _fragmentSize;
    +        if (lastSize == 0)
    +            lastSize = _fragmentSize;
    +        for (int i = 0; i < _numFragments; i++) {
    +            if (needsSending(i)) {
    +                if (i + 1 == _numFragments)
    +                    rv += lastSize;
    +                else
    +                    rv += _fragmentSize;
                 }
             }
             return rv;
         }
     
    -    public boolean needsSending(int fragment) {
    -        
    -        short sends[] = _fragmentSends;
    -        if ( (sends == null) || (fragment >= sends.length) || (fragment < 0) )
    -            return false;
    -        return (sends[fragment] >= (short)0);
    +    public synchronized boolean needsSending(int fragment) {
    +        return (_fragmentAcks & mask(fragment)) != 0;
         }
     
         public long getLifetime() { return _context.clock().now() - _startedOn; }
         
         /**
    -     * Ack all the fragments in the ack list.  As a side effect, if there are
    -     * still unacked fragments, the 'next send' time will be updated under the
    -     * assumption that that all of the packets within a volley would reach the
    -     * peer within that ack frequency (2-400ms).
    +     * Ack all the fragments in the ack list.
          *
          * @return true if the message was completely ACKed
          */
    -    public boolean acked(ACKBitfield bitfield) {
    +    public synchronized boolean acked(ACKBitfield bitfield) {
             // stupid brute force, but the cardinality should be trivial
    -        short sends[] = _fragmentSends;
    -        if (sends != null)
    -            for (int i = 0; i < bitfield.fragmentCount() && i < sends.length; i++)
    -                if (bitfield.received(i))
    -                    sends[i] = (short)-1;
    -        
    -        boolean rv = isComplete();
    -      /****
    -        if (!rv && false) { // don't do the fast retransmit... lets give it time to get ACKed
    -            long nextTime = _context.clock().now() + Math.max(_peer.getRTT(), ACKSender.ACK_FREQUENCY);
    -            //_nextSendTime = Math.max(now, _startedOn+PeerState.MIN_RTO);
    -            if (_nextSendTime <= 0)
    -                _nextSendTime = nextTime;
    -            else
    -                _nextSendTime = Math.min(_nextSendTime, nextTime);
    -            
    -            //if (now + 100 > _nextSendTime)
    -            //    _nextSendTime = now + 100;
    -            //_nextSendTime = now;
    +        int highest = bitfield.highestReceived();
    +        for (int i = 0; i <= highest && i < _numFragments; i++) {
    +            if (bitfield.received(i))
    +                _fragmentAcks &= ~mask(i);
             }
    -      ****/
    -        return rv;
    +        return isComplete();
         }
         
         public long getNextSendTime() { return _nextSendTime; }
    @@ -255,105 +166,45 @@ class OutboundMessageState implements CDPQEntry {
          *  The max number of sends for any fragment, which is the
          *  same as the push count, at least as it's coded now.
          */
    -    public int getMaxSends() { return _maxSends; }
    +    public synchronized int getMaxSends() { return _maxSends; }
     
         /**
          *  The number of times we've pushed some fragments, which is the
          *  same as the max sends, at least as it's coded now.
          */
    -    public int getPushCount() { return _pushCount; }
    +    public synchronized int getPushCount() { return _pushCount; }
     
    -    /** note that we have pushed the message fragments */
    -    public void push() { 
    +    /**
    +     * Note that we have pushed the message fragments.
    +     * Increments push count (and max sends... why?)
    +     */
    +    public synchronized void push() { 
             // these will never be different...
             _pushCount++; 
    -        if (_pushCount > _maxSends)
    -            _maxSends = (short)_pushCount;
    -        if (_fragmentSends != null)
    -            for (int i = 0; i < _fragmentSends.length; i++)
    -                if (_fragmentSends[i] >= (short)0)
    -                    _fragmentSends[i] = (short)(1 + _fragmentSends[i]);
    -        
    -    }
    -
    -    /**
    -     * Whether fragment() has been called.
    -     * NOT whether it has more than one fragment.
    -     *
    -     * Caller should synchronize
    -     *
    -     * @return true iff fragment() has been called previously
    -     */
    -    public boolean isFragmented() { return _fragmentSends != null; }
    -
    -    /**
    -     * Prepare the message for fragmented delivery, using no more than
    -     * fragmentSize bytes per fragment.
    -     *
    -     * Caller should synchronize
    -     *
    -     * @throws IllegalStateException if called more than once
    -     */
    -    public void fragment(int fragmentSize) {
    -        if (_fragmentSends != null)
    -            throw new IllegalStateException();
    -        initBuf();
    -        int numFragments = _totalSize / fragmentSize;
    -        if (numFragments * fragmentSize < _totalSize)
    -            numFragments++;
    -        // This should never happen, as 534 bytes * 64 fragments > 32KB, and we won't bid on > 32KB
    -        if (numFragments > InboundMessageState.MAX_FRAGMENTS)
    -            throw new IllegalArgumentException("Fragmenting a " + _totalSize + " message into " + numFragments + " fragments - too many!");
    -        if (_log.shouldLog(Log.DEBUG))
    -            _log.debug("Fragmenting a " + _totalSize + " message into " + numFragments + " fragments");
    -        
    -        //_fragmentEnd = new int[numFragments];
    -        _fragmentSends = new short[numFragments];
    -        //Arrays.fill(_fragmentEnd, -1);
    -        //Arrays.fill(_fragmentSends, (short)0);
    -        
    -        _fragmentSize = fragmentSize;
    +        _maxSends = _pushCount;
         }
     
         /**
          * How many fragments in the message.
    -     * Only valid after fragment() has been called.
    -     * Returns -1 before then.
    -     *
    -     * Caller should synchronize
          */
         public int getFragmentCount() { 
    -        if (_fragmentSends == null) 
    -            return -1;
    -        else
    -            return _fragmentSends.length; 
    +            return _numFragments; 
         }
     
         /**
          * The size of the I2NP message. Does not include any SSU overhead.
    -     *
    -     * Caller should synchronize
          */
    -    public int getMessageSize() { return _totalSize; }
    +    public int getMessageSize() { return _messageBuf.length; }
     
         /**
    -     * Should we continue sending this fragment?
    -     * Only valid after fragment() has been called.
    -     * Throws NPE before then.
    +     * The size in bytes of the fragment
          *
    -     * Caller should synchronize
    -     */
    -    public boolean shouldSend(int fragmentNum) { return _fragmentSends[fragmentNum] >= (short)0; }
    -
    -    /**
    -     * This assumes fragment(int size) has been called
          * @param fragmentNum the number of the fragment 
          * @return the size of the fragment specified by the number
          */
         public int fragmentSize(int fragmentNum) {
    -        if (_messageBuf == null) return -1;
    -        if (fragmentNum + 1 == _fragmentSends.length) {
    -            int valid = _totalSize;
    +        if (fragmentNum + 1 == _numFragments) {
    +            int valid = _messageBuf.length;
                 if (valid <= _fragmentSize)
                     return valid;
                 // bugfix 0.8.12
    @@ -366,63 +217,19 @@ class OutboundMessageState implements CDPQEntry {
     
         /**
          * Write a part of the the message onto the specified buffer.
    -     * See releaseResources() above for synchronization information.
    -     * This assumes fragment(int size) has been called.
          *
          * @param out target to write
          * @param outOffset into outOffset to begin writing
          * @param fragmentNum fragment to write (0 indexed)
          * @return bytesWritten
          */
    -    public synchronized int writeFragment(byte out[], int outOffset, int fragmentNum) {
    -        if (_messageBuf == null) return -1;
    -        if (_released) {
    -            /******
    -                Solved by synchronization with releaseResources() and simply returning -1.
    -                Previous output:
    -
    -                23:50:57.013 ERROR [acket pusher] sport.udp.OutboundMessageState: SSU OMS Use after free
    -                java.lang.Exception: Released on Wed Dec 23 23:50:57 GMT 2009 by:
    -                	at net.i2p.router.transport.udp.OutboundMessageState.releaseResources(OutboundMessageState.java:133)
    -                	at net.i2p.router.transport.udp.PeerState.acked(PeerState.java:1391)
    -                	at net.i2p.router.transport.udp.OutboundMessageFragments.acked(OutboundMessageFragments.java:404)
    -                	at net.i2p.router.transport.udp.InboundMessageFragments.receiveACKs(InboundMessageFragments.java:191)
    -                	at net.i2p.router.transport.udp.InboundMessageFragments.receiveData(InboundMessageFragments.java:77)
    -                	at net.i2p.router.transport.udp.PacketHandler$Handler.handlePacket(PacketHandler.java:485)
    -                	at net.i2p.router.transport.udp.PacketHandler$Handler.receivePacket(PacketHandler.java:282)
    -                	at net.i2p.router.transport.udp.PacketHandler$Handler.handlePacket(PacketHandler.java:231)
    -                	at net.i2p.router.transport.udp.PacketHandler$Handler.run(PacketHandler.java:136)
    -                	at java.lang.Thread.run(Thread.java:619)
    -                	at net.i2p.util.I2PThread.run(I2PThread.java:71)
    -                23:50:57.014 ERROR [acket pusher] ter.transport.udp.PacketPusher: SSU Output Queue Error
    -                java.lang.RuntimeException: SSU OMS Use after free: Message 2381821417 with 4 fragments of size 0 volleys: 2 lifetime: 1258 pending fragments: 0 1 2 3 
    -                	at net.i2p.router.transport.udp.OutboundMessageState.writeFragment(OutboundMessageState.java:298)
    -                	at net.i2p.router.transport.udp.PacketBuilder.buildPacket(PacketBuilder.java:170)
    -                	at net.i2p.router.transport.udp.OutboundMessageFragments.preparePackets(OutboundMessageFragments.java:332)
    -                	at net.i2p.router.transport.udp.OutboundMessageFragments.getNextVolley(OutboundMessageFragments.java:297)
    -                	at net.i2p.router.transport.udp.PacketPusher.run(PacketPusher.java:38)
    -                	at java.lang.Thread.run(Thread.java:619)
    -                	at net.i2p.util.I2PThread.run(I2PThread.java:71)
    -            *******/
    -            if (_log.shouldLog(Log.WARN))
    -                _log.log(Log.WARN, "SSU OMS Use after free: " + toString(), _releasedBy);
    -            return -1;
    -            //throw new RuntimeException("SSU OMS Use after free: " + toString());
    -        }
    +    public int writeFragment(byte out[], int outOffset, int fragmentNum) {
             int start = _fragmentSize * fragmentNum;
    -        int end = start + fragmentSize(fragmentNum);
    -        int toSend = end - start;
    -        byte buf[] = _messageBuf.getData();
    -        if ( (buf != null) && (start + toSend <= buf.length) && (outOffset + toSend <= out.length) ) {
    -            System.arraycopy(buf, start, out, outOffset, toSend);
    -            if (_log.shouldLog(Log.DEBUG))
    -                _log.debug("Raw fragment[" + fragmentNum + "] for " + _messageId 
    -                           + "[" + start + "-" + (start+toSend) + "/" + _totalSize + "/" + _fragmentSize + "]: " 
    -                           + Base64.encode(out, outOffset, toSend));
    +        int toSend = fragmentSize(fragmentNum);
    +        int end = start + toSend;
    +        if (end <= _messageBuf.length && outOffset + toSend <= out.length) {
    +            System.arraycopy(_messageBuf, start, out, outOffset, toSend);
                 return toSend;
    -        } else if (buf == null) {
    -            if (_log.shouldLog(Log.WARN))
    -                _log.warn("Error: null buf");
             } else {
                 if (_log.shouldLog(Log.WARN))
                     _log.warn("Error: " + start + '/' + end + '/' + outOffset + '/' + out.length);
    @@ -452,7 +259,6 @@ class OutboundMessageState implements CDPQEntry {
          */
         public void drop() {
             _peer.getTransport().failed(this, false);
    -        releaseResources();
         }
     
         /**
    @@ -482,19 +288,18 @@ class OutboundMessageState implements CDPQEntry {
     
         @Override
         public String toString() {
    -        short sends[] = _fragmentSends;
             StringBuilder buf = new StringBuilder(256);
    -        buf.append("OB Message ").append(_messageId);
    -        if (sends != null)
    -            buf.append(" with ").append(sends.length).append(" fragments");
    -        buf.append(" of size ").append(_totalSize);
    +        buf.append("OB Message ").append(_i2npMessage.getUniqueId());
    +        buf.append(" with ").append(_numFragments).append(" fragments");
    +        buf.append(" of size ").append(_messageBuf.length);
             buf.append(" volleys: ").append(_maxSends);
             buf.append(" lifetime: ").append(getLifetime());
    -        if (sends != null) {
    +        if (!isComplete()) {
                 buf.append(" pending fragments: ");
    -            for (int i = 0; i < sends.length; i++)
    -                if (sends[i] >= 0)
    +            for (int i = 0; i < _numFragments; i++) {
    +                if (needsSending(i))
                         buf.append(i).append(' ');
    +            }
             }
             return buf.toString();
         }
    diff --git a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
    index 053703cb1..feb2b4240 100644
    --- a/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
    +++ b/router/java/src/net/i2p/router/transport/udp/PacketBuilder.java
    @@ -13,7 +13,7 @@ import net.i2p.I2PAppContext;
     import net.i2p.data.Base64;
     import net.i2p.data.DataHelper;
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterIdentity;
    +import net.i2p.data.router.RouterIdentity;
     import net.i2p.data.SessionKey;
     import net.i2p.data.Signature;
     import net.i2p.util.Addresses;
    @@ -130,8 +130,10 @@ class PacketBuilder {
         /** if no extended options or rekey data, which we don't support  = 37 */
         public static final int HEADER_SIZE = UDPPacket.MAC_SIZE + UDPPacket.IV_SIZE + 1 + 4;
     
    +    /** 4 byte msg ID + 3 byte fragment info */
    +    public static final int FRAGMENT_HEADER_SIZE = 7;
         /** not including acks. 46 */
    -    public static final int DATA_HEADER_SIZE = HEADER_SIZE + 9;
    +    public static final int DATA_HEADER_SIZE = HEADER_SIZE + 2 + FRAGMENT_HEADER_SIZE;
     
         /** IPv4 only */
         public static final int IP_HEADER_SIZE = 20;
    @@ -178,6 +180,49 @@ class PacketBuilder {
         }
     ****/
     
    +    /**
    +     *  Class for passing multiple fragments to buildPacket()
    +     *
    +     *  @since 0.9.16
    +     */
    +    public static class Fragment {
    +        public final OutboundMessageState state;
    +        public final int num;
    +
    +        public Fragment(OutboundMessageState state, int num) {
    +            this.state = state;
    +            this.num = num;
    +        }
    +
    +        @Override
    +        public String toString() {
    +            return "Fragment " + num + " (" + state.fragmentSize(num) + " bytes) of " + state;
    +        }
    +    }
    +
    +    /**
    +     *  Will a packet to 'peer' that already has 'numFragments' fragments
    +     *  totalling 'curDataSize' bytes fit another fragment of size 'newFragSize' ??
    +     *
    +     *  This doesn't leave anything for acks.
    +     *
    +     *  @param numFragments >= 1
    +     *  @since 0.9.16
    +     */
    +    public static int getMaxAdditionalFragmentSize(PeerState peer, int numFragments, int curDataSize) {
    +        int available = peer.getMTU() - curDataSize;
    +        if (peer.isIPv6())
    +            available -= MIN_IPV6_DATA_PACKET_OVERHEAD;
    +        else
    +            available -= MIN_DATA_PACKET_OVERHEAD;
    +        // OVERHEAD above includes 1 * FRAGMENT+HEADER_SIZE;
    +        // this adds for the others, plus the new one.
    +        available -= numFragments * FRAGMENT_HEADER_SIZE;
    +        //if (_log.shouldLog(Log.DEBUG))
    +        //    _log.debug("now: " + numFragments + " / " + curDataSize + " avail: " + available);
    +        return available;
    +    }
    +
         /**
          * This builds a data packet (PAYLOAD_TYPE_DATA).
          * See the methods below for the other message types.
    @@ -231,37 +276,65 @@ class PacketBuilder {
         public UDPPacket buildPacket(OutboundMessageState state, int fragment, PeerState peer,
                                      List ackIdsRemaining, int newAckCount,
                                      List partialACKsRemaining) {
    +        List frags = Collections.singletonList(new Fragment(state, fragment));
    +        return buildPacket(frags, peer, ackIdsRemaining, newAckCount, partialACKsRemaining);
    +    }
    +
    +    /*
    +     *  Multiple fragments
    +     *
    +     *  @since 0.9.16
    +     */
    +    public UDPPacket buildPacket(List fragments, PeerState peer,
    +                                 List ackIdsRemaining, int newAckCount,
    +                                 List partialACKsRemaining) {
    +        StringBuilder msg = null;
    +        if (_log.shouldLog(Log.INFO)) {
    +            msg = new StringBuilder(256);
    +            msg.append("Data pkt to ").append(peer.getRemotePeer().toBase64());
    +        }
    +
    +        // calculate data size
    +        int numFragments = fragments.size();
    +        int dataSize = 0;
    +        for (int i = 0; i < numFragments; i++) {
    +            Fragment frag = fragments.get(i);
    +            OutboundMessageState state = frag.state;
    +            int fragment = frag.num;
    +            int sz = state.fragmentSize(fragment);
    +            dataSize += sz;
    +            if (msg != null) {
    +                msg.append(" Fragment ").append(i);
    +                msg.append(": msg ").append(state.getMessageId()).append(' ').append(fragment);
    +                msg.append('/').append(state.getFragmentCount());
    +                msg.append(' ').append(sz);
    +            }
    +        }
    +        
    +        if (dataSize < 0)
    +            return null;
    +
    +        // calculate size available for acks
    +        int currentMTU = peer.getMTU();
    +        int availableForAcks = currentMTU - dataSize;
    +        int ipHeaderSize;
    +        if (peer.isIPv6()) {
    +            availableForAcks -= MIN_IPV6_DATA_PACKET_OVERHEAD;
    +            ipHeaderSize = IPV6_HEADER_SIZE;
    +        } else {
    +            availableForAcks -= MIN_DATA_PACKET_OVERHEAD;
    +            ipHeaderSize = IP_HEADER_SIZE;
    +        }
    +        if (numFragments > 1)
    +            availableForAcks -= (numFragments - 1) * FRAGMENT_HEADER_SIZE;
    +        int availableForExplicitAcks = availableForAcks;
    +
    +        // make the packet
             UDPPacket packet = buildPacketHeader((byte)(UDPPacket.PAYLOAD_TYPE_DATA << 4));
             DatagramPacket pkt = packet.getPacket();
             byte data[] = pkt.getData();
             int off = HEADER_SIZE;
     
    -        StringBuilder msg = null;
    -        if (_log.shouldLog(Log.INFO)) {
    -            msg = new StringBuilder(128);
    -            msg.append("Data pkt to ").append(peer.getRemotePeer().toBase64());
    -            msg.append(" msg ").append(state.getMessageId()).append(" frag:").append(fragment);
    -            msg.append('/').append(state.getFragmentCount());
    -        }
    -        
    -        int dataSize = state.fragmentSize(fragment);
    -        if (dataSize < 0) {
    -            packet.release();
    -            return null;
    -        }
    -
    -        int currentMTU = peer.getMTU();
    -        int availableForAcks = currentMTU - dataSize;
    -        int ipHeaderSize;
    -        if (peer.getRemoteIP().length == 4) {
    -            availableForAcks -= MIN_DATA_PACKET_OVERHEAD;
    -            ipHeaderSize = IP_HEADER_SIZE;
    -        } else {
    -            availableForAcks -= MIN_IPV6_DATA_PACKET_OVERHEAD;
    -            ipHeaderSize = IPV6_HEADER_SIZE;
    -        }
    -        int availableForExplicitAcks = availableForAcks;
    -
             // ok, now for the body...
             
             // just always ask for an ACK for now...
    @@ -276,7 +349,15 @@ class PacketBuilder {
                         break;  // ack count
                     if (bf.receivedComplete())
                         continue;
    -                int acksz = 4 + (bf.fragmentCount() / 7) + 1;
    +                // only send what we have to
    +                //int acksz = 4 + (bf.fragmentCount() / 7) + 1;
    +                int bits = bf.highestReceived() + 1;
    +                if (bits <= 0)
    +                    continue;
    +                int acksz = bits / 7;
    +                if (bits % 7 > 0)
    +                    acksz++;
    +                acksz += 4;
                     if (partialAcksToSend == 0)
                         acksz++;  // ack count
                     if (availableForExplicitAcks >= acksz) {
    @@ -299,7 +380,7 @@ class PacketBuilder {
             off++;
     
             if (msg != null) {
    -            msg.append(" data: ").append(dataSize).append(" bytes, mtu: ")
    +            msg.append(" Total data: ").append(dataSize).append(" bytes, mtu: ")
                    .append(currentMTU).append(", ")
                    .append(newAckCount).append(" new full acks requested, ")
                    .append(ackIdsRemaining.size() - newAckCount).append(" resend acks requested, ")
    @@ -325,7 +406,7 @@ class PacketBuilder {
                     DataHelper.toLong(data, off, 4, ackId.longValue());
                     off += 4;        
                     if (msg != null) // logging it
    -                    msg.append(" full ack: ").append(ackId.longValue());
    +                    msg.append(' ').append(ackId.longValue());
                 }
                 //acksIncluded = true;
             }
    @@ -341,10 +422,16 @@ class PacketBuilder {
                 for (int i = 0; i < partialAcksToSend && iter.hasNext(); i++) {
                     ACKBitfield bitfield = iter.next();
                     if (bitfield.receivedComplete()) continue;
    +                // only send what we have to
    +                //int bits = bitfield.fragmentCount();
    +                int bits = bitfield.highestReceived() + 1;
    +                if (bits <= 0)
    +                    continue;
    +                int size = bits / 7;
    +                if (bits % 7 > 0)
    +                    size++;
                     DataHelper.toLong(data, off, 4, bitfield.getMessageId());
                     off += 4;
    -                int bits = bitfield.fragmentCount();
    -                int size = (bits / 7) + 1;
                     for (int curByte = 0; curByte < size; curByte++) {
                         if (curByte + 1 < size)
                             data[off] |= (byte)(1 << 7);
    @@ -357,7 +444,7 @@ class PacketBuilder {
                     }
                     iter.remove();
                     if (msg != null) // logging it
    -                    msg.append(" partial ack: ").append(bitfield);
    +                    msg.append(' ').append(bitfield).append(" with ack bytes: ").append(size);
                 }
                 //acksIncluded = true;
                 // now jump back and fill in the number of bitfields *actually* included
    @@ -367,30 +454,42 @@ class PacketBuilder {
             //if ( (msg != null) && (acksIncluded) )
             //  _log.debug(msg.toString());
             
    -        DataHelper.toLong(data, off, 1, 1); // only one fragment in this message
    +        DataHelper.toLong(data, off, 1, numFragments);
             off++;
             
    -        DataHelper.toLong(data, off, 4, state.getMessageId());
    -        off += 4;
    +        // now write each fragment
    +        int sizeWritten = 0;
    +        for (int i = 0; i < numFragments; i++) {
    +            Fragment frag = fragments.get(i);
    +            OutboundMessageState state = frag.state;
    +            int fragment = frag.num;
    +
    +            DataHelper.toLong(data, off, 4, state.getMessageId());
    +            off += 4;
             
    -        data[off] |= fragment << 1;
    -        if (fragment == state.getFragmentCount() - 1)
    -            data[off] |= 1; // isLast
    -        off++;
    +            data[off] |= fragment << 1;
    +            if (fragment == state.getFragmentCount() - 1)
    +                data[off] |= 1; // isLast
    +            off++;
             
    -        DataHelper.toLong(data, off, 2, dataSize);
    -        data[off] &= (byte)0x3F; // 2 highest bits are reserved
    -        off += 2;
    +            int fragSize = state.fragmentSize(fragment);
    +            DataHelper.toLong(data, off, 2, fragSize);
    +            data[off] &= (byte)0x3F; // 2 highest bits are reserved
    +            off += 2;
             
    -        int sizeWritten = state.writeFragment(data, off, fragment);
    +            int sz = state.writeFragment(data, off, fragment);
    +            off += sz;
    +            sizeWritten += sz;
    +        }
    +
             if (sizeWritten != dataSize) {
                 if (sizeWritten < 0) {
                     // probably already freed from OutboundMessageState
                     if (_log.shouldLog(Log.WARN))
    -                    _log.warn("Write failed for fragment " + fragment + " of " + state.getMessageId());
    +                    _log.warn("Write failed for " + DataHelper.toString(fragments));
                 } else {
    -                _log.error("Size written: " + sizeWritten + " but size: " + dataSize 
    -                           + " for fragment " + fragment + " of " + state.getMessageId());
    +                _log.error("Size written: " + sizeWritten + " but size: " + dataSize +
    +                           " for " + DataHelper.toString(fragments));
                 }
                 packet.release();
                 return null;
    @@ -398,31 +497,44 @@ class PacketBuilder {
             //    _log.debug("Size written: " + sizeWritten + " for fragment " + fragment 
             //               + " of " + state.getMessageId());
             }
    +
             // put this after writeFragment() since dataSize will be zero for use-after-free
             if (dataSize == 0) {
                 // OK according to the protocol but if we send it, it's a bug
    -            _log.error("Sending zero-size fragment " + fragment + " of " + state + " for " + peer);
    +            _log.error("Sending zero-size fragment??? for " + DataHelper.toString(fragments));
             }
    -        off += dataSize;
    -
             
             // pad up so we're on the encryption boundary
             off = pad1(data, off);
             off = pad2(data, off, currentMTU - (ipHeaderSize + UDP_HEADER_SIZE));
             pkt.setLength(off);
     
    -        authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
    -        setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
    -        
    -        if (_log.shouldLog(Log.INFO)) {
    +        if (msg != null) {
    +            // verify multi-fragment packet
    +            //if (numFragments > 1) {
    +            //    msg.append("\nDataReader dump\n:");
    +            //    UDPPacketReader reader = new UDPPacketReader(_context);
    +            //    reader.initialize(packet);
    +            //    UDPPacketReader.DataReader dreader = reader.getDataReader();
    +            //    try {
    +            //        msg.append(dreader.toString());
    +            //    } catch (Exception e) {
    +            //        _log.info("blowup, dump follows", e);
    +            //        msg.append('\n');
    +            //        msg.append(net.i2p.util.HexDump.dump(data, 0, off));
    +            //    }
    +            //}
                 msg.append(" pkt size ").append(off + (ipHeaderSize + UDP_HEADER_SIZE));
                 _log.info(msg.toString());
             }
    +
    +        authenticate(packet, peer.getCurrentCipherKey(), peer.getCurrentMACKey());
    +        setTo(packet, peer.getRemoteIPAddress(), peer.getRemotePort());
    +        
             // the packet could have been built before the current mtu got lowered, so
             // compare to LARGE_MTU
             if (off + (ipHeaderSize + UDP_HEADER_SIZE) > PeerState.LARGE_MTU) {
                 _log.error("Size is " + off + " for " + packet +
    -                       " fragment " + fragment +
                            " data size " + dataSize +
                            " pkt size " + (off + (ipHeaderSize + UDP_HEADER_SIZE)) +
                            " MTU " + currentMTU +
    @@ -430,7 +542,7 @@ class PacketBuilder {
                            availableForExplicitAcks + " for full acks " + 
                            explicitToSend + " full acks included " +
                            partialAcksToSend + " partial acks included " +
    -                       " OMS " + state, new Exception());
    +                       " Fragments: " + DataHelper.toString(fragments), new Exception());
             }
             
             return packet;
    @@ -509,11 +621,16 @@ class PacketBuilder {
                 off++;
                 for (int i = 0; i < ackBitfields.size(); i++) {
                     ACKBitfield bitfield = ackBitfields.get(i);
    -                if (bitfield.receivedComplete()) continue;
    +                // no, this will corrupt the packet
    +                //if (bitfield.receivedComplete()) continue;
                     DataHelper.toLong(data, off, 4, bitfield.getMessageId());
                     off += 4;
    -                int bits = bitfield.fragmentCount();
    -                int size = (bits / 7) + 1;
    +                // only send what we have to
    +                //int bits = bitfield.fragmentCount();
    +                int bits = bitfield.highestReceived() + 1;
    +                int size = bits / 7;
    +                if (bits == 0 || bits % 7 > 0)
    +                    size++;
                     for (int curByte = 0; curByte < size; curByte++) {
                         if (curByte + 1 < size)
                             data[off] |= (byte)(1 << 7);
    @@ -526,7 +643,7 @@ class PacketBuilder {
                     }
                     
                     if (msg != null) // logging it
    -                    msg.append(" partial ack: ").append(bitfield);
    +                    msg.append(" partial ack: ").append(bitfield).append(" with ack bytes: ").append(size);
                 }
             }
             
    @@ -596,14 +713,22 @@ class PacketBuilder {
             off += 4;
             DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
             off += 4;
    -        System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
    -        off += Signature.SIGNATURE_BYTES;
    -        // ok, we need another 8 bytes of random padding
    -        // (ok, this only gives us 63 bits, not 64)
    -        long l = _context.random().nextLong();
    -        if (l < 0) l = 0 - l;
    -        DataHelper.toLong(data, off, 8, l);
    -        off += 8;
    +
    +        // handle variable signature size
    +        Signature sig = state.getSentSignature();
    +        int siglen = sig.length();
    +        System.arraycopy(sig.getData(), 0, data, off, siglen);
    +        off += siglen;
    +        // ok, we need another few bytes of random padding
    +        int rem = siglen % 16;
    +        int padding;
    +        if (rem > 0) {
    +            padding = 16 - rem;
    +            _context.random().nextBytes(data, off, padding);
    +            off += padding;
    +        } else {
    +            padding = 0;
    +        }
             
             if (_log.shouldLog(Log.DEBUG)) {
                 StringBuilder buf = new StringBuilder(128);
    @@ -612,9 +737,9 @@ class PacketBuilder {
                 buf.append(" Bob: ").append(Addresses.toString(state.getReceivedOurIP(), externalPort));
                 buf.append(" RelayTag: ").append(state.getSentRelayTag());
                 buf.append(" SignedOn: ").append(state.getSentSignedOnTime());
    -            buf.append(" signature: ").append(Base64.encode(state.getSentSignature().getData()));
    +            buf.append(" signature: ").append(Base64.encode(sig.getData()));
                 buf.append("\nRawCreated: ").append(Base64.encode(data, 0, off)); 
    -            buf.append("\nsignedTime: ").append(Base64.encode(data, off-8-Signature.SIGNATURE_BYTES-4, 4));
    +            buf.append("\nsignedTime: ").append(Base64.encode(data, off - padding - siglen - 4, 4));
                 _log.debug(buf.toString());
             }
             
    @@ -623,7 +748,7 @@ class PacketBuilder {
             byte[] iv = SimpleByteCache.acquire(UDPPacket.IV_SIZE);
             _context.random().nextBytes(iv);
             
    -        int encrWrite = Signature.SIGNATURE_BYTES + 8;
    +        int encrWrite = siglen + padding;
             int sigBegin = off - encrWrite;
             _context.aes().encrypt(data, sigBegin, data, sigBegin, state.getCipherKey(), iv, encrWrite);
             
    @@ -774,8 +899,11 @@ class PacketBuilder {
                 DataHelper.toLong(data, off, 4, state.getSentSignedOnTime());
                 off += 4;
                 
    +            // handle variable signature size
                 // we need to pad this so we're at the encryption boundary
    -            int mod = (off + Signature.SIGNATURE_BYTES) & 0x0f;
    +            Signature sig = state.getSentSignature();
    +            int siglen = sig.length();
    +            int mod = (off + siglen) & 0x0f;
                 if (mod != 0) {
                     int paddingRequired = 16 - mod;
                     // add an arbitrary number of 16byte pad blocks too ???
    @@ -787,8 +915,8 @@ class PacketBuilder {
                 // so trailing non-mod-16 data is ignored. That truncates the sig.
                 
                 // BUG: NPE here if null signature
    -            System.arraycopy(state.getSentSignature().getData(), 0, data, off, Signature.SIGNATURE_BYTES);
    -            off += Signature.SIGNATURE_BYTES;
    +            System.arraycopy(sig.getData(), 0, data, off, siglen);
    +            off += siglen;
             } else {
                 // We never get here (see above)
     
    diff --git a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
    index 8ad73330e..206e9c44f 100644
    --- a/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
    +++ b/router/java/src/net/i2p/router/transport/udp/PacketHandler.java
    @@ -8,6 +8,7 @@ import java.util.concurrent.BlockingQueue;
     import net.i2p.router.Router;
     import net.i2p.router.RouterContext;
     import net.i2p.router.util.CoDelBlockingQueue;
    +import net.i2p.data.DataFormatException;
     import net.i2p.data.DataHelper;
     import net.i2p.util.I2PThread;
     import net.i2p.util.LHMCache;
    @@ -691,33 +692,35 @@ class PacketHandler {
                             state = _establisher.receiveData(outState);
                         if (_log.shouldLog(Log.DEBUG))
                             _log.debug("Received new DATA packet from " + state + ": " + packet);
    +                    UDPPacketReader.DataReader dr = reader.getDataReader();
                         if (state != null) {
    -                        UDPPacketReader.DataReader dr = reader.getDataReader();
                             if (_log.shouldLog(Log.DEBUG)) {
                                 StringBuilder msg = new StringBuilder(512);
                                 msg.append("Receive ").append(System.identityHashCode(packet));
                                 msg.append(" from ").append(state.getRemotePeer().toBase64()).append(" ").append(state.getRemoteHostId());
    -                            for (int i = 0; i < dr.readFragmentCount(); i++) {
    -                                msg.append(" msg ").append(dr.readMessageId(i));
    -                                msg.append(":").append(dr.readMessageFragmentNum(i));
    -                                if (dr.readMessageIsLast(i))
    -                                    msg.append("*");
    -                            }
    +                            try {
    +                                int count = dr.readFragmentCount();
    +                                for (int i = 0; i < count; i++) {
    +                                    msg.append(" msg ").append(dr.readMessageId(i));
    +                                    msg.append(":").append(dr.readMessageFragmentNum(i));
    +                                    if (dr.readMessageIsLast(i))
    +                                        msg.append("*");
    +                                }
    +                            } catch (DataFormatException dfe) {}
                                 msg.append(": ").append(dr.toString());
                                 _log.debug(msg.toString());
                             }
                             //packet.beforeReceiveFragments();
                             _inbound.receiveData(state, dr);
                             _context.statManager().addRateData("udp.receivePacketSize.dataKnown", packet.getPacket().getLength(), packet.getLifetime());
    -                        if (dr.readFragmentCount() <= 0)
    -                            _context.statManager().addRateData("udp.receivePacketSize.dataKnownAck", packet.getPacket().getLength(), packet.getLifetime());
                         } else {
                             // doesn't happen
                             _context.statManager().addRateData("udp.receivePacketSize.dataUnknown", packet.getPacket().getLength(), packet.getLifetime());
    -                        UDPPacketReader.DataReader dr = reader.getDataReader();
    +                    }
    +                    try {
                             if (dr.readFragmentCount() <= 0)
                                 _context.statManager().addRateData("udp.receivePacketSize.dataUnknownAck", packet.getPacket().getLength(), packet.getLifetime());
    -                    }
    +                    } catch (DataFormatException dfe) {}
                         break;
                     case UDPPacket.PAYLOAD_TYPE_TEST:
                         _state = 51;
    diff --git a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
    index db7fb4ea3..f28ee9645 100644
    --- a/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
    +++ b/router/java/src/net/i2p/router/transport/udp/PacketPusher.java
    @@ -37,11 +37,10 @@ class PacketPusher implements Runnable {
         public void run() {
             while (_alive) {
                 try {
    -                UDPPacket packets[] = _fragments.getNextVolley();
    +                List packets = _fragments.getNextVolley();
                     if (packets != null) {
    -                    for (int i = 0; i < packets.length; i++) {
    -                        if (packets[i] != null) // null for ACKed fragments
    -                            send(packets[i]);
    +                    for (int i = 0; i < packets.size(); i++) {
    +                         send(packets.get(i));
                         }
                     }
                 } catch (Exception e) {
    diff --git a/router/java/src/net/i2p/router/transport/udp/PeerState.java b/router/java/src/net/i2p/router/transport/udp/PeerState.java
    index 06a440ada..22a92f22e 100644
    --- a/router/java/src/net/i2p/router/transport/udp/PeerState.java
    +++ b/router/java/src/net/i2p/router/transport/udp/PeerState.java
    @@ -242,6 +242,9 @@ class PeerState {
         private static final int MINIMUM_WINDOW_BYTES = DEFAULT_SEND_WINDOW_BYTES;
         private static final int MAX_SEND_WINDOW_BYTES = 1024*1024;
     
    +    /** max number of msgs returned from allocateSend() */
    +    private static final int MAX_ALLOCATE_SEND = 2;
    +
         /**
          *  Was 32 before 0.9.2, but since the streaming lib goes up to 128,
          *  we would just drop our own msgs right away during slow start.
    @@ -1032,7 +1035,7 @@ class PeerState {
          *            no full bitfields are included.
          */
         void fetchPartialACKs(List rv) {
    -        InboundMessageState states[] = null;
    +        List states = null;
             int curState = 0;
             synchronized (_inboundMessages) {
                 int numMessages = _inboundMessages.size();
    @@ -1049,17 +1052,17 @@ class PeerState {
                     } else {
                         if (!state.isComplete()) {
                             if (states == null)
    -                            states = new InboundMessageState[numMessages];
    -                        states[curState++] = state;
    +                            states = new ArrayList(numMessages);
    +                        states.add(state);
                         }
                     }
                 }
             }
             if (states != null) {
    -            // _inboundMessages is a Map (unordered), so why bother going backwards?
    -            for (int i = curState-1; i >= 0; i--) {
    -                if (states[i] != null)
    -                    rv.add(states[i].createACKBitfield());
    +            for (InboundMessageState ims : states) {
    +                ACKBitfield abf = ims.createACKBitfield();
    +                if (!abf.receivedComplete())
    +                    rv.add(abf);
                 }
             }
         }
    @@ -1072,7 +1075,9 @@ class PeerState {
     
             public FullACKBitfield(long id) { _msgId = id; }
     
    -        public int fragmentCount() { return 0; }
    +        public int fragmentCount() { return 1; }
    +        public int ackCount() { return 1; }
    +        public int highestReceived() { return 0; }
             public long getMessageId() { return _msgId; }
             public boolean received(int fragmentNum) { return true; }
             public boolean receivedComplete() { return true; }
    @@ -1084,7 +1089,7 @@ class PeerState {
                 return _msgId == ((ACKBitfield)o).getMessageId();
             }
             @Override
    -        public String toString() { return "Full ACK of " + _msgId; }
    +        public String toString() { return "Full ACK " + _msgId; }
         }
             
         /**
    @@ -1538,7 +1543,6 @@ class PeerState {
             for (int i = 0; succeeded != null && i < succeeded.size(); i++) {
                 OutboundMessageState state = succeeded.get(i);
                 _transport.succeeded(state);
    -            state.releaseResources();
                 OutNetMessage msg = state.getMessage();
                 if (msg != null)
                     msg.timestamp("sending complete");
    @@ -1556,22 +1560,22 @@ class PeerState {
                     if (_log.shouldLog(Log.WARN))
                         _log.warn("Unable to send a direct message: " + state);
                 }
    -            state.releaseResources();
             }
             
             return rv + _outboundQueue.size();
         }
         
         /**
    -     * Pick a message we want to send and allocate it out of our window
    +     * Pick one or more messages we want to send and allocate them out of our window
          * High usage -
          * OutboundMessageFragments.getNextVolley() calls this 2nd, if finishMessages() returned > 0.
          * TODO combine finishMessages(), allocateSend(), and getNextDelay() so we don't iterate 3 times.
          *
    -     * @return allocated message to send, or null if no messages or no resources
    +     * @return allocated messages to send (never empty), or null if no messages or no resources
          */
    -    public OutboundMessageState allocateSend() {
    +    public List allocateSend() {
             if (_dead) return null;
    +        List rv = null;
             synchronized (_outboundMessages) {
                 for (OutboundMessageState state : _outboundMessages) {
                     // We have 3 return values, because if allocateSendingBytes() returns false,
    @@ -1588,44 +1592,54 @@ class PeerState {
                                 msg.timestamp("not reached for allocation " + msgs.size() + " other peers");
                         }
                          */
    -                    return state;
    +                    if (rv == null)
    +                        rv = new ArrayList(MAX_ALLOCATE_SEND);
    +                    rv.add(state);
    +                    if (rv.size() >= MAX_ALLOCATE_SEND)
    +                        return rv;
                     } else if (should == ShouldSend.NO_BW) {
                         // no more bandwidth available
                         // we don't bother looking for a smaller msg that would fit.
                         // By not looking further, we keep strict sending order, and that allows
                         // some efficiency in acked() below.
    -                    if (_log.shouldLog(Log.DEBUG))
    +                    if (rv == null && _log.shouldLog(Log.DEBUG))
                             _log.debug("Nothing to send (BW) to " + _remotePeer + ", with " + _outboundMessages.size() +
                                        " / " + _outboundQueue.size() + " remaining");
    -                    return null;
    +                    return rv;
                     } /* else {
                         OutNetMessage msg = state.getMessage();
                         if (msg != null)
                             msg.timestamp("passed over for allocation with " + msgs.size() + " peers");
                     } */
                 }
    +
                 // Peek at head of _outboundQueue and see if we can send it.
                 // If so, pull it off, put it in _outbundMessages, test
                 // again for bandwidth if necessary, and return it.
    -            OutboundMessageState state = _outboundQueue.peek();
    -            if (state != null && ShouldSend.YES == locked_shouldSend(state)) {
    +            OutboundMessageState state;
    +            while ((state = _outboundQueue.peek()) != null &&
    +                   ShouldSend.YES == locked_shouldSend(state)) {
                     // we could get a different state, or null, when we poll,
                     // due to AQM drops, so we test again if necessary
                     OutboundMessageState dequeuedState = _outboundQueue.poll();
                     if (dequeuedState != null) {
                         _outboundMessages.add(dequeuedState);
    -                    if (dequeuedState == state || ShouldSend.YES == locked_shouldSend(dequeuedState)) {
    +                    if (dequeuedState == state || ShouldSend.YES == locked_shouldSend(state)) {
                             if (_log.shouldLog(Log.DEBUG))
                                 _log.debug("Allocate sending (NEW) to " + _remotePeer + ": " + dequeuedState.getMessageId());
    -                        return dequeuedState;
    +                        if (rv == null)
    +                            rv = new ArrayList(MAX_ALLOCATE_SEND);
    +                        rv.add(state);
    +                        if (rv.size() >= MAX_ALLOCATE_SEND)
    +                            return rv;
                         }
                     }
                 }
             }
    -        if (_log.shouldLog(Log.DEBUG))
    +        if ( rv == null && _log.shouldLog(Log.DEBUG))
                 _log.debug("Nothing to send to " + _remotePeer + ", with " + _outboundMessages.size() +
                            " / " + _outboundQueue.size() + " remaining");
    -        return null;
    +        return rv;
         }
         
         /**
    @@ -1694,9 +1708,9 @@ class PeerState {
          *  how much payload data can we shove in there?
          *  @return MTU - 87, i.e. 533 or 1397 (IPv4), MTU - 107 (IPv6)
          */
    -    private int fragmentSize() {
    +    public int fragmentSize() {
             // 46 + 20 + 8 + 13 = 74 + 13 = 87 (IPv4)
    -        // 46 + 40 + 8 + 13 = 74 + 13 = 107 (IPv6)
    +        // 46 + 40 + 8 + 13 = 94 + 13 = 107 (IPv6)
             return _mtu -
                    (_remoteIP.length == 4 ? PacketBuilder.MIN_DATA_PACKET_OVERHEAD : PacketBuilder.MIN_IPV6_DATA_PACKET_OVERHEAD) -
                    MIN_ACK_SIZE;
    @@ -1713,16 +1727,6 @@ class PeerState {
         private ShouldSend locked_shouldSend(OutboundMessageState state) {
             long now = _context.clock().now();
             if (state.getNextSendTime() <= now) {
    -            if (!state.isFragmented()) {
    -                state.fragment(fragmentSize());
    -                if (state.getMessage() != null)
    -                    state.getMessage().timestamp("fragment into " + state.getFragmentCount());
    -
    -                if (_log.shouldLog(Log.INFO))
    -                    _log.info("Fragmenting " + state);
    -            }
    -
    -            
                 OutboundMessageState retrans = _retransmitter;
                 if ( (retrans != null) && ( (retrans.isExpired() || retrans.isComplete()) ) ) {
                     _retransmitter = null;
    @@ -1844,7 +1848,6 @@ class PeerState {
                 //if (getSendWindowBytesRemaining() > 0)
                 //    _throttle.unchoke(peer.getRemotePeer());
                 
    -            state.releaseResources();
             } else {
                 // dupack, likely
                 //if (_log.shouldLog(Log.DEBUG))
    @@ -1894,12 +1897,7 @@ class PeerState {
             if (state != null) {
                 int numSends = state.getMaxSends();
                             
    -            int bits = bitfield.fragmentCount();
    -            int numACKed = 0;
    -            for (int i = 0; i < bits; i++)
    -                if (bitfield.received(i))
    -                    numACKed++;
    -            
    +            int numACKed = bitfield.ackCount();
                 _context.statManager().addRateData("udp.partialACKReceived", numACKed);
                 
                 if (_log.shouldLog(Log.INFO))
    @@ -1921,7 +1919,6 @@ class PeerState {
                     //if (state.getPeer().getSendWindowBytesRemaining() > 0)
                     //    _throttle.unchoke(state.getPeer().getRemotePeer());
     
    -                state.releaseResources();
                 } else {
                     //if (state.getMessage() != null)
                     //    state.getMessage().timestamp("partial ack after " + numSends + ": " + bitfield.toString());
    diff --git a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
    index 48a5e0822..bc47fba87 100644
    --- a/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
    +++ b/router/java/src/net/i2p/router/transport/udp/PeerTestManager.java
    @@ -9,8 +9,8 @@ import java.util.concurrent.LinkedBlockingQueue;
     
     import net.i2p.data.Base64;
     import net.i2p.data.DataHelper;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.SessionKey;
     import net.i2p.router.CommSystemFacade;
     import net.i2p.router.RouterContext;
    diff --git a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
    index f05205582..88551d588 100644
    --- a/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
    +++ b/router/java/src/net/i2p/router/transport/udp/UDPAddress.java
    @@ -5,7 +5,7 @@ import java.net.UnknownHostException;
     import java.util.Map;
     
     import net.i2p.data.Base64;
    -import net.i2p.data.RouterAddress;
    +import net.i2p.data.router.RouterAddress;
     import net.i2p.data.SessionKey;
     import net.i2p.util.LHMCache;
     import net.i2p.util.SystemVersion;
    diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
    index 252490cfc..1f5f65a6a 100644
    --- a/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
    +++ b/router/java/src/net/i2p/router/transport/udp/UDPPacket.java
    @@ -166,7 +166,11 @@ class UDPPacket implements CDQEntry {
         int getMessageType() { return _messageType; }
         /** only for debugging and stats, does not go on the wire */
         void setMessageType(int type) { _messageType = type; }
    +
    +    /** only for debugging and stats */
         int getFragmentCount() { return _fragmentCount; }
    +
    +    /** only for debugging and stats */
         void setFragmentCount(int count) { _fragmentCount = count; }
     
         RemoteHostId getRemoteHost() {
    diff --git a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
    index affaf09df..1f61cb197 100644
    --- a/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
    +++ b/router/java/src/net/i2p/router/transport/udp/UDPPacketReader.java
    @@ -2,6 +2,7 @@ package net.i2p.router.transport.udp;
     
     import net.i2p.I2PAppContext;
     import net.i2p.data.Base64;
    +import net.i2p.data.DataFormatException;
     import net.i2p.data.DataHelper;
     import net.i2p.data.SessionKey;
     import net.i2p.data.Signature;
    @@ -12,6 +13,9 @@ import net.i2p.util.Log;
      * the appropriate fields.  If the interesting bits are in message specific
      * elements, grab the appropriate subreader.
      *
    + * Many of the methods here and in the subclasses will throw AIOOBE on
    + * malformed packets, that should be caught also.
    + *
      */
     class UDPPacketReader {
         private final I2PAppContext _context;
    @@ -203,9 +207,10 @@ class UDPPacketReader {
                 return rv;
             }
             
    -        public void readEncryptedSignature(byte target[], int targetOffset) {
    +        /** @param size the amount to be copied, including padding to mod 16 */
    +        public void readEncryptedSignature(byte target[], int targetOffset, int size) {
                 int offset = readBodyOffset() + Y_LENGTH + 1 + readIPSize() + 2 + 4 + 4;
    -            System.arraycopy(_message, offset, target, targetOffset, Signature.SIGNATURE_BYTES + 8);
    +            System.arraycopy(_message, offset, target, targetOffset, size);
             }
             
             public void readIV(byte target[], int targetOffset) {
    @@ -239,7 +244,11 @@ class UDPPacketReader {
                 System.arraycopy(_message, readOffset, target, targetOffset, len);
             }
             
    -        /** read the time at which the signature was generated */
    +        /**
    +         *  Read the time at which the signature was generated.
    +         *  TODO must be completely in final fragment.
    +         *  Time and sig cannot be split across fragments.
    +         */
             public long readFinalFragmentSignedOnTime() {
                 if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
                     throw new IllegalStateException("This is not the final fragment");
    @@ -247,12 +256,19 @@ class UDPPacketReader {
                 return DataHelper.fromLong(_message, readOffset, 4);
             }
             
    -        /** read the signature from the final sessionConfirmed packet */
    -        public void readFinalSignature(byte target[], int targetOffset) {
    +        /**
    +         *  Read the signature from the final sessionConfirmed packet.
    +         *  TODO must be completely in final fragment.
    +         *  Time and sig cannot be split across fragments.
    +         *  @param size not including padding
    +         */
    +        public void readFinalSignature(byte target[], int targetOffset, int size) {
                 if (readCurrentFragmentNum() != readTotalFragmentNum()-1)
                     throw new IllegalStateException("This is not the final fragment");
    -            int readOffset = _payloadBeginOffset + _payloadLength - Signature.SIGNATURE_BYTES;
    -            System.arraycopy(_message, readOffset, target, targetOffset, Signature.SIGNATURE_BYTES);
    +            int readOffset = _payloadBeginOffset + _payloadLength - size;
    +            if (readOffset < readBodyOffset() + (1 + 2 + 4))
    +                throw new IllegalStateException("Sig split across fragments");
    +            System.arraycopy(_message, readOffset, target, targetOffset, size);
             }
         }
         
    @@ -267,26 +283,33 @@ class UDPPacketReader {
             public boolean readACKsIncluded() {
                 return flagSet(UDPPacket.DATA_FLAG_EXPLICIT_ACK);
             }
    +
             public boolean readACKBitfieldsIncluded() {
                 return flagSet(UDPPacket.DATA_FLAG_ACK_BITFIELDS);
             }
    +
             public boolean readECN() {
                 return flagSet(UDPPacket.DATA_FLAG_ECN);
             }
    +
             public boolean readWantPreviousACKs() {
                 return flagSet(UDPPacket.DATA_FLAG_WANT_ACKS);
             }
    +
             public boolean readReplyRequested() { 
                 return flagSet(UDPPacket.DATA_FLAG_WANT_REPLY);
             }
    +
             public boolean readExtendedDataIncluded() {
                 return flagSet(UDPPacket.DATA_FLAG_EXTENDED);
             }
    +
             public int readACKCount() {
                 if (!readACKsIncluded()) return 0;
                 int off = readBodyOffset() + 1;
                 return (int)DataHelper.fromLong(_message, off, 1);
             }
    +
             public long readACK(int index) {
                 if (!readACKsIncluded()) return -1;
                 int off = readBodyOffset() + 1;
    @@ -294,7 +317,8 @@ class UDPPacketReader {
                 off++;
                 return DataHelper.fromLong(_message, off + (4 * index), 4);
             }
    -        public ACKBitfield[] readACKBitfields() {
    +
    +        public ACKBitfield[] readACKBitfields() throws DataFormatException {
                 if (!readACKBitfieldsIncluded()) return null;
                 int off = readBodyOffset() + 1;
                 if (readACKsIncluded()) {
    @@ -314,7 +338,7 @@ class UDPPacketReader {
                 return rv;
             }
             
    -        public int readFragmentCount() {
    +        public int readFragmentCount() throws DataFormatException {
                 int off = readBodyOffset() + 1;
                 if (readACKsIncluded()) {
                     int numACKs = (int)DataHelper.fromLong(_message, off, 1);
    @@ -338,38 +362,39 @@ class UDPPacketReader {
                 return _message[off];
             }
             
    -        public long readMessageId(int fragmentNum) {
    +        public long readMessageId(int fragmentNum) throws DataFormatException {
                 int fragmentBegin = getFragmentBegin(fragmentNum);
                 return DataHelper.fromLong(_message, fragmentBegin, 4);
             }
    -        public int readMessageFragmentNum(int fragmentNum) {
    +
    +        public int readMessageFragmentNum(int fragmentNum) throws DataFormatException {
                 int off = getFragmentBegin(fragmentNum);
                 off += 4; // messageId
                 return (_message[off] & 0xFF) >>> 1;
             }
    -        public boolean readMessageIsLast(int fragmentNum) {
    +
    +        public boolean readMessageIsLast(int fragmentNum) throws DataFormatException {
                 int off = getFragmentBegin(fragmentNum);
                 off += 4; // messageId
                 return ((_message[off] & 1) != 0);
             }
    -        public int readMessageFragmentSize(int fragmentNum) {
    +
    +        public int readMessageFragmentSize(int fragmentNum) throws DataFormatException {
                 int off = getFragmentBegin(fragmentNum);
    -            off += 4; // messageId
    -            off++; // fragment info
    +            off += 5; // messageId + fragment info
                 return ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
             }
     
             public void readMessageFragment(int fragmentNum, byte target[], int targetOffset)
    -                                                      throws ArrayIndexOutOfBoundsException {
    +                                                      throws DataFormatException {
                 int off = getFragmentBegin(fragmentNum);
    -            off += 4; // messageId
    -            off++; // fragment info
    +            off += 5; // messageId + fragment info
                 int size = ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
                 off += 2;
                 System.arraycopy(_message, off, target, targetOffset, size);
             }
             
    -        private int getFragmentBegin(int fragmentNum) {
    +        private int getFragmentBegin(int fragmentNum) throws DataFormatException {
                 int off = readBodyOffset() + 1;
                 if (readACKsIncluded()) {
                     int numACKs = (int)DataHelper.fromLong(_message, off, 1);
    @@ -393,16 +418,14 @@ class UDPPacketReader {
                 }
                 off++; // # fragments
                 
    -            if (fragmentNum == 0) {
    -                return off;
    -            } else {
    +            if (fragmentNum > 0) {
                     for (int i = 0; i < fragmentNum; i++) {
                         off += 5; // messageId+info
                         off += ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
                         off += 2;
                     }
    -                return off;
                 }
    +            return off;
             }
     
             private boolean flagSet(byte flag) {
    @@ -433,10 +456,15 @@ class UDPPacketReader {
                     off++;
                     buf.append("with partial ACKs for ");
     
    -                for (int i = 0; i < numBitfields; i++) {
    -                    PacketACKBitfield bf = new PacketACKBitfield(off);
    -                    buf.append(bf.getMessageId()).append(' ');
    -                    off += bf.getByteLength();
    +                try {
    +                    for (int i = 0; i < numBitfields; i++) {
    +                        PacketACKBitfield bf = new PacketACKBitfield(off);
    +                        buf.append(bf.getMessageId()).append(' ');
    +                        off += bf.getByteLength();
    +                    }
    +                } catch (DataFormatException dfe) {
    +                    buf.append("CORRUPT");
    +                    return buf.toString();
                     }
                 }
                 if (readExtendedDataIncluded()) {
    @@ -465,16 +493,15 @@ class UDPPacketReader {
                     buf.append(" isLast? ").append(isLast);
                     buf.append(" info ").append(_message[off-1]);
                     int size = ((int)DataHelper.fromLong(_message, off, 2)) & 0x3FFF;
    -                buf.append(" with ").append(size).append(" bytes");
    -                buf.append(' ');
    -                off += size;
                     off += 2;
    +                buf.append(" with ").append(size).append(" bytes; ");
    +                off += size;
                 }
                 
                 return buf.toString();
             }
             
    -        public void toRawString(StringBuilder buf) { 
    +        public void toRawString(StringBuilder buf) throws DataFormatException { 
                 UDPPacketReader.this.toRawString(buf); 
                 buf.append(" payload: ");
                       
    @@ -495,14 +522,19 @@ class UDPPacketReader {
             private final int _bitfieldStart;
             private final int _bitfieldSize;
     
    -        public PacketACKBitfield(int start) {
    +        public PacketACKBitfield(int start) throws DataFormatException {
                 _start = start;
                 _bitfieldStart = start + 4;
                 int bfsz = 1;
                 // bitfield is an array of bytes where the high bit is 1 if 
                 // further bytes in the bitfield follow
    -            while ((_message[_bitfieldStart + bfsz - 1] & UDPPacket.BITFIELD_CONTINUATION) != 0x0)
    +            while ((_message[_bitfieldStart + bfsz - 1] & UDPPacket.BITFIELD_CONTINUATION) != 0x0) {
                     bfsz++;
    +                //if (bfsz > InboundMessageState.MAX_PARTIAL_BITFIELD_BYTES)
    +                //    throw new DataFormatException();
    +            }
    +            if (bfsz > InboundMessageState.MAX_PARTIAL_BITFIELD_BYTES)
    +                throw new DataFormatException("bitfield size: " + bfsz);
                 _bitfieldSize = bfsz;
             }
     
    @@ -511,6 +543,46 @@ class UDPPacketReader {
             public int fragmentCount() { return _bitfieldSize * 7; }
             public boolean receivedComplete() { return false; }
     
    +        /**
    +         *  Number of fragments acked in this bitfield.
    +         *  Faster than looping through received()
    +         *  @since 0.9.16
    +         */
    +        public int ackCount() {
    +            int rv = 0;
    +            for (int i = _bitfieldStart; i < _bitfieldStart + _bitfieldSize; i++) {
    +                byte b = _message[i];
    +                if ((b & 0x7f) != 0) {
    +                    for (int j = 0; j < 7; j++) {
    +                        if ((b & 0x01) != 0)
    +                            rv++;
    +                        b >>= 1;
    +                    }
    +                }
    +            }
    +            return rv;
    +        }
    +
    +        /**
    +         *  Highest fragment number acked in this bitfield.
    +         *  @return highest fragment number acked, or -1 if none
    +         *  @since 0.9.16
    +         */
    +        public int highestReceived() {
    +            int count = fragmentCount();
    +            for (int i = _bitfieldSize - 1; i >= 0; i--) {
    +                byte b = _message[_bitfieldStart + i];
    +                if ((b & 0x7f) == 0)
    +                    continue;
    +                for (int j = 6; j >= 0; j--) {
    +                    if ((b & 0x40) != 0)
    +                        return (7 * i) + j;
    +                    b <<= 1;
    +                }
    +            }
    +            return -1;
    +        }
    +
             public boolean received(int fragmentNum) {
                 if ( (fragmentNum < 0) || (fragmentNum >= _bitfieldSize*7) )
                     return false;
    @@ -523,16 +595,17 @@ class UDPPacketReader {
             @Override
             public String toString() { 
                 StringBuilder buf = new StringBuilder(64);
    -            buf.append("Read partial ACK of ");
    +            buf.append("IB Partial ACK of ");
                 buf.append(getMessageId());
    -            buf.append(" with ACKs for: ");
    +            buf.append(" highest: ").append(highestReceived());
    +            buf.append(" with ACKs for: [");
                 int numFrags = fragmentCount();
                 for (int i = 0; i < numFrags; i++) {
    -                if (received(i))
    -                    buf.append(i).append(" ");
    -                else
    -                    buf.append('!').append(i).append(" ");
    +                if (!received(i))
    +                    buf.append('!');
    +                buf.append(i).append(' ');
                 }
    +            buf.append("] / ").append(numFrags);
                 return buf.toString();
             }
         }
    diff --git a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
    index b9c2ad802..63fc95760 100644
    --- a/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
    +++ b/router/java/src/net/i2p/router/transport/udp/UDPTransport.java
    @@ -25,9 +25,9 @@ import java.util.concurrent.CopyOnWriteArrayList;
     import net.i2p.data.DatabaseEntry;
     import net.i2p.data.DataHelper;
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterAddress;
    -import net.i2p.data.RouterIdentity;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterAddress;
    +import net.i2p.data.router.RouterIdentity;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.SessionKey;
     import net.i2p.data.i2np.DatabaseStoreMessage;
     import net.i2p.data.i2np.I2NPMessage;
    @@ -1550,6 +1550,12 @@ public class UDPTransport extends TransportImpl implements TimedWeightedPriority
                     return null;
                 }
     
    +            // Check for supported sig type
    +            if (toAddress.getIdentity().getSigningPublicKey().getType() == null) {
    +                markUnreachable(to);
    +                return null;
    +            }
    +
                 if (!allowConnection())
                     return _cachedBid[TRANSIENT_FAIL_BID];
     
    diff --git a/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java b/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java
    index 3fa40c984..bb18c44b2 100644
    --- a/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java
    +++ b/router/java/src/net/i2p/router/tunnel/InboundGatewayReceiver.java
    @@ -1,7 +1,7 @@
     package net.i2p.router.tunnel;
     
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.i2np.TunnelDataMessage;
     import net.i2p.router.JobImpl;
     import net.i2p.router.OutNetMessage;
    diff --git a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
    index ec3529b48..660aedab8 100644
    --- a/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
    +++ b/router/java/src/net/i2p/router/tunnel/InboundMessageDistributor.java
    @@ -4,7 +4,7 @@ import net.i2p.data.DatabaseEntry;
     import net.i2p.data.Hash;
     import net.i2p.data.LeaseSet;
     import net.i2p.data.Payload;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.TunnelId;
     import net.i2p.data.i2np.DataMessage;
     import net.i2p.data.i2np.DatabaseSearchReplyMessage;
    diff --git a/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java b/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java
    index 364c5970a..3961af80f 100644
    --- a/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java
    +++ b/router/java/src/net/i2p/router/tunnel/OutboundMessageDistributor.java
    @@ -4,7 +4,7 @@ import java.util.HashSet;
     import java.util.Set;
     
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.TunnelId;
     import net.i2p.data.i2np.I2NPMessage;
     import net.i2p.data.i2np.TunnelGatewayMessage;
    diff --git a/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java b/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java
    index 03923ecfe..4aef14902 100644
    --- a/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java
    +++ b/router/java/src/net/i2p/router/tunnel/OutboundReceiver.java
    @@ -1,7 +1,7 @@
     package net.i2p.router.tunnel;
     
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.i2np.TunnelDataMessage;
     import net.i2p.router.JobImpl;
     import net.i2p.router.OutNetMessage;
    diff --git a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
    index b53d8a643..d58060279 100644
    --- a/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
    +++ b/router/java/src/net/i2p/router/tunnel/TunnelParticipant.java
    @@ -1,7 +1,7 @@
     package net.i2p.router.tunnel;
     
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.TunnelId;
     import net.i2p.data.i2np.I2NPMessage;
     import net.i2p.data.i2np.TunnelDataMessage;
    diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
    index b771589a1..42cff28c7 100644
    --- a/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
    +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildExecutor.java
    @@ -10,7 +10,7 @@ import java.util.concurrent.ConcurrentHashMap;
     
     import net.i2p.data.Hash;
     import net.i2p.data.i2np.I2NPMessage;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.router.CommSystemFacade;
     import net.i2p.router.RouterContext;
     import net.i2p.router.TunnelManagerFacade;
    diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
    index afe3940bf..0f914fbcc 100644
    --- a/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
    +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildHandler.java
    @@ -9,8 +9,8 @@ import net.i2p.data.Base64;
     import net.i2p.data.ByteArray;
     import net.i2p.data.DataHelper;
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterIdentity;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterIdentity;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.TunnelId;
     import net.i2p.data.i2np.BuildRequestRecord;
     import net.i2p.data.i2np.BuildResponseRecord;
    @@ -85,6 +85,11 @@ class BuildHandler implements Runnable {
          */
         private static final int NEXT_HOP_SEND_TIMEOUT = 25*1000;
     
    +    private static final long MAX_REQUEST_FUTURE = 5*60*1000;
    +    /** must be > 1 hour due to rouding down */
    +    private static final long MAX_REQUEST_AGE = 65*60*1000;
    +
    +
         public BuildHandler(RouterContext ctx, TunnelPoolManager manager, BuildExecutor exec) {
             _context = ctx;
             _log = ctx.logManager().getLog(getClass());
    @@ -101,8 +106,10 @@ class BuildHandler implements Runnable {
             _context.statManager().createRateStat("tunnel.reject.50", "How often we reject a tunnel because of a critical issue (shutdown, etc)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
     
             _context.statManager().createRequiredRateStat("tunnel.decryptRequestTime", "Time to decrypt a build request (ms)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
    -        _context.statManager().createRequiredRateStat("tunnel.rejectTimeout", "Reject tunnel count (unknown next hop)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
    -        _context.statManager().createRequiredRateStat("tunnel.rejectTimeout2", "Reject tunnel count (can't contact next hop)", "Tunnels", new long[] { 60*1000, 10*60*1000 });
    +        _context.statManager().createRateStat("tunnel.rejectTooOld", "Reject tunnel count (too old)", "Tunnels", new long[] { 3*60*60*1000 });
    +        _context.statManager().createRateStat("tunnel.rejectFuture", "Reject tunnel count (time in future)", "Tunnels", new long[] { 3*60*60*1000 });
    +        _context.statManager().createRateStat("tunnel.rejectTimeout", "Reject tunnel count (unknown next hop)", "Tunnels", new long[] { 60*60*1000 });
    +        _context.statManager().createRateStat("tunnel.rejectTimeout2", "Reject tunnel count (can't contact next hop)", "Tunnels", new long[] { 60*60*1000 });
             _context.statManager().createRequiredRateStat("tunnel.rejectDupID", "Part. tunnel dup ID", "Tunnels", new long[] { 24*60*60*1000 });
             _context.statManager().createRequiredRateStat("tunnel.ownDupID", "Our tunnel dup. ID", "Tunnels", new long[] { 24*60*60*1000 });
             _context.statManager().createRequiredRateStat("tunnel.rejectHostile", "Reject malicious tunnel", "Tunnels", new long[] { 24*60*60*1000 });
    @@ -587,11 +594,24 @@ class BuildHandler implements Runnable {
                 }
             }
     
    -        // time is in hours, and only for log below - what's the point?
    -        // tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it is not.
    +        // time is in hours, rounded down.
    +        // tunnel-alt-creation.html specifies that this is enforced +/- 1 hour but it was not.
    +        // As of 0.9.16, allow + 5 minutes to - 65 minutes.
             long time = req.readRequestTime();
             long now = (_context.clock().now() / (60l*60l*1000l)) * (60*60*1000);
    -        int ourSlot = -1;
    +        long timeDiff = now - time;
    +        if (timeDiff > MAX_REQUEST_AGE) {
    +            _context.statManager().addRateData("tunnel.rejectTooOld", 1);
    +            if (_log.shouldLog(Log.WARN))
    +                _log.warn("Dropping build request too old... replay attack? " + DataHelper.formatDuration(timeDiff));
    +            return;
    +        }
    +        if (timeDiff < 0 - MAX_REQUEST_FUTURE) {
    +            _context.statManager().addRateData("tunnel.rejectFuture", 1);
    +            if (_log.shouldLog(Log.WARN))
    +                _log.warn("Dropping build request too far in future " + DataHelper.formatDuration(0 - timeDiff));
    +            return;
    +        }
     
             int response;
             if (_context.router().isHidden()) {
    @@ -764,6 +784,7 @@ class BuildHandler implements Runnable {
     
             byte reply[] = BuildResponseRecord.create(_context, response, req.readReplyKey(), req.readReplyIV(), state.msg.getUniqueId());
             int records = state.msg.getRecordCount();
    +        int ourSlot = -1;
             for (int j = 0; j < records; j++) {
                 if (state.msg.getRecord(j) == null) {
                     ourSlot = j;
    @@ -780,7 +801,7 @@ class BuildHandler implements Runnable {
                           + " accepted? " + response + " receiving on " + ourId 
                           + " sending to " + nextId
                           + " on " + nextPeer
    -                      + " inGW? " + isInGW + " outEnd? " + isOutEnd + " time difference " + (now-time)
    +                      + " inGW? " + isInGW + " outEnd? " + isOutEnd
                           + " recvDelay " + recvDelay + " replyMessage " + req.readReplyMessageId()
                           + " replyKey " + req.readReplyKey() + " replyIV " + Base64.encode(req.readReplyIV()));
     
    diff --git a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
    index fbfca2bcc..31aaa8665 100644
    --- a/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
    +++ b/router/java/src/net/i2p/router/tunnel/pool/BuildRequestor.java
    @@ -8,7 +8,7 @@ import net.i2p.data.ByteArray;
     import net.i2p.data.DataHelper;
     import net.i2p.data.Hash;
     import net.i2p.data.PublicKey;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.data.TunnelId;
     import net.i2p.data.i2np.TunnelBuildMessage;
     import net.i2p.data.i2np.VariableTunnelBuildMessage;
    diff --git a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
    index 14eab47e2..8bb657831 100644
    --- a/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
    +++ b/router/java/src/net/i2p/router/tunnel/pool/TunnelPeerSelector.java
    @@ -16,7 +16,7 @@ import net.i2p.I2PAppContext;
     import net.i2p.crypto.SHA256Generator;
     import net.i2p.data.DataFormatException;
     import net.i2p.data.Hash;
    -import net.i2p.data.RouterInfo;
    +import net.i2p.data.router.RouterInfo;
     import net.i2p.router.Router;
     import net.i2p.router.RouterContext;
     import net.i2p.router.TunnelPoolSettings;