diff --git a/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java b/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java index ffdc4aa3ec..14d8b3bd29 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java @@ -166,10 +166,10 @@ class AddressBook { private static final int MAX_DEST_LENGTH = MIN_DEST_LENGTH + 100; // longer than any known cert type for now /** - * Do basic validation of the hostname and dest + * Do basic validation of the hostname * hostname was already converted to lower case by ConfigParser.parse() */ - private static boolean valid(String host, String dest) { + public static boolean isValidKey(String host) { return host.endsWith(".i2p") && host.length() > 4 && @@ -193,8 +193,15 @@ class AddressBook { (! host.equals("console.i2p")) && (! host.endsWith(".proxy.i2p")) && (! host.endsWith(".router.i2p")) && - (! host.endsWith(".console.i2p")) && + (! host.endsWith(".console.i2p")) + ; + } + /** + * Do basic validation of the b64 dest, without bothering to instantiate it + */ + private static boolean isValidDest(String dest) { + return // null cert ends with AAAA but other zero-length certs would be AA ((dest.length() == MIN_DEST_LENGTH && dest.endsWith("AA")) || (dest.length() > MIN_DEST_LENGTH && dest.length() <= MAX_DEST_LENGTH)) && @@ -221,7 +228,7 @@ class AddressBook { String otherKey = entry.getKey(); String otherValue = entry.getValue(); - if (valid(otherKey, otherValue)) { + if (isValidKey(otherKey) && isValidDest(otherValue)) { if (this.addresses.containsKey(otherKey) && !overwrite) { if (!this.addresses.get(otherKey).equals(otherValue) && log != null) { diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java index 857ae25b3a..fd0e42eadb 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java @@ -29,6 +29,10 @@ import java.util.List; import java.util.Map; import net.i2p.I2PAppContext; +import net.i2p.client.naming.NamingService; +import net.i2p.client.naming.SingleFileNamingService; +import net.i2p.data.DataFormatException; +import net.i2p.data.Destination; import net.i2p.util.SecureDirectory; /** @@ -55,6 +59,7 @@ public class Daemon { * @param published * The published AddressBook. This address book is published on * the user's eepsite so that others may subscribe to it. + * If non-null, overwrite with the new addressbook. * @param subscriptions * A SubscriptionList listing the remote address books to update * from. @@ -75,6 +80,71 @@ public class Daemon { subscriptions.write(); } + /** + * Update the router and published address books using remote data from the + * subscribed address books listed in subscriptions. + * + * @param router + * The router AddressBook. This is the address book read by + * client applications. + * @param published + * The published AddressBook. This address book is published on + * the user's eepsite so that others may subscribe to it. + * If non-null, overwrite with the new addressbook. + * @param subscriptions + * A SubscriptionList listing the remote address books to update + * from. + * @param log + * The log to write changes and conflicts to. + * @since 0.8.6 + */ + public static void update(NamingService router, File published, SubscriptionList subscriptions, Log log) { + NamingService publishedNS = null; + Iterator iter = subscriptions.iterator(); + while (iter.hasNext()) { + // yes, the EepGet fetch() is done in next() + AddressBook sub = iter.next(); + for (Map.Entry entry : sub.getAddresses().entrySet()) { + String key = entry.getKey(); + Destination oldDest = router.lookup(key); + try { + if (oldDest == null) { + if (AddressBook.isValidKey(key)) { + Destination dest = new Destination(entry.getValue()); + boolean success = router.put(key, dest); + if (log != null) { + if (success) + log.append("New address " + key + + " added to address book. From: " + sub.getLocation()); + else + log.append("Save to naming service " + router + " failed for new key " + key); + } + // now update the published addressbook + if (published != null) { + if (publishedNS == null) + publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath()); + success = publishedNS.putIfAbsent(key, dest); + if (!success) + log.append("Save to published addressbook " + published.getAbsolutePath() + " failed for new key " + key); + } + } else if (log != null) { + log.append("Bad hostname " + key + " from " + + sub.getLocation()); + } + } else if (!oldDest.toBase64().equals(entry.getValue()) && log != null) { + log.append("Conflict for " + key + " from " + + sub.getLocation() + + ". Destination in remote address book is " + + entry.getValue()); + } + } catch (DataFormatException dfe) { + if (log != null) + log.append("Invalid b64 for" + key + " From: " + sub.getLocation()); + } + } + } + } + /** * Run an update, using the Map settings to provide the parameters. * @@ -120,7 +190,35 @@ public class Daemon { .get("proxy_host"), Integer.parseInt(settings.get("proxy_port"))); Log log = new Log(logFile); - update(master, router, published, subscriptions, log); + if (true) + update(getNamingService(), published, subscriptions, log); + else + update(master, router, published, subscriptions, log); + } + + /** depth-first search */ + private static NamingService searchNamingService(NamingService ns, String srch) + { + String name = ns.getName(); + if (name == srch) + return ns; + List list = ns.getNamingServices(); + if (list != null) { + for (NamingService nss : list) { + NamingService rv = searchNamingService(nss, srch); + if (rv != null) + return rv; + } + } + return null; + } + + /** @return the NamingService for the current file name, or the root NamingService */ + private static NamingService getNamingService() + { + NamingService root = I2PAppContext.getGlobalContext().namingService(); + NamingService rv = searchNamingService(root, "hosts.txt"); + return rv != null ? rv : root; } /** diff --git a/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java b/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java index b3239a2d73..f5901b505e 100644 --- a/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java +++ b/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java @@ -79,6 +79,7 @@ public class AddressbookBean { return addressbook != null && !addressbook.isEmpty(); } + public AddressbookBean() { properties = new Properties(); @@ -86,9 +87,11 @@ public class AddressbookBean beginIndex = 0; endIndex = DISPLAY_SIZE - 1; } + private long configLastLoaded = 0; private static final String PRIVATE_BOOK = "private_addressbook"; private static final String DEFAULT_PRIVATE_BOOK = "../privatehosts.txt"; + protected void loadConfig() { long currentTime = System.currentTimeMillis(); @@ -113,6 +116,7 @@ public class AddressbookBean try { fis.close(); } catch (IOException ioe) {} } } + public String getFileName() { loadConfig(); @@ -144,7 +148,7 @@ public class AddressbookBean book.compareToIgnoreCase( "router" ) != 0 && book.compareToIgnoreCase( "private" ) != 0 && book.compareToIgnoreCase( "published" ) != 0 )) - book = "master"; + book = "router"; return book; } @@ -215,40 +219,49 @@ public class AddressbookBean * addressbook.jsp catches the case where the whole book is empty. */ protected String generateLoadMessage() { - String message = ""; + String message; String filterArg = ""; - if( search != null && search.length() > 0 ) { - message = _("Search") + ' '; - } + int resultCount = resultSize(); if( filter != null && filter.length() > 0 ) { if( search != null && search.length() > 0 ) - message = _("Search within filtered list") + ' '; + message = ngettext("One result for search within filtered list.", + "{0} results for search within filtered list.", + resultCount); else - message = _("Filtered list") + ' '; + message = ngettext("Filtered list contains 1 entry.", + "Fltered list contains {0} entries.", + resultCount); filterArg = "&filter=" + filter; - } - if (entries.length == 0) { - message += "- " + _("no matches") + '.'; - } else if (getBeginInt() == 0 && getEndInt() == entries.length - 1) { - if (message.length() == 0) - message = _("Addressbook") + ' '; - if (entries.length <= 0) - message += _("contains no entries"); + } else if( search != null && search.length() > 0 ) { + message = ngettext("One result for search.", + "{0} results for search.", + resultCount); + } else { + if (resultCount <= 0) + // covered in jsp + //message = _("This addressbook is empty."); + message = ""; else - message += _(entries.length, "contains 1 entry", "contains {0} entries"); - message += '.'; + message = ngettext("Addressbook contains 1 entry.", + "Addressbook contains {0} entries.", + resultCount); + } + if (resultCount <= 0) { + // nothing to display + } else if (getBeginInt() == 0 && getEndInt() == resultCount - 1) { + // nothing to display } else { if (getBeginInt() > 0) { int newBegin = Math.max(0, getBeginInt() - DISPLAY_SIZE); int newEnd = Math.max(0, getBeginInt() - 1); - message += "" + newBegin + '-' + newEnd + " | "; } - message += _("Showing {0} of {1}", "" + getBegin() + '-' + getEnd(), entries.length); - if (getEndInt() < entries.length - 1) { - int newBegin = Math.min(entries.length - 1, getEndInt() + 1); - int newEnd = Math.min(entries.length, getEndInt() + DISPLAY_SIZE); + message += ' ' + _("Showing {0} of {1}", "" + getBegin() + '-' + getEnd(), Integer.valueOf(resultCount)); + if (getEndInt() < resultCount - 1) { + int newBegin = Math.min(resultCount - 1, getEndInt() + 1); + int newEnd = Math.min(resultCount, getEndInt() + DISPLAY_SIZE); message += " | " + newBegin + '-' + newEnd + ""; @@ -313,7 +326,8 @@ public class AddressbookBean if (deleted == 1) message = _("Destination {0} deleted.", name); else - message = _("{0} destinations deleted.", deleted); + // parameter will always be >= 2 + message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted); } } if( changed ) { @@ -394,29 +408,76 @@ public class AddressbookBean public void setHostname(String hostname) { this.hostname = DataHelper.stripHTML(hostname).trim(); // XSS } + protected int getBeginInt() { - return Math.max(0, Math.min(entries.length - 1, beginIndex)); + return Math.max(0, Math.min(resultSize() - 1, beginIndex)); } + public String getBegin() { return "" + getBeginInt(); } + + /** + * @return beginning index into results + * @since 0.8.6 + */ + public String getResultBegin() { + return isPrefiltered() ? "0" : Integer.toString(getBeginInt()); + } + public void setBegin(String s) { try { beginIndex = Integer.parseInt(s); } catch (NumberFormatException nfe) {} } + protected int getEndInt() { - return Math.max(0, Math.max(getBeginInt(), Math.min(entries.length - 1, endIndex))); + return Math.max(0, Math.max(getBeginInt(), Math.min(resultSize() - 1, endIndex))); } + public String getEnd() { return "" + getEndInt(); } + + /** + * @return ending index into results + * @since 0.8.6 + */ + public String getResultEnd() { + return Integer.toString(isPrefiltered() ? resultSize() - 1 : getEndInt()); + } + public void setEnd(String s) { try { endIndex = Integer.parseInt(s); } catch (NumberFormatException nfe) {} } + /** + * Does the entries map contain only the lookup result, + * or must we index into it? + * @since 0.8.6 + */ + protected boolean isPrefiltered() { + return false; + } + + /** + * @return the size of the lookup result + * @since 0.8.6 + */ + protected int resultSize() { + return entries.length; + } + + /** + * @return the total size of the address book + * @since 0.8.6 + */ + protected int totalSize() { + return entries.length; + } + /** translate */ protected static String _(String s) { return Messages.getString(s); @@ -433,7 +494,7 @@ public class AddressbookBean } /** translate (ngettext) @since 0.8.6 */ - protected static String _(int n, String s, String p) { + protected static String ngettext(String s, String p, int n) { return Messages.getString(n, s, p); } } diff --git a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java index 3caf8d602f..1e31eb7d88 100644 --- a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java +++ b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java @@ -37,24 +37,58 @@ import net.i2p.data.DataHelper; import net.i2p.data.Destination; /** - * Talk to the NamingService API instead of modifying the hosts.txt files directly + * Talk to the NamingService API instead of modifying the hosts.txt files directly, + * except for the 'published' addressbook. * - * @since 0.8.5 + * @since 0.8.6 */ public class NamingServiceBean extends AddressbookBean { private static final String DEFAULT_NS = "BlockfileNamingService"; + private boolean isDirect() { + return getBook().equals("published"); + } + + @Override + protected boolean isPrefiltered() { + if (isDirect()) + return super.isPrefiltered(); + return (search == null || search.length() <= 0) && + (filter == null || filter.length() <= 0); + // and right naming service... + } + + @Override + protected int resultSize() { + if (isDirect()) + return super.resultSize(); + return isPrefiltered() ? totalSize() : entries.length; + } + + @Override + protected int totalSize() { + if (isDirect()) + return super.totalSize(); + // only blockfile needs the list property + Properties props = new Properties(); + props.setProperty("list", getFileName()); + return getNamingService().size(props); + } @Override public boolean isNotEmpty() { - return getNamingService().size() > 0; + if (isDirect()) + return super.isNotEmpty(); + return totalSize() > 0; } @Override public String getFileName() { + if (isDirect()) + return super.getFileName(); loadConfig(); String filename = properties.getProperty( getBook() + "_addressbook" ); int slash = filename.lastIndexOf('/'); @@ -64,7 +98,7 @@ public class NamingServiceBean extends AddressbookBean } /** depth-first search */ - private NamingService searchNamingService(NamingService ns, String srch) + private static NamingService searchNamingService(NamingService ns, String srch) { String name = ns.getName(); if (name == srch || name == DEFAULT_NS) @@ -88,10 +122,16 @@ public class NamingServiceBean extends AddressbookBean return rv != null ? rv : root; } - /** Load addressbook and apply filter, returning messages about this. */ + /** + * Load addressbook and apply filter, returning messages about this. + * To control memory, don't load the whole addressbook if we can help it... + * only load what is searched for. + */ @Override public String getLoadBookMessages() { + if (isDirect()) + return super.getLoadBookMessages(); NamingService service = getNamingService(); Debug.debug("Searching within " + service + " with filename=" + getFileName() + " and with filter=" + filter + " and with search=" + search); String message = ""; @@ -100,16 +140,22 @@ public class NamingServiceBean extends AddressbookBean Map results; Properties searchProps = new Properties(); // only blockfile needs this - searchProps.setProperty("list", getFileName()); + searchProps.setProperty("list", getFileName()); if (filter != null) { - String startsAt = filter == "0-9" ? "0" : filter; + String startsAt = filter.equals("0-9") ? "[0-9]" : filter; searchProps.setProperty("startsWith", startsAt); } - if (beginIndex > 0) - searchProps.setProperty("skip", Integer.toString(beginIndex)); - int limit = 1 + endIndex - beginIndex; - if (limit > 0) - searchProps.setProperty("limit", Integer.toString(limit)); + if (isPrefiltered()) { + // Only limit if we not searching or filtering, so we will + // know the total number of results + if (beginIndex > 0) + searchProps.setProperty("skip", Integer.toString(beginIndex)); + int limit = 1 + endIndex - beginIndex; + if (limit > 0) + searchProps.setProperty("limit", Integer.toString(limit)); + } + if (search != null && search.length() > 0) + searchProps.setProperty("search", search.toLowerCase()); results = service.getEntries(searchProps); Debug.debug("Result count: " + results.size()); @@ -151,6 +197,8 @@ public class NamingServiceBean extends AddressbookBean @Override public String getMessages() { + if (isDirect()) + return super.getMessages(); // Loading config and addressbook moved into getLoadBookMessages() String message = ""; @@ -168,23 +216,22 @@ public class NamingServiceBean extends AddressbookBean } else if (oldDest != null && !action.equals(_("Replace"))) { message = _("Host name {0} is already in addressbook with a different destination. Click \"Replace\" to overwrite.", hostname); } else { - boolean valid = true; try { Destination dest = new Destination(destination); - getNamingService().put(hostname, dest, nsOptions); + boolean success = getNamingService().put(hostname, dest, nsOptions); + if (success) { + changed = true; + if (oldDest == null) + message = _("Destination added for {0}.", hostname); + else + message = _("Destination changed for {0}.", hostname); + // clear form + hostname = null; + destination = null; + } else { + message = _("Failed to add Destination for {0} to naming service {1}", hostname, getNamingService()) + "
"; + } } catch (DataFormatException dfe) { - valid = false; - } - if (valid) { - changed = true; - if (oldDest == null) - message = _("Destination added for {0}.", hostname); - else - message = _("Destination changed for {0}.", hostname); - // clear form - hostname = null; - destination = null; - } else { message = _("Invalid Base 64 destination."); } } @@ -197,17 +244,20 @@ public class NamingServiceBean extends AddressbookBean String name = null; int deleted = 0; for (String n : deletionMarks) { - getNamingService().remove(n, nsOptions); - if (deleted++ == 0) { + boolean success = getNamingService().remove(n, nsOptions); + if (!success) { + message += _("Failed to delete Destination for {0} from naming service {1}", name, getNamingService()) + "
"; + } else if (deleted++ == 0) { changed = true; name = n; } } if( changed ) { if (deleted == 1) - message = _("Destination {0} deleted.", name); + message += _("Destination {0} deleted.", name); else - message = _("{0} destinations deleted.", deleted); + // parameter will always be >= 2 + message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted); } } if( changed ) { diff --git a/apps/susidns/src/jsp/addressbook.jsp b/apps/susidns/src/jsp/addressbook.jsp index e6729f6b05..848a3160e0 100644 --- a/apps/susidns/src/jsp/addressbook.jsp +++ b/apps/susidns/src/jsp/addressbook.jsp @@ -144,7 +144,7 @@ ${book.loadBookMessages} <%=intl._("Destination")%> - + "> diff --git a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java index 61b393489e..d7337f3dc2 100644 --- a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java +++ b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java @@ -476,13 +476,17 @@ public class BlockfileNamingService extends DummyNamingService { * from that list (default "hosts.txt", NOT all lists) * Key "skip": skip that many entries * Key "limit": max number to return + * Key "search": return only those matching substring * Key "startsWith": return only those starting with + * ("[0-9]" allowed) * Key "beginWith": start here in the iteration - * Don't use both + * Don't use both startsWith and beginWith. + * Search, startsWith, and beginWith values must be lower case. */ @Override public Map getEntries(Properties options) { String listname = FALLBACK_LIST; + String search = null; String startsWith = null; String beginWith = null; int limit = Integer.MAX_VALUE; @@ -491,10 +495,15 @@ public class BlockfileNamingService extends DummyNamingService { String ln = options.getProperty("list"); if (ln != null) listname = ln; + search = options.getProperty("search"); startsWith = options.getProperty("startsWith"); beginWith = options.getProperty("beginWith"); - if (beginWith == null) - beginWith = startsWith; + if (beginWith == null && startsWith != null) { + if (startsWith.equals("[0-9]")) + beginWith = "0"; + else + beginWith = startsWith; + } String lim = options.getProperty("limit"); try { limit = Integer.parseInt(lim); @@ -505,7 +514,9 @@ public class BlockfileNamingService extends DummyNamingService { } catch (NumberFormatException nfe) {} } if (_log.shouldLog(Log.DEBUG)) - _log.debug("Searching " + listname + " beginning with " + beginWith + " starting with " + startsWith + " limit=" + limit + " skip=" + skip); + _log.debug("Searching " + listname + " beginning with " + beginWith + + " starting with " + startsWith + " search string " + search + + " limit=" + limit + " skip=" + skip); synchronized(_bf) { try { SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer); @@ -523,12 +534,21 @@ public class BlockfileNamingService extends DummyNamingService { for (int i = 0; i < skip && iter.hasNext(); i++) { iter.next(); } - for (int i = 0; i < limit && iter.hasNext(); i++) { - String key = (String) iter.nextKey(); - if (startsWith != null && !key.startsWith(startsWith)) - break; - DestEntry de = (DestEntry) iter.next(); - rv.put(key, de.dest); + for (int i = 0; i < limit && iter.hasNext(); ) { + String key = (String) iter.nextKey(); + if (startsWith != null) { + if (startsWith.equals("[0-9]")) { + if (key.charAt(0) > '9') + break; + } else if (!key.startsWith(startsWith)) { + break; + } + } + DestEntry de = (DestEntry) iter.next(); + if (search != null && key.indexOf(search) < 0) + continue; + rv.put(key, de.dest); + i++; } return rv; } catch (IOException ioe) { diff --git a/core/java/src/net/i2p/client/naming/SingleFileNamingService.java b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java index c24853f9ab..4a5d48d1a3 100644 --- a/core/java/src/net/i2p/client/naming/SingleFileNamingService.java +++ b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java @@ -53,6 +53,10 @@ public class SingleFileNamingService extends NamingService { private final static Log _log = new Log(SingleFileNamingService.class); private final File _file; private final ReentrantReadWriteLock _fileLock; + /** cached number of entries */ + private int _size; + /** last write time */ + private long _lastWrite; public SingleFileNamingService(I2PAppContext context, String filename) { super(context); @@ -292,14 +296,18 @@ public class SingleFileNamingService extends NamingService { /** * @param options As follows: + * Key "search": return only those matching substring * Key "startsWith": return only those starting with + * ("[0-9]" allowed) */ @Override public Map getEntries(Properties options) { if (!_file.exists()) return Collections.EMPTY_MAP; - String startsWith = ""; + String searchOpt = null; + String startsWith = null; if (options != null) { + searchOpt = options.getProperty("search"); startsWith = options.getProperty("startsWith"); } BufferedReader in = null; @@ -307,11 +315,19 @@ public class SingleFileNamingService extends NamingService { try { in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024); String line = null; - String search = startsWith + '='; + String search = startsWith == null ? null : startsWith + '='; Map rv = new HashMap(); while ( (line = in.readLine()) != null) { - if ((!startsWith.equals("")) && !line.startsWith(search)) + if (line.length() <= 0) continue; + if (search != null) { + if (startsWith.equals("[0-9]")) { + if (line.charAt(0) < '0' || line.charAt(0) > '9') + continue; + } else if (!line.startsWith(search)) { + continue; + } + } if (line.startsWith("#")) continue; if (line.indexOf('#') > 0) // trim off any end of line comment @@ -320,12 +336,18 @@ public class SingleFileNamingService extends NamingService { if (split <= 0) continue; String key = line.substring(split); + if (searchOpt != null && key.indexOf(searchOpt) < 0) + continue; String b64 = line.substring(split+1); //.trim() ?????????????? try { Destination dest = new Destination(b64); rv.put(key, dest); } catch (DataFormatException dfe) {} } + if (searchOpt == null && startsWith == null) { + _lastWrite = _file.lastModified(); + _size = rv.size(); + } return rv; } catch (IOException ioe) { _log.error("getEntries error", ioe); @@ -346,14 +368,18 @@ public class SingleFileNamingService extends NamingService { BufferedReader in = null; getReadLock(); try { + if (_file.lastModified() <= _lastWrite) + return _size; in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024); String line = null; int rv = 0; while ( (line = in.readLine()) != null) { - if (line.startsWith("#")) + if (line.startsWith("#") || line.length() <= 0) continue; rv++; } + _lastWrite = _file.lastModified(); + _size = rv; return rv; } catch (IOException ioe) { _log.error("size() error", ioe);