From b69677b7098d5c5a1af23c23ab5131fcb84c84b6 Mon Sep 17 00:00:00 2001 From: zzz Date: Sun, 17 Apr 2016 20:20:10 +0000 Subject: [PATCH] Addressbook: Add initial support for signatures in subscriptions More cleanups SingleFileNamingService: Store signature properties on write --- .../src/net/i2p/addressbook/AddressBook.java | 32 +- .../net/i2p/addressbook/ConfigIterator.java | 30 +- .../java/src/net/i2p/addressbook/Daemon.java | 51 +++- .../src/net/i2p/addressbook/HostTxtEntry.java | 281 ++++++++++++++++++ .../net/i2p/addressbook/HostTxtParser.java | 224 ++++++++++++++ .../i2p/addressbook/SubscriptionIterator.java | 2 +- .../naming/SingleFileNamingService.java | 52 +++- history.txt | 15 + .../src/net/i2p/router/RouterVersion.java | 2 +- 9 files changed, 635 insertions(+), 54 deletions(-) create mode 100644 apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java create mode 100644 apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java diff --git a/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java b/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java index c611fb053d..98926da9af 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/AddressBook.java @@ -43,11 +43,11 @@ import net.i2p.util.SecureFile; * @author Ragnarok * */ -class AddressBook implements Iterable> { +class AddressBook implements Iterable> { private final String location; /** either addresses or subFile will be non-null, but not both */ - private final Map addresses; + private final Map addresses; private final File subFile; private boolean modified; private static final boolean DEBUG = false; @@ -79,7 +79,7 @@ class AddressBook implements Iterable> { * A Map containing human readable addresses as keys, mapped to * base64 i2p destinations. */ - public AddressBook(Map addresses) { + public AddressBook(Map addresses) { this.addresses = addresses; this.subFile = null; this.location = null; @@ -130,7 +130,7 @@ class AddressBook implements Iterable> { * @param proxyPort port number of proxy */ public AddressBook(Subscription subscription, String proxyHost, int proxyPort) { - Map a = null; + Map a = null; File subf = null; try { File tmp = SecureFile.createTempFile("addressbook", null, I2PAppContext.getGlobalContext().getTempDir()); @@ -167,11 +167,11 @@ class AddressBook implements Iterable> { */ public AddressBook(File file) { this.location = file.toString(); - Map a; + Map a; try { - a = ConfigParser.parse(file); + a = HostTxtParser.parse(file); } catch (IOException exp) { - a = new HashMap(); + a = new HashMap(); } this.addresses = a; this.subFile = null; @@ -181,14 +181,14 @@ class AddressBook implements Iterable> { * Return an iterator over the addresses in the AddressBook. * @since 0.8.7 */ - public Iterator> iterator() { + public Iterator> iterator() { if (this.subFile != null) { try { return new ConfigIterator(this.subFile); } catch (IOException ioe) { return new ConfigIterator(); } - } + } return this.addresses.entrySet().iterator(); } @@ -231,7 +231,7 @@ class AddressBook implements Iterable> { /** * Do basic validation of the hostname - * hostname was already converted to lower case by ConfigParser.parse() + * hostname was already converted to lower case by HostTxtParser.parse() */ public static boolean isValidKey(String host) { return @@ -293,15 +293,15 @@ class AddressBook implements Iterable> { public void merge(AddressBook other, boolean overwrite, Log log) { if (this.addresses == null) throw new IllegalStateException(); - for (Iterator> iter = other.iterator(); iter.hasNext(); ) { - Map.Entry entry = iter.next(); + for (Iterator> iter = other.iterator(); iter.hasNext(); ) { + Map.Entry entry = iter.next(); String otherKey = entry.getKey(); - String otherValue = entry.getValue(); + HostTxtEntry otherValue = entry.getValue(); - if (isValidKey(otherKey) && isValidDest(otherValue)) { + if (isValidKey(otherKey) && isValidDest(otherValue.getDest())) { if (this.addresses.containsKey(otherKey) && !overwrite) { if (DEBUG && log != null && - !this.addresses.get(otherKey).equals(otherValue)) { + !this.addresses.get(otherKey).equals(otherValue.getDest())) { log.append("Conflict for " + otherKey + " from " + other.location + ". Destination in remote address book is " @@ -334,7 +334,7 @@ class AddressBook implements Iterable> { throw new IllegalStateException(); if (this.modified) { try { - ConfigParser.write(this.addresses, file); + HostTxtParser.write(this.addresses, file); } catch (IOException exp) { System.err.println("Error writing addressbook " + file.getAbsolutePath() + " : " + exp.toString()); } diff --git a/apps/addressbook/java/src/net/i2p/addressbook/ConfigIterator.java b/apps/addressbook/java/src/net/i2p/addressbook/ConfigIterator.java index df13332f2a..02bb6c4e26 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/ConfigIterator.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/ConfigIterator.java @@ -42,9 +42,12 @@ import net.i2p.data.DataHelper; * Callers should iterate all the way through or call close() * to ensure the underlying stream is closed. * + * Warning - misnamed - this is not used for config files. + * It is only used for subscriptions. + * * @since 0.8.7 */ -class ConfigIterator implements Iterator>, Closeable { +class ConfigIterator implements Iterator>, Closeable { private BufferedReader input; private ConfigEntry next; @@ -70,14 +73,11 @@ class ConfigIterator implements Iterator>, Closeable { try { String inputLine; while ((inputLine = input.readLine()) != null) { - inputLine = ConfigParser.stripComments(inputLine); - if (inputLine.length() == 0) + HostTxtEntry he = HostTxtParser.parse(inputLine); + if (he == null) continue; - String[] splitLine = DataHelper.split(inputLine, "=", 2); - if (splitLine.length == 2) { - next = new ConfigEntry(splitLine[0].trim().toLowerCase(Locale.US), splitLine[1].trim()); - return true; - } + next = new ConfigEntry(he.getName(), he); + return true; } } catch (IOException ioe) {} try { input.close(); } catch (IOException ioe) {} @@ -86,10 +86,10 @@ class ConfigIterator implements Iterator>, Closeable { return false; } - public Map.Entry next() { + public Map.Entry next() { if (!hasNext()) throw new NoSuchElementException(); - Map.Entry rv = next; + Map.Entry rv = next; next = null; return rv; } @@ -112,11 +112,11 @@ class ConfigIterator implements Iterator>, Closeable { /** * The object returned by the iterator. */ - private static class ConfigEntry implements Map.Entry { + private static class ConfigEntry implements Map.Entry { private final String key; - private final String value; + private final HostTxtEntry value; - public ConfigEntry(String k, String v) { + public ConfigEntry(String k, HostTxtEntry v) { key = k; value = v; } @@ -125,11 +125,11 @@ class ConfigIterator implements Iterator>, Closeable { return key; } - public String getValue() { + public HostTxtEntry getValue() { return value; } - public String setValue(String v) { + public HostTxtEntry setValue(HostTxtEntry v) { throw new UnsupportedOperationException(); } diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java index 1ce8c06e79..385c62a284 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java @@ -36,6 +36,7 @@ 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.OrderedProperties; import net.i2p.util.SecureDirectory; import net.i2p.util.SystemVersion; @@ -53,6 +54,12 @@ public class Daemon { private static final String DEFAULT_SUB = "http://i2p-projekt.i2p/hosts.txt"; /** @since 0.9.12 */ static final String OLD_DEFAULT_SUB = "http://www.i2p2.i2p/hosts.txt"; + /** Any properties we receive from the subscription, we store to the + * addressbook with this prefix, so it knows it's part of the signature. + * This is also chosen so that it can't be spoofed. + */ + private static final String RCVD_PROP_PREFIX = "="; + private static final boolean MUST_VALIDATE = false; /** * Update the router and published address books using remote data from the @@ -137,8 +144,8 @@ public class Daemon { start = end; } int old = 0, nnew = 0, invalid = 0, conflict = 0, total = 0; - for (Iterator> eIter = sub.iterator(); eIter.hasNext(); ) { - Map.Entry entry = eIter.next(); + for (Iterator> eIter = sub.iterator(); eIter.hasNext(); ) { + Map.Entry entry = eIter.next(); String key = entry.getKey(); boolean isKnown; Destination oldDest = null; @@ -157,9 +164,26 @@ public class Daemon { try { if (!isKnown) { if (AddressBook.isValidKey(key)) { - Destination dest = new Destination(entry.getValue()); - Properties props = new Properties(); + HostTxtEntry he = entry.getValue(); + Destination dest = new Destination(he.getDest()); + Properties props = new OrderedProperties(); props.setProperty("s", sub.getLocation()); + if (he.hasValidSig()) { + props.setProperty("v", "true"); + } else if (MUST_VALIDATE) { + // TODO + //if (log != null) + // log.append("Bad signature for new key " + key); + continue; + } + Properties hprops = he.getProps(); + if (hprops != null) { + // merge in all the received properties + for (Map.Entry e : hprops.entrySet()) { + // Add prefix to indicate received property + props.setProperty(RCVD_PROP_PREFIX + e.getKey(), (String) e.getValue()); + } + } boolean success = router.put(key, dest, props); if (log != null) { if (success) @@ -172,7 +196,7 @@ public class Daemon { if (published != null) { if (publishedNS == null) publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath()); - success = publishedNS.putIfAbsent(key, dest); + success = publishedNS.putIfAbsent(key, dest, props); if (log != null && !success) { try { log.append("Save to published address book " + published.getCanonicalPath() + " failed for new key " + key); @@ -237,16 +261,12 @@ public class Daemon { File published = null; boolean should_publish = Boolean.parseBoolean(settings.get("should_publish")); if (should_publish) - published = new File(home, settings - .get("published_addressbook")); - File subscriptionFile = new File(home, settings - .get("subscriptions")); + published = new File(home, settings.get("published_addressbook")); + File subscriptionFile = new File(home, settings.get("subscriptions")); 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, settings - .get("last_fetched")); + File lastModifiedFile = new File(home, settings.get("last_modified")); + File lastFetchedFile = new File(home, settings.get("last_fetched")); long delay; try { delay = Long.parseLong(settings.get("update_delay")); @@ -260,8 +280,9 @@ public class Daemon { defaultSubs.add(DEFAULT_SUB); SubscriptionList subscriptions = new SubscriptionList(subscriptionFile, - etagsFile, lastModifiedFile, lastFetchedFile, delay, defaultSubs, settings - .get("proxy_host"), Integer.parseInt(settings.get("proxy_port"))); + etagsFile, lastModifiedFile, lastFetchedFile, + delay, defaultSubs, settings.get("proxy_host"), + Integer.parseInt(settings.get("proxy_port"))); Log log = SystemVersion.isAndroid() ? null : new Log(logFile); // If false, add hosts via naming service; if true, write hosts.txt file directly diff --git a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java new file mode 100644 index 0000000000..1540e36c36 --- /dev/null +++ b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java @@ -0,0 +1,281 @@ +package net.i2p.addressbook; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.util.Map; + +import net.i2p.crypto.DSAEngine; +import net.i2p.crypto.SigType; +import net.i2p.data.Base64; +import net.i2p.data.DataFormatException; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.data.Signature; +import net.i2p.data.SigningPublicKey; +import net.i2p.util.OrderedProperties; +// for testing only +import java.io.File; +import java.io.OutputStreamWriter; +import java.io.StringWriter; +import net.i2p.data.PrivateKeyFile; +import net.i2p.data.SigningPrivateKey; + + +/** + * A hostname, b64 destination, and optional properties. + * + * @since 0.9.26 + */ +class HostTxtEntry { + + private final String name; + private final String dest; + private final OrderedProperties props; + private boolean isValidated; + private boolean isValid; + + public static final char KV_SEPARATOR = '='; + public static final String PROPS_SEPARATOR = "#!"; + public static final char PROP_SEPARATOR = '#'; + public static final String PROP_ACTION = "action"; + public static final String PROP_DATE = "date"; + public static final String PROP_DEST = "dest"; + public static final String PROP_EXPIRES = "expires"; + public static final String PROP_NAME = "name"; + public static final String PROP_OLDDEST = "olddest"; + public static final String PROP_OLDNAME = "oldname"; + public static final String PROP_OLDSIG = "oldsig"; + public static final String PROP_SIG = "sig"; + public static final String ACTION_ADDDEST = "adddest"; + public static final String ACTION_ADDNAME = "addname"; + public static final String ACTION_ADDSUBDOMAIN = "addsubdomain"; + public static final String ACTION_CHANGEDEST = "changedest"; + public static final String ACTION_CHANGENAME = "changename"; + public static final String ACTION_REMOVE = "remove"; + public static final String ACTION_REMOVEALL = "removeall"; + public static final String ACTION_UPDATE = "update"; + + /** + * Properties will be null + */ + public HostTxtEntry(String name, String dest) { + this(name, dest, (OrderedProperties) null); + } + + /** + * @param sprops line part after the #!, non-null + * @throws IllegalArgumentException on dup key in sprops and other errors + */ + public HostTxtEntry(String name, String dest, String sprops) throws IllegalArgumentException { + this.name = name; + this.dest = dest; + this.props = parseProps(sprops); + } + + /** + * @param props may be null + */ + public HostTxtEntry(String name, String dest, OrderedProperties props) { + this.name = name; + this.dest = dest; + this.props = props; + } + + public String getName() { + return name; + } + + public String getDest() { + return dest; + } + + public OrderedProperties getProps() { + return props; + } + + /** + * @param line part after the #! + * @throws IllegalArgumentException on dup key and other errors + */ + private OrderedProperties parseProps(String line) throws IllegalArgumentException { + line = line.trim(); + OrderedProperties rv = new OrderedProperties(); + String[] entries = DataHelper.split(line, "#"); + for (int i = 0; i < entries.length; i++) { + String kv = entries[i]; + int eq = kv.indexOf("="); + if (eq <= 0 || eq == kv.length() - 1) + throw new IllegalArgumentException("No value: \"" + kv + '"'); + String k = kv.substring(0, eq); + String v = kv.substring(eq + 1); + Object old = rv.setProperty(k, v); + if (old != null) + throw new IllegalArgumentException("Dup key: " + k); + } + return rv; + } + + public void write(BufferedWriter out) throws IOException { + out.write(name); + out.write(KV_SEPARATOR); + out.write(dest); + if (props != null && props.size() > 0) { + boolean started = false; + for (Map.Entry e : props.entrySet()) { + if (started) { + out.write(PROP_SEPARATOR); + } else { + started = true; + out.write(PROPS_SEPARATOR); + } + String k = (String) e.getKey(); + String v = (String) e.getValue(); + out.write(k); + out.write(KV_SEPARATOR); + out.write(v); + } + } + out.newLine(); + } + + public boolean hasValidSig() { + if (props == null) + return false; + if (!isValidated) { + isValidated = true; + StringBuilder buf = new StringBuilder(1024); + String sig = null; + buf.append(name); + buf.append(KV_SEPARATOR); + buf.append(dest); + boolean started = false; + for (Map.Entry e : props.entrySet()) { + String k = (String) e.getKey(); + String v = (String) e.getValue(); + if (k.equals(PROP_SIG)) { + if (sig != null) + return false; + sig = v; + // remove from the written data + continue; + } + if (started) { + buf.append(PROP_SEPARATOR); + } else { + started = true; + buf.append(PROPS_SEPARATOR); + } + buf.append(k); + buf.append(KV_SEPARATOR); + buf.append(v); + } + if (sig == null) + return false; + byte[] sdata = Base64.decode(sig); + if (sdata == null) + return false; + Destination d; + try { + d = new Destination(dest); + } catch (DataFormatException dfe) { + return false; + } + SigningPublicKey spk = d.getSigningPublicKey(); + SigType type = spk.getType(); + if (type == null) + return false; + Signature s; + try { + s = new Signature(type, sdata); + } catch (IllegalArgumentException iae) { + return false; + } + isValid = DSAEngine.getInstance().verifySignature(s, DataHelper.getUTF8(buf.toString()), spk); + } + return isValid; + } + + @Override + public int hashCode() { + return dest.hashCode(); + } + + /** + * Compares Destination only, not properties + */ + @Override + public boolean equals(Object o) { + if (o == this) + return true; + if (!(o instanceof HostTxtEntry)) + return false; + HostTxtEntry he = (HostTxtEntry) o; + return dest.equals(he.getDest()); + } + + /** for testing only */ + private void sign(SigningPrivateKey spk) { + if (props == null) + throw new IllegalStateException(); + Destination d; + try { + d = new Destination(dest); + } catch (DataFormatException dfe) { + throw new IllegalStateException("bah", dfe); + } + StringBuilder buf = new StringBuilder(1024); + buf.append(name); + buf.append(KV_SEPARATOR); + buf.append(dest); + boolean started = false; + for (Map.Entry e : props.entrySet()) { + String k = (String) e.getKey(); + String v = (String) e.getValue(); + if (k.equals(PROP_SIG)) + throw new IllegalStateException(); + if (started) { + buf.append(PROP_SEPARATOR); + } else { + started = true; + buf.append(PROPS_SEPARATOR); + } + buf.append(k); + buf.append(KV_SEPARATOR); + buf.append(v); + } + Signature s = DSAEngine.getInstance().sign(DataHelper.getUTF8(buf.toString()), spk); + if (s == null) + throw new IllegalArgumentException("sig failed"); + props.setProperty(PROP_SIG, s.toBase64()); + } + + public static void main(String[] args) throws Exception { + File f = new File("tmp-eepPriv.dat"); + PrivateKeyFile pkf = new PrivateKeyFile(f); + pkf.createIfAbsent(SigType.EdDSA_SHA512_Ed25519); + OrderedProperties props = new OrderedProperties(); + props.setProperty("c", "ccccccccccc"); + props.setProperty("a", "aaaa"); + HostTxtEntry he = new HostTxtEntry("foo.i2p", pkf.getDestination().toBase64(), props); + BufferedWriter out = new BufferedWriter(new OutputStreamWriter(System.out)); + out.write("Before signing:\n"); + he.write(out); + out.flush(); + SigningPrivateKey priv = pkf.getSigningPrivKey(); + he.sign(priv); + out.write("After signing:\n"); + he.write(out); + out.flush(); + System.out.println("Orig has valid sig? " + he.hasValidSig()); + // 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("foo.i2p", pkf.getDestination().toBase64(), line); + System.out.println("Dupl. has valid sig? " + he2.hasValidSig()); + f.delete(); + } +} diff --git a/apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java new file mode 100644 index 0000000000..f2c2df1491 --- /dev/null +++ b/apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java @@ -0,0 +1,224 @@ +package net.i2p.addressbook; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import net.i2p.data.DataHelper; +import net.i2p.util.SecureFile; +import net.i2p.util.SecureFileOutputStream; +import net.i2p.util.SystemVersion; + +/** + * Utility class providing methods to parse and write files in a hosts.txt file + * format, and subscription file format. + * + * @since 0.9.26 modified from ConfigParser + */ +class HostTxtParser { + + private static final boolean isWindows = SystemVersion.isWindows(); + + /** + * Return a Map using the contents of BufferedReader input. input must have + * a single key, value pair on each line, in the format: key=value. Lines + * starting with '#' or ';' are considered comments, and ignored. Lines that + * are obviously not in the format key=value are also ignored. + * The key is converted to lower case. + * + * @param input + * A BufferedReader with lines in key=value format to parse into + * a Map. + * @return A Map containing the key, value pairs from input. + * @throws IOException + * if the BufferedReader cannot be read. + * + */ + private static Map parse(BufferedReader input) throws IOException { + try { + Map result = new HashMap(); + String inputLine; + while ((inputLine = input.readLine()) != null) { + HostTxtEntry he = parse(inputLine); + if (he == null) + continue; + result.put(he.getName(), he); + } + return result; + } finally { + try { input.close(); } catch (IOException ioe) {} + } + } + + /** + * Return a HostTxtEntry from the contents of the inputLine. + * + * @param inputLine key=value[#!k1=v1#k2=v2...] + * @return null if no entry found or on error + */ + public static HostTxtEntry parse(String inputLine) { + if (inputLine.startsWith(";")) + return null; + int comment = inputLine.indexOf("#"); + if (comment == 0) + return null; + String kv; + String sprops; + if (comment > 0) { + int shebang = inputLine.indexOf(HostTxtEntry.PROPS_SEPARATOR); + if (shebang == comment && shebang + 2 < inputLine.length()) + sprops = inputLine.substring(shebang + 2); + else + 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(); + HostTxtEntry he; + if (sprops != null) { + try { + he = new HostTxtEntry(name, dest, sprops); + } catch (IllegalArgumentException iae) { + return null; + } + } else { + he = new HostTxtEntry(name, dest); + } + return he; + } + + /** + * Return a Map using the contents of the File file. See parseBufferedReader + * for details of the input format. + * + * @param file + * A File to parse. + * @return A Map containing the key, value pairs from file. + * @throws IOException + * if file cannot be read. + */ + public static Map parse(File file) throws IOException { + FileInputStream fileStream = null; + try { + fileStream = new FileInputStream(file); + BufferedReader input = new BufferedReader(new InputStreamReader( + fileStream, "UTF-8")); + Map rv = parse(input); + return rv; + } finally { + if (fileStream != null) { + try { + fileStream.close(); + } catch (IOException ioe) {} + } + } + } + + /** + * 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. + * + * @param file + * A File to attempt to parse. + * @param map + * A Map containing values to use as defaults. + * @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; + try { + result = parse(file); + for (Map.Entry entry : map.entrySet()) { + if (!result.containsKey(entry.getKey())) + result.put(entry.getKey(), entry.getValue()); + } + } catch (IOException exp) { + result = map; + try { + write(result, file); + } catch (IOException exp2) { + } + } + return result; + } + + /** + * Write contents of Map map to BufferedWriter output. Output is written + * with one key, value pair on each line, in the format: key=value. + * + * @param map + * A Map to write to output. + * @param output + * A BufferedWriter to write the Map to. + * @throws IOException + * if the BufferedWriter cannot be written to. + */ + private static void write(Map map, BufferedWriter output) throws IOException { + try { + for (Map.Entry entry : map.entrySet()) { + entry.getValue().write(output); + } + } finally { + try { output.close(); } catch (IOException ioe) {} + } + } + + /** + * Write contents of Map map to the File file. Output is written + * with one key, value pair on each line, in the format: key=value. + * Write to a temp file in the same directory and then rename, to not corrupt + * simultaneous accesses by the router. Except on Windows where renameTo() + * will fail if the target exists. + * + * @param map + * A Map to write to file. + * @param file + * A File to write the Map to. + * @throws IOException + * if file cannot be written to. + */ + public static void write(Map map, File file) throws IOException { + boolean success = false; + if (!isWindows) { + File tmp = SecureFile.createTempFile("temp-", ".tmp", file.getAbsoluteFile().getParentFile()); + write(map, new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(tmp), "UTF-8"))); + success = tmp.renameTo(file); + if (!success) { + tmp.delete(); + //System.out.println("Warning: addressbook rename fail from " + tmp + " to " + file); + } + } + if (!success) { + // hmm, that didn't work, try it the old way + write(map, new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8"))); + } + } + + public static void main(String[] args) throws Exception { + File f = new File("tmp-hosts.txt"); + Map map = parse(f); + for (HostTxtEntry e : map.values()) { + System.out.println("Host: " + e.getName() + + "\nDest: " + e.getDest() + + "\nValid? " + e.hasValidSig()); + } + } + +} diff --git a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java index 4cab462c16..0472d2f266 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/SubscriptionIterator.java @@ -85,7 +85,7 @@ class SubscriptionIterator implements Iterator { // DataHelper.formatDuration(I2PAppContext.getGlobalContext().clock().now() - sub.getLastFetched()) + // " ago but the minimum delay is " + // DataHelper.formatDuration(this.delay)); - return new AddressBook(Collections. emptyMap()); + return new AddressBook(Collections.emptyMap()); } } diff --git a/core/java/src/net/i2p/client/naming/SingleFileNamingService.java b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java index 38a174b7e7..5f69b919f5 100644 --- a/core/java/src/net/i2p/client/naming/SingleFileNamingService.java +++ b/core/java/src/net/i2p/client/naming/SingleFileNamingService.java @@ -59,6 +59,10 @@ public class SingleFileNamingService extends NamingService { private long _lastWrite; private volatile boolean _isClosed; + private static final String RCVD_PROP_PREFIX = "="; + private static final String PROPS_SEPARATOR = "#!"; + private static final char PROP_SEPARATOR = '#'; + public SingleFileNamingService(I2PAppContext context, String filename) { super(context); File file = new File(filename); @@ -165,7 +169,8 @@ public class SingleFileNamingService extends NamingService { /** * @param hostname case-sensitive; caller should convert to lower case - * @param options ignored + * @param options if non-null, any prefixed with '=' will be appended + * in subscription format */ @Override public boolean put(String hostname, Destination d, Properties options) { @@ -196,6 +201,9 @@ public class SingleFileNamingService extends NamingService { out.write(hostname); out.write('='); out.write(d.toBase64()); + // subscription options + if (options != null) + writeOptions(options, out); out.newLine(); out.close(); boolean success = FileUtil.rename(tmp, _file); @@ -215,11 +223,12 @@ public class SingleFileNamingService extends NamingService { /** * @param hostname case-sensitive; caller should convert to lower case - * @param options ignored + * @param options if non-null, any prefixed with '=' will be appended + * in subscription format */ @Override public boolean putIfAbsent(String hostname, Destination d, Properties options) { - OutputStream out = null; + BufferedWriter out = null; if (!getWriteLock()) return false; try { @@ -236,11 +245,14 @@ public class SingleFileNamingService extends NamingService { } // else new file } - out = new SecureFileOutputStream(_file, true); + out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(_file, true), "UTF-8")); // FIXME fails if previous last line didn't have a trailing \n - out.write(hostname.getBytes("UTF-8")); + out.write(hostname); out.write('='); - out.write(DataHelper.getASCII(d.toBase64())); + out.write(d.toBase64()); + // subscription options + if (options != null) + writeOptions(options, out); out.write('\n'); out.close(); for (NamingServiceListener nsl : _listeners) { @@ -254,6 +266,34 @@ public class SingleFileNamingService extends NamingService { } finally { releaseWriteLock(); } } + /** + * Write the subscription options part of the line (after the #!). + * Only options starting with '=' (if any) are written (with the '=' stripped). + * Does not write a newline. + * + * @param options non-null + * @since 0.9.26 + */ + private static void writeOptions(Properties options, Writer out) throws IOException { + boolean started = false; + for (Map.Entry e : options.entrySet()) { + String k = (String) e.getKey(); + if (!k.startsWith(RCVD_PROP_PREFIX)) + continue; + k = k.substring(1); + String v = (String) e.getValue(); + if (started) { + out.write(PROP_SEPARATOR); + } else { + started = true; + out.write(PROPS_SEPARATOR); + } + out.write(k); + out.write('='); + out.write(v); + } + } + /** * @param hostname case-sensitive; caller should convert to lower case * @param options ignored diff --git a/history.txt b/history.txt index 73c946d743..db6407fbf7 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,18 @@ +2016-04-17 zzz + * Addressbook: + - Several cleanups and refactoring + - Add initial support for signatures in subscriptions + - Fix main-class in addressbook.jar + - Fix corrupted manifest in addressbook.jar + * Build: Fix broken build from scratch in jetty build.xml + * Console: + - Add JSTL version to /logs + - Update version warnings + - Add OpenJDK check for ARM + * PrivateKeyFile: Add method to specify sig type on creation + * SingleFileNamingService: Store signature properties on write + * TunnelId: Add max value check + 2016-04-13 zzz * SOCKS: Fix NPE on lookup failure in SOCKS 4a diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 282c18b422..0725033fa6 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 2; + public final static long BUILD = 3; /** for example "-test" */ public final static String EXTRA = "";