diff --git a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java index 808297fb53..ba977a6e80 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbHelper.java @@ -19,7 +19,8 @@ public class NetDbHelper extends HelperBase { _x("All Routers"), // 3 _x("All Routers with Full Stats"), // 4 "LeaseSet Debug", // 5 - _x("LeaseSets") }; // 6 + _x("LeaseSets"), // 6 + "Sybil" }; // 7 private static final String links[] = {"", // 0 @@ -28,7 +29,8 @@ public class NetDbHelper extends HelperBase { "?f=2", // 3 "?f=1", // 4 "?l=2", // 5 - "?l=1" }; // 6 + "?l=1", // 6 + "?f=3" }; // 7 public void setRouter(String r) { if (r != null) @@ -77,6 +79,8 @@ public class NetDbHelper extends HelperBase { renderer.renderRouterInfoHTML(_out, _routerPrefix, _version, _country); else if (_lease) renderer.renderLeaseSetHTML(_out, _debug); + else if (_full == 3) + (new SybilRenderer(_context)).getNetDbSummary(_out); else renderer.renderStatusHTML(_out, _full); } catch (IOException ioe) { @@ -101,6 +105,8 @@ public class NetDbHelper extends HelperBase { return 3; if (_full == 1) return 4; + if (_full == 3) + return 7; return 0; } @@ -119,7 +125,7 @@ public class NetDbHelper extends HelperBase { for (int i = 0; i < titles.length; i++) { if (i == 2 && tab != 2) continue; // can't nav to lookup - if (i == 5 && !_context.getBooleanProperty(PROP_ADVANCED)) + if ((i == 5 || i == 7) && !_context.getBooleanProperty(PROP_ADVANCED)) continue; if (i == tab) { // we are there 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 48c5c3a30d..c9a34ece06 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/NetDbRenderer.java @@ -40,7 +40,7 @@ import net.i2p.util.ObjectCounter; import net.i2p.util.Translate; import net.i2p.util.VersionComparator; -public class NetDbRenderer { +class NetDbRenderer { private final RouterContext _context; public NetDbRenderer (RouterContext ctx) { @@ -69,7 +69,7 @@ public class NetDbRenderer { _us = us; } public int compare(LeaseSet l, LeaseSet r) { - return HashDistance.getDistance(_us, l.getRoutingKey()).subtract(HashDistance.getDistance(_us, r.getRoutingKey())).signum(); + return HashDistance.getDistance(_us, l.getRoutingKey()).compareTo(HashDistance.getDistance(_us, r.getRoutingKey())); } } @@ -266,7 +266,7 @@ public class NetDbRenderer { * http://forums.sun.com/thread.jspa?threadID=597652 * @since 0.7.14 */ - private static double biLog2(BigInteger a) { + public static double biLog2(BigInteger a) { int b = a.bitLength() - 1; double c = 0; double d = 0.5; diff --git a/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java b/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java new file mode 100644 index 0000000000..d2a3143aec --- /dev/null +++ b/apps/routerconsole/java/src/net/i2p/router/web/SybilRenderer.java @@ -0,0 +1,640 @@ +package net.i2p.router.web; + +import java.io.IOException; +import java.io.Serializable; +import java.io.Writer; +import java.math.BigInteger; +import java.text.DecimalFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Hash; +import net.i2p.data.LeaseSet; +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.peermanager.PeerProfile; +import net.i2p.router.tunnel.pool.TunnelPool; +import net.i2p.router.util.HashDistance; +import net.i2p.util.Log; +import net.i2p.util.ObjectCounter; +import net.i2p.util.Translate; +import net.i2p.util.VersionComparator; + +/** + * For debugging only. + * Parts may later move to router as a periodic monitor. + * Adapted from NetDbRenderer. + * + * @since 0.9.24 + * + */ +class SybilRenderer { + + private final RouterContext _context; + + private static final int PAIRMAX = 10; + private static final int MAX = 10; + // multiplied by size - 1 + private static final double POINTS32 = 15.0; + // multiplied by size - 1 + private static final double POINTS24 = 2.0; + private static final double MIN_CLOSE = 242.0; + private static final double MIN_DISPLAY_POINTS = 3.0; + + public SybilRenderer(RouterContext ctx) { + _context = ctx; + } + + /** + * Entry point + */ + public String getNetDbSummary(Writer out) throws IOException { + renderRouterInfoHTML(out, (String)null); + return ""; + } + + private static class RouterInfoRoutingKeyComparator implements Comparator, Serializable { + private final Hash _us; + /** @param us ROUTING KEY */ + public RouterInfoRoutingKeyComparator(Hash us) { + _us = us; + } + public int compare(RouterInfo l, RouterInfo r) { + return HashDistance.getDistance(_us, l.getHash()).compareTo(HashDistance.getDistance(_us, r.getHash())); + } + } + + private static class Points implements Comparable { + private double points; + private final List reasons; + /** @param us ROUTING KEY */ + public Points(double points, String reason) { + this.points = points; + reasons = new ArrayList(4); + reasons.add(reason); + } + public int compareTo(Points r) { + if (points > r.points) + return 1; + if (points < r.points) + return -1; + return 0; + } + } + + private static class PointsComparator implements Comparator, Serializable { + private final Map _points; + /** @param us ROUTING KEY */ + public PointsComparator(Map points) { + _points = points; + } + public int compare(Hash l, Hash r) { + // reverse + return _points.get(r).compareTo(_points.get(l)); + } + } + + private static void addPoints(Map points, Hash h, double d, String reason) { + Points dd = points.get(h); + if (dd != null) { + dd.points += d; + dd.reasons.add(reason); + } else { + points.put(h, new Points(d, reason)); + } + } + + /** + * The whole thing + * + * @param routerPrefix ignored + */ + private void renderRouterInfoHTML(Writer out, String routerPrefix) throws IOException { + Set ffs = _context.peerManager().getPeersByCapability('f'); + List ris = new ArrayList(ffs.size()); + Hash us = _context.routerHash(); + Hash ourRKey = _context.router().getRouterInfo().getRoutingKey(); + for (Hash ff : ffs) { + if (ff.equals(us)) + continue; + RouterInfo ri = _context.netDb().lookupRouterInfoLocally(ff); + if (ri != null) + ris.add(ri); + } + if (ris.isEmpty()) { + out.write("

No known floodfills

"); + return; + } + + StringBuilder buf = new StringBuilder(4*1024); + buf.append("

This is an experimental network database tool for debugging and analysis. Do not panic even if you see warnings below. " + + "Possible \"threats\" are summarized at the bottom, however these are unlikely to be real threats. " + + "If you see anything you would like to discuss with the devs, contact us on IRC #i2p-dev.

"); + renderRouterInfo(buf, _context.router().getRouterInfo(), null, true, false); + buf.append("

Known Floodfills: ").append(ris.size()).append("

"); + + double tot = 0; + int count = 200; + byte[] b = new byte[32]; + for (int i = 0; i < count; i++) { + _context.random().nextBytes(b); + Hash h = new Hash(b); + double d = closestDistance(h, ris); + tot += d; + } + DecimalFormat fmt = new DecimalFormat("#0.00"); + double avgMinDist = tot / count; + buf.append("

Average closest floodfill distance: " + fmt.format(avgMinDist) + "

"); + buf.append("

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

Next Routing Data: \"").append(DataHelper.getUTF8(_context.routerKeyGenerator().getNextModData())) + .append("\" Rotates in: ").append(DataHelper.formatDuration(_context.routerKeyGenerator().getTimeTillMidnight())); + buf.append("

"); + + Map points = new HashMap(64); + renderIPGroups32(out, buf, ris, points); + renderIPGroups24(out, buf, ris, points); + renderIPGroups16(out, buf, ris); + + renderPairDistance(out, buf, ris, points); + + buf.append("

Closest Floodfills to Our Routing Key (Where we Store our RI)

"); + renderRouterInfoHTML(out, buf, ourRKey, avgMinDist, ris, points); + + buf.append("

Closest Floodfills to Our Router Hash (DHT Neighbors if we are Floodfill)

"); + renderRouterInfoHTML(out, buf, us, avgMinDist, ris, points); + + Map clientInboundPools = _context.tunnelManager().getInboundClientPools(); + List destinations = new ArrayList(clientInboundPools.keySet()); + boolean debug = _context.getBooleanProperty(HelperBase.PROP_ADVANCED); + for (Hash client : destinations) { + boolean isLocal = _context.clientManager().isLocal(client); + if (!isLocal) + continue; + if (! _context.clientManager().shouldPublishLeaseSet(client)) + continue; + LeaseSet ls = _context.netDb().lookupLeaseSetLocally(client); + if (ls == null) + continue; + Hash rkey = ls.getRoutingKey(); + TunnelPool in = clientInboundPools.get(client); + String name = (in != null) ? in.getSettings().getDestinationNickname() : client.toBase64().substring(0,4); + buf.append("

Closest floodfills to the Routing Key for Our Destination " + DataHelper.escapeHTML(name) + " (where we store our LS)

"); + renderRouterInfoHTML(out, buf, rkey, avgMinDist, ris, points); + } + + if (!points.isEmpty()) { + List warns = new ArrayList(points.keySet()); + Collections.sort(warns, new PointsComparator(points)); + buf.append("

Routers with Most Threat Points

"); + for (Hash h : warns) { + RouterInfo ri = _context.netDb().lookupRouterInfoLocally(h); + if (h == null) + continue; + Points pp = points.get(h); + double p = pp.points; + if (p < MIN_DISPLAY_POINTS) + break; // sorted + buf.append("

Threat Points: " + fmt.format(p) + "

    "); + for (String s : pp.reasons) { + buf.append("
  • ").append(s).append("
  • "); + } + buf.append("

"); + renderRouterInfo(buf, ri, null, false, false); + } + } + + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + private static class Pair implements Comparable { + public final RouterInfo r1, r2; + public final BigInteger dist; + public Pair(RouterInfo ri1, RouterInfo ri2, BigInteger distance) { + r1 = ri1; r2 = ri2; dist = distance; + } + public int compareTo(Pair p) { + return this.dist.compareTo(p.dist); + } + } + + private void renderPairDistance(Writer out, StringBuilder buf, List ris, Map points) throws IOException { + buf.append("

Closest Floodfill Pairs by Hash

"); + int sz = ris.size(); + List pairs = new ArrayList(PAIRMAX); + for (int i = 0; i < sz; i++) { + RouterInfo info1 = ris.get(i); + for (int j = i + 1; j < sz; j++) { + RouterInfo info2 = ris.get(j); + BigInteger dist = HashDistance.getDistance(info1.getHash(), info2.getHash()); + if (pairs.isEmpty()) { + pairs.add(new Pair(info1, info2, dist)); + } else if (pairs.size() < PAIRMAX) { + pairs.add(new Pair(info1, info2, dist)); + Collections.sort(pairs); + } else if (dist.compareTo(pairs.get(PAIRMAX - 1).dist) < 0) { + pairs.set(PAIRMAX - 1, new Pair(info1, info2, dist)); + Collections.sort(pairs); + } + } + } + DecimalFormat fmt = new DecimalFormat("#0.00"); + for (Pair p : pairs) { + double distance = biLog2(p.dist); + buf.append("

Hash Distance: ").append(fmt.format(distance)).append(": "); + buf.append("

"); + renderRouterInfo(buf, p.r1, null, false, false); + renderRouterInfo(buf, p.r2, null, false, false); + double point = MIN_CLOSE - distance; + if (point > 0) { + addPoints(points, p.r1.getHash(), point, fmt.format(point) + ": Too close to other floodfill " + p.r2.getHash().toBase64()); + addPoints(points, p.r2.getHash(), point, fmt.format(point) + ": Too close to other floodfill " + p.r1.getHash().toBase64()); + } + } + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + private double closestDistance(Hash h, List ris) throws IOException { + BigInteger min = (new BigInteger("2")).pow(256); + for (RouterInfo info : ris) { + BigInteger dist = HashDistance.getDistance(h, info.getHash()); + if (dist.compareTo(min) < 0) + min = dist; + } + return biLog2(min); + } + + /** v4 only */ + private static byte[] getIP(RouterInfo ri) { + for (RouterAddress ra : ri.getAddresses()) { + byte[] rv = ra.getIP(); + if (rv != null && rv.length == 4) + return rv; + } + return null; + } + + private static class FooComparator implements Comparator, Serializable { + private final ObjectCounter _o; + public FooComparator(ObjectCounter o) { _o = o;} + public int compare(Integer l, Integer r) { + // reverse by count + int rv = _o.count(r) - _o.count(l); + if (rv != 0) + return rv; + // foward by IP + return l.intValue() - r.intValue(); + } + } + + private void renderIPGroups32(Writer out, StringBuilder buf, List ris, Map points) throws IOException { + buf.append("

Floodfills with the Same IP

"); + int sz = ris.size(); + ObjectCounter oc = new ObjectCounter(); + for (RouterInfo info : ris) { + byte[] ip = getIP(info); + if (ip == null) + continue; + Integer x = Integer.valueOf((int) DataHelper.fromLong(ip, 0, 4)); + oc.increment(x); + } + List foo = new ArrayList(); + for (Integer ii : oc.objects()) { + int count = oc.count(ii); + if (count >= 2) + foo.add(ii); + } + Collections.sort(foo, new FooComparator(oc)); + boolean found = false; + DecimalFormat fmt = new DecimalFormat("#0.00"); + for (Integer ii : foo) { + int count = oc.count(ii); + int i = ii.intValue(); + int i0 = (i >> 24) & 0xff; + int i1 = (i >> 16) & 0xff; + int i2 = (i >> 8) & 0xff; + int i3 = i & 0xff; + buf.append("

").append(count).append(" floodfills with IP ").append(i0).append('.') + .append(i1).append('.').append(i2).append('.').append(i3) + .append(":

"); + for (RouterInfo info : ris) { + byte[] ip = getIP(info); + if (ip == null) + continue; + if ((ip[0] & 0xff) != i0) + continue; + if ((ip[1] & 0xff) != i1) + continue; + if ((ip[2] & 0xff) != i2) + continue; + if ((ip[3] & 0xff) != i3) + continue; + found = true; + renderRouterInfo(buf, info, null, false, false); + double point = POINTS32 * (count - 1); + addPoints(points, info.getHash(), point, fmt.format(point) + ": Same IP with " + (count - 1) + " other"); + } + } + if (!found) + buf.append("

None

"); + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + private void renderIPGroups24(Writer out, StringBuilder buf, List ris, Map points) throws IOException { + buf.append("

Floodfills in the Same /24 (2 minimum)

"); + int sz = ris.size(); + ObjectCounter oc = new ObjectCounter(); + for (RouterInfo info : ris) { + byte[] ip = getIP(info); + if (ip == null) + continue; + Integer x = Integer.valueOf((int) DataHelper.fromLong(ip, 0, 3)); + oc.increment(x); + } + List foo = new ArrayList(); + for (Integer ii : oc.objects()) { + int count = oc.count(ii); + if (count >= 2) + foo.add(ii); + } + Collections.sort(foo, new FooComparator(oc)); + DecimalFormat fmt = new DecimalFormat("#0.00"); + for (Integer ii : foo) { + int count = oc.count(ii); + int i = ii.intValue(); + int i0 = i >> 16; + int i1 = (i >> 8) & 0xff; + int i2 = i & 0xff; + buf.append("

").append(count).append(" floodfills in ").append(i0).append('.') + .append(i1).append('.').append(i2).append(".0/24:

"); + for (RouterInfo info : ris) { + byte[] ip = getIP(info); + if (ip == null) + continue; + if ((ip[0] & 0xff) != i0) + continue; + if ((ip[1] & 0xff) != i1) + continue; + if ((ip[2] & 0xff) != i2) + continue; + renderRouterInfo(buf, info, null, false, false); + double point = POINTS24 * (count - 1); + addPoints(points, info.getHash(), point, fmt.format(point) + ": Same /24 IP with " + (count - 1) + " other"); + } + } + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + /** no points */ + private void renderIPGroups16(Writer out, StringBuilder buf, List ris) throws IOException { + buf.append("

Floodfills in the Same /16 (4 minimum)

"); + int sz = ris.size(); + ObjectCounter oc = new ObjectCounter(); + for (RouterInfo info : ris) { + byte[] ip = getIP(info); + if (ip == null) + continue; + Integer x = Integer.valueOf((int) DataHelper.fromLong(ip, 0, 2)); + oc.increment(x); + } + List foo = new ArrayList(); + for (Integer ii : oc.objects()) { + int count = oc.count(ii); + if (count >= 4) + foo.add(ii); + } + Collections.sort(foo, new FooComparator(oc)); + for (Integer ii : foo) { + int count = oc.count(ii); + int i = ii.intValue(); + int i0 = i >> 8; + int i1 = i & 0xff; + buf.append("

").append(count).append(" floodfills in ").append(i0).append('.') + .append(i1).append(".0.0/16:

"); + for (RouterInfo info : ris) { + byte[] ip = getIP(info); + if (ip == null) + continue; + if ((ip[0] & 0xff) != i0) + continue; + if ((ip[1] & 0xff) != i1) + continue; + renderRouterInfo(buf, info, null, false, false); + } + } + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + private void renderRouterInfoHTML(Writer out, StringBuilder buf, Hash us, double avgMinDist, + List ris, Map points) throws IOException { + Collections.sort(ris, new RouterInfoRoutingKeyComparator(us)); + double min = 256; + double max = 0; + double tot = 0; + double median = 0; + int count = Math.min(MAX, ris.size()); + boolean isEven = (count % 2) == 0; + int medIdx = isEven ? (count / 2) - 1 : (count / 2); + DecimalFormat fmt = new DecimalFormat("#0.00"); + for (int i = 0; i < count; i++) { + RouterInfo ri = ris.get(i); + double dist = renderRouterInfo(buf, ri, us, false, false); + if (dist < avgMinDist) { + if (i == 0) { + //buf.append("

Not to worry, but above router is closer than average minimum distance " + fmt.format(avgMinDist) + "

"); + } else if (i == 1) { + buf.append("

Not to worry, but above routers are closer than average minimum distance " + fmt.format(avgMinDist) + "

"); + } else if (i == 2) { + buf.append("

Possible Sybil Warning - above routers are closer than average minimum distance " + fmt.format(avgMinDist) + "

"); + } else { + buf.append("

Major Sybil Warning - above router is closer than average minimum distance " + fmt.format(avgMinDist) + "

"); + } + } + // this is dumb because they are already sorted + if (dist < min) + min = dist; + if (dist > max) + max = dist; + tot += dist; + if (i == medIdx) + median = dist; + else if (i == medIdx + 1 && isEven) + median = (median + dist) / 2; + double point = MIN_CLOSE - dist; + if (point > 0) { + point *= 2.0; + addPoints(points, ri.getHash(), point, fmt.format(point) + ": Too close to our key " + us.toBase64()); + } + if (i >= MAX - 1) + break; + } + double avg = tot / count; + buf.append("

Totals for " + count + " floodfills: MIN=" + fmt.format(min) + " AVG=" + fmt.format(avg) + " MEDIAN=" + fmt.format(median) + " MAX=" + fmt.format(max) + "

\n"); + out.write(buf.toString()); + out.flush(); + buf.setLength(0); + } + + /** + * For debugging + * http://forums.sun.com/thread.jspa?threadID=597652 + * @since 0.7.14 + */ + private static double biLog2(BigInteger a) { + return NetDbRenderer.biLog2(a); + } + + /** + * Countries now in a separate bundle + * @param code two-letter country code + * @since 0.9.9 + */ + private String getTranslatedCountry(String code) { + String name = _context.commSystem().getCountryName(code); + return Translate.getString(name, _context, Messages.COUNTRY_BUNDLE_NAME); + } + + /** + * Be careful to use stripHTML for any displayed routerInfo data + * to prevent vulnerabilities + * + * @param us ROUTING KEY or null + * @param full ignored + * @return distance to us if non-null, else 0 + */ + private double renderRouterInfo(StringBuilder buf, RouterInfo info, Hash us, boolean isUs, boolean full) { + String hash = info.getIdentity().getHash().toBase64(); + buf.append("\n"); + buf.append("
"); + double distance = 0; + if (isUs) { + buf.append("" + _t("Our info") + ": ").append(hash).append("
\n"); + } else { + buf.append("" + _t("Peer info for") + ": ").append(hash).append("\n"); + if (!full) { + buf.append("[").append(_t("Full entry")).append("]"); + } + buf.append("
\n"); + if (us != null) { + DecimalFormat fmt = new DecimalFormat("#0.00"); + BigInteger dist = HashDistance.getDistance(us, info.getHash()); + distance = biLog2(dist); + buf.append("Hash Distance: ").append(fmt.format(distance)).append("
"); + } + } + buf.append("Routing Key: ").append(info.getRoutingKey().toBase64()).append("
\n"); + buf.append("Version: ").append(DataHelper.stripHTML(info.getVersion())).append("
\n"); + buf.append("Caps: ").append(DataHelper.stripHTML(info.getCapabilities())).append("
\n"); + String kls = info.getOption("netdb.knownLeaseSets"); + if (kls != null) + buf.append("Lease Sets: ").append(DataHelper.stripHTML(kls)).append("
\n"); + String kr = info.getOption("netdb.knownRouters"); + if (kr != null) + buf.append("Routers: ").append(DataHelper.stripHTML(kr)).append("
\n"); + + long now = _context.clock().now(); + if (!isUs) { + PeerProfile prof = _context.profileOrganizer().getProfileNonblocking(info.getHash()); + if (prof != null) { + long heard = prof.getFirstHeardAbout(); + if (heard > 0) { + long age = now - heard; + if (age > 0) { + buf.append("First heard about: ") + .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("
\n"); + } else { + // shouldnt happen + buf.append("First heard about: in ").append(DataHelper.formatDuration2(0-age)).append("???
\n"); + } + } + // any other profile stuff? + } + } + long age = now - info.getPublished(); + if (isUs && _context.router().isHidden()) { + buf.append("").append(_t("Hidden")).append(", ").append(_t("Updated")).append(": ") + .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("
\n"); + } else if (age > 0) { + buf.append("").append(_t("Published")).append(": ") + .append(_t("{0} ago", DataHelper.formatDuration2(age))).append("
\n"); + } else { + // shouldnt happen + buf.append("" + _t("Published") + ": in ").append(DataHelper.formatDuration2(0-age)).append("???
\n"); + } + buf.append("").append(_t("Signing Key")).append(": ") + .append(info.getIdentity().getSigningPublicKey().getType().toString()); + buf.append("
\n" + _t("Addresses") + ": "); + String country = _context.commSystem().getCountry(info.getIdentity().getHash()); + if(country != null) { + buf.append("\"").append(country.toUpperCase(Locale.US)).append('\"'); "); + } + for (RouterAddress addr : info.getAddresses()) { + String style = addr.getTransportStyle(); + buf.append("").append(DataHelper.stripHTML(style)).append(": "); + Map p = addr.getOptionsMap(); + for (Map.Entry e : p.entrySet()) { + String name = (String) e.getKey(); + if (name.equals("key") || name.startsWith("ikey") || name.startsWith("itag") || name.startsWith("iport")) + continue; + String val = (String) e.getValue(); + buf.append('[').append(_t(DataHelper.stripHTML(name))).append('='); + if (name.equals("host")) + buf.append(""); + buf.append(DataHelper.stripHTML(val)).append("] "); + if (name.equals("host")) + buf.append(""); + } + } + buf.append("
\n"); + return distance; + } + + /** translate a string */ + private String _t(String s) { + return Messages.getString(s, _context); + } + + /** tag only */ + private static final String _x(String s) { + return s; + } + + /** + * translate a string with a parameter + * This is a lot more expensive than _t(s), so use sparingly. + * + * @param s string to be translated containing {0} + * The {0} will be replaced by the parameter. + * Single quotes must be doubled, i.e. ' -> '' in the string. + * @param o parameter, not translated. + * To translate parameter also, use _t("foo {0} bar", _t("baz")) + * Do not double the single quotes in the parameter. + * Use autoboxing to call with ints, longs, floats, etc. + */ + private String _t(String s, Object o) { + return Messages.getString(s, o, _context); + } +} diff --git a/history.txt b/history.txt index 31be99462d..7d8eaafac5 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,6 @@ +2015-12-03 zzz + * Console: Add experimental Sybil analysis tool + 2015-12-01 zzz * i2psnark: - Consolidate default tunnel length definition diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index c10128fe62..fecba78d69 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 6; + public final static long BUILD = 7; /** for example "-test" */ public final static String EXTRA = "";