From 2fb8faa166fe39b47543a3610d0832d867946092 Mon Sep 17 00:00:00 2001 From: zzz <zzz@mail.i2p> Date: Sat, 23 Apr 2016 15:53:02 +0000 Subject: [PATCH] Addressbook: - Enable parsing and handling of 'remove' actions - Logging improvements BFNS: Limit max dests per host HostTxtEntry: Test improvements --- .../java/src/net/i2p/addressbook/Daemon.java | 133 +++++++++++------- .../src/net/i2p/addressbook/HostTxtEntry.java | 81 ++++++----- .../net/i2p/addressbook/HostTxtIterator.java | 7 +- .../net/i2p/addressbook/HostTxtParser.java | 48 +++++-- .../client/naming/BlockfileNamingService.java | 8 +- 5 files changed, 173 insertions(+), 104 deletions(-) diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java index d7df8fed33..099cdc6579 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java @@ -87,10 +87,9 @@ public class Daemon { */ public static void update(AddressBook master, AddressBook router, File published, SubscriptionList subscriptions, Log log) { - Iterator<AddressBook> iter = subscriptions.iterator(); - while (iter.hasNext()) { + for (AddressBook book : subscriptions) { // yes, the EepGet fetch() is done in next() - router.merge(iter.next(), false, log); + router.merge(book, false, log); } router.write(); if (published != null) { @@ -147,6 +146,7 @@ public class Daemon { int deleted = 0; for (Map.Entry<String, HostTxtEntry> entry : addressbook) { total++; + // may be null for 'remove' entries String key = entry.getKey(); boolean isKnown; // NOT set for text file NamingService @@ -159,9 +159,9 @@ public class Daemon { knownNames = router.getNames(opts); } oldDest = null; - isKnown = knownNames.contains(key); + isKnown = key != null ? knownNames.contains(key) : null; } else { - oldDest = router.lookup(key); + oldDest = key != null ? router.lookup(key) : null; isKnown = oldDest != null; } try { @@ -169,19 +169,25 @@ public class Daemon { Properties hprops = he.getProps(); boolean mustValidate = MUST_VALIDATE || hprops != null; String action = hprops != null ? hprops.getProperty(HostTxtEntry.PROP_ACTION) : null; - if (mustValidate && !he.hasValidSig()) { + if (key == null && !he.hasValidRemoveSig()) { + if (log != null) { + log.append("Bad signature of action " + action + " for key " + + hprops.getProperty(HostTxtEntry.PROP_NAME) + + ". From: " + addressbook.getLocation()); + } + invalid++; + } else if (key != null && mustValidate && !he.hasValidSig()) { if (log != null) { - if (isKnown) - log.append("Bad signature for old key " + key); - else - log.append("Bad signature for new key " + key); + log.append("Bad signature of action " + action + " for key " + key + + ". From: " + addressbook.getLocation()); } invalid++; } else if (action != null || !isKnown) { - if (AddressBook.isValidKey(key)) { + if (key != null && AddressBook.isValidKey(key)) { Destination dest = new Destination(he.getDest()); Properties props = new OrderedProperties(); props.setProperty("s", addressbook.getLocation()); + boolean allowExistingKeyInPublished = false; if (mustValidate) { // sig checked above props.setProperty("v", "true"); @@ -237,6 +243,7 @@ public class Daemon { if (published != null) { if (publishedNS == null) publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath()); + // FIXME this fails, no support in SFNS success = publishedNS.addDestination(key, dest, props); if (log != null && !success) log.append("Add to published address book " + published.getAbsolutePath() + " failed for " + key); @@ -365,7 +372,7 @@ public class Daemon { log.append("Replacing " + pod2.size() + " destinations for " + key + ". From: " + addressbook.getLocation()); } - // TODO set flag to do non-putifabsent for published below + allowExistingKeyInPublished = true; } else { // mismatch, disallow logMismatch(log, action, key, pod2, polddest, addressbook); @@ -429,13 +436,57 @@ public class Daemon { invalid++; continue; } - } else if (action.equals(HostTxtEntry.ACTION_REMOVE)) { - // FIXME can't get here, no key or dest - // delete this entry - if (!isKnown) { - old++; - continue; + } else if (action.equals(HostTxtEntry.ACTION_REMOVE) || + action.equals(HostTxtEntry.ACTION_REMOVEALL)) { + // w/o name=dest handled below + if (log != null) + log.append("Action: " + action + " with name=dest invalid" + + ". From: " + addressbook.getLocation()); + invalid++; + continue; + } else if (action.equals(HostTxtEntry.ACTION_UPDATE)) { + if (isKnown) { + allowExistingKeyInPublished = true; } + } else { + if (log != null) + log.append("Action: " + action + " unrecognized" + + ". From: " + addressbook.getLocation()); + invalid++; + continue; + } + } // action != null + boolean success = router.put(key, dest, props); + if (log != null) { + if (success) + log.append("New address " + key + + " added to address book. From: " + addressbook.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()); + if (allowExistingKeyInPublished) + success = publishedNS.put(key, dest, props); + else + success = publishedNS.putIfAbsent(key, dest, props); + if (log != null && !success) { + log.append("Save to published address book " + published.getAbsolutePath() + " failed for new key " + key); + } + } + if (isTextFile) + // keep track for later dup check + knownNames.add(key); + nnew++; + } else if (key == null) { + // 'remove' actions + // isKnown is false + if (action != null) { + // Process commands. hprops is non-null. + if (action.equals(HostTxtEntry.ACTION_REMOVE)) { + // delete this entry String polddest = hprops.getProperty(HostTxtEntry.PROP_DEST); String poldname = hprops.getProperty(HostTxtEntry.PROP_NAME); if (polddest != null && poldname != null) { @@ -469,6 +520,8 @@ public class Daemon { // mismatch, disallow logMismatch(log, action, key, pod2, polddest, addressbook); invalid++; + } else { + old++; } } else { if (log != null) @@ -476,14 +529,8 @@ public class Daemon { ". From: " + addressbook.getLocation()); invalid++; } - continue; } else if (action.equals(HostTxtEntry.ACTION_REMOVEALL)) { - // FIXME can't get here, no key or dest // delete all entries with this destination - if (!isKnown) { - old++; - continue; - } String polddest = hprops.getProperty(HostTxtEntry.PROP_DEST); // oldname is optional, but nice because not all books support reverse lookup if (polddest != null) { @@ -519,6 +566,8 @@ public class Daemon { // mismatch, disallow logMismatch(log, action, key, pod2, polddest, addressbook); invalid++; + } else { + old++; } } // reverse lookup, delete all @@ -564,40 +613,20 @@ public class Daemon { ". From: " + addressbook.getLocation()); invalid++; } - continue; - } else if (action.equals(HostTxtEntry.ACTION_UPDATE)) { - if (isKnown) { - // TODO set flag to do non-putifabsent for published below - } } else { if (log != null) - log.append("Action: " + action + " unrecognized" + + log.append("Action: " + action + " w/o name=dest unrecognized" + ". From: " + addressbook.getLocation()); invalid++; - continue; - } - } - boolean success = router.put(key, dest, props); - if (log != null) { - if (success) - log.append("New address " + key + - " added to address book. From: " + addressbook.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, props); - if (log != null && !success) { - log.append("Save to published address book " + published.getAbsolutePath() + " failed for new key " + key); } + continue; + } else { + if (log != null) + log.append("No action in command line" + + ". From: " + addressbook.getLocation()); + invalid++; + continue; } - if (isTextFile) - // keep track for later dup check - knownNames.add(key); - nnew++; } else if (log != null) { log.append("Bad hostname " + key + ". From: " + addressbook.getLocation()); diff --git a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java index d3706b6622..fe19277068 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java @@ -132,27 +132,34 @@ class HostTxtEntry { * Includes newline. */ public void write(BufferedWriter out) throws IOException { - out.write(name); - out.write(KV_SEPARATOR); - out.write(dest); + if (name != null && dest != null) { + out.write(name); + out.write(KV_SEPARATOR); + out.write(dest); + } writeProps(out, false, false); out.newLine(); } /** * Write as a "remove" line #!dest=dest#name=name#k1=v1#sig=sig...] + * This works whether constructed with name and dest, or just properties. * Includes newline. * Must have been constructed with non-null properties. */ public void writeRemove(BufferedWriter out) throws IOException { if (props == null) throw new IllegalStateException(); - props.setProperty(PROP_NAME, name); - props.setProperty(PROP_DEST, dest); + if (name != null && dest != null) { + props.setProperty(PROP_NAME, name); + props.setProperty(PROP_DEST, dest); + } writeProps(out, false, false); out.newLine(); - props.remove(PROP_NAME); - props.remove(PROP_DEST); + if (name != null && dest != null) { + props.remove(PROP_NAME); + props.remove(PROP_DEST); + } } /** @@ -185,7 +192,7 @@ class HostTxtEntry { * Verify with the dest public key using the "sig" property */ public boolean hasValidSig() { - if (props == null) + if (props == null || name == null || dest == null) return false; if (!isValidated) { isValidated = true; @@ -230,7 +237,7 @@ class HostTxtEntry { * Verify with the "olddest" property's public key using the "oldsig" property */ public boolean hasValidInnerSig() { - if (props == null) + if (props == null || name == null || dest == null) return false; boolean rv = false; // don't cache result @@ -450,35 +457,35 @@ class HostTxtEntry { //he.write(out); //out.flush(); SigningPrivateKey priv = pkf.getSigningPrivKey(); - if (inner) { - SigningPrivateKey priv2 = pkf2.getSigningPrivKey(); - he.signInner(priv2); - //out.write("After signing inner:\n"); - //he.write(out); - } - he.sign(priv); - //out.write("After signing:\n"); - he.write(out); - out.flush(); - if (inner && !he.hasValidInnerSig()) - throw new IllegalStateException("Inner fail 1"); - if (!he.hasValidSig()) - throw new IllegalStateException("Outer fail 1"); - // now create 2nd, read in StringWriter sw = new StringWriter(1024); BufferedWriter buf = new BufferedWriter(sw); - he.write(buf); - buf.flush(); - String line = sw.toString(); - line = line.substring(line.indexOf(PROPS_SEPARATOR) + 2); - HostTxtEntry he2 = new HostTxtEntry(host, pkf.getDestination().toBase64(), line); - if (inner && !he2.hasValidInnerSig()) - throw new IllegalStateException("Inner fail 2"); - if (!he2.hasValidSig()) - throw new IllegalStateException("Outer fail 2"); - - // 'remove' tests (corrupts earlier sigs) - if (remove) { + if (!remove) { + if (inner) { + SigningPrivateKey priv2 = pkf2.getSigningPrivKey(); + he.signInner(priv2); + //out.write("After signing inner:\n"); + //he.write(out); + } + he.sign(priv); + //out.write("After signing:\n"); + he.write(out); + out.flush(); + if (inner && !he.hasValidInnerSig()) + throw new IllegalStateException("Inner fail 1"); + if (!he.hasValidSig()) + throw new IllegalStateException("Outer fail 1"); + // now create 2nd, read in + he.write(buf); + buf.flush(); + String line = sw.toString(); + line = line.substring(line.indexOf(PROPS_SEPARATOR) + 2); + HostTxtEntry he2 = new HostTxtEntry(host, pkf.getDestination().toBase64(), line); + if (inner && !he2.hasValidInnerSig()) + throw new IllegalStateException("Inner fail 2"); + if (!he2.hasValidSig()) + throw new IllegalStateException("Outer fail 2"); + } else { + // 'remove' tests (corrupts earlier sigs) he.getProps().remove(PROP_SIG); he.signRemove(priv); //out.write("Remove entry:\n"); @@ -488,7 +495,7 @@ class HostTxtEntry { buf.flush(); out.write(sw.toString()); out.flush(); - line = sw.toString().substring(2).trim(); + String line = sw.toString().substring(2).trim(); HostTxtEntry he3 = new HostTxtEntry(line); if (!he3.hasValidRemoveSig()) throw new IllegalStateException("Remove verify fail"); diff --git a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtIterator.java b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtIterator.java index ad676a348a..ef1b99103c 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtIterator.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtIterator.java @@ -73,7 +73,7 @@ class HostTxtIterator implements Iterator<Map.Entry<String, HostTxtEntry>>, Clos try { String inputLine; while ((inputLine = input.readLine()) != null) { - HostTxtEntry he = HostTxtParser.parse(inputLine); + HostTxtEntry he = HostTxtParser.parse(inputLine, true); if (he == null) continue; next = new MapEntry(he.getName(), he); @@ -86,6 +86,11 @@ class HostTxtIterator implements Iterator<Map.Entry<String, HostTxtEntry>>, Clos return false; } + /** + * 'remove' entries will be returned with a null key, + * and the value will contain a null name, null dest, + * and non-null props. + */ public Map.Entry<String, HostTxtEntry> next() { if (!hasNext()) throw new NoSuchElementException(); diff --git a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java index 92bbce817a..cfcd7a6251 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java @@ -36,6 +36,8 @@ class HostTxtParser { * are obviously not in the format key=value are also ignored. * The key is converted to lower case. * + * Returned map will not contain null ("remove") entries. + * * @param input * A BufferedReader with lines in key=value format to parse into * a Map. @@ -49,7 +51,7 @@ class HostTxtParser { Map<String, HostTxtEntry> result = new HashMap<String, HostTxtEntry>(); String inputLine; while ((inputLine = input.readLine()) != null) { - HostTxtEntry he = parse(inputLine); + HostTxtEntry he = parse(inputLine, false); if (he == null) continue; result.put(he.getName(), he); @@ -64,32 +66,48 @@ class HostTxtParser { * Return a HostTxtEntry from the contents of the inputLine. * * @param inputLine key=value[#!k1=v1#k2=v2...] + * @param allowCommandOnly if true, a line starting with #! will return + * a HostTxtEntry with a null name and dest and non-null props. + * If false, these lines will return null. * @return null if no entry found or on error */ - public static HostTxtEntry parse(String inputLine) { + public static HostTxtEntry parse(String inputLine, boolean allowCommandOnly) { if (inputLine.startsWith(";")) return null; int comment = inputLine.indexOf("#"); - if (comment == 0) - return null; String kv; String sprops; - if (comment > 0) { + if (comment >= 0) { int shebang = inputLine.indexOf(HostTxtEntry.PROPS_SEPARATOR); - if (shebang == comment && shebang + 2 < inputLine.length()) + if (shebang == comment && shebang + 2 < inputLine.length()) { + if (comment == 0 && !allowCommandOnly) + return null; sprops = inputLine.substring(shebang + 2); - else + } else { + if (comment == 0) + return null; sprops = null; + } kv = inputLine.substring(0, comment); } else { sprops = null; kv = inputLine; } - String[] splitLine = DataHelper.split(kv, "=", 2); - if (splitLine.length < 2) - return null; - String name = splitLine[0].trim().toLowerCase(Locale.US); - String dest = splitLine[1].trim(); + String name, dest; + if (comment != 0) { + // we have a name=dest + String[] splitLine = DataHelper.split(kv, "=", 2); + if (splitLine.length < 2) + return null; + name = splitLine[0].trim().toLowerCase(Locale.US); + dest = splitLine[1].trim(); + if (name.length() == 0 || dest.length() == 0) + return null; + } else { + // line starts with #!, rv will contain props only + name = null; + dest = null; + } HostTxtEntry he; if (sprops != null) { try { @@ -104,9 +122,11 @@ class HostTxtParser { } /** - * Return a Map using the contents of the File file. See parseBufferedReader + * Return a Map using the contents of the File file. See parse(BufferedReader) * for details of the input format. * + * Returned map will not contain null ("remove") entries. + * * @param file * A File to parse. * @return A Map containing the key, value pairs from file. @@ -134,6 +154,8 @@ class HostTxtParser { * Return a Map using the contents of the File file. If file cannot be read, * use map instead, and write the result to where file should have been. * + * Returned map will not contain null ("remove") entries. + * * @param file * A File to attempt to parse. * @param map diff --git a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java index 2ba0c7b233..3e7003d8db 100644 --- a/core/java/src/net/i2p/client/naming/BlockfileNamingService.java +++ b/core/java/src/net/i2p/client/naming/BlockfileNamingService.java @@ -137,6 +137,7 @@ public class BlockfileNamingService extends DummyNamingService { private static final String DUMMY = ""; private static final int NEGATIVE_CACHE_SIZE = 32; private static final int MAX_VALUE_LENGTH = 4096; + private static final int MAX_DESTS_PER_HOST = 8; /** * Opens the database at hostsdb.blockfile or creates a new @@ -1486,6 +1487,8 @@ public class BlockfileNamingService extends DummyNamingService { return put(hostname, d, options, false); if (dests.contains(d)) return false; + if (dests.size() >= MAX_DESTS_PER_HOST) + return false; List<Destination> newDests = new ArrayList<Destination>(dests.size() + 1); newDests.addAll(dests); // TODO better sort by sigtype preference. @@ -1523,7 +1526,10 @@ public class BlockfileNamingService extends DummyNamingService { } List<Properties> storedOptions = new ArrayList<Properties>(4); synchronized(_bf) { - List<Destination> dests = lookupAll(hostname, options, storedOptions); + // We use lookupAll2(), not lookupAll(), because if hostname starts with www., + // we do not want to read in from the + // non-www hostname and then copy it to a new www hostname. + List<Destination> dests = lookupAll2(hostname, options, storedOptions); if (dests == null) return false; for (int i = 0; i < dests.size(); i++) { -- GitLab