forked from I2P_Developers/i2p.i2p
Addressbook: Add initial support for signatures in subscriptions
More cleanups SingleFileNamingService: Store signature properties on write
This commit is contained in:
@ -43,11 +43,11 @@ import net.i2p.util.SecureFile;
|
||||
* @author Ragnarok
|
||||
*
|
||||
*/
|
||||
class AddressBook implements Iterable<Map.Entry<String, String>> {
|
||||
class AddressBook implements Iterable<Map.Entry<String, HostTxtEntry>> {
|
||||
|
||||
private final String location;
|
||||
/** either addresses or subFile will be non-null, but not both */
|
||||
private final Map<String, String> addresses;
|
||||
private final Map<String, HostTxtEntry> addresses;
|
||||
private final File subFile;
|
||||
private boolean modified;
|
||||
private static final boolean DEBUG = false;
|
||||
@ -79,7 +79,7 @@ class AddressBook implements Iterable<Map.Entry<String, String>> {
|
||||
* A Map containing human readable addresses as keys, mapped to
|
||||
* base64 i2p destinations.
|
||||
*/
|
||||
public AddressBook(Map<String, String> addresses) {
|
||||
public AddressBook(Map<String, HostTxtEntry> addresses) {
|
||||
this.addresses = addresses;
|
||||
this.subFile = null;
|
||||
this.location = null;
|
||||
@ -130,7 +130,7 @@ class AddressBook implements Iterable<Map.Entry<String, String>> {
|
||||
* @param proxyPort port number of proxy
|
||||
*/
|
||||
public AddressBook(Subscription subscription, String proxyHost, int proxyPort) {
|
||||
Map<String, String> a = null;
|
||||
Map<String, HostTxtEntry> a = null;
|
||||
File subf = null;
|
||||
try {
|
||||
File tmp = SecureFile.createTempFile("addressbook", null, I2PAppContext.getGlobalContext().getTempDir());
|
||||
@ -167,11 +167,11 @@ class AddressBook implements Iterable<Map.Entry<String, String>> {
|
||||
*/
|
||||
public AddressBook(File file) {
|
||||
this.location = file.toString();
|
||||
Map<String, String> a;
|
||||
Map<String, HostTxtEntry> a;
|
||||
try {
|
||||
a = ConfigParser.parse(file);
|
||||
a = HostTxtParser.parse(file);
|
||||
} catch (IOException exp) {
|
||||
a = new HashMap<String, String>();
|
||||
a = new HashMap<String, HostTxtEntry>();
|
||||
}
|
||||
this.addresses = a;
|
||||
this.subFile = null;
|
||||
@ -181,14 +181,14 @@ class AddressBook implements Iterable<Map.Entry<String, String>> {
|
||||
* Return an iterator over the addresses in the AddressBook.
|
||||
* @since 0.8.7
|
||||
*/
|
||||
public Iterator<Map.Entry<String, String>> iterator() {
|
||||
public Iterator<Map.Entry<String, HostTxtEntry>> 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<Map.Entry<String, String>> {
|
||||
|
||||
/**
|
||||
* 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<Map.Entry<String, String>> {
|
||||
public void merge(AddressBook other, boolean overwrite, Log log) {
|
||||
if (this.addresses == null)
|
||||
throw new IllegalStateException();
|
||||
for (Iterator<Map.Entry<String, String>> iter = other.iterator(); iter.hasNext(); ) {
|
||||
Map.Entry<String, String> entry = iter.next();
|
||||
for (Iterator<Map.Entry<String, HostTxtEntry>> iter = other.iterator(); iter.hasNext(); ) {
|
||||
Map.Entry<String, HostTxtEntry> 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<Map.Entry<String, String>> {
|
||||
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());
|
||||
}
|
||||
|
@ -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<Map.Entry<String, String>>, Closeable {
|
||||
class ConfigIterator implements Iterator<Map.Entry<String, HostTxtEntry>>, Closeable {
|
||||
|
||||
private BufferedReader input;
|
||||
private ConfigEntry next;
|
||||
@ -70,14 +73,11 @@ class ConfigIterator implements Iterator<Map.Entry<String, String>>, 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<Map.Entry<String, String>>, Closeable {
|
||||
return false;
|
||||
}
|
||||
|
||||
public Map.Entry<String, String> next() {
|
||||
public Map.Entry<String, HostTxtEntry> next() {
|
||||
if (!hasNext())
|
||||
throw new NoSuchElementException();
|
||||
Map.Entry<String, String> rv = next;
|
||||
Map.Entry<String, HostTxtEntry> rv = next;
|
||||
next = null;
|
||||
return rv;
|
||||
}
|
||||
@ -112,11 +112,11 @@ class ConfigIterator implements Iterator<Map.Entry<String, String>>, Closeable {
|
||||
/**
|
||||
* The object returned by the iterator.
|
||||
*/
|
||||
private static class ConfigEntry implements Map.Entry<String, String> {
|
||||
private static class ConfigEntry implements Map.Entry<String, HostTxtEntry> {
|
||||
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<Map.Entry<String, String>>, Closeable {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
public HostTxtEntry getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public String setValue(String v) {
|
||||
public HostTxtEntry setValue(HostTxtEntry v) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
@ -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<Map.Entry<String, String>> eIter = sub.iterator(); eIter.hasNext(); ) {
|
||||
Map.Entry<String, String> entry = eIter.next();
|
||||
for (Iterator<Map.Entry<String, HostTxtEntry>> eIter = sub.iterator(); eIter.hasNext(); ) {
|
||||
Map.Entry<String, HostTxtEntry> 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<Object, Object> 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
|
||||
|
281
apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java
Normal file
281
apps/addressbook/java/src/net/i2p/addressbook/HostTxtEntry.java
Normal file
@ -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<Object, Object> 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<Object, Object> 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<Object, Object> 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();
|
||||
}
|
||||
}
|
224
apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java
Normal file
224
apps/addressbook/java/src/net/i2p/addressbook/HostTxtParser.java
Normal file
@ -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<String, HostTxtEntry> parse(BufferedReader input) throws IOException {
|
||||
try {
|
||||
Map<String, HostTxtEntry> result = new HashMap<String, HostTxtEntry>();
|
||||
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<String, HostTxtEntry> parse(File file) throws IOException {
|
||||
FileInputStream fileStream = null;
|
||||
try {
|
||||
fileStream = new FileInputStream(file);
|
||||
BufferedReader input = new BufferedReader(new InputStreamReader(
|
||||
fileStream, "UTF-8"));
|
||||
Map<String, HostTxtEntry> 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<String, HostTxtEntry> parse(File file, Map<String, HostTxtEntry> map) {
|
||||
Map<String, HostTxtEntry> result;
|
||||
try {
|
||||
result = parse(file);
|
||||
for (Map.Entry<String, HostTxtEntry> 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<String, HostTxtEntry> map, BufferedWriter output) throws IOException {
|
||||
try {
|
||||
for (Map.Entry<String, HostTxtEntry> 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<String, HostTxtEntry> 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<String, HostTxtEntry> map = parse(f);
|
||||
for (HostTxtEntry e : map.values()) {
|
||||
System.out.println("Host: " + e.getName() +
|
||||
"\nDest: " + e.getDest() +
|
||||
"\nValid? " + e.hasValidSig());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -85,7 +85,7 @@ class SubscriptionIterator implements Iterator<AddressBook> {
|
||||
// DataHelper.formatDuration(I2PAppContext.getGlobalContext().clock().now() - sub.getLastFetched()) +
|
||||
// " ago but the minimum delay is " +
|
||||
// DataHelper.formatDuration(this.delay));
|
||||
return new AddressBook(Collections.<String, String> emptyMap());
|
||||
return new AddressBook(Collections.<String, HostTxtEntry>emptyMap());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Object, Object> 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
|
||||
|
15
history.txt
15
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
|
||||
|
||||
|
@ -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 = "";
|
||||
|
Reference in New Issue
Block a user