diff --git a/LICENSE.txt b/LICENSE.txt index 11dd51e38f7f5dc9fd1b9080e109b645ee1cce7e..7681ce65aedfa53a0b5bb46c597783727fd889ad 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 47e9bb0fbb58d7a11fc9eec022af6fa9e99024eb..5fda52940222d29180a0ff9445acfa0d97c37d0f 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 a4fd04c06393fa91f2b1708aa0ce3ce5f8fec478..5a4d3dd2496f1ba78bad21587eadecb060edf102 100644 --- a/apps/i2psnark/java/build.xml +++ b/apps/i2psnark/java/build.xml @@ -100,15 +100,15 @@ <target name="war" depends="jar, bundle, warUpToDate, listChangedFiles" unless="war.uptodate" > <!-- set if unset --> <property name="workspace.changes.tr" value="" /> - <copy todir="build/icons/.icons" > - <fileset dir="../icons/" /> + <copy todir="build/resources/.resources" > + <fileset dir="../resources/" /> </copy> <!-- mime.properties must be in with the classes --> <copy file="../mime.properties" todir="build/obj/org/klomp/snark/web" /> <war destfile="../i2psnark.war" webxml="../web.xml" > <!-- include only the web stuff, as of 0.7.12 the router will add i2psnark.jar to the classpath for the war --> <classes dir="./build/obj" includes="**/web/*" /> - <fileset dir="build/icons/" /> + <fileset dir="build/resources/" /> <manifest> <attribute name="Implementation-Version" value="${full.version}" /> <attribute name="Built-By" value="${build.built-by}" /> @@ -121,7 +121,7 @@ <target name="warUpToDate"> <uptodate property="war.uptodate" targetfile="../i2psnark.war" > - <srcfiles dir= "." includes="build/obj/org/klomp/snark/web/*.class ../icons/* ../web.xml" /> + <srcfiles dir= "." includes="build/obj/org/klomp/snark/web/*.class ../resources/**/* ../web.xml" /> </uptodate> </target> diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java index ab15bb85ca070b7674b6aa46de1a8689be73a160..a7c4f4127a714e92942bd77d3fa8d6a99e7b2107 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<Peer> 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<Peer> * @since 0.8.4 */ public void downloaded(int size) { - downloaded += size; + downloaded.addAndGet(size); } /** @@ -626,7 +626,7 @@ public class Peer implements Comparable<Peer> * @since 0.8.4 */ public void uploaded(int size) { - uploaded += size; + uploaded.addAndGet(size); } /** @@ -635,7 +635,7 @@ public class Peer implements Comparable<Peer> */ public long getDownloaded() { - return downloaded; + return downloaded.get(); } /** @@ -644,7 +644,7 @@ public class Peer implements Comparable<Peer> */ public long getUploaded() { - return uploaded; + return uploaded.get(); } /** @@ -652,8 +652,8 @@ public class Peer implements Comparable<Peer> */ 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 740b7c2be406ff475e001348864b2a388b36d80f..2bb841329503574adecd0b62c78065ad45815de9 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 73f273c6a928ba8b8f6d7e3a8a6d417a8b59ed7f..cb50fb6fbeb84ac8140acf1592daca3fcee38d6b 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 a984f0ce8183b7fbdafdec33016d0c3ded2e5eb7..f1c6658c9463ef4b508d242fea2a317547f7c98b 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 + "<html>\n" + "<head><link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">\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("</title>\n"); @@ -324,7 +304,8 @@ public class I2PSnarkServlet extends BasicServlet { out.write("<div class=\"snarknavbar\"><a href=\"" + _contextPath + "/\" title=\""); out.write(_("Torrents")); out.write("\" class=\"snarkRefresh\">"); - out.write("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "arrow_refresh.png\"> "); + 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("<div class=\"snarknavbar\"><a href=\"" + _contextPath + '/' + peerString + "\" title=\""); out.write(_("Refresh page")); out.write("\" class=\"snarkRefresh\">"); - out.write("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "arrow_refresh.png\"> "); + 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<String> 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 + "\">" + - "<img src=\"" + _imgPath + "delete.png\" title=\"" + _("clear messages") + - "\" alt=\"" + _("clear messages") + "\"></a>" + + out.write("action=Clear&nonce=" + _nonce + "\">"); + String tx = _("clear messages"); + out.write(toThemeImg("delete", tx, tx)); + out.write("</a>" + "<ul>"); 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("<form action=\"_post\" method=\"POST\">\n"); - out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n"); - // don't lose peer setting - if (peerParam != null) - out.write("<input type=\"hidden\" name=\"p\" value=\"" + peerParam + "\" >\n"); - // ...or st setting - if (stParam != null) - out.write("<input type=\"hidden\" name=\"st\" value=\"" + stParam + "\" >\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("<tr><th><img border=\"0\" src=\"" + _imgPath + "status.png\" title=\""); - out.write(_("Status")); - out.write("\" alt=\""); - out.write(_("Status")); - out.write("\"></th>\n<th>"); + String currentSort = req.getParameter("sort"); + boolean showSort = total > 1; + out.write("<tr><th>"); + String sort = ("2".equals(currentSort)) ? "-2" : "2"; + if (showSort) { + out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort)); + out.write("\">"); + } + String tx = _("Status"); + out.write(toThemeImg("status", tx, + showSort ? _("Sort by {0}", tx) + : tx)); + if (showSort) + out.write("</a>"); + out.write("</th>\n<th>"); if (_manager.util().connected() && !snarks.isEmpty()) { out.write(" <a href=\"" + _contextPath + '/'); if (peerParam != null) { - if (stParam != null) { - out.write("?st="); - out.write(stParam); - } - out.write("\">"); - out.write("<img border=\"0\" src=\"" + _imgPath + "hidepeers.png\" title=\""); - out.write(_("Hide Peers")); - out.write("\" alt=\""); - out.write(_("Hide Peers")); + // disable peer view 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("<img border=\"0\" src=\"" + _imgPath + "showpeers.png\" title=\""); - out.write(_("Show Peers")); - out.write("\" alt=\""); - out.write(_("Show Peers")); + // enable peer view + out.write(getQueryString(req, "1", null, null)); out.write("\">"); + tx = _("Show Peers"); + out.write(toThemeImg("showpeers", tx, tx)); } out.write("</a><br>\n"); } out.write("</th>\n<th colspan=\"2\" align=\"left\">"); - out.write("<img border=\"0\" src=\"" + _imgPath + "torrent.png\" title=\""); - out.write(_("Torrent")); - out.write("\" alt=\""); - out.write(_("Torrent")); - out.write("\"></th>\n<th align=\"center\">"); + // 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("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort)); + out.write("\">"); + } + tx = _("Torrent"); + out.write(toThemeImg("torrent", tx, + showSort ? _("Sort by {0}", (isTypeSort ? _("File type") : tx)) + : tx)); + if (showSort) + out.write("</a>"); + out.write("</th>\n<th align=\"center\">"); if (total > 0 && (start > 0 || total > pageSize)) { - writePageNav(out, start, pageSize, total, peerParam, noThinsp); + writePageNav(out, req, start, pageSize, total, noThinsp); } out.write("</th>\n<th align=\"right\">"); if (_manager.util().connected() && !snarks.isEmpty()) { - out.write("<img border=\"0\" src=\"" + _imgPath + "eta.png\" title=\""); - out.write(_("Estimated time remaining")); - out.write("\" alt=\""); + if (showSort) { + sort = ("4".equals(currentSort)) ? "-4" : "4"; + out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort)); + 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("</a>"); } out.write("</th>\n<th align=\"right\">"); - out.write("<img border=\"0\" src=\"" + _imgPath + "head_rx.png\" title=\""); - out.write(_("Downloaded")); - out.write("\" alt=\""); + // cycle through sort by size or downloaded + boolean isDlSort = false; + if (showSort) { + if ("5".equals(currentSort)) { + sort = "-5"; + } else if ("-5".equals(currentSort)) { + sort = "6"; + isDlSort = true; + } else if ("6".equals(currentSort)) { + sort = "-6"; + isDlSort = true; + } else { + sort = "5"; + } + out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort)); + 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("</a>"); out.write("</th>\n<th align=\"right\">"); + boolean isRatSort = false; if (!snarks.isEmpty()) { - out.write("<img border=\"0\" src=\"" + _imgPath + "head_tx.png\" title=\""); - out.write(_("Uploaded")); - out.write("\" alt=\""); + // cycle through sort by uploaded or ratio + boolean nextRatSort = false; + if (showSort) { + if ("7".equals(currentSort)) { + sort = "-7"; + } else if ("-7".equals(currentSort)) { + sort = "11"; + nextRatSort = true; + } else if ("11".equals(currentSort)) { + sort = "-11"; + nextRatSort = true; + isRatSort = true; + } else if ("-11".equals(currentSort)) { + sort = "7"; + isRatSort = true; + } else { + sort = "7"; + } + out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort)); + 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("</a>"); } out.write("</th>\n<th align=\"right\">"); if (_manager.util().connected() && !snarks.isEmpty()) { - out.write("<img border=\"0\" src=\"" + _imgPath + "head_rxspeed.png\" title=\""); - out.write(_("Down Rate")); - out.write("\" alt=\""); + if (showSort) { + sort = ("8".equals(currentSort)) ? "-8" : "8"; + out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort)); + 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("</a>"); } out.write("</th>\n<th align=\"right\">"); if (_manager.util().connected() && !snarks.isEmpty()) { - out.write("<img border=\"0\" src=\"" + _imgPath + "head_txspeed.png\" title=\""); - out.write(_("Up Rate")); - out.write("\" alt=\""); + if (showSort) { + sort = ("9".equals(currentSort)) ? "-9" : "9"; + out.write("<a href=\"" + _contextPath + '/' + getQueryString(req, null, null, sort)); + 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("</a>"); } out.write("</th>\n<th align=\"center\">"); @@ -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("<input type=\"hidden\" name=\"nonce\" value=\"") + .append(_nonce).append("\" >\n"); + String peerParam = req.getParameter("p"); + if (peerParam != null) { + buf.append("<input type=\"hidden\" name=\"p\" value=\"") + .append(DataHelper.stripHTML(peerParam)).append("\" >\n"); + } + String stParam = req.getParameter("st"); + if (stParam != null) { + buf.append("<input type=\"hidden\" name=\"st\" value=\"") + .append(DataHelper.stripHTML(stParam)).append("\" >\n"); + } + String soParam = req.getParameter("sort"); + if (soParam != null) { + buf.append("<input type=\"hidden\" name=\"sort\" value=\"") + .append(DataHelper.stripHTML(soParam)).append("\" >\n"); + } + if (action != null) { + buf.append("<input type=\"hidden\" name=\"action\" value=\"") + .append(action).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("<a href=\"" + _contextPath); - if (peerParam != null) - out.write("?p=" + peerParam); - out.write("\">" + - "<img alt=\"" + _("First") + "\" title=\"" + _("First page") + "\" border=\"0\" src=\"" + - _imgPath + "control_rewind_blue.png\">" + - "</a> "); + out.write(getQueryString(req, null, "", null)); + out.write("\">"); + out.write(toThemeImg("control_rewind_blue", _("First"), _("First page"))); + out.write("</a> "); int prev = Math.max(0, start - pageSize); //if (prev > 0) { if (true) { // Back - out.write(" <a href=\"" + _contextPath + "?st=" + prev); - if (peerParam != null) - out.write("&p=" + peerParam); - out.write("\">" + - "<img alt=\"" + _("Prev") + "\" title=\"" + _("Previous page") + "\" border=\"0\" src=\"" + - _imgPath + "control_back_blue.png\">" + - "</a> "); + out.write(" <a href=\"" + _contextPath); + String sprev = (prev > 0) ? Integer.toString(prev) : ""; + out.write(getQueryString(req, null, sprev, null)); + out.write("\">"); + out.write(toThemeImg("control_back_blue", _("Prev"), _("Previous page"))); + out.write("</a> "); } } else { out.write( @@ -691,23 +845,19 @@ public class I2PSnarkServlet extends BasicServlet { //if (next + pageSize < total) { if (true) { // Next - out.write(" <a href=\"" + _contextPath + "?st=" + next); - if (peerParam != null) - out.write("&p=" + peerParam); - out.write("\">" + - "<img alt=\"" + _("Next") + "\" title=\"" + _("Next page") + "\" border=\"0\" src=\"" + - _imgPath + "control_play_blue.png\">" + - "</a> "); + out.write(" <a href=\"" + _contextPath); + out.write(getQueryString(req, null, Integer.toString(next), null)); + out.write("\">"); + out.write(toThemeImg("control_play_blue", _("Next"), _("Next page"))); + out.write("</a> "); } // Last int last = ((total - 1) / pageSize) * pageSize; - out.write(" <a href=\"" + _contextPath + "?st=" + last); - if (peerParam != null) - out.write("&p=" + peerParam); - out.write("\">" + - "<img alt=\"" + _("Last") + "\" title=\"" + _("Last page") + "\" border=\"0\" src=\"" + - _imgPath + "control_fastforward_blue.png\">" + - "</a> "); + out.write(" <a href=\"" + _contextPath); + out.write(getQueryString(req, null, Integer.toString(last), null)); + out.write("\">"); + out.write(toThemeImg("control_fastforward_blue", _("Last"), _("Last page"))); + out.write("</a> "); } else { out.write(" " + "<img alt=\"\" border=\"0\" class=\"disable\" src=\"" + @@ -1111,10 +1261,13 @@ public class I2PSnarkServlet extends BasicServlet { _manager.addMessage(_("Removed") + ": " + DataHelper.stripHTML(k)); changed = true; } - } else if (k.startsWith("open_")) { - open.add(k.substring(5)); - } else if (k.startsWith("private_")) { - priv.add(k.substring(8)); + } else if (k.startsWith("ttype_")) { + String val = req.getParameter(k); + k = k.substring(6); + if ("1".equals(val)) + open.add(k); + else if ("2".equals(val)) + priv.add(k); } } if (changed) { @@ -1149,12 +1302,12 @@ public class I2PSnarkServlet extends BasicServlet { Map<String, Tracker> 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<String> newOpen = new ArrayList<String>(_manager.util().getOpenTrackers()); newOpen.add(aurl); _manager.saveOpenTrackers(newOpen); - } else if (req.getParameter("_add_private_") != null) { + } else if ("2".equals(type)) { List<String> newPriv = new ArrayList<String>(_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<Snark>, 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<Snark> getSortedSnarks(HttpServletRequest req) { ArrayList<Snark> rv = new ArrayList<Snark>(_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 = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Checking") + "\"></td>" + + statusString = toThemeImg("stalled", "", _("Checking")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("Checking"); } else if (snark.isAllocating()) { - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Allocating") + "\"></td>" + + statusString = toThemeImg("stalled", "", _("Allocating")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("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) + "</a>"; //else if (isRunning) if (isRunning) - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td>" + + statusString = toThemeImg("trackererror", "", err) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("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 = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td>" + + statusString = toThemeImg("trackererror", "", err) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("Tracker Error"); } } else if (snark.isStarting()) { - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Starting") + "\"></td>" + + statusString = toThemeImg("stalled", "", _("Starting")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("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 = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + img + ".png\" title=\"" + txt + "\"></td>" + + statusString = toThemeImg(img, "", txt) + "</td>" + "<td class=\"snarkTorrentStatus\">" + txt + - ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + stParam + "\">" + + ": <a href=\"" + uri + getQueryString(req, Base64.encode(snark.getInfoHash()), null, null) + "\">" + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; else - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + img + ".png\" title=\"" + txt + "\"></td>" + + statusString = toThemeImg(img, "", txt) + "</td>" + "<td class=\"snarkTorrentStatus\">" + txt + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers); } else { - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "complete.png\" title=\"" + _("Complete") + "\"></td>" + + statusString = toThemeImg("complete", "", _("Complete")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("Complete"); } } else { if (isRunning && curPeers > 0 && downBps > 0 && !showPeers) - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" title=\"" + _("OK") + "\"></td>" + + statusString = toThemeImg("downloading", "", _("OK")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("OK") + - ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + stParam + "\">" + + ": <a href=\"" + uri + getQueryString(req, Base64.encode(snark.getInfoHash()), null, null) + "\">" + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; else if (isRunning && curPeers > 0 && downBps > 0) - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" title=\"" + _("OK") + "\"></td>" + + statusString = toThemeImg("downloading", "", _("OK")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("OK") + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers); else if (isRunning && curPeers > 0 && !showPeers) - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Stalled") + "\"></td>" + + statusString = toThemeImg("stalled", "", _("Stalled")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("Stalled") + - ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + stParam + "\">" + + ": <a href=\"" + uri + getQueryString(req, Base64.encode(snark.getInfoHash()), null, null) + "\">" + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; else if (isRunning && curPeers > 0) - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" title=\"" + _("Stalled") + "\"></td>" + + statusString = toThemeImg("stalled", "", _("Stalled")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("Stalled") + ": " + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers); else if (isRunning && knownPeers > 0) - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "nopeers.png\" title=\"" + _("No Peers") + "\"></td>" + + statusString = toThemeImg("nopeers", "", _("No Peers")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("No Peers") + ": 0" + thinsp(noThinsp) + knownPeers ; else if (isRunning) - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "nopeers.png\" title=\"" + _("No Peers") + "\"></td>" + + statusString = toThemeImg("nopeers", "", _("No Peers")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("No Peers"); else - statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stopped.png\" title=\"" + _("Stopped") + "\"></td>" + + statusString = toThemeImg("stopped", "", _("Stopped")) + "</td>" + "<td class=\"snarkTorrentStatus\">" + _("Stopped"); } @@ -1467,8 +1608,17 @@ public class I2PSnarkServlet extends BasicServlet { // out.write("??"); // no meta size yet out.write("</td>\n\t"); out.write("<td align=\"right\" class=\"snarkTorrentUploaded\">"); - 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("</td>\n\t"); out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">"); if (isRunning && needed > 0) @@ -1485,7 +1635,8 @@ public class I2PSnarkServlet extends BasicServlet { } else if (isRunning) { // Stop Button if (isDegraded) - out.write("<a href=\"" + _contextPath + "/?action=Stop_" + b64 + "&nonce=" + _nonce + stParam + "\"><img title=\""); + out.write("<a href=\"" + _contextPath + "/?action=Stop_" + b64 + "&nonce=" + _nonce + + getQueryString(req, "", null, null).replace("?", "&") + "\"><img title=\""); else out.write("<input type=\"image\" name=\"action_Stop_" + b64 + "\" value=\"foo\" title=\""); out.write(_("Stop the torrent")); @@ -1499,7 +1650,8 @@ public class I2PSnarkServlet extends BasicServlet { // Start Button // This works in Opera but it's displayed a little differently, so use noThinsp here too so all 3 icons are consistent if (noThinsp) - out.write("<a href=\"" + _contextPath + "/?action=Start_" + b64 + "&nonce=" + _nonce + stParam + "\"><img title=\""); + out.write("<a href=\"" + _contextPath + "/?action=Start_" + b64 + "&nonce=" + _nonce + + getQueryString(req, "", null, null).replace("?", "&") + "\"><img title=\""); else out.write("<input type=\"image\" name=\"action_Start_" + b64 + "\" value=\"foo\" title=\""); out.write(_("Start the torrent")); @@ -1513,7 +1665,8 @@ public class I2PSnarkServlet extends BasicServlet { // Remove Button // Doesnt work with Opera so use noThinsp instead of isDegraded if (noThinsp) - out.write("<a href=\"" + _contextPath + "/?action=Remove_" + b64 + "&nonce=" + _nonce + stParam + "\"><img title=\""); + out.write("<a href=\"" + _contextPath + "/?action=Remove_" + b64 + "&nonce=" + _nonce + + getQueryString(req, "", null, null).replace("?", "&") + "\"><img title=\""); else out.write("<input type=\"image\" name=\"action_Remove_" + b64 + "\" value=\"foo\" title=\""); out.write(_("Remove the torrent from the active list, deleting the .torrent file")); @@ -1534,7 +1687,8 @@ public class I2PSnarkServlet extends BasicServlet { // Delete Button // Doesnt work with Opera so use noThinsp instead of isDegraded if (noThinsp) - out.write("<a href=\"" + _contextPath + "/?action=Delete_" + b64 + "&nonce=" + _nonce + stParam + "\"><img title=\""); + out.write("<a href=\"" + _contextPath + "/?action=Delete_" + b64 + "&nonce=" + _nonce + + getQueryString(req, "", null, null).replace("?", "&") + "\"><img title=\""); else out.write("<input type=\"image\" name=\"action_Delete_" + b64 + "\" value=\"foo\" title=\""); out.write(_("Delete the .torrent file and the associated data file(s)")); @@ -1773,9 +1927,9 @@ public class I2PSnarkServlet extends BasicServlet { String linkUrl = getTrackerLinkUrl(announce, infohash); if (linkUrl != null) { StringBuilder buf = new StringBuilder(128); - buf.append(linkUrl) - .append("<img alt=\"").append(_("Info")).append("\" border=\"0\" src=\"") - .append(_imgPath).append("details.png\"></a>"); + buf.append(linkUrl); + toThemeImg(buf, "details", _("Info"), ""); + buf.append("</a>"); return buf.toString(); } return null; @@ -1843,14 +1997,10 @@ public class I2PSnarkServlet extends BasicServlet { out.write("<div class=\"snarkNewTorrent\">\n"); // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file out.write("<form action=\"_post\" method=\"POST\">\n"); - out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n"); - out.write("<input type=\"hidden\" name=\"action\" value=\"Add\" >\n"); - // don't lose peer setting - String peerParam = req.getParameter("p"); - if (peerParam != null) - out.write("<input type=\"hidden\" name=\"p\" value=\"" + DataHelper.stripHTML(peerParam) + "\" >\n"); + writeHiddenInputs(out, req, "Add"); out.write("<div class=\"addtorrentsection\"><span class=\"snarkConfigTitle\">"); - out.write("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "add.png\"> "); + out.write(toThemeImg("add")); + out.write(' '); out.write(_("Add Torrent")); out.write("</span><hr>\n<table border=\"0\"><tr><td>"); out.write(_("From URL")); @@ -1875,14 +2025,10 @@ public class I2PSnarkServlet extends BasicServlet { out.write("<a name=\"add\"></a><div class=\"newtorrentsection\"><div class=\"snarkNewTorrent\">\n"); // *not* enctype="multipart/form-data", so that the input type=file sends the filename, not the file out.write("<form action=\"_post\" method=\"POST\">\n"); - out.write("<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n"); - out.write("<input type=\"hidden\" name=\"action\" value=\"Create\" >\n"); - // don't lose peer setting - String peerParam = req.getParameter("p"); - if (peerParam != null) - out.write("<input type=\"hidden\" name=\"p\" value=\"" + DataHelper.stripHTML(peerParam) + "\" >\n"); + writeHiddenInputs(out, req, "Create"); out.write("<span class=\"snarkConfigTitle\">"); - out.write("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "create.png\"> "); + out.write(toThemeImg("create")); + out.write(' '); out.write(_("Create Torrent")); out.write("</span><hr>\n<table border=\"0\"><tr><td>"); //out.write("From file: <input type=\"file\" name=\"newFile\" size=\"50\" value=\"" + newFile + "\" /><br>\n"); @@ -1946,11 +2092,11 @@ public class I2PSnarkServlet extends BasicServlet { //int seedPct = 0; out.write("<form action=\"" + _contextPath + "/configure\" method=\"POST\">\n" + - "<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n" + - "<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n" + - "<input type=\"hidden\" name=\"action\" value=\"Save\" >\n" + - "<span class=\"snarkConfigTitle\">" + - "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "config.png\"> "); + "<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n"); + writeHiddenInputs(out, req, "Save"); + out.write("<span class=\"snarkConfigTitle\">"); + out.write(toThemeImg("config")); + out.write(' '); out.write(_("Configuration")); out.write("</span><hr>\n" + "<table border=\"0\"><tr><td>"); @@ -1979,6 +2125,7 @@ public class I2PSnarkServlet extends BasicServlet { out.write(": <td><select name='theme'>"); String theme = _manager.getTheme(); String[] themes = _manager.getThemes(); + Arrays.sort(themes); for(int i = 0; i < themes.length; i++) { if(themes[i].equals(theme)) out.write("\n<OPTION value=\"" + themes[i] + "\" SELECTED>" + themes[i]); @@ -2132,11 +2279,11 @@ public class I2PSnarkServlet extends BasicServlet { private void writeTrackerForm(PrintWriter out, HttpServletRequest req) throws IOException { StringBuilder buf = new StringBuilder(1024); buf.append("<form action=\"" + _contextPath + "/configure\" method=\"POST\">\n" + - "<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n" + - "<input type=\"hidden\" name=\"nonce\" value=\"" + _nonce + "\" >\n" + - "<input type=\"hidden\" name=\"action\" value=\"Save2\" >\n" + - "<span class=\"snarkConfigTitle\">" + - "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "config.png\"> "); + "<div class=\"configsectionpanel\"><div class=\"snarkConfig\">\n"); + writeHiddenInputs(buf, req, "Save2"); + buf.append("<span class=\"snarkConfigTitle\">"); + toThemeImg(buf, "config"); + buf.append(' '); buf.append(_("Trackers")); buf.append("</span><hr>\n" + "<table class=\"trackerconfig\"><tr><th>") @@ -2146,6 +2293,8 @@ public class I2PSnarkServlet extends BasicServlet { .append("</th><th>") .append(_("Website URL")) .append("</th><th>") + .append(_("Standard")) + .append("</th><th>") .append(_("Open")) .append("</th><th>") .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("<tr><td><input type=\"checkbox\" class=\"optbox\" name=\"delete_") .append(name).append("\" title=\"").append(_("Delete")).append("\">" + "</td><td>").append(name) .append("</td><td>").append(urlify(homeURL, 35)) - .append("</td><td><input type=\"checkbox\" class=\"optbox\" name=\"open_") + .append("</td><td><input type=\"radio\" class=\"optbox\" value=\"0\" name=\"ttype_") + .append(announceURL).append("\""); + if (!(isOpen || isPrivate)) + buf.append(" checked=\"checked\""); + buf.append(">" + + "</td><td><input type=\"radio\" class=\"optbox\" value=\"1\" name=\"ttype_") .append(announceURL).append("\""); - if (openTrackers.contains(t.announceURL)) + if (isOpen) buf.append(" checked=\"checked\""); buf.append(">" + - "</td><td><input type=\"checkbox\" class=\"optbox\" name=\"private_") + "</td><td><input type=\"radio\" class=\"optbox\" value=\"2\" name=\"ttype_") .append(announceURL).append("\""); - if (privateTrackers.contains(t.announceURL)) { + if (isPrivate) { buf.append(" checked=\"checked\""); } else { if (SnarkManager.DEFAULT_TRACKER_ANNOUNCES.contains(t.announceURL)) @@ -2183,27 +2339,29 @@ public class I2PSnarkServlet extends BasicServlet { .append(_("Add")).append(":</b></td>" + "<td><input type=\"text\" class=\"trackername\" name=\"tname\" spellcheck=\"false\"></td>" + "<td><input type=\"text\" class=\"trackerhome\" name=\"thurl\" spellcheck=\"false\"></td>" + - "<td><input type=\"checkbox\" class=\"optbox\" name=\"_add_open_\"></td>" + - "<td><input type=\"checkbox\" class=\"optbox\" name=\"_add_private_\"></td>" + + "<td><input type=\"radio\" class=\"optbox\" value=\"0\" name=\"add_tracker_type\" checked=\"checked\"></td>" + + "<td><input type=\"radio\" class=\"optbox\" value=\"1\" name=\"add_tracker_type\"></td>" + + "<td><input type=\"radio\" class=\"optbox\" value=\"2\" name=\"add_tracker_type\"></td>" + "<td><input type=\"text\" class=\"trackerannounce\" name=\"taurl\" spellcheck=\"false\"></td></tr>\n" + - "<tr><td colspan=\"6\"> </td></tr>\n" + // spacer - "<tr><td colspan=\"2\"></td><td colspan=\"4\">\n" + + "<tr><td colspan=\"7\"> </td></tr>\n" + // spacer + "<tr><td colspan=\"2\"></td><td colspan=\"5\">\n" + "<input type=\"submit\" name=\"taction\" class=\"default\" value=\"").append(_("Add tracker")).append("\">\n" + "<input type=\"submit\" name=\"taction\" class=\"delete\" value=\"").append(_("Delete selected")).append("\">\n" + + "<input type=\"submit\" name=\"taction\" class=\"add\" value=\"").append(_("Add tracker")).append("\">\n" + "<input type=\"submit\" name=\"taction\" class=\"accept\" value=\"").append(_("Save tracker configuration")).append("\">\n" + // "<input type=\"reset\" class=\"cancel\" value=\"").append(_("Cancel")).append("\">\n" + "<input type=\"submit\" name=\"taction\" class=\"reload\" value=\"").append(_("Restore defaults")).append("\">\n" + - "<input type=\"submit\" name=\"taction\" class=\"add\" value=\"").append(_("Add tracker")).append("\">\n" + "</td></tr>" + - "<tr><td colspan=\"6\"> </td></tr>\n" + // spacer + "<tr><td colspan=\"7\"> </td></tr>\n" + // spacer "</table></div></div></form>\n"); out.write(buf.toString()); } private void writeConfigLink(PrintWriter out) throws IOException { out.write("<div class=\"configsection\"><span class=\"snarkConfig\">\n" + - "<span class=\"snarkConfigTitle\"><a href=\"configure\">" + - "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "config.png\"> "); + "<span class=\"snarkConfigTitle\"><a href=\"configure\">"); + out.write(toThemeImg("config")); + out.write(' '); out.write(_("Configuration")); out.write("</a></span></span></div>\n"); } @@ -2336,23 +2494,6 @@ public class I2PSnarkServlet extends BasicServlet { private static final String FOOTER = "</div></center></body></html>"; - /** - * Sort alphabetically in current locale, ignore case, - * directories first - * @since 0.9.6 - */ - private static class ListingComparator implements Comparator<File>, 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<String, String[]> postParams) + private String getListHTML(File xxxr, String base, boolean parent, Map<String, String[]> 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("<HTML><HEAD><TITLE>"); 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("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" + - "</HEAD><BODY>\n<center><div class=\"snarknavbar\"><a href=\"").append(_contextPath).append("/\" title=\"Torrents\""); - buf.append(" class=\"snarkRefresh\"><img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("arrow_refresh.png\"> "); + buf.append("</TITLE>\n").append(HEADER_A).append(_themePath).append(HEADER_B) + .append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">\n"); + if (showPriority) + buf.append("<script src=\"").append(_contextPath).append(WARBASE + "js/folder.js\" type=\"text/javascript\"></script>\n"); + buf.append("</HEAD><BODY"); + if (showPriority) + buf.append(" onload=\"setupbuttons()\""); + buf.append(">\n<center><div class=\"snarknavbar\"><a href=\"").append(_contextPath).append("/\" title=\"Torrents\""); + buf.append(" class=\"snarkRefresh\">"); + toThemeImg(buf, "arrow_refresh"); + buf.append(" "); if (_contextName.equals(DEFAULT_NAME)) buf.append(_("I2PSnark")); else @@ -2454,11 +2608,13 @@ public class I2PSnarkServlet extends BasicServlet { if (parent) // always true buf.append("<div class=\"page\"><div class=\"mainsection\">"); - boolean showPriority = snark != null && snark.getStorage() != null && !snark.getStorage().complete() && - r.isDirectory(); if (showPriority) { buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n"); buf.append("<input type=\"hidden\" name=\"nonce\" value=\"").append(_nonce).append("\" >\n"); + if (sortParam != null) { + buf.append("<input type=\"hidden\" name=\"sort\" value=\"") + .append(DataHelper.stripHTML(sortParam)).append("\" >\n"); + } } if (snark != null) { // first table - torrent info @@ -2471,14 +2627,16 @@ public class I2PSnarkServlet extends BasicServlet { String fullPath = snark.getName(); String baseName = encodePath((new File(fullPath)).getName()); - buf.append("<tr><td>") - .append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" > <b>") + buf.append("<tr><td>"); + toThemeImg(buf, "file"); + buf.append(" <b>") .append(_("Torrent file")) .append(":</b> <a href=\"").append(_contextPath).append('/').append(baseName).append("\">") .append(DataHelper.escapeHTML(fullPath)) .append("</a></td></tr>\n"); - buf.append("<tr><td>") - .append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" > <b>") + buf.append("<tr><td>"); + toThemeImg(buf, "file"); + buf.append(" <b>") .append(_("Data location")) .append(":</b> ") .append(DataHelper.escapeHTML(snark.getStorage().getBase().getPath())) @@ -2495,17 +2653,19 @@ public class I2PSnarkServlet extends BasicServlet { buf.append("<tr><td>"); String trackerLink = getTrackerLink(announce, snark.getInfoHash()); if (trackerLink != null) - buf.append(trackerLink).append(' '); - buf.append("<b>").append(_("Primary Tracker")).append(":</b> "); + buf.append(trackerLink); + else + toThemeImg(buf, "details"); + buf.append(" <b>").append(_("Primary Tracker")).append(":</b> "); buf.append(getShortTrackerLink(announce, snark.getInfoHash())); buf.append("</td></tr>"); } List<List<String>> alist = meta.getAnnounceList(); if (alist != null) { - buf.append("<tr><td>" + - "<img alt=\"\" border=\"0\" src=\"") - .append(_imgPath).append("details.png\"> <b>"); - buf.append(_("Tracker List")).append(":</b> "); + buf.append("<tr><td>"); + toThemeImg(buf, "details"); + buf.append(" <b>") + .append(_("Tracker List")).append(":</b> "); for (List<String> 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("<tr><td><img alt=\"\" border=\"0\" src=\"") - .append(_imgPath).append("details.png\"> <b>") + buf.append("<tr><td>"); + toThemeImg(buf, "details"); + buf.append(" <b>") .append(_("Comment")).append(":</b> ") .append(DataHelper.stripHTML(com)) .append("</td></tr>\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("<tr><td><img alt=\"\" border=\"0\" src=\"") - .append(_imgPath).append("details.png\"> <b>") + buf.append("<tr><td>"); + toThemeImg(buf, "details"); + buf.append(" <b>") .append(_("Created")).append(":</b> ") .append(date).append(" UTC") .append("</td></tr>\n"); @@ -2546,8 +2708,9 @@ public class I2PSnarkServlet extends BasicServlet { if (cby != null) { if (cby.length() > 128) cby = com.substring(0, 128); - buf.append("<tr><td><img alt=\"\" border=\"0\" src=\"") - .append(_imgPath).append("details.png\"> <b>") + buf.append("<tr><td>"); + toThemeImg(buf, "details"); + buf.append(" <b>") .append(_("Created By")).append(":</b> ") .append(DataHelper.stripHTML(cby)) .append("</td></tr>\n"); @@ -2582,41 +2745,70 @@ public class I2PSnarkServlet extends BasicServlet { //buf.append("<tr><td>").append(_("Maggot link")).append(": <a href=\"").append(MAGGOT).append(hex).append(':').append(hex).append("\">") // .append(MAGGOT).append(hex).append(':').append(hex).append("</a></td></tr>"); - buf.append("<tr><td>") - .append("<img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("size.png\" > <b>") + buf.append("<tr><td>"); + toThemeImg(buf, "size"); + buf.append(" <b>") .append(_("Size")) .append(":</b> ") .append(formatSize(snark.getTotalLength())); int pieces = snark.getPieces(); double completion = (pieces - snark.getNeeded()) / (double) pieces; + buf.append(" "); + toThemeImg(buf, "head_rx"); + buf.append(" <b>"); if (completion < 1.0) - buf.append(" <img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("head_rx.png\" > <b>") - .append(_("Completion")) + buf.append(_("Completion")) .append(":</b> ") .append((new DecimalFormat("0.00%")).format(completion)); else - buf.append(" <img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("head_rx.png\" > ") - .append(_("Complete")); - // else unknown + buf.append(_("Complete")).append("</b>"); + // up ratio + buf.append(" "); + toThemeImg(buf, "head_tx"); + buf.append(" <b>") + .append(_("Upload ratio")) + .append(":</b> "); + 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(" <img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("head_rx.png\" > <b>") + if (needed < 0) { + // including skipped files, valid when not running + needed = snark.getRemainingLength(); + } + if (needed > 0) { + buf.append(" "); + toThemeImg(buf, "head_rx"); + buf.append(" <b>") .append(_("Remaining")) .append(":</b> ") .append(formatSize(needed)); + } if (meta != null) { List<List<String>> files = meta.getFiles(); int fileCount = files != null ? files.size() : 1; - buf.append(" <img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" > <b>") + buf.append(" "); + toThemeImg(buf, "file"); + buf.append(" <b>") .append(_("Files")) .append(":</b> ") .append(fileCount); } - buf.append(" <img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" > <b>") + buf.append(" "); + toThemeImg(buf, "file"); + buf.append(" <b>") .append(_("Pieces")) .append(":</b> ") .append(pieces); - buf.append(" <img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("file.png\" > <b>") + buf.append(" "); + toThemeImg(buf, "file"); + buf.append(" <b>") .append(_("Piece size")) .append(":</b> ") .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<Sorters.FileAndIndex> fileList = new ArrayList<Sorters.FileAndIndex>(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("<table class=\"snarkDirInfo\"><thead>\n"); buf.append("<tr>\n") - .append("<th colspan=2>") - .append("<img border=\"0\" src=\"").append(_imgPath).append("file.png\" title=\"") - .append(_("Directory")) - .append(": ") - .append(directory) - .append("\" alt=\"") - .append(_("Directory")) - .append("\"></th>\n"); - buf.append("<th align=\"right\">") - .append("<img border=\"0\" src=\"").append(_imgPath).append("size.png\" title=\"") - .append(_("Size")) - .append("\" alt=\"") - .append(_("Size")) - .append("\"></th>\n"); - buf.append("<th class=\"headerstatus\">") - .append("<img border=\"0\" src=\"").append(_imgPath).append("status.png\" title=\"") - .append(_("Status")) - .append("\" alt=\"") - .append(_("Status")) - .append("\"></th>\n"); - if (showPriority) - buf.append("<th class=\"headerpriority\">") - .append("<img border=\"0\" src=\"").append(_imgPath).append("priority.png\" title=\"") - .append(_("Priority")) - .append("\" alt=\"") - .append(_("Priority")) - .append("\"></th>\n"); - buf.append("</tr>\n</thead>\n"); + .append("<th colspan=2>"); + 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("<a href=\"").append(base) + .append(getQueryString(sort)).append("\">"); + } + toThemeImg(buf, "file", tx, + showSort ? _("Sort by {0}", (isTypeSort ? _("File type") : _("Name"))) + : tx + ": " + directory); + if (showSort) + buf.append("</a>"); + buf.append("</th>\n<th align=\"right\">"); + if (showSort) { + sort = ("5".equals(sortParam)) ? "-5" : "5"; + buf.append("<a href=\"").append(base) + .append(getQueryString(sort)).append("\">"); + } + tx = _("Size"); + toThemeImg(buf, "size", tx, + showSort ? _("Sort by {0}", tx) : tx); + if (showSort) + buf.append("</a>"); + buf.append("</th>\n<th class=\"headerstatus\">"); + if (showSort) { + sort = ("10".equals(sortParam)) ? "-10" : "10"; + buf.append("<a href=\"").append(base) + .append(getQueryString(sort)).append("\">"); + } + tx = _("Status"); + toThemeImg(buf, "status", tx, + showSort ? _("Sort by {0}", _("Remaining")) : tx); + if (showSort) + buf.append("</a>"); + if (showPriority) { + buf.append("</th>\n<th class=\"headerpriority\">"); + if (showSort) { + sort = ("13".equals(sortParam)) ? "-13" : "13"; + buf.append("<a href=\"").append(base) + .append(getQueryString(sort)).append("\">"); + } + tx = _("Priority"); + toThemeImg(buf, "priority", tx, + showSort ? _("Sort by {0}", tx) : tx); + if (showSort) + buf.append("</a>"); + } + buf.append("</th>\n</tr>\n</thead>\n"); buf.append("<tr><td colspan=\"" + (showPriority ? '5' : '4') + "\" class=\"ParentDir\"><A HREF=\""); URIUtil.encodePath(buf, addPaths(decodedBase,"../")); - buf.append("\"><img alt=\"\" border=\"0\" src=\"").append(_imgPath).append("up.png\"> ") + buf.append("\">"); + toThemeImg(buf, "up"); + buf.append(' ') .append(_("Up to higher level directory")) .append("</A></td></tr>\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("<TR class=\"").append(rowClass).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("<td class=\"priority\">"); if ((!complete) && (!item.isDirectory())) { - buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(fileIndex).append("\" "); + buf.append("\n<input type=\"radio\" onclick=\"priorityclicked();\" class=\"prihigh\" value=\"5\" name=\"pri.").append(fileIndex).append("\" "); if (priority > 0) - buf.append("checked=\"true\""); + buf.append("checked=\"checked\""); buf.append('>').append(_("High")); - buf.append("<input type=\"radio\" value=\"0\" name=\"pri.").append(fileIndex).append("\" "); + buf.append("\n<input type=\"radio\" onclick=\"priorityclicked();\" class=\"prinorm\" value=\"0\" name=\"pri.").append(fileIndex).append("\" "); if (priority == 0) - buf.append("checked=\"true\""); + buf.append("checked=\"checked\""); buf.append('>').append(_("Normal")); - buf.append("<input type=\"radio\" value=\"-9\" name=\"pri.").append(fileIndex).append("\" "); + buf.append("\n<input type=\"radio\" onclick=\"priorityclicked();\" class=\"priskip\" value=\"-9\" name=\"pri.").append(fileIndex).append("\" "); if (priority < 0) - buf.append("checked=\"true\""); + buf.append("checked=\"checked\""); buf.append('>').append(_("Skip")); showSaveButton = true; } @@ -2803,9 +3046,16 @@ public class I2PSnarkServlet extends BasicServlet { buf.append("</TR>\n"); } if (showSaveButton) { - buf.append("<thead><tr><th colspan=\"4\"> </th><th class=\"headerpriority\"><input type=\"submit\" value=\""); - buf.append(_("Save priorities")); - buf.append("\" name=\"foo\" ></th></tr></thead>\n"); + buf.append("<thead><tr><th colspan=\"4\"> </th><th class=\"headerpriority\">" + + "<a class=\"control\" id=\"setallhigh\" href=\"javascript:void(null);\" onclick=\"setallhigh();\">") + .append(toImg("clock_red")).append(_("Set all high")).append("</a>\n" + + "<a class=\"control\" id=\"setallnorm\" href=\"javascript:void(null);\" onclick=\"setallnorm();\">") + .append(toImg("clock")).append(_("Set all normal")).append("</a>\n" + + "<a class=\"control\" id=\"setallskip\" href=\"javascript:void(null);\" onclick=\"setallskip();\">") + .append(toImg("cancel")).append(_("Skip all")).append("</a>\n" + + "<br><br><input type=\"submit\" class=\"accept\" value=\"").append(_("Save priorities")) + .append("\" name=\"savepri\" >\n" + + "</th></tr></thead>\n"); } buf.append("</table>\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 "<img alt=\"\" height=\"16\" width=\"16\" src=\"" + _contextPath + "/.icons/" + icon + ".png\">"; + 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 "<img alt=\"" + altText + "\" height=\"16\" width=\"16\" src=\"" + _contextPath + "/.icons/" + icon + ".png\">"; + return "<img alt=\"" + altText + "\" height=\"16\" width=\"16\" src=\"" + _contextPath + WARBASE + "icons/" + icon + ".png\">"; + } + + /** + * 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("<img alt=\"").append(altText).append("\" src=\"").append(_imgPath).append(image).append(".png\""); + if (titleText.length() > 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 0000000000000000000000000000000000000000..8098520b1cb2b485c6a22bbc2313fa51db9470c0 --- /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 + * + *<ul> + *<li>0, 1: Name + *<li>2: Status + *<li>3: Peers + *<li>4: ETA + *<li>5: Size + *<li>6: Downloaded + *<li>7: Uploaded + *<li>8: Down rate + *<li>9: Up rate + *<li>10: Remaining (needed) + *<li>11: Upload ratio + *<li>12: File type + *</ul> + * + * @param servlet for file type callback only + */ + public static Comparator<Snark> getComparator(int type, I2PSnarkServlet servlet) { + boolean rev = type < 0; + Comparator<Snark> 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<Snark>, 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<Snark>, 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 + * + *<ul> + *<li>0, 1: Name + *<li>5: Size + *<li>10: Remaining (needed) + *<li>12: File type + *<li>13: Priority + *</ul> + * + * @param servlet for file type callback only + */ + public static Comparator<FileAndIndex> getFileComparator(int type, I2PSnarkServlet servlet) { + boolean rev = type < 0; + Comparator<FileAndIndex> 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<FileAndIndex>, 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<FileAndIndex>, 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 b251fb72ea4d8fb0edfaf5ee678938423d66c58e..fff1a696e900c3eaadac33b3278299d6b9b71c00 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 0000000000000000000000000000000000000000..0cec9e5c979f17c6cd488a5badb66a7e7ad3d7f3 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 0000000000000000000000000000000000000000..fbc906bf92b1b07e21b7cfead20bf8e788945b4e --- /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 0e52f17a1c938d08c31c060be48b314dad4fc450..1bfbada56a21f707c9d7098f3742a9abfa5593fa 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 7dd7308456dfde7207217d4fb970f63e3f5972ba..0000000000000000000000000000000000000000 --- 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 4813fa2a17a9504fff0cc9bc7e4bdf8cb48ac437..858458e3ffe8516162da4c6e384c41e2eddcfb48 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 0597db5f19865e77cf20d61fb026b5c9a2f2d1f0..b49f51a07f3cd117b6449dae579b5472a8cc1b04 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 bb7de443d01ab0406a78aeb6458da964f0e89e4c..fd9a9a45f70822c26c058c7b3313a8251ed80239 100644 --- a/apps/jetty/build.xml +++ b/apps/jetty/build.xml @@ -1,9 +1,9 @@ <?xml version="1.0" encoding="UTF-8"?> <project basedir="." default="all" name="jetty"> - <property name="jetty.ver" value="8.1.15.v20140411" /> + <property name="jetty.ver" value="8.1.16.v20140903" /> <property name="jetty.base" value="jetty-distribution-${jetty.ver}" /> - <property name="jetty.sha1" value="41ec2b5e5605c038fb28d1f118669f06b4479e71" /> + <property name="jetty.sha1" value="5440b33a722d82b746b9ce50168bfce3c22af349" /> <property name="jetty.filename" value="${jetty.base}.zip" /> <property name="jetty.url" value="http://download.eclipse.org/jetty/${jetty.ver}/dist/${jetty.filename}" /> <property name="verified.filename" value="verified.txt" /> 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 5dceaf49ce9bf3acfeee7254147cae77df59b25e..ce1acb18bcbf14740de5d705e854c1124005197f 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 d83efa1931f794528c3a8cecb98f24b1f9cc3a3e..d3339b061d852a18edc588a0268ee8bb2aff9f83 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 330344363ead1e70b00ededaa974c8195e1bf667..30189c766c563200ce13ff1519a37092aee041e4 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 f2554fa395191179b6ac56938fb9e7c2331174f7..a9afd7cc3a610558c910eccb3f3291fc5f4432a7 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 a00233fbf7670d1c5a74812f9444caa38668fb18..f0eb0c51a413ca1607a580d30a3d321557d64ddd 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 5004187daadd66db8fbfcf793a66dc805a98f87f..240c6705204a7a601db7de2a33c6ef754e21d66e 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 bafcf7293c3d245ab443e7e87c996611e8aa8fb4..e5bde43b82e74f0fa1b09f4072d1a7a7b5854dc2 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 ded9cf48906ffa961558cd90e1e40a7462e1301f..ae8ac5577764dbcb31ce55877eca68673274bdc2 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 556dc68fe2398ea56bdc2e694e2e6897ab61629f..eb2fa57db19be8a66f83401be190982302bc5eb4 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 d30991166a5b7b7a0f74ab455d0083af52daf027..6fd7146ed492621dadfc059155915f99209ca54c 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 170d66fc2b08954d420a3ec3b74fc2bad94d4e12..5c3c3460f2f748b302702248d6cd115e140c1c75 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 17ebd18e7a108b039c9c5e93cb0e5b31aba27a40..85fd7e0982c914521f166d521437364c285a7aae 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 5f922235da011efd2eb1ac6826805a68041db35f..1e485de6d7bf20d3af4f2a0725ce401a4e293132 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 9c16152913ecb7fa8ace96f827d300d2262e84f7..8ea5baa136932c0ec385691beaeef623059b57af 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 efe9f4f819dacf5c8c52a82697b8e0816714cabe..7f13242d3dab6f000ec75610012ef5abf52a8358 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 9fae3ab935a7bedce85f1dc42224368013084e36..712830f59a8c0cca05d73d9ed35ef66a2f4cd299 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("<p><b>Total Leasesets: ").append(leases.size()); buf.append("</b></p><p><b>Published (RAP) Leasesets: ").append(netdb.getKnownLeaseSets()); - buf.append("</b></p><p><b>Mod Data: \"").append(DataHelper.getUTF8(_context.routingKeyGenerator().getModData())) - .append("\" Last Changed: ").append(new Date(_context.routingKeyGenerator().getLastChanged())); - buf.append("</b></p><p><b>Next Mod Data: \"").append(DataHelper.getUTF8(_context.routingKeyGenerator().getNextModData())) - .append("\" Change in: ").append(DataHelper.formatDuration(_context.routingKeyGenerator().getTimeTillMidnight())); + buf.append("</b></p><p><b>Mod Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData())) + .append("\" Last Changed: ").append(new Date(_context.routerKeyGenerator().getLastChanged())); + buf.append("</b></p><p><b>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("</b></p><p><b>Known Floodfills: ").append(ff); buf.append("</b></p><p><b>Currently Floodfill? "); @@ -415,7 +415,9 @@ public class NetDbRenderer { // shouldnt happen buf.append("<b>" + _("Published") + ":</b> in ").append(DataHelper.formatDuration2(0-age)).append("???<br>\n"); } - buf.append("<b>" + _("Address(es)") + ":</b> "); + buf.append("<b>").append(_("Signing Key")).append(":</b> ") + .append(info.getIdentity().getSigningPublicKey().getType().toString()); + buf.append("<br>\n<b>" + _("Address(es)") + ":</b> "); String country = _context.commSystem().getCountry(info.getIdentity().getHash()); if(country != null) { buf.append("<img height=\"11\" width=\"16\" alt=\"").append(country.toUpperCase(Locale.US)).append('\"'); diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ProfileOrganizerRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/ProfileOrganizerRenderer.java index e2cd422705e6188bea878a31d40c2298ecb7033d..1a5461246233f301712523774730f01d93152ca0 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ProfileOrganizerRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ProfileOrganizerRenderer.java @@ -10,7 +10,7 @@ import java.util.TreeSet; 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.RouterContext; import net.i2p.router.peermanager.DBHistory; import net.i2p.router.peermanager.PeerProfile; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ProofHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ProofHelper.java index 70ab9a1af68e147bb636a3d7f1f3963e65992b96..01ed5836ad11ae81d9d2febc8c5828965d1f329f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ProofHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ProofHelper.java @@ -3,8 +3,8 @@ package net.i2p.router.web; import java.util.Date; 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.Signature; /** diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java index c60ded011077de2304d9d33fbf8d08d893f86465..953fdb34f74cd8f7a7e848b05e54292c0db6924f 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/SummaryHelper.java @@ -15,8 +15,8 @@ import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; 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.CommSystemFacade; import net.i2p.router.Router; import net.i2p.router.RouterContext; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/TunnelRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/TunnelRenderer.java index 1b2b7b23026716892f8f4a47fc7501a7620c60e0..9fff49d1e5de8dd6b45b6534a924a4c0cd3e2317 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/TunnelRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/TunnelRenderer.java @@ -11,7 +11,7 @@ import java.util.Map; 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.TunnelId; import net.i2p.router.Router; import net.i2p.router.RouterContext; diff --git a/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java b/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java index 102bfa6cc18bcaaf63d8bcdf8ea10a3a676a1b55..51b8baef730071efd4f5031497b5aab72eaac95c 100644 --- a/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java +++ b/apps/streaming/java/src/net/i2p/client/streaming/impl/PacketQueue.java @@ -44,7 +44,7 @@ class PacketQueue implements SendMessageStatusListener { private static final int FINAL_TAGS_TO_SEND = 4; private static final int FINAL_TAG_THRESHOLD = 2; private static final long REMOVE_EXPIRED_TIME = 67*1000; - private static final boolean ENABLE_STATUS_LISTEN = false; + private static final boolean ENABLE_STATUS_LISTEN = true; public PacketQueue(I2PAppContext context, I2PSession session, ConnectionManager mgr) { _context = context; @@ -267,6 +267,20 @@ class PacketQueue implements SendMessageStatusListener { _messageStatusMap.remove(id); break; + case MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET: + // Ideally we would like to make this a hard failure, + // but it caused far too many fast-fails that were then + // resolved by the user clicking reload in his browser. + // Until the LS fetch is faster and more reliable, + // or we increase the timeout for it, + // we can't treat this one as a hard fail. + // Let the streaming retransmission paper over the problem. + if (_log.shouldLog(Log.WARN)) + _log.warn("LS lookup (soft) failure for msg " + msgId + " on " + con); + _messageStatusMap.remove(id); + break; + + case MessageStatusMessage.STATUS_SEND_FAILURE_LOCAL: case MessageStatusMessage.STATUS_SEND_FAILURE_ROUTER: case MessageStatusMessage.STATUS_SEND_FAILURE_NETWORK: @@ -280,7 +294,6 @@ class PacketQueue implements SendMessageStatusListener { case MessageStatusMessage.STATUS_SEND_FAILURE_DESTINATION: case MessageStatusMessage.STATUS_SEND_FAILURE_BAD_LEASESET: case MessageStatusMessage.STATUS_SEND_FAILURE_EXPIRED_LEASESET: - case MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET: case SendMessageStatusListener.STATUS_CANCELLED: if (con.getHighestAckedThrough() >= 0) { // a retxed SYN succeeded before the first SYN failed diff --git a/build.xml b/build.xml index 873284998af3356231de4d296e4380dd0e5745db..9f3364fe4e2d002e7c6b2cff37fd0a15afbe8ba5 100644 --- a/build.xml +++ b/build.xml @@ -548,7 +548,7 @@ windowtitle="I2P Anonymous Network - Java Documentation - Version ${release.number}"> <group title="Core SDK (i2p.jar)" packages="net.i2p:net.i2p.*:net.i2p.client:net.i2p.client.*:net.i2p.internal:net.i2p.internal.*:freenet.support.CPUInformation:org.bouncycastle.oldcrypto:org.bouncycastle.oldcrypto.*:gnu.crypto.*:gnu.getopt:gnu.gettext:com.nettgryppa.security:net.metanotion:net.metanotion.*" /> <group title="Streaming Library" packages="net.i2p.client.streaming:net.i2p.client.streaming.impl" /> - <group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:org.cybergarage.*:org.freenetproject:org.xlattice.crypto.filters" /> + <group title="Router" packages="net.i2p.router:net.i2p.router.*:net.i2p.data.i2np:net.i2p.data.router:org.cybergarage.*:org.freenetproject:org.xlattice.crypto.filters" /> <group title="Router Console" packages="net.i2p.router.web:net.i2p.router.update" /> <!-- apps and bridges starting here, alphabetical please --> <group title="Addressbook Application" packages="net.i2p.addressbook" /> diff --git a/core/java/src/net/i2p/I2PAppContext.java b/core/java/src/net/i2p/I2PAppContext.java index 8357a485ac14110622603bf06e90e7c1153c3942..79c3108f78d9c4b27d530056819aa8ce053129b5 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 0000000000000000000000000000000000000000..8d22284804bd835ef3793f252c6a6d0d353f8bf9 --- /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 e1ac37aadd528bbc8842bc16a7dec83ef85b8c61..a80e0a99eae998bacdf7f6fa2690ecb1bbd1f6cb 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 f078aaa5c036d9ff6d0a9d20c2f71830f59c7a88..c23a215232f59995d4d1b6834c983727163b8b3f 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 09cc3ed781b213980cb2bb804d7d171b67bd2c73..c492dc663066419c56009a59ee0063dff9fe51b7 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 d3dc3685831d09b175077cd041f9ec71bb906f40..ac51f8128cf0acd46ab09206156259a3c9fee9a6 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<? extends DataStructure> sortStructures(Collection<? extends DataStructure> 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<DataStructure> rv = new ArrayList(dataStructures.size()); - //TreeMap<String, DataStructure> tm = new TreeMap(); - //for (DataStructure struct : dataStructures) { - // tm.put(struct.calculateHash().toString(), struct); - //} - //for (DataStructure struct : tm.values()) { - // rv.add(struct); - //} - ArrayList<DataStructure> rv = new ArrayList<DataStructure>(dataStructures); - sortStructureList(rv); - return rv; - } - - /** - * See above. - * DEPRECATED - Only used by RouterInfo. - * - * @since 0.9 - */ - static void sortStructureList(List<? extends DataStructure> dataStructures) { - Collections.sort(dataStructures, new DataStructureComparator()); - } - - /** - * See sortStructures() comments. - * @since 0.8.3 - */ - private static class DataStructureComparator implements Comparator<DataStructure>, 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 4b84ed106e0775a620f2b112794a1486ac384968..2adc066bfee05f4b1243e3030cbcfb7e992cd9c4 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 b0a8a845b7932ed3d7f6f2551fb7031eec4016eb..309799a01fa7099bbe85a589d5e333bd297a192c 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 f10248189ec18674bc312b16f6fbdc330b16cc5c..163edcc3f5dc791dd3be60042021b5b1c2bec74e 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 012310b9feee3dcf00f9dc991ebdc69198e3b50f..42a26ef367f6beb8a95e42040ea5d5a88b654272 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 367089442a8fcda8346a039709d9947bd0323318..20e2218ee5548421c2011319ee1ecfb887b7110a 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 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 - */ - 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; - } +public abstract class RoutingKeyGenerator { /** - * How long until midnight (ms) + * Get the generator for this context. * - * @return could be slightly negative - * @since 0.9.10 moved from UpdateRoutingKeyModifierJob + * @return null in I2PAppContext; non-null in RouterContext. */ - 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); + public static RoutingKeyGenerator getInstance() { + return I2PAppContext.getGlobalContext().routingKeyGenerator(); } /** - * Generate mod data from _cal. - * Caller must synch. - * @since 0.9.10 + * The version of the current (today's) mod data. + * Use to determine if the routing key should be regenerated. */ - 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; - } + public abstract long getLastChanged(); /** - * 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. - * - * @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 + * Get the routing key for a key. * * @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); + public abstract Hash getRoutingKey(Hash origKey); - 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 a8fcbb208196c88c8310f5b3e33b382a91057e39..07b8969e3c5499746a4318928f80ecf74db16bb0 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 afa22de9d25e128461d69487b00aaa2770f311ed..331ee3dad85ed0d53b285f08681602214012aba2 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 ef0e9419468c1bddbfcb7066e259f81062a7c3a5..67a67ffa4722aada3d2f691c97d8049fa61bebf1 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 0000000000000000000000000000000000000000..e5c271e234dbf44b87728fdfeff6192292711818 --- /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 + +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<html><head> +<title>_("Warning: Eepsite Unreachable")</title> +<link rel="shortcut icon" href="http://proxy.i2p/themes/console/images/favicon.ico"> +<link href="http://proxy.i2p/themes/console/default/console.css" rel="stylesheet" type="text/css"> +</head> +<body> +<div class="logo"> + <a href="http://127.0.0.1:7657/" title="_("Router Console")"><img src="http://proxy.i2p/themes/console/images/i2plogo.png" alt="_("I2P Router Console")" border="0"></a><hr> + <a href="http://127.0.0.1:7657/config.jsp">_("Configuration")</a> <a href="http://127.0.0.1:7657/help.jsp">_("Help")</a> <a href="http://127.0.0.1:7657/susidns/index">_("Addressbook")</a> +</div> +<div class="warning" id="warning"> +<h3>_("Warning: Eepsite Unreachable")</h3> +<p> +_("The eepsite was not reachable, because it uses encryption options that are not supported by your I2P or Java version.") +<hr> +<p><b>_("Could not connect to the following destination:")</b> +</p> diff --git a/installer/resources/proxy/encp-header.ht b/installer/resources/proxy/encp-header.ht new file mode 100644 index 0000000000000000000000000000000000000000..9d53fb70760894f79a2e1d830e20f2c10fc44bce --- /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 + +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> +<html><head> +<title>_("Warning: Outproxy Unreachable")</title> +<link rel="shortcut icon" href="http://proxy.i2p/themes/console/images/favicon.ico"> +<link href="http://proxy.i2p/themes/console/default/console.css" rel="stylesheet" type="text/css"> +</head> +<body> +<div class="logo"> + <a href="http://127.0.0.1:7657/" title="_("Router Console")"><img src="http://proxy.i2p/themes/console/images/i2plogo.png" alt="_("I2P Router Console")" border="0"></a><hr> + <a href="http://127.0.0.1:7657/config.jsp">_("Configuration")</a> <a href="http://127.0.0.1:7657/help.jsp">_("Help")</a> <a href="http://127.0.0.1:7657/susidns/index">_("Addressbook")</a> +</div> +<div class="warning" id="warning"> +<h3>_("Warning: Outproxy Unreachable")</h3> +<p> +_("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).", "<a href=\"javascript:parent.window.location.reload()\">", "</a>", "<a href=\"http://127.0.0.1:7657/i2ptunnel/index.jsp\">", "</a>") +_("If you continue to have trouble you may want to edit your outproxy list {0}here{1}.", "<a href=\"http://127.0.0.1:7657/i2ptunnel/edit.jsp?tunnel=0\">", "</a>") +</p> +<hr><p><b>_("Could not connect to the following destination:")</b></p> diff --git a/installer/resources/themes/snark/light/snark.css b/installer/resources/themes/snark/light/snark.css index cf61bf1029719fb21dfbece0f4f3bb3d102bef6b..c195775c06038574fe94657eb831bebd0f4bceb4 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 4a57291f5e781807e0d28d3ef48b77ea5bac4fdf..f2e09b4f94cb3aecc395c33c8879e767e9c3983b 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 6afd7c428b2f718216647de4454c01299edfea23..ddbcd8e8c5e72897488b386f337bf7b2ac00efa8 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 10533cfac830557ee0bd65b2d85f0c1ac70c43c3..805c8cc4d7ea4d40b7cfd55a8c0113b0d7c3ade1 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 37ff20186c661d38b40b4d9df7aeaf3f1f04a3b1..790e376227234a03a9439fb63225e7500ddd61a5 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 8e48a7439968b04f09c1d03483d6e9e5def9dccd..b34912e8d817a902c15ec4ce8bb29376ce7ce3a5 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 960e495faf2e290410e2ecef8d2f0b5262a6a1a0..ca0f94d08abb79eddaa71201f38210460268bf4c 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 346bb5f8d9442b2555647834dfff4f4c246c7374..6dc7ca2d752e84c327b5e89ffb75f9589be5eaca 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 22f9ce5ddce74a7eab8f0d8be6b90de132343016..390b013ec5e9c6f35109929367c82e155e433e55 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<Hash>) DataHelper.sortStructures(peers); + peers = (Collection<Hash>) 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 0000000000000000000000000000000000000000..69e6aaceca317488cce3034b2d7e705dc5d7740a --- /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 0000000000000000000000000000000000000000..2f466ca055d80a18a42cbc5ae7ca2bc7acdaa7e8 --- /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 0000000000000000000000000000000000000000..0c40fa3edab3716f53e4e19797c411e96ab82ff5 --- /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<? extends DataStructure> sortStructures(Collection<? extends DataStructure> 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<DataStructure> rv = new ArrayList(dataStructures.size()); + //TreeMap<String, DataStructure> tm = new TreeMap(); + //for (DataStructure struct : dataStructures) { + // tm.put(struct.calculateHash().toString(), struct); + //} + //for (DataStructure struct : tm.values()) { + // rv.add(struct); + //} + ArrayList<DataStructure> rv = new ArrayList<DataStructure>(dataStructures); + sortStructureList(rv); + return rv; + } + + /** + * See above. + * DEPRECATED - Only used by RouterInfo. + * + * @since 0.9 + */ + static void sortStructureList(List<? extends DataStructure> dataStructures) { + Collections.sort(dataStructures, new DataStructureComparator()); + } + + /** + * See sortStructures() comments. + * @since 0.8.3 + */ + private static class DataStructureComparator implements Comparator<DataStructure>, 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 0000000000000000000000000000000000000000..fac87d49818e7f2dc8ea519d67398422660b638e --- /dev/null +++ b/router/java/src/net/i2p/data/router/package.html @@ -0,0 +1,7 @@ +<html> +<body> +<p> +Classes formerly in net.i2p.data but moved here as they are only used by the router. +</p> +</body> +</html> diff --git a/router/java/src/net/i2p/router/Blocklist.java b/router/java/src/net/i2p/router/Blocklist.java index 093a6fdc5ac02a43a6b6165c17eacd0d5bcd573e..a6fe1d20930f03e7c577ee90363a870a999c9ff8 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 9c7d92338d147feb1ae22d1f0eac2bf242fbf03e..59a22eb9ca92e9e5188e7200b1be78bc652a0b9d 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 c1c9832cdcaf6d62918820ac5874b01bf6bc23f9..62e2074a5f2ddb1161698a863a8d45768495f630 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 28296cebba446e79aa01610b1dba399eef9c9ec3..fb4c2a632c2f85f87fbabee70c8d0db853794a09 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 ce4992f8bd54deef53e963b2b4a4a6173e71dcd6..2807e8fe7fc37683729d3db8179b2241a04fc4e2 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 abfc566df9c5748e16ec866862159017df64ec4b..849e54f47fca006f333fa63ccf67b600916ee6fc 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 5e8cb47608b005235a163a9ae1d1a716fd5cf5c5..abcd32e3d05e505d936d18d32946a42c7962ad61 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 7fc3c5082609f52b8d50e4b9342320f8d03be924..fdd1fd30fb204f2b154aaebdfb7f0e4450fce303 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 633416640cbf9c2a4221da05d9e7c41e0eb494e8..54434ac4b73b705d05ce6439b9e9b55ec7ef0ec2 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 a3e71ee8e28392ef942297c84213e5fb5fb45bcd..920eec7d298c34f8888bc79f57da829cd18b22e0 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("<td>"); - 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 106efe00dd40b8849f9cc608cfd68e9595d97dd7..8fd08eb795f47e7cfb797fd6cc349754543b1aab 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 1f8a38af68571d41d15eaead9713521197d0bf04..fd5c65775953215c6623473f4d9979c3e6e1a55b 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<Runnable> _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 805e7bbbc30702de796b4de815b0ad82104a1efb..275e4d85a4667fd1522ef496b52e07b47c5bf488 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 cdcc0008cfced816ce40d533948c0815d4b7b5cb..382552ce68728ac2c8c69cf24cdd1c24e2d5dbf2 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 be08388ba2c195dc4fc89e364819ba1b12307968..911365b8e80a49726f22f9571dc953b81444b05f 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 11b9419f382784ef69a46aca033b83a90107c8d2..460d7a7125ecde2f4049c5157c90822c5d71c6fd 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<Hash, RouterInfo> _routers; - private RouterContext _context; + private final Map<Hash, RouterInfo> _routers; + private final RouterContext _context; public DummyNetworkDatabaseFacade(RouterContext ctx) { _routers = Collections.synchronizedMap(new HashMap<Hash, RouterInfo>()); @@ -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 9fe299e0c3ec6dc4291fc6b53fda6f16378f9105..4970a589f908daa3381cd4a2a62465159b834090 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 c44ec2f055399a6106ed88e77dea961bb332c8c0..3a762b6fd704800e579d319e63c0e1b063754371 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 9392526b3ff9f102342424dc7971d25fa17da6fb..9a22ec6e842d1bce895df7898bfd2faccadf6ef4 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 5040606e8d7177b06982b32de5d85ebb91b8ccb3..d9f7a0a516324d478c1feb055ec285344c951207 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 6d0cfdbcf2aebd8c4681f3b8a39edfb75f8bf271..cc7a337cc5ef7147ea021bbaab8c47e1d5995a87 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 685e18512c03b5939a9a5204b348162fa2681cf0..9931444aaf10f147f37c83d11621383e6cccab8a 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 f6a29355b716e2ddeb3e1641dd2b0755fff8cb2d..8b235260e30f2d1c20e04a81e323b2262bd22da8 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 ebcde49c693bbb9912dacdaf102a51384985e041..f916e976f1ce30b261652692a1cb68bfe5a4b6bc 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 c2ea790a5a5adec972f1b62cef481add81e25dc9..b52df8e1790906561eaf82e48370d39a76b62e56 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 88122c0e27b39dd625b3496a5a8285734c6fd25f..aff802b7724e96f1f0b58612868e9f77cf4b5571 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 8f3a193a5ae9430aa613446a219bba274a0d6d04..46f985a9172c45640bfad53aa59391bc46020e78 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 8ef121d0605e8a4260335827cb8db82484f01265..a07ac8c70829b7687a7e3f1d07d59cb18723e9ae 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 e4c6ce71af9727503c3bf25f3ea8445dcfc1cd14..99d8c62658a6c959a1e3369c9d6d7873e2bd19ac 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 99c4beda29ccb80e0fb5c1fbf679066c2e167fe7..d94475ff20597b1c1a8c11bfa2d30a6e918837f0 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 24a7bc40a169e9a516d1840fe9187a363f28cd00..9358092260ea0e75d9e0f2c3b72718096d18129c 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<Hash> _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<Hash> 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<Hash> 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 fdf47bf3ccb2db8b6e0f740ef2bf6a237d7b71a9..6870edcd5833239955c8a5a189556b08e9fa8433 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 f66b50020a770914a7dc9feec16073a99703eb27..085c0c921a7bcea39b43d24425469603602b99ba 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 0d03d7d4fed88baaabf0889ab22f3f06eb60a2e3..eceb4b576e4809b62e83b509d9416318db04e982 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 f623022ca7e134adda1e224be3818ae6689b3028..ffb6d77f00f4106a84bef18b9303b9a4f93e4966 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 a69a2bc9063810c78c899f2ea79ce68232c389f2..6bd687185ca833693a982d6084fd5579b3a8d021 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 13d76368877357176c371ba621fb8166b5aa454a..ac283109caa317bd9e87174d3e25b34284cdcfab 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 7a2fa777096ae2d97e37b6e3efd13321b91f45de..490a45a2181b88f4b957a31ea3657c96898cc0bd 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 b4d108d024bbca3744bc35f5ed7976d312e845dd..5507710cdc0cf2e0000135767d25a1b39fe77733 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 15ae87a8bc06d08b66db151c03d531d3513825e6..ff9d3d50115ca1b8de222a1481b5f3bfd5856945 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 abbb67ed624a99aff4192008fd090c84dd5c6177..1784f9434d075a7f892deea38f934a3acb69a109 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<Hash> counter; + private final Map<Hash, Destination> 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<Hash>(); + this.badDests = new LHMCache<Hash, Destination>(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 f73eb00fef5aa41f8f86568460cf2ffa2ecf4863..4eb23e13cbd9dbf8d4cbdd814b0d29f6c17660b1 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 620679b7e498f9766bc79f9d57b7a0aa1129347c..6c60e3a41c82456028cc72f655b6d369e794b481 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 852a5c7920c2366fc3e4557c9722176415431daf..20dd2a075433698b05cc00eaacd349516533f23b 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 54a93831965866c70c25051aa87cf07a2eae5386..f0edded8b0d63888bed92992069459cd4b032fbd 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 3d756529d833aa60b65c372df52d2b7901c23c42..30e8e4b18ac35e2e735600ff0e40bd0787d94456 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 8d8e5f19d9cc45b46fda1a374a126f1dc46428f0..a8354b096fcbfec3aaa95d649d26c1fb5c0ea616 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 106c8ad4dcf35444522aac8c91b00387c2dcf0ea..61bd1b645d61e66a68e3ebf60c110f7e2a071c66 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<Hash> _pendingPeers; + private final Set<Hash> _pendingPeers; private final Map<Hash, Long> _pendingPeerTimes; - private final HashSet<Hash> _attemptedPeers; - private final HashSet<Hash> _failedPeers; - private final HashSet<Hash> _successfulPeers; - private final HashSet<Hash> _repliedPeers; + private final Set<Hash> _attemptedPeers; + private final Set<Hash> _failedPeers; + private final Set<Hash> _successfulPeers; + private final Set<Hash> _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<Hash>(_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 22602b497a6d7d787dceab1f2145be80e6955a55..63cc1c18f8947384830e6eaaf3b88c7f6d40d99b 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 711510fb4d39c6b1dbed94e6bde557a5019652a4..62a878d6dedf70988315cebba31198febc6f06e1 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 cced2f5466511db1b0efac7acf1a4116faeb58bf..7938ab072c200b7f957ec8f4d444a6695bd11063 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 311f2ba8f1c7a663ad420b5efb82f302dbd1c777..81bef6950aceeabf9229ec81d8bca6aa1f8f856f 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 6901f0afa6911af69dc08362f0b4652da48c70b6..352c19cd710a3b586adac18bb2651cf5ccfe97bf 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 2a7318d25f08e64da79cb6fb119e0677a4bc7f93..11603efb989bc52d827e533157a5efd92a8e0096 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 0000000000000000000000000000000000000000..0159eb02017f56d6f9b5230a143a01c6a490ee95 --- /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 ef7b1de349bbd7578e45aa763c7befca998cbbe4..d2de16d9ba5c4a60c8fa51f561824995b91d4b0e 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 24176a208872b882b9605c7dd5045b1fc65abf87..466c63ec190e0507eea0625c2f829eef94f1088c 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 a224f576ac93e4eda765f09356cfe41879eb30a2..7e89cab2550be77b9591e0add5e634a862f8a6ed 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 3af4164b5eb8e0f680fca7525dfd7da05e721e4e..305869badae5c22bd6754d0709d67aa015e02b1e 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 bf8d36a7729b4c8c16d2319325207115da1fa25c..e512f9ea381819409c6ead9b27d038f9dcf19d6a 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 7ac5254f0e936484681751c3503ab67ebc38865f..33f4010236e1f76b76e85a10f44b6f30c3bf7686 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 3b88a2d9cd179e485ae098a2bfc3145e3b625693..566204068b865e436b978109d5e480dfca2687f4 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 498cbe6658be5ecd009ba3e120d075fbd7aba3be..83f2c7a7efee30d1b3f70ad973a551227b3ec383 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.keys2 file format: This is the + * same "eepPriv.dat" format used by the client code, + * as documented in PrivateKeyFile. * - * router.keys file format: Note that this is NOT the + * Old router.keys file format: Note that this is NOT the * same "eepPriv.dat" format used by the client code. *<pre> * - 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())); + Object keypair[] = getContext().keyGenerator().generatePKIKeypair(); + 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 = getContext().router().createCertificate(); + Certificate cert = createCertificate(getContext(), signingPubKey); 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]; 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 9a560da5f42ecebda2508d463e60b1b970985f7a..e02ef04aa1ed2ce4f26ea30c6da511def9e740fd 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 b611114d06bb1953addb61e885873c851c920e00..ef0826cf5fbcb99525f687249bd0319c581260ed 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 50d840d4322be182f20990c1d1d584e909cb49c7..672bf702aab4eb6b79ed5da0d54dbd21784d6f01 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 d44b2a53971054454d61dac7934d983765163b1e..23149c7599697990cde79ee146d655eafea549cc 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 c2ded9b49131698f975243a8be8c3b77e66f53ce..5d86a291b05b0c854eb18f673b9553392c25f758 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 c29dd257725eb251c538f9124f2b42b27b13bb36..6a14a51346d510e4da9935e71744b4061d1af620 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 2952c2eda430fc37df6a487414ea52a48995e838..5b367620be13d277279258cbc486b23c67d3239f 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 bc3ad9543a820e44727230493fb3ab0a4364382c..b9e38f9da3bdb876676410f50ed73c464cdab197 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 e232fa7e46f6b3a944d0553fad3c91a88c54371d..459c846806a0f188bfb4649f0bd40127fc0aeaba 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 9a6d80d1ca63945617f9dcbe61927b155e14c035..b8437efd1f72d46031032d36a9c0c761909f0e1b 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 194ae3016d6df0f2227607f39329bba52b752cf2..6dd93b4c59244fa39ab44e403978c174bd980f33 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 d9e0c37985a669087d370e769efdd06c3df3f90b..75e843707cbffa222bb096f379cdad206e19d636 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 1682abd91ce0c63ce472c142c626f41503fba6c7..648119f91db09837fdc83fb8e0bfa4c12cc43331 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 0d49a656aa01af69a95c9b27804e836461008e3a..061bbc09e878270a5b16b0fc0ede5424c6d46c10 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 bc6e37bc4d05289d2dacc1144b9e95fc75a46eea..4697e15f6d83a7a4ac9074c191c8591bd6f33caf 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); + 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() + "got the RI size: " + sz); + _aliceIdentSize = sz; + _state = State.IB_GOT_RI_SIZE; + + // 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 = 16-rem; + padding = AES_SIZE-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 (_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() + "alice ident size decrypted as " + _aliceIdentSize + + ", making the padding at " + padding + " and total size at " + + _sz_aliceIdent_tsA_padding_aliceSigSize); + } - if (_sz_aliceIdent_tsA_padding_aliceSig.size() >= _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); - - byte toVerify[] = new byte[_X.length+_Y.length+Hash.HASH_LENGTH+4+4]; + // 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[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 d4fd7521acaab9ba5719aa4be396cf2e8e0ef8dd..b6f539c962c014db3e417a0caeedae749f4cc170 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 2b2ae6a442acb257b44c09b2f536b49e8fdaf302..a9752a286250a2ec406c749c25aed8f71257b184 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 0ad8d88a9460c6b5f999f94bdda8c2d3db9f3c9e..f1269c772f09707b5732c1f2c914653ab6484163 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 c8b7c96309e414a9b14954f1bf090fd3e3b5aa19..b8c1a5f7e1e773ed96f76e5531a1858e30ed7db5 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 7d935530263069a1cd67a087d8afbd7f987e96b9..b7cb7d6cbe6034a15451dd7c772e11ce0ad4b798 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 267c4fc2a5b6846f8ac48b67752c72ffb49be768..96c4282c8cdebd371049cb034d236681e5d5a77f 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 abdd59e15dab5871ded6b824979fc5e9b6e0a56c..6d1f1775549bb7f9f16c6322252f631b40bb9508 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 78e41112f27d07e52edc950f4bbd501d06fd8ad1..f69ecb0c521deba9965bce5e290a6a69a8d1c983 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 4c11aa0f4700c0e48532eca4d73522901345b4d0..fcaae36dd05f10a1ac301e6466f42f358e3f4da9 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; + } + + /** + * @param fragment 0-63 + */ + private static long mask(int fragment) { + return 1L << fragment; } - public int fragmentCount() { return _fragmentsReceived.length; } + 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 37db45880148bc5aaa060610382cc9a63701fe23..7e55cf1ce3e19dcdc769a4369187eeb422711aa0 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 ba4375dbdb8f8ec34623bd162a7c38df0a7b9d5c..9f519f5ae7ee84cda6a75efdf6e5372ea2c0522e 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 034f3e60548e10671f2ca013e842cbe211a9e2ed..ab91478f171b5e7642370e2d85d0ae42638bb4ee 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<UDPPacket> getNextVolley() { PeerState peer = null; - OutboundMessageState state = null; + List<OutboundMessageState> 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<UDPPacket> 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) ) { + private List<UDPPacket> preparePackets(List<OutboundMessageState> states, PeerState peer) { + if (states == null || peer == null) + return null; + + // ok, simplest possible thing is to always tack on the bitfields if + List<Long> msgIds = peer.getCurrentFullACKs(); + int newFullAckCount = msgIds.size(); + msgIds.addAll(peer.getCurrentResendACKs()); + List<ACKBitfield> partialACKBitfields = new ArrayList<ACKBitfield>(); + 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<Long> remaining = new ArrayList<Long>(msgIds); + + // build the list of fragments to send + List<Fragment> toSend = new ArrayList<Fragment>(8); + for (OutboundMessageState state : states) { int fragments = state.getFragmentCount(); - if (fragments < 0) - return null; - - // ok, simplest possible thing is to always tack on the bitfields if - List<Long> msgIds = peer.getCurrentFullACKs(); - int newFullAckCount = msgIds.size(); - msgIds.addAll(peer.getCurrentResendACKs()); - List<ACKBitfield> partialACKBitfields = new ArrayList<ACKBitfield>(); - 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<Long> remaining = new ArrayList<Long>(msgIds); - int sparseCount = 0; - UDPPacket rv[] = new UDPPacket[fragments]; //sparse + 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++; + } + } + // 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<Fragment> sendNext = new ArrayList<Fragment>(Math.min(toSend.size(), 4)); + List<UDPPacket> rv = new ArrayList<UDPPacket>(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 } } - if (sparseCount > 0) - remaining.clear(); + + 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<Fragment>, 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 39746e737f0531a9b6d66d2adbff387d5b214a1c..bc625bea417a9d4e10b6e1d4d54a36b3263d69cb 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)); - } - - /** - * lazily inits the message buffer unless already inited - */ - 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); + // 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; } /** - * @since 0.9.3 + * @param fragment 0-63 */ - 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; } - - /** note that we have pushed the message fragments */ - public 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]); - - } + public synchronized int getPushCount() { return _pushCount; } /** - * Whether fragment() has been called. - * NOT whether it has more than one fragment. - * - * Caller should synchronize - * - * @return true iff fragment() has been called previously + * Note that we have pushed the message fragments. + * Increments push count (and max sends... why?) */ - 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; + public synchronized void push() { + // these will never be different... + _pushCount++; + _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 053703cb1c2021e6d045ef87114db9d4b7b70d62..feb2b42403a091cdf05ba20532dbcb5c2a34b781 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<Long> ackIdsRemaining, int newAckCount, List<ACKBitfield> partialACKsRemaining) { - UDPPacket packet = buildPacketHeader((byte)(UDPPacket.PAYLOAD_TYPE_DATA << 4)); - DatagramPacket pkt = packet.getPacket(); - byte data[] = pkt.getData(); - int off = HEADER_SIZE; + List<Fragment> frags = Collections.singletonList(new Fragment(state, fragment)); + return buildPacket(frags, peer, ackIdsRemaining, newAckCount, partialACKsRemaining); + } + /* + * Multiple fragments + * + * @since 0.9.16 + */ + public UDPPacket buildPacket(List<Fragment> fragments, PeerState peer, + List<Long> ackIdsRemaining, int newAckCount, + List<ACKBitfield> partialACKsRemaining) { StringBuilder msg = null; if (_log.shouldLog(Log.INFO)) { - msg = new StringBuilder(128); + msg = new StringBuilder(256); msg.append("Data pkt to ").append(peer.getRemotePeer().toBase64()); - msg.append(" msg ").append(state.getMessageId()).append(" frag:").append(fragment); - msg.append('/').append(state.getFragmentCount()); + } + + // 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); + } } - int dataSize = state.fragmentSize(fragment); - if (dataSize < 0) { - packet.release(); + if (dataSize < 0) return null; - } + // calculate size available for acks 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 { + 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; + // 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 8ad73330e47858314452db2ee4e5eaccec407729..206e9c44f7aea9964f813d7fcd9b7ba0c8ae73d4 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 db7fb4ea34f400f6b1a0b2bbea8a4ddd1a82bc96..f28ee96450360ba618761ccf55d79563dba01313 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<UDPPacket> 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 06a440ada5facda25868a0715eb43f11dc887dbe..22a92f22eb595a82c381886e5bca642e9cb25e0c 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<ACKBitfield> rv) { - InboundMessageState states[] = null; + List<InboundMessageState> 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<InboundMessageState>(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<OutboundMessageState> allocateSend() { if (_dead) return null; + List<OutboundMessageState> 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<OutboundMessageState>(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<OutboundMessageState>(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 48a5e082256ffafdf0cec0505b33b5660116a549..bc47fba87c793c2d9e7509e7a8adf971400bbea1 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 f05205582d4eb43df5309dc7fa4354d84ee4e3bd..88551d588c029612ca498fab1ea2133acb6d63cd 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 252490cfc666cad5fcbdf4c10bbafbeb1b268e51..1f5f65a6a7c78d2b22657e7e9688551ff27df820 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 affaf09df8cd4655c33cb8fd75d7ee84068f590b..1f61cb197a27824fd62ceb32989c03c1de230ed0 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 b9c2ad80241662b04a07e6561e46db56e3777b9a..63fc9576000bb0108eaec71d6105e5de9754fadc 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 3fa40c984e32432483f974bc375bc69d353770b6..bb18c44b229a2f3a0f52c6bfeb32393d9169c9f0 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 ec3529b48f79f38b699c194acdd26f0ae70a215c..660aedab81d93e06dc02088d96d57c8d1e9e7aa2 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 364c5970ab12a850e0e5e26cf1b8ae6e02d580ab..3961af80fc5ffef73481dafa687146126356e4ec 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 03923ecfeb71db4fc9107d0d7247e9a94cec090b..4aef149028e1dfa9ac37f1846d80625620ff1443 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 b53d8a6430291d8d9892285477b44e38f0dba346..d5806027932d300e56340678637475a3e9b4341c 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 b771589a1f7474f0d408be4a5b52571007bcc039..42cff28c7a3dbb9b37a7d9b91660e110719b89b9 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 afe3940bff989a407f7f0952afa6e4b57d4fc5d5..0f914fbcc00575cc94f51f6a2d90c7c6fcad4f53 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 fbfca2bcc9b879063329679b560526534db6dc17..31aaa86654af300d55b77b5c90ce5160715e91cf 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 14eab47e2cc20db9f30a5494778c13952fcee51f..8bb657831c2b2e112bb7ef086f564908bf514d1d 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;