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.crypto.SigType; 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, String caps, String ip, String sybil, int port, SigType type, String mtu, String ipv6, String ssucaps, int cost) throws IOException { StringBuilder buf = new StringBuilder(4*1024); List sybils = sybil != null ? new ArrayList(128) : null; if (".".equals(routerPrefix)) { renderRouterInfo(buf, _context.router().getRouterInfo(), true, true); } else { boolean notFound = true; Set routers = _context.netDb().getRouters(); int ipMode = 0; if (ip != null) { if (ip.endsWith("/24")) { ipMode = 1; } else if (ip.endsWith("/16")) { ipMode = 2; } else if (ip.endsWith("/8")) { ipMode = 3; } for (int i = 0; i < ipMode; i++) { int last = ip.substring(0, ip.length() - 1).lastIndexOf('.'); if (last > 0) ip = ip.substring(0, last + 1); } } 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"))) || (caps != null && ri.getCapabilities().contains(caps)) || (type != null && type == ri.getIdentity().getSigType())) { renderRouterInfo(buf, ri, false, true); if (sybil != null) sybils.add(key); notFound = false; } else if (ip != null) { for (RouterAddress ra : ri.getAddresses()) { if (ipMode == 0) { if (ip.equals(ra.getHost())) { renderRouterInfo(buf, ri, false, true); if (sybil != null) sybils.add(key); notFound = false; break; } } else { String host = ra.getHost(); if (host != null && host.startsWith(ip)) { renderRouterInfo(buf, ri, false, true); if (sybil != null) sybils.add(key); notFound = false; break; } } } } else if (port != 0) { for (RouterAddress ra : ri.getAddresses()) { if (port == ra.getPort()) { renderRouterInfo(buf, ri, false, true); if (sybil != null) sybils.add(key); notFound = false; break; } } } else if (mtu != null) { for (RouterAddress ra : ri.getAddresses()) { if (mtu.equals(ra.getOption("mtu"))) { renderRouterInfo(buf, ri, false, true); if (sybil != null) sybils.add(key); notFound = false; break; } } } else if (ipv6 != null) { for (RouterAddress ra : ri.getAddresses()) { String host = ra.getHost(); if (host != null && host.startsWith(ipv6)) { renderRouterInfo(buf, ri, false, true); if (sybil != null) sybils.add(key); notFound = false; break; } } } else if (ssucaps != null) { for (RouterAddress ra : ri.getAddresses()) { if (!"SSU".equals(ra.getTransportStyle())) continue; if (ssucaps.equals(ra.getOption("caps"))) { renderRouterInfo(buf, ri, false, true); if (sybil != null) sybils.add(key); notFound = false; break; } } } else if (cost != 0) { for (RouterAddress ra : ri.getAddresses()) { if (cost == ra.getCost()) { renderRouterInfo(buf, ri, false, true); if (sybil != null) sybils.add(key); notFound = false; break; } } } } 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(); if (sybil != null) SybilRenderer.renderSybilHTML(out, _context, sybils, sybil); } /** * @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; // Summary FloodfillNetworkDatabaseFacade netdb = (FloodfillNetworkDatabaseFacade)_context.netDb(); if (debug) { buf.append("\n"); } else { buf.append("
\n"); } buf.append("") .append("\n") .append("\n"); if (debug) { buf.append("\n") .append("") .append("\n") .append("") .append("\n"); } int ff = _context.peerManager().getPeersByCapability(FloodfillNetworkDatabaseFacade.CAPABILITY_FLOODFILL).size(); buf.append("\n") .append("\n"); if (debug) { buf.append("") .append("\n"); // 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("\n"); buf.append("\n"); } buf.append("
Leaseset Summary[") .append(_t("Configure Floodfill Participation")) .append("]
Total Leasesets:").append(leases.size()).append("
Published (RAP) Leasesets:").append(netdb.getKnownLeaseSets()).append("
Mod Data:").append(DataHelper.getUTF8(_context.routerKeyGenerator().getModData())).append("Last Changed:").append(new Date(_context.routerKeyGenerator().getLastChanged())).append("
Next Mod Data:").append(DataHelper.getUTF8(_context.routerKeyGenerator().getNextModData())).append("Change in:").append(DataHelper.formatDuration(_context.routerKeyGenerator().getTimeTillMidnight())).append("
Known Floodfills:").append(ff).append("
Currently Floodfill?").append(netdb.floodfillEnabled() ? "yes" : "no").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)).append("
Estimated total floodfills:").append(total).append("
Estimated total leasesets:").append(total * rapCount / 4); } else { buf.append("Not floodfill or no data."); } buf.append("
\n"); if (leases.isEmpty()) { if (!debug) buf.append("
").append(_t("No Leasesets currently active.")).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("\n") .append(""); if (_context.clientManager().isLocal(dest)) { buf.append("\n"); String host = _context.namingService().reverseLookup(dest); if (host == null) { buf.append(""); } } else { buf.append(""); } else { String b32 = dest.toBase32(); buf.append("").append(dest.toBase64().substring(0, 6)).append("") .append("\n") .append("\n") .append(""); } } buf.append("\n\n"); if (debug) { buf.append("\n\n"); } for (int i = 0; i < ls.getLeaseCount(); i++) { Lease lease = ls.getLease(i); buf.append("\n"); } buf.append("
").append(_t("LeaseSet")).append(": ").append(key.toBase64()).append("" + _t("Local") + " "); if (! _context.clientManager().shouldPublishLeaseSet(key)) buf.append(_t("Unpublished") + ' '); buf.append("").append(_t("Destination")).append(": "); 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("
"); String b32 = dest.toBase32(); buf.append("").append(b32).append("").append("").append(_t("Add to local addressbook")).append("").append(_t("Destination")).append(": "); String host = _context.namingService().reverseLookup(dest); if (host != null) { buf.append("").append(host).append("
").append(b32).append("").append(_t("Add to local addressbook")).append("
\n"); long exp = ls.getLatestLeaseDate()-now; if (exp > 0) buf.append("").append(_t("Expires in {0}", DataHelper.formatDuration2(exp))).append(""); else buf.append("").append(_t("Expired {0} ago", DataHelper.formatDuration2(0-exp))).append(""); buf.append("
"); buf.append("RAP? ").append(ls.getReceivedAsPublished()); buf.append(" RAR? ").append(ls.getReceivedAsReply()); BigInteger dist = HashDistance.getDistance(ourRKey, ls.getRoutingKey()); if (ls.getReceivedAsPublished()) { if (c++ == medianCount) median = dist; } buf.append(" Distance: ").append(fmt.format(biLog2(dist))).append(""); buf.append("
"); //buf.append(dest.toBase32()).append("
"); buf.append("Signature type: ").append(dest.getSigningPublicKey().getType()); buf.append(" Encryption Key: ").append(ls.getEncryptionKey().toBase64().substring(0, 20)).append("…"); buf.append("
"); buf.append("Routing Key: ").append(ls.getRoutingKey().toBase64()); buf.append("
"); buf.append("").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("").append(_t("Expires in {0}", DataHelper.formatDuration2(exl))).append(""); else buf.append("").append(_t("Expired {0} ago", DataHelper.formatDuration2(0-exl))).append(""); } buf.append("
\n"); out.write(buf.toString()); buf.setLength(0); } // for each } // !empty 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") + "
"); buf.append("\"").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("") .append("\n"); long age = _context.clock().now() - info.getPublished(); if (isUs && _context.router().isHidden()) { buf.append("") .append(""); } else if (age > 0) { buf.append("") .append(""); } else { // shouldnt happen buf.append(""); } buf.append("\n\n") .append("") .append("\n"); if (full) { buf.append("\n"); } buf.append("
"); if (isUs) { buf.append("" + _t("Our info") + ": ").append(hash).append(""); } else { buf.append("" + _t("Peer info for") + ": ").append(hash).append(""); if (!full) { buf.append("[").append(_t("Full entry")).append("]"); } } buf.append("
").append(_t("Hidden")).append(", ").append(_t("Updated")).append(":") .append(_t("{0} ago", DataHelper.formatDuration2(age))) .append("").append(_t("Published")).append(":") .append(_t("{0} ago", DataHelper.formatDuration2(age))) .append("").append(_t("Published")).append(": in ").append(DataHelper.formatDuration2(0-age)).append("???
"); buf.append("").append(_t("Signing Key")).append(": ") .append("") .append(info.getIdentity().getSigningPublicKey().getType().toString()); buf.append("
" + _t("Address(es)") + ":"); String country = _context.commSystem().getCountry(info.getIdentity().getHash()); if(country != null) { buf.append(""); buf.append("\"").append(country.toUpperCase(Locale.US)).append('\"'); ").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); } }