diff --git a/LICENSE.txt b/LICENSE.txt index f36fa5a43..0539f2bcd 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -72,6 +72,10 @@ Public domain except as listed below: Contains some code Copyright 2006 Sun Microsystems, Inc. See licenses/LICENSE-InstallCert.txt + BlockFile: + Copyright (c) 2006, Matthew Estes + See licenses/LICENSE-BlockFile.txt + Router: Public domain except as listed below: diff --git a/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java b/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java index 22bf33b5c..14d8b3bd2 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java @@ -24,7 +24,6 @@ package net.i2p.addressbook; import java.io.File; import java.io.IOException; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import net.i2p.I2PAppContext; @@ -42,7 +41,7 @@ class AddressBook { private String location; - private Map addresses; + private Map addresses; private boolean modified; @@ -53,7 +52,7 @@ class AddressBook { * A Map containing human readable addresses as keys, mapped to * base64 i2p destinations. */ - public AddressBook(Map addresses) { + public AddressBook(Map addresses) { this.addresses = addresses; } @@ -139,7 +138,7 @@ class AddressBook { * is a human readable name, and the value is a base64 i2p * destination. */ - public Map getAddresses() { + public Map getAddresses() { return this.addresses; } @@ -167,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 && @@ -194,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)) && @@ -218,13 +224,11 @@ class AddressBook { * The log to write messages about new addresses or conflicts to. */ public void merge(AddressBook other, boolean overwrite, Log log) { - Iterator otherIter = other.addresses.keySet().iterator(); + for (Map.Entry entry : other.addresses.entrySet()) { + String otherKey = entry.getKey(); + String otherValue = entry.getValue(); - while (otherIter.hasNext()) { - String otherKey = (String) otherIter.next(); - String otherValue = (String) other.addresses.get(otherKey); - - 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/ConfigParser.java b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java index 7efc27ea5..b09804f23 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java @@ -30,7 +30,6 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.StringReader; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -85,8 +84,8 @@ class ConfigParser { * if the BufferedReader cannot be read. * */ - public static Map parse(BufferedReader input) throws IOException { - Map result = new HashMap(); + public static Map parse(BufferedReader input) throws IOException { + Map result = new HashMap(); String inputLine; inputLine = input.readLine(); while (inputLine != null) { @@ -111,11 +110,11 @@ class ConfigParser { * @throws IOException * if file cannot be read. */ - public static Map parse(File file) throws IOException { + public static Map parse(File file) throws IOException { FileInputStream fileStream = new FileInputStream(file); BufferedReader input = new BufferedReader(new InputStreamReader( fileStream)); - Map rv = ConfigParser.parse(input); + Map rv = ConfigParser.parse(input); try { fileStream.close(); } catch (IOException ioe) {} @@ -132,7 +131,7 @@ class ConfigParser { * @throws IOException * if file cannot be read. */ - public static Map parse(String string) throws IOException { + public static Map parse(String string) throws IOException { StringReader stringReader = new StringReader(string); BufferedReader input = new BufferedReader(stringReader); return ConfigParser.parse(input); @@ -149,14 +148,13 @@ class ConfigParser { * @return A Map containing the key, value pairs from file, or if file * cannot be read, map. */ - public static Map parse(File file, Map map) { - Map result; + public static Map parse(File file, Map map) { + Map result; try { result = ConfigParser.parse(file); - for (Iterator iter = map.keySet().iterator(); iter.hasNext(); ) { - String key = (String) iter.next(); - if (!result.containsKey(key)) - result.put(key, map.get(key)); + for (Map.Entry entry : map.entrySet()) { + if (!result.containsKey(entry.getKey())) + result.put(entry.getKey(), entry.getValue()); } } catch (IOException exp) { result = map; @@ -177,9 +175,9 @@ class ConfigParser { * @throws IOException * if input cannot be read. */ - public static List parseSubscriptions(BufferedReader input) + public static List parseSubscriptions(BufferedReader input) throws IOException { - List result = new LinkedList(); + List result = new LinkedList(); String inputLine = input.readLine(); while (inputLine != null) { inputLine = ConfigParser.stripComments(inputLine).trim(); @@ -201,11 +199,11 @@ class ConfigParser { * @throws IOException * if file cannot be read. */ - public static List parseSubscriptions(File file) throws IOException { + public static List parseSubscriptions(File file) throws IOException { FileInputStream fileStream = new FileInputStream(file); BufferedReader input = new BufferedReader(new InputStreamReader( fileStream)); - List rv = ConfigParser.parseSubscriptions(input); + List rv = ConfigParser.parseSubscriptions(input); try { fileStream.close(); } catch (IOException ioe) {} @@ -221,7 +219,7 @@ class ConfigParser { * @throws IOException * if string cannot be read. */ - public static List parseSubscriptions(String string) throws IOException { + public static List parseSubscriptions(String string) throws IOException { StringReader stringReader = new StringReader(string); BufferedReader input = new BufferedReader(stringReader); return ConfigParser.parseSubscriptions(input); @@ -238,8 +236,8 @@ class ConfigParser { * @return A List consisting of one element for each line in file, or if * file cannot be read, list. */ - public static List parseSubscriptions(File file, List list) { - List result; + public static List parseSubscriptions(File file, List list) { + List result; try { result = ConfigParser.parseSubscriptions(file); } catch (IOException exp) { @@ -263,12 +261,9 @@ class ConfigParser { * @throws IOException * if the BufferedWriter cannot be written to. */ - public static void write(Map map, BufferedWriter output) throws IOException { - Iterator keyIter = map.keySet().iterator(); - - while (keyIter.hasNext()) { - String key = (String) keyIter.next(); - output.write(key + "=" + (String) map.get(key)); + public static void write(Map map, BufferedWriter output) throws IOException { + for (Map.Entry entry : map.entrySet()) { + output.write(entry.getKey() + '=' + entry.getValue()); output.newLine(); } output.close(); @@ -288,7 +283,7 @@ class ConfigParser { * @throws IOException * if file cannot be written to. */ - public static void write(Map map, File file) throws IOException { + public static void write(Map map, File file) throws IOException { boolean success = false; if (!isWindows) { File tmp = SecureFile.createTempFile("temp-", ".tmp", file.getAbsoluteFile().getParentFile()); @@ -318,12 +313,10 @@ class ConfigParser { * @throws IOException * if output cannot be written to. */ - public static void writeSubscriptions(List list, BufferedWriter output) + public static void writeSubscriptions(List list, BufferedWriter output) throws IOException { - Iterator iter = list.iterator(); - - while (iter.hasNext()) { - output.write((String) iter.next()); + for (String s : list) { + output.write(s); output.newLine(); } output.close(); @@ -340,7 +333,7 @@ class ConfigParser { * @throws IOException * if output cannot be written to. */ - public static void writeSubscriptions(List list, File file) + public static void writeSubscriptions(List list, File file) throws IOException { ConfigParser.writeSubscriptions(list, new BufferedWriter( new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8"))); diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java index 274fa8c4f..fd0e42ead 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,19 +59,20 @@ 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. * @param log * The log to write changes and conflicts to. */ - public void update(AddressBook master, AddressBook router, + public static void update(AddressBook master, AddressBook router, File published, SubscriptionList subscriptions, Log log) { router.merge(master, true, null); - Iterator iter = subscriptions.iterator(); + Iterator iter = subscriptions.iterator(); while (iter.hasNext()) { // yes, the EepGet fetch() is done in next() - router.merge((AddressBook) iter.next(), false, log); + router.merge(iter.next(), false, log); } router.write(); if (published != null) @@ -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. * @@ -83,26 +153,26 @@ public class Daemon { * @param home * The directory containing addressbook's configuration files. */ - public void update(Map settings, String home) { - File masterFile = new File(home, (String) settings + public static void update(Map settings, String home) { + File masterFile = new File(home, settings .get("master_addressbook")); - File routerFile = new File(home, (String) settings + File routerFile = new File(home, settings .get("router_addressbook")); File published = null; if ("true".equals(settings.get("should_publish"))) - published = new File(home, (String) settings + published = new File(home, settings .get("published_addressbook")); - File subscriptionFile = new File(home, (String) settings + File subscriptionFile = new File(home, settings .get("subscriptions")); - File logFile = new File(home, (String) settings.get("log")); - File etagsFile = new File(home, (String) settings.get("etags")); - File lastModifiedFile = new File(home, (String) settings + File logFile = new File(home, settings.get("log")); + File etagsFile = new File(home, settings.get("etags")); + File lastModifiedFile = new File(home, settings .get("last_modified")); - File lastFetchedFile = new File(home, (String) settings + File lastFetchedFile = new File(home, settings .get("last_fetched")); long delay; try { - delay = Long.parseLong((String) settings.get("update_delay")); + delay = Long.parseLong(settings.get("update_delay")); } catch (NumberFormatException nfe) { delay = 12; } @@ -111,16 +181,44 @@ public class Daemon { AddressBook master = new AddressBook(masterFile); AddressBook router = new AddressBook(routerFile); - List defaultSubs = new LinkedList(); + List defaultSubs = new LinkedList(); // defaultSubs.add("http://i2p/NF2RLVUxVulR3IqK0sGJR0dHQcGXAzwa6rEO4WAWYXOHw-DoZhKnlbf1nzHXwMEJoex5nFTyiNMqxJMWlY54cvU~UenZdkyQQeUSBZXyuSweflUXFqKN-y8xIoK2w9Ylq1k8IcrAFDsITyOzjUKoOPfVq34rKNDo7fYyis4kT5bAHy~2N1EVMs34pi2RFabATIOBk38Qhab57Umpa6yEoE~rbyR~suDRvD7gjBvBiIKFqhFueXsR2uSrPB-yzwAGofTXuklofK3DdKspciclTVzqbDjsk5UXfu2nTrC1agkhLyqlOfjhyqC~t1IXm-Vs2o7911k7KKLGjB4lmH508YJ7G9fLAUyjuB-wwwhejoWqvg7oWvqo4oIok8LG6ECR71C3dzCvIjY2QcrhoaazA9G4zcGMm6NKND-H4XY6tUWhpB~5GefB3YczOqMbHq4wi0O9MzBFrOJEOs3X4hwboKWANf7DT5PZKJZ5KorQPsYRSq0E3wSOsFCSsdVCKUGsAAAA/i2p/hosts.txt"); defaultSubs.add("http://www.i2p2.i2p/hosts.txt"); SubscriptionList subscriptions = new SubscriptionList(subscriptionFile, - etagsFile, lastModifiedFile, lastFetchedFile, delay, defaultSubs, (String) settings - .get("proxy_host"), Integer.parseInt((String) settings.get("proxy_port"))); + etagsFile, lastModifiedFile, lastFetchedFile, delay, defaultSubs, settings + .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; } /** @@ -149,7 +247,7 @@ public class Daemon { homeFile = new SecureDirectory(System.getProperty("user.dir")); } - Map defaultSettings = new HashMap(); + Map defaultSettings = new HashMap(); defaultSettings.put("proxy_host", "127.0.0.1"); defaultSettings.put("proxy_port", "4444"); defaultSettings.put("master_addressbook", "../userhosts.txt"); @@ -173,7 +271,7 @@ public class Daemon { File settingsFile = new File(homeFile, settingsLocation); - Map settings = ConfigParser.parse(settingsFile, defaultSettings); + Map settings = ConfigParser.parse(settingsFile, defaultSettings); // wait try { Thread.sleep(5*60*1000 + I2PAppContext.getGlobalContext().random().nextLong(5*60*1000)); @@ -181,7 +279,7 @@ public class Daemon { } catch (InterruptedException ie) {} while (_running) { - long delay = Long.parseLong((String) settings.get("update_delay")); + long delay = Long.parseLong(settings.get("update_delay")); if (delay < 1) { delay = 1; } diff --git a/apps/addressbook/java/src/net/i2p/addressbook/DaemonThread.java b/apps/addressbook/java/src/net/i2p/addressbook/DaemonThread.java index b2ff2c511..44592f8ef 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/DaemonThread.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/DaemonThread.java @@ -21,13 +21,18 @@ package net.i2p.addressbook; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.client.naming.NamingServiceUpdater; + /** * A thread that waits five minutes, then runs the addressbook daemon. * * @author Ragnarok * */ -class DaemonThread extends Thread { +class DaemonThread extends Thread implements NamingServiceUpdater { private String[] args; @@ -49,11 +54,21 @@ class DaemonThread extends Thread { // Thread.sleep(5 * 60 * 1000); //} catch (InterruptedException exp) { //} + I2PAppContext.getGlobalContext().namingService().registerUpdater(this); Daemon.main(this.args); + I2PAppContext.getGlobalContext().namingService().unregisterUpdater(this); } public void halt() { Daemon.stop(); interrupt(); } + + /** + * The NamingServiceUpdater interface + * @since 0.8.6 + */ + public void update(Properties options) { + interrupt(); + } } diff --git a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java index 6a362b847..d4c0ee98c 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java @@ -35,9 +35,9 @@ import net.i2p.data.DataHelper; // debug * * @author Ragnarok */ -class SubscriptionIterator implements Iterator { +class SubscriptionIterator implements Iterator { - private Iterator subIterator; + private Iterator subIterator; private String proxyHost; private int proxyPort; private final long delay; @@ -51,7 +51,7 @@ class SubscriptionIterator implements Iterator { * @param proxyHost proxy hostname * @param proxyPort proxt port number */ - public SubscriptionIterator(List subscriptions, long delay, String proxyHost, int proxyPort) { + public SubscriptionIterator(List subscriptions, long delay, String proxyHost, int proxyPort) { this.subIterator = subscriptions.iterator(); this.delay = delay; this.proxyHost = proxyHost; @@ -72,8 +72,8 @@ class SubscriptionIterator implements Iterator { * see java.util.Iterator#next() * @return an AddressBook (empty if the minimum delay has not been met) */ - public Object next() { - Subscription sub = (Subscription) this.subIterator.next(); + public AddressBook next() { + Subscription sub = this.subIterator.next(); if (sub.getLastFetched() + this.delay < I2PAppContext.getGlobalContext().clock().now()) { //System.err.println("Fetching addressbook from " + sub.getLocation()); return new AddressBook(sub, this.proxyHost, this.proxyPort); diff --git a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionList.java b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionList.java index d67cd9af5..9b64bb316 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionList.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionList.java @@ -24,7 +24,6 @@ package net.i2p.addressbook; import java.io.File; import java.io.IOException; import java.util.HashMap; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -37,7 +36,7 @@ import java.util.Map; */ class SubscriptionList { - private List subscriptions; + private List subscriptions; private File etagsFile; @@ -68,7 +67,7 @@ class SubscriptionList { * @param proxyPort proxy port number */ public SubscriptionList(File locationsFile, File etagsFile, - File lastModifiedFile, File lastFetchedFile, long delay, List defaultSubs, String proxyHost, + File lastModifiedFile, File lastFetchedFile, long delay, List defaultSubs, String proxyHost, int proxyPort) { this.subscriptions = new LinkedList(); this.etagsFile = etagsFile; @@ -77,11 +76,10 @@ class SubscriptionList { this.delay = delay; this.proxyHost = proxyHost; this.proxyPort = proxyPort; - Map etags; - Map lastModified; - Map lastFetched; - String location; - List locations = ConfigParser.parseSubscriptions(locationsFile, + Map etags; + Map lastModified; + Map lastFetched; + List locations = ConfigParser.parseSubscriptions(locationsFile, defaultSubs); try { etags = ConfigParser.parse(etagsFile); @@ -98,12 +96,10 @@ class SubscriptionList { } catch (IOException exp) { lastFetched = new HashMap(); } - Iterator iter = locations.iterator(); - while (iter.hasNext()) { - location = (String) iter.next(); - this.subscriptions.add(new Subscription(location, (String) etags.get(location), - (String) lastModified.get(location), - (String) lastFetched.get(location))); + for (String location : locations) { + this.subscriptions.add(new Subscription(location, etags.get(location), + lastModified.get(location), + lastFetched.get(location))); } } @@ -125,13 +121,10 @@ class SubscriptionList { * won't be read back correctly; the '=' should be escaped. */ public void write() { - Iterator iter = this.subscriptions.iterator(); - Subscription sub; - Map etags = new HashMap(); - Map lastModified = new HashMap(); - Map lastFetched = new HashMap(); - while (iter.hasNext()) { - sub = (Subscription) iter.next(); + Map etags = new HashMap(); + Map lastModified = new HashMap(); + Map lastFetched = new HashMap(); + for (Subscription sub : this.subscriptions) { if (sub.getEtag() != null) { etags.put(sub.getLocation(), sub.getEtag()); } diff --git a/apps/susidns/src/java/src/i2p/susi/dns/AddressBean.java b/apps/susidns/src/java/src/i2p/susi/dns/AddressBean.java index 5d91d469c..8e611a112 100644 --- a/apps/susidns/src/java/src/i2p/susi/dns/AddressBean.java +++ b/apps/susidns/src/java/src/i2p/susi/dns/AddressBean.java @@ -24,6 +24,10 @@ package i2p.susi.dns; +import net.i2p.I2PAppContext; +import net.i2p.data.Base32; +import net.i2p.data.Base64; + public class AddressBean { private String name, destination; @@ -58,4 +62,14 @@ public class AddressBean { this.name = name; } + + /** @since 0.8.6 */ + public String getB32() + { + byte[] dest = Base64.decode(destination); + if (dest == null) + return ""; + byte[] hash = I2PAppContext.getGlobalContext().sha().calculateHash(dest).getData(); + return Base32.encode(hash) + ".b32.i2p"; + } } diff --git a/apps/susidns/src/java/src/i2p/susi/dns/AddressByNameSorter.java b/apps/susidns/src/java/src/i2p/susi/dns/AddressByNameSorter.java index 1c503a999..6421f23a9 100644 --- a/apps/susidns/src/java/src/i2p/susi/dns/AddressByNameSorter.java +++ b/apps/susidns/src/java/src/i2p/susi/dns/AddressByNameSorter.java @@ -26,13 +26,10 @@ package i2p.susi.dns; import java.util.Comparator; -public class AddressByNameSorter implements Comparator +public class AddressByNameSorter implements Comparator { - public int compare(Object arg0, Object arg1) + public int compare(AddressBean a, AddressBean b) { - AddressBean a = (AddressBean)arg0; - AddressBean b = (AddressBean)arg1; - if( a == null ) return 1; 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 b6af8a12d..f5901b505 100644 --- a/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java +++ b/apps/susidns/src/java/src/i2p/susi/dns/AddressbookBean.java @@ -42,12 +42,13 @@ import net.i2p.util.SecureFileOutputStream; public class AddressbookBean { - private String book, action, serial, lastSerial, filter, search, hostname, destination; - private int beginIndex, endIndex; - private Properties properties, addressbook; + protected String book, action, serial, lastSerial, filter, search, hostname, destination; + protected int beginIndex, endIndex; + protected final Properties properties; + private Properties addressbook; private int trClass; - private LinkedList deletionMarks; - private static Comparator sorter; + protected final LinkedList deletionMarks; + protected static final Comparator sorter; private static final int DISPLAY_SIZE=100; static { @@ -78,6 +79,7 @@ public class AddressbookBean { return addressbook != null && !addressbook.isEmpty(); } + public AddressbookBean() { properties = new Properties(); @@ -85,10 +87,12 @@ 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"; - private void loadConfig() + + protected void loadConfig() { long currentTime = System.currentTimeMillis(); @@ -112,6 +116,7 @@ public class AddressbookBean try { fis.close(); } catch (IOException ioe) {} } } + public String getFileName() { loadConfig(); @@ -123,11 +128,14 @@ public class AddressbookBean } catch (IOException ioe) {} return filename; } - private Object[] entries; - public Object[] getEntries() + + protected AddressBean[] entries; + + public AddressBean[] getEntries() { return entries; } + public String getAction() { return action; } @@ -140,7 +148,7 @@ public class AddressbookBean book.compareToIgnoreCase( "router" ) != 0 && book.compareToIgnoreCase( "private" ) != 0 && book.compareToIgnoreCase( "published" ) != 0 )) - book = "master"; + book = "router"; return book; } @@ -167,7 +175,7 @@ public class AddressbookBean try { fis = new FileInputStream( getFileName() ); addressbook.load( fis ); - LinkedList list = new LinkedList(); + LinkedList list = new LinkedList(); Enumeration e = addressbook.keys(); while( e.hasMoreElements() ) { String name = (String)e.nextElement(); @@ -189,52 +197,11 @@ public class AddressbookBean } list.addLast( new AddressBean( name, destination ) ); } - Object array[] = list.toArray(); + AddressBean array[] = list.toArray(new AddressBean[list.size()]); Arrays.sort( array, sorter ); entries = array; - // Format a message about filtered addressbook size, and the number of displayed entries - // addressbook.jsp catches the case where the whole book is empty. - String filterArg = ""; - if( search != null && search.length() > 0 ) { - message = _("Search") + ' '; - } - if( filter != null && filter.length() > 0 ) { - if( search != null && search.length() > 0 ) - message = _("Search within filtered list") + ' '; - else - message = _("Filtered list") + ' '; - 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 (entries.length == 1) - message += _("contains 1 entry"); - else - message += _("contains {0} entries", entries.length); - message += '.'; - } 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 += " | " + newBegin + - '-' + newEnd + ""; - } - } + message = generateLoadMessage(); } catch (Exception e) { Debug.debug( e.getClass().getName() + ": " + e.getMessage() ); @@ -246,6 +213,63 @@ public class AddressbookBean message = "

" + message + "

"; return message; } + + /** + * Format a message about filtered addressbook size, and the number of displayed entries + * addressbook.jsp catches the case where the whole book is empty. + */ + protected String generateLoadMessage() { + String message; + String filterArg = ""; + int resultCount = resultSize(); + if( filter != null && filter.length() > 0 ) { + if( search != null && search.length() > 0 ) + message = ngettext("One result for search within filtered list.", + "{0} results for search within filtered list.", + resultCount); + else + message = ngettext("Filtered list contains 1 entry.", + "Fltered list contains {0} entries.", + resultCount); + filterArg = "&filter=" + filter; + } 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 = 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(), 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 + ""; + } + } + return message; + } + /** Perform actions, returning messages about this. */ public String getMessages() { @@ -255,8 +279,6 @@ public class AddressbookBean if( action != null ) { if( lastSerial != null && serial != null && serial.compareTo( lastSerial ) == 0 ) { boolean changed = false; - int deleted = 0; - String name = null; if (action.equals(_("Add")) || action.equals(_("Replace"))) { if( addressbook != null && hostname != null && destination != null ) { String oldDest = (String) addressbook.get(hostname); @@ -291,18 +313,21 @@ public class AddressbookBean // clear search when adding search = null; } else if (action.equals(_("Delete Selected"))) { - Iterator it = deletionMarks.iterator(); - while( it.hasNext() ) { - name = (String)it.next(); - addressbook.remove( name ); - changed = true; - deleted++; + String name = null; + int deleted = 0; + for (String n : deletionMarks) { + addressbook.remove(n); + if (deleted++ == 0) { + changed = true; + name = n; + } } if( changed ) { 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 ) { @@ -337,6 +362,7 @@ public class AddressbookBean fos.close(); } catch (IOException ioe) {} } + public String getFilter() { return filter; } @@ -382,41 +408,93 @@ public class AddressbookBean public void setHostname(String hostname) { this.hostname = DataHelper.stripHTML(hostname).trim(); // XSS } - private int getBeginInt() { - return Math.max(0, Math.min(entries.length - 1, beginIndex)); + + protected int getBeginInt() { + 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) {} } - private int getEndInt() { - return Math.max(0, Math.max(getBeginInt(), Math.min(entries.length - 1, endIndex))); + + protected int getEndInt() { + 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 */ - private static String _(String s) { + protected static String _(String s) { return Messages.getString(s); } /** translate */ - private static String _(String s, Object o) { + protected static String _(String s, Object o) { return Messages.getString(s, o); } /** translate */ - private static String _(String s, Object o, Object o2) { + protected static String _(String s, Object o, Object o2) { return Messages.getString(s, o, o2); } + + /** translate (ngettext) @since 0.8.6 */ + 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/Messages.java b/apps/susidns/src/java/src/i2p/susi/dns/Messages.java index b596a3be9..7d35e80f0 100644 --- a/apps/susidns/src/java/src/i2p/susi/dns/Messages.java +++ b/apps/susidns/src/java/src/i2p/susi/dns/Messages.java @@ -31,4 +31,9 @@ public class Messages { public static String getString(String s, Object o, Object o2) { return Translate.getString(s, o, o2, I2PAppContext.getGlobalContext(), BUNDLE_NAME); } + + /** translate (ngettext) @since 0.8.6 */ + public static String getString(int n, String s, String p) { + return Translate.getString(n, s, p, I2PAppContext.getGlobalContext(), BUNDLE_NAME); + } } diff --git a/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java new file mode 100644 index 000000000..1d5d2a12b --- /dev/null +++ b/apps/susidns/src/java/src/i2p/susi/dns/NamingServiceBean.java @@ -0,0 +1,283 @@ +/* + * This file is part of susidns project, see http://susi.i2p/ + * + * Copyright (C) 2005 + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @since 0.8.6 + */ + +package i2p.susi.dns; + +import java.util.Arrays; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import net.i2p.I2PAppContext; +import net.i2p.client.naming.NamingService; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; + +/** + * Talk to the NamingService API instead of modifying the hosts.txt files directly, + * except for the 'published' addressbook. + * + * @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) && + getNamingService().getName().equals(DEFAULT_NS); + } + + @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() + { + if (isDirect()) + return super.isNotEmpty(); + return totalSize() > 0; + } + + @Override + public String getFileName() + { + if (isDirect()) + return super.getFileName(); + loadConfig(); + String filename = properties.getProperty( getBook() + "_addressbook" ); + return basename(filename); + } + + /** depth-first search */ + private static NamingService searchNamingService(NamingService ns, String srch) + { + String name = ns.getName(); + if (name.equals(srch) || basename(name).equals(srch) || name.equals(DEFAULT_NS)) + 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; + } + + private static String basename(String filename) { + int slash = filename.lastIndexOf('/'); + if (slash >= 0) + filename = filename.substring(slash + 1); + return filename; + } + + /** @return the NamingService for the current file name, or the root NamingService */ + private NamingService getNamingService() + { + NamingService root = I2PAppContext.getGlobalContext().namingService(); + NamingService rv = searchNamingService(root, getFileName()); + return rv != null ? rv : root; + } + + /** + * 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 = ""; + try { + LinkedList list = new LinkedList(); + Map results; + Properties searchProps = new Properties(); + // only blockfile needs this + searchProps.setProperty("list", getFileName()); + if (filter != null) { + String startsAt = filter.equals("0-9") ? "[0-9]" : filter; + searchProps.setProperty("startsWith", startsAt); + } + 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()); + for (Map.Entry entry : results.entrySet()) { + String name = entry.getKey(); + if( filter != null && filter.length() > 0 ) { + if( filter.compareTo( "0-9" ) == 0 ) { + char first = name.charAt(0); + if( first < '0' || first > '9' ) + continue; + } + else if( ! name.toLowerCase().startsWith( filter.toLowerCase() ) ) { + continue; + } + } + if( search != null && search.length() > 0 ) { + if( name.indexOf( search ) == -1 ) { + continue; + } + } + String destination = entry.getValue().toBase64(); + list.addLast( new AddressBean( name, destination ) ); + } + AddressBean array[] = list.toArray(new AddressBean[list.size()]); + Arrays.sort( array, sorter ); + entries = array; + + message = generateLoadMessage(); + } + catch (Exception e) { + Debug.debug( e.getClass().getName() + ": " + e.getMessage() ); + } + if( message.length() > 0 ) + message = "

" + message + "

"; + return message; + } + + /** Perform actions, returning messages about this. */ + @Override + public String getMessages() + { + if (isDirect()) + return super.getMessages(); + // Loading config and addressbook moved into getLoadBookMessages() + String message = ""; + + if( action != null ) { + Properties nsOptions = new Properties(); + // only blockfile needs this + nsOptions.setProperty("list", getFileName()); + if( lastSerial != null && serial != null && serial.compareTo( lastSerial ) == 0 ) { + boolean changed = false; + if (action.equals(_("Add")) || action.equals(_("Replace"))) { + if(hostname != null && destination != null) { + Destination oldDest = getNamingService().lookup(hostname, nsOptions, null); + if (oldDest != null && destination.equals(oldDest.toBase64())) { + message = _("Host name {0} is already in addressbook, unchanged.", hostname); + } 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 { + try { + Destination dest = new Destination(destination); + nsOptions.setProperty("s", _("Manually added via SusiDNS")); + 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) { + message = _("Invalid Base 64 destination."); + } + } + } else { + message = _("Please enter a host name and destination"); + } + // clear search when adding + search = null; + } else if (action.equals(_("Delete Selected"))) { + String name = null; + int deleted = 0; + for (String n : deletionMarks) { + 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); + else + // parameter will always be >= 2 + message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted); + } + } + if( changed ) { + message += "
" + _("Addressbook saved."); + } + } + else { + message = _("Invalid form submission, probably because you used the \"back\" or \"reload\" button on your browser. Please resubmit."); + } + } + + action = null; + + if( message.length() > 0 ) + message = "

" + message + "

"; + return message; + } +} diff --git a/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java b/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java index 176561aca..88a6552be 100644 --- a/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java +++ b/apps/susidns/src/java/src/i2p/susi/dns/SubscriptionsBean.java @@ -33,6 +33,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.util.Properties; +import net.i2p.I2PAppContext; import net.i2p.util.SecureFileOutputStream; public class SubscriptionsBean @@ -130,15 +131,19 @@ public class SubscriptionsBean if (action.equals(_("Save"))) { save(); String nonce = System.getProperty("addressbook.nonce"); + /******* if (nonce != null) { // Yes this is a hack. // No it doesn't work on a text-mode browser. // Fetching from the addressbook servlet // with the correct parameters will kick off a // config reload and fetch. - message = _("Subscriptions saved, updating addressbook from subscription sources now.") + - "\"\""; + *******/ + if (content != null && content.length() > 2) { + message = _("Subscriptions saved, updating addressbook from subscription sources now."); + // + "\"\""; + I2PAppContext.getGlobalContext().namingService().requestUpdate(null); } else { message = _("Subscriptions saved."); } diff --git a/apps/susidns/src/java/src/i2p/susi/dns/VersionBean.java b/apps/susidns/src/java/src/i2p/susi/dns/VersionBean.java index ffc701046..0949ec31f 100644 --- a/apps/susidns/src/java/src/i2p/susi/dns/VersionBean.java +++ b/apps/susidns/src/java/src/i2p/susi/dns/VersionBean.java @@ -26,8 +26,9 @@ package i2p.susi.dns; public class VersionBean { - private static String version = "0.5"; - private static String url = "http://susi.i2p/?i2paddresshelper=T2DU1KAz3meB0B53U8Y06-I7vHR7XmC0qXAJfLW6b-1L1FVKoySRZz4xazHAwyv2xtRpvKrv6ukLm1tThEW0zQWtZPtX8G6KkzMibD8t7IS~4yw-9VkBtUydyYfsX08AK3v~-egSW8HCXTdyIJVtrETJb337VDUHW-7D4L1JLbwSH4if2ooks6yFTrljK5aVMi-16dZOVvmoyJc3jBqSdK6kraO4gW5-vHTmbLwL498p9nug1KOg1DqgN2GeU5X1QlVrlpFb~IIfdP~O8NT7u-LAjW3jSJsMbLDHMSYTIhC7xmJIiBoi-qk8p6TLynAmvJ7HRvbx4N1EB-uJHyD16wsZkkHyEOfmXbj0ZqLyKEGb3thPwCz-M9v~c2Qt3WbwjXJAtHpjlHkdJ4Fg91cX2oak~JoapnPf6Syw8hko5syf6VVoCYLnrrYyM8oGl8mLclHkj~VCidQNqMSM74IhrHfK6HmRikqtZBexb5M6wfMTTqBvaHURdD21GOpFKYBUAAAA"; + private static String version = "0.6"; + //private static String url = "http://susi.i2p/?i2paddresshelper=T2DU1KAz3meB0B53U8Y06-I7vHR7XmC0qXAJfLW6b-1L1FVKoySRZz4xazHAwyv2xtRpvKrv6ukLm1tThEW0zQWtZPtX8G6KkzMibD8t7IS~4yw-9VkBtUydyYfsX08AK3v~-egSW8HCXTdyIJVtrETJb337VDUHW-7D4L1JLbwSH4if2ooks6yFTrljK5aVMi-16dZOVvmoyJc3jBqSdK6kraO4gW5-vHTmbLwL498p9nug1KOg1DqgN2GeU5X1QlVrlpFb~IIfdP~O8NT7u-LAjW3jSJsMbLDHMSYTIhC7xmJIiBoi-qk8p6TLynAmvJ7HRvbx4N1EB-uJHyD16wsZkkHyEOfmXbj0ZqLyKEGb3thPwCz-M9v~c2Qt3WbwjXJAtHpjlHkdJ4Fg91cX2oak~JoapnPf6Syw8hko5syf6VVoCYLnrrYyM8oGl8mLclHkj~VCidQNqMSM74IhrHfK6HmRikqtZBexb5M6wfMTTqBvaHURdD21GOpFKYBUAAAA"; + private static String url = "http://susi.i2p/"; public String getVersion() { return version; diff --git a/apps/susidns/src/jsp/addressbook.jsp b/apps/susidns/src/jsp/addressbook.jsp index 15c76dfc8..9c34fe741 100644 --- a/apps/susidns/src/jsp/addressbook.jsp +++ b/apps/susidns/src/jsp/addressbook.jsp @@ -32,7 +32,7 @@ <%@ page contentType="text/html"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> - + @@ -55,10 +55,10 @@