package net.i2p.router.web; /* * 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.IOException; import java.io.Serializable; import java.io.Writer; import java.math.BigInteger; // debug import java.text.Collator; import java.text.DecimalFormat; // debug import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.TreeSet; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.data.Hash; import net.i2p.data.Lease; 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.util.HashDistance; // debug import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.util.Log; import net.i2p.util.ObjectCounter; import net.i2p.util.Translate; import net.i2p.util.VersionComparator; class NetDbRenderer { private final RouterContext _context; public NetDbRenderer (RouterContext ctx) { _context = ctx; } /** * Inner class, can't be Serializable */ private class LeaseSetComparator implements Comparator { public int compare(LeaseSet l, LeaseSet r) { Destination dl = l.getDestination(); Destination dr = r.getDestination(); boolean locall = _context.clientManager().isLocal(dl); boolean localr = _context.clientManager().isLocal(dr); if (locall && !localr) return -1; if (localr && !locall) return 1; return dl.calculateHash().toBase64().compareTo(dr.calculateHash().toBase64()); } } /** for debugging @since 0.7.14 */ private static class LeaseSetRoutingKeyComparator implements Comparator, Serializable { private final Hash _us; public LeaseSetRoutingKeyComparator(Hash us) { _us = us; } public int compare(LeaseSet l, LeaseSet r) { return HashDistance.getDistance(_us, l.getRoutingKey()).compareTo(HashDistance.getDistance(_us, r.getRoutingKey())); } } private static class RouterInfoComparator implements Comparator, Serializable { public int compare(RouterInfo l, RouterInfo r) { return l.getIdentity().getHash().toBase64().compareTo(r.getIdentity().getHash().toBase64()); } } /** * One String must be non-null * * @param routerPrefix may be null. "." for our router only * @param version may be null * @param country may be null * @param family may be null */ public void renderRouterInfoHTML(Writer out, String routerPrefix, String version, String country, String family) throws IOException { StringBuilder buf = new StringBuilder(4*1024); if (".".equals(routerPrefix)) { renderRouterInfo(buf, _context.router().getRouterInfo(), true, true); } else { boolean notFound = true; Set routers = _context.netDb().getRouters(); for (RouterInfo ri : routers) { Hash key = ri.getIdentity().getHash(); if ((routerPrefix != null && key.toBase64().startsWith(routerPrefix)) || (version != null && version.equals(ri.getVersion())) || (country != null && country.equals(_context.commSystem().getCountry(key))) || (family != null && family.equals(ri.getOption("family")))) { renderRouterInfo(buf, ri, false, true); notFound = false; } } if (notFound) { buf.append(_t("Router")).append(' '); if (routerPrefix != null) buf.append(routerPrefix); else if (version != null) buf.append(version); else if (country != null) buf.append(country); else if (family != null) buf.append(_t("Family")).append(' ').append(family); buf.append(' ').append(_t("not found in network database")); } } out.write(buf.toString()); out.flush(); } /** * @param debug @since 0.7.14 sort by distance from us, display * median distance, and other stuff, useful when floodfill */ public void renderLeaseSetHTML(Writer out, boolean debug) throws IOException { StringBuilder buf = new StringBuilder(4*1024); if (debug) buf.append("

Debug mode - Sorted by hash distance, closest first

\n"); Hash ourRKey; Set leases; DecimalFormat fmt; if (debug) { ourRKey = _context.routerHash(); leases = new TreeSet(new LeaseSetRoutingKeyComparator(ourRKey)); fmt = new DecimalFormat("#0.00"); } else { ourRKey = null; leases = new TreeSet(new LeaseSetComparator()); fmt = null; } leases.addAll(_context.netDb().getLeases()); int medianCount = 0; int rapCount = 0; BigInteger median = null; int c = 0; if (leases.isEmpty()) { if (!debug) buf.append("").append(_t("none")).append(""); } else { if (debug) { // Find the center of the RAP leasesets for (LeaseSet ls : leases) { if (ls.getReceivedAsPublished()) rapCount++; } medianCount = rapCount / 2; } long now = _context.clock().now(); for (LeaseSet ls : leases) { Destination dest = ls.getDestination(); Hash key = dest.calculateHash(); buf.append("").append(_t("LeaseSet")).append(": ").append(key.toBase64()).append("\n"); if (_context.clientManager().isLocal(dest)) { buf.append(" (" + _t("Local") + " "); if (! _context.clientManager().shouldPublishLeaseSet(key)) buf.append(_t("Unpublished") + ' '); buf.append(_t("Destination") + ' '); TunnelPoolSettings in = _context.tunnelManager().getInboundSettings(key); if (in != null && in.getDestinationNickname() != null) buf.append(in.getDestinationNickname()); else buf.append(dest.toBase64().substring(0, 6)); buf.append(")
\n"); String b32 = dest.toBase32(); buf.append("").append(b32).append("
\n"); String host = _context.namingService().reverseLookup(dest); if (host == null) { buf.append("").append(_t("Add to local addressbook")).append("
\n"); } } else { buf.append(" (").append(_t("Destination")).append(' '); String host = _context.namingService().reverseLookup(dest); if (host != null) { buf.append("").append(host).append(")
\n"); } else { String b32 = dest.toBase32(); buf.append(dest.toBase64().substring(0, 6)).append(")
\n" + "").append(b32).append("
\n" + "").append(_t("Add to local addressbook")).append("
\n"); } } long exp = ls.getLatestLeaseDate()-now; if (exp > 0) buf.append(_t("Expires in {0}", DataHelper.formatDuration2(exp))); else buf.append(_t("Expired {0} ago", DataHelper.formatDuration2(0-exp))); buf.append("
\n"); if (debug) { buf.append("RAP? " + ls.getReceivedAsPublished()); buf.append(" RAR? " + ls.getReceivedAsReply()); BigInteger dist = HashDistance.getDistance(ourRKey, ls.getRoutingKey()); if (ls.getReceivedAsPublished()) { if (c++ == medianCount) median = dist; } buf.append(" Dist: ").append(fmt.format(biLog2(dist))).append("
"); //buf.append(dest.toBase32()).append("
"); buf.append("Sig type: ").append(dest.getSigningPublicKey().getType()).append("
"); buf.append("Routing Key: ").append(ls.getRoutingKey().toBase64()); buf.append("
"); buf.append("Encryption Key: ").append(ls.getEncryptionKey().toBase64().substring(0, 20)).append("...
"); } for (int i = 0; i < ls.getLeaseCount(); i++) { Lease lease = ls.getLease(i); buf.append(_t("Lease")).append(' ').append(i + 1).append(": ").append(_t("Gateway")).append(' '); buf.append(_context.commSystem().renderPeerHTML(lease.getGateway())); buf.append(' ').append(_t("Tunnel")).append(' ').append(lease.getTunnelId().getTunnelId()).append(' '); if (debug) { long exl = lease.getEndDate().getTime() - now; if (exl > 0) buf.append(_t("Expires in {0}", DataHelper.formatDuration2(exl))); else buf.append(_t("Expired {0} ago", DataHelper.formatDuration2(0-exl))); } buf.append("
\n"); } buf.append("
\n"); out.write(buf.toString()); buf.setLength(0); } // for each } // !empty if (debug) { FloodfillNetworkDatabaseFacade netdb = (FloodfillNetworkDatabaseFacade)_context.netDb(); buf.append("

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

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

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

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

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

Currently Floodfill? "); buf.append(netdb.floodfillEnabled() ? "yes" : "no"); buf.append("

Network data (only valid if floodfill):"); //buf.append("

Center of Key Space (router hash): " + ourRKey.toBase64()); if (median != null) { double log2 = biLog2(median); buf.append("

Median distance (bits): ").append(fmt.format(log2)); // 2 for 4 floodfills... -1 for median // this can be way off for unknown reasons int total = (int) Math.round(Math.pow(2, 2 + 256 - 1 - log2)); buf.append("

Estimated total floodfills: ").append(total); buf.append("

Estimated total leasesets: ").append(total * rapCount / 4); } else { buf.append("

Not floodfill or no data"); } buf.append("

"); } out.write(buf.toString()); out.flush(); } /** * For debugging * http://forums.sun.com/thread.jspa?threadID=597652 * @since 0.7.14 */ public static double biLog2(BigInteger a) { int b = a.bitLength() - 1; double c = 0; double d = 0.5; for (int i = b; i >= 0; --i) { if (a.testBit(i)) c += d; d /= 2; } return b + c; } /** * @param mode 0: charts only; 1: full routerinfos; 2: abbreviated routerinfos */ public void renderStatusHTML(Writer out, int mode) throws IOException { if (!_context.netDb().isInitialized()) { out.write(_t("Not initialized")); out.flush(); return; } Log log = _context.logManager().getLog(NetDbRenderer.class); long start = System.currentTimeMillis(); boolean full = mode == 1; boolean shortStats = mode == 2; boolean showStats = full || shortStats; // this means show the router infos Hash us = _context.routerHash(); StringBuilder buf = new StringBuilder(8192); if (showStats) { RouterInfo ourInfo = _context.router().getRouterInfo(); renderRouterInfo(buf, ourInfo, true, true); out.write(buf.toString()); buf.setLength(0); } ObjectCounter versions = new ObjectCounter(); ObjectCounter countries = new ObjectCounter(); int[] transportCount = new int[TNAMES.length]; Set routers = new TreeSet(new RouterInfoComparator()); routers.addAll(_context.netDb().getRouters()); for (RouterInfo ri : routers) { Hash key = ri.getIdentity().getHash(); boolean isUs = key.equals(us); if (!isUs) { if (showStats) { renderRouterInfo(buf, ri, false, full); out.write(buf.toString()); buf.setLength(0); } String routerVersion = ri.getOption("router.version"); if (routerVersion != null) versions.increment(routerVersion); String country = _context.commSystem().getCountry(key); if(country != null) countries.increment(country); transportCount[classifyTransports(ri)]++; } } long end = System.currentTimeMillis(); if (log.shouldWarn()) log.warn("part 1 took " + (end - start)); start = end; // // don't bother to reindent // if (!showStats) { // the summary table buf.append("
") .append(_t("Network Database Router Statistics")) .append("
"); // versions table List versionList = new ArrayList(versions.objects()); if (!versionList.isEmpty()) { Collections.sort(versionList, Collections.reverseOrder(new VersionComparator())); buf.append("\n"); buf.append("\n"); for (String routerVersion : versionList) { int num = versions.count(routerVersion); String ver = DataHelper.stripHTML(routerVersion); buf.append("\n"); } buf.append("
" + _t("Version") + "" + _t("Count") + "
").append(ver); buf.append("").append(num).append("
\n"); } buf.append("
"); out.write(buf.toString()); buf.setLength(0); end = System.currentTimeMillis(); if (log.shouldWarn()) log.warn("part 2 took " + (end - start)); start = end; // transports table buf.append("\n"); buf.append("\n"); for (int i = 0; i < TNAMES.length; i++) { int num = transportCount[i]; if (num > 0) { buf.append("\n"); } } buf.append("
" + _t("Transports") + "" + _t("Count") + "
").append(_t(TNAMES[i])); buf.append("").append(num).append("
\n"); buf.append("
"); out.write(buf.toString()); buf.setLength(0); end = System.currentTimeMillis(); if (log.shouldWarn()) log.warn("part 3 took " + (end - start)); start = end; // country table List countryList = new ArrayList(countries.objects()); if (!countryList.isEmpty()) { Collections.sort(countryList, new CountryComparator()); buf.append("\n"); buf.append("\n"); for (String country : countryList) { int num = countries.count(country); buf.append("\n"); } buf.append("
" + _t("Country") + "" + _t("Count") + "
\"").append(country.toUpperCase(Locale.US)).append("\""); "); buf.append(getTranslatedCountry(country)); buf.append("").append(num).append("
\n"); } buf.append("
"); end = System.currentTimeMillis(); if (log.shouldWarn()) log.warn("part 4 took " + (end - start)); start = end; // // don't bother to reindent // } // if !showStats out.write(buf.toString()); out.flush(); } /** * 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); } /** * Sort by translated country name using rules for the current language setting * Inner class, can't be Serializable */ private class CountryComparator implements Comparator { private static final long serialVersionUID = 1L; private final Collator coll; public CountryComparator() { super(); coll = Collator.getInstance(new Locale(Messages.getLanguage(_context))); } public int compare(String l, String r) { return coll.compare(getTranslatedCountry(l), getTranslatedCountry(r)); } } /** * Be careful to use stripHTML for any displayed routerInfo data * to prevent vulnerabilities */ private void renderRouterInfo(StringBuilder buf, RouterInfo info, boolean isUs, boolean full) { String hash = info.getIdentity().getHash().toBase64(); buf.append("\n"); if (full) { buf.append("\n"); } buf.append("
"); 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"); } long age = _context.clock().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("Address(es)") + ": "); 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(": "); int cost = addr.getCost(); if (!((style.equals("SSU") && cost == 5) || (style.equals("NTCP") && cost == 10))) buf.append('[').append(_t("cost")).append('=').append("" + cost).append("] "); Map p = addr.getOptionsMap(); for (Map.Entry e : p.entrySet()) { String name = (String) e.getKey(); String val = (String) e.getValue(); buf.append('[').append(_t(DataHelper.stripHTML(name))).append('=').append(DataHelper.stripHTML(val)).append("] "); } } buf.append("
" + _t("Stats") + ":
"); Map p = info.getOptionsMap(); for (Map.Entry e : p.entrySet()) { String key = (String) e.getKey(); String val = (String) e.getValue(); buf.append(DataHelper.stripHTML(key)).append(" = ").append(DataHelper.stripHTML(val)).append("
\n"); } buf.append("
\n"); } private static final int SSU = 1; private static final int SSUI = 2; private static final int NTCP = 4; private static final int IPV6 = 8; private static final String[] TNAMES = { _x("Hidden or starting up"), _x("SSU"), _x("SSU with introducers"), "", _x("NTCP"), _x("NTCP and SSU"), _x("NTCP and SSU with introducers"), "", "", _x("IPv6 SSU"), _x("IPv6 Only SSU, introducers"), _x("IPv6 SSU, introducers"), _x("IPv6 NTCP"), _x("IPv6 NTCP, SSU"), _x("IPv6 Only NTCP, SSU, introducers"), _x("IPv6 NTCP, SSU, introducers") }; /** * what transport types */ private static int classifyTransports(RouterInfo info) { int rv = 0; for (RouterAddress addr : info.getAddresses()) { String style = addr.getTransportStyle(); if (style.equals("NTCP")) { rv |= NTCP; } else if (style.equals("SSU")) { if (addr.getOption("iport0") != null) rv |= SSUI; else rv |= SSU; } String host = addr.getHost(); if (host != null && host.contains(":")) rv |= IPV6; } return rv; } /** 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); } }