I2P Address: [http://git.idk.i2p]

Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • equincey/i2p.i2p
  • marek/i2p.i2p
  • kytv/i2p.i2p
  • agentoocat/i2p.i2p
  • aargh/i2p.i2p
  • Kalhintz/i2p.i2p
  • longyap/i2p.i2p
  • kelare/i2p.i2p
  • apsoyka/i2p.i2p
  • mesh/i2p.i2p
  • ashtod/i2p.i2p
  • y2kboy23/i2p.i2p
  • Lfrr/i2p.i2p
  • anonymousmaybe/i2p.i2p
  • obscuratus/i2p.i2p
  • zzz/i2p.i2p
  • lbt/i2p.i2p
  • 31337/i2p.i2p
  • DuncanIdaho/i2p.i2p
  • loveisgrief/i2p.i2p
  • i2p-hackers/i2p.i2p
  • thebland/i2p.i2p
  • elde/i2p.i2p
  • echelon/i2p.i2p
  • welshlyluvah1967/i2p.i2p
  • zlatinb/i2p.i2p
  • sadie/i2p.i2p
  • pVT0/i2p.i2p
  • idk/i2p.i2p
29 results
Show changes
Showing
with 5474 additions and 129 deletions
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package addressbook;
import java.util.List;
import java.util.LinkedList;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.io.File;
import java.io.IOException;
/**
* A list of Subscriptions loaded from a file.
*
* @author Ragnarok
*
*/
public class SubscriptionList {
private List subscriptions;
private File etagsFile;
private File lastModifiedFile;
private String proxyHost;
private int proxyPort;
/**
* Construct a SubscriptionList using the urls from locationsFile and, if
* available, the etags and last-modified headers loaded from etagsFile and
* lastModifiedFile.
*
* @param locationsFile
* A file containing one url on each line.
* @param etagsFile
* A file containg the etag headers used for conditional GET. The
* file is in the format "url=etag".
* @param lastModifiedFile
* A file containg the last-modified headers used for conditional
* GET. The file is in the format "url=leastmodified".
*/
public SubscriptionList(File locationsFile, File etagsFile,
File lastModifiedFile, List defaultSubs, String proxyHost,
int proxyPort) {
this.subscriptions = new LinkedList();
this.etagsFile = etagsFile;
this.lastModifiedFile = lastModifiedFile;
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
Map etags;
Map lastModified;
String location;
List locations = ConfigParser.parseSubscriptions(locationsFile,
defaultSubs);
try {
etags = ConfigParser.parse(etagsFile);
} catch (IOException exp) {
etags = new HashMap();
}
try {
lastModified = ConfigParser.parse(lastModifiedFile);
} catch (IOException exp) {
lastModified = 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)));
}
}
/**
* Return an iterator over the AddressBooks represented by the Subscriptions
* in this SubscriptionList.
*
* @return A SubscriptionIterator.
*/
public SubscriptionIterator iterator() {
return new SubscriptionIterator(this.subscriptions, this.proxyHost,
this.proxyPort);
}
/**
* Write the etag and last-modified headers for each Subscription to files.
*/
public void write() {
Iterator iter = this.subscriptions.iterator();
Subscription sub;
Map etags = new HashMap();
Map lastModified = new HashMap();
while (iter.hasNext()) {
sub = (Subscription) iter.next();
if (sub.getEtag() != null) {
etags.put(sub.getLocation(), sub.getEtag());
}
if (sub.getLastModified() != null) {
lastModified.put(sub.getLocation(), sub.getLastModified());
}
}
try {
ConfigParser.write(etags, this.etagsFile);
ConfigParser.write(lastModified, this.lastModifiedFile);
} catch (IOException exp) {
}
}
}
\ No newline at end of file
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.i2p.addressbook;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Pattern;
import net.i2p.I2PAppContext;
import net.i2p.client.naming.HostTxtEntry;
import net.i2p.util.EepGet;
import net.i2p.util.SecureFile;
/**
* An address book for storing human readable names mapped to base64 i2p
* destinations. AddressBooks can be created from local and remote files, merged
* together, and written out to local files.
*
* Methods are NOT thread-safe.
*
* @author Ragnarok
*
*/
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, HostTxtEntry> addresses;
private final File subFile;
private boolean modified;
private static final boolean DEBUG = false;
private static final int MIN_DEST_LENGTH = 516;
private static final int MAX_DEST_LENGTH = MIN_DEST_LENGTH + 100; // longer than any known cert type for now
/**
* 5-67 chars lower/upper case
*/
private static final Pattern HOST_PATTERN =
Pattern.compile("^[0-9a-zA-Z\\.-]{5,67}$");
/**
* 52 chars lower/upper case
* Always ends in 'a' or 'q'
*/
private static final Pattern B32_PATTERN =
Pattern.compile("^[2-7a-zA-Z]{51}[aAqQ]$");
/** not a complete qualification, just a quick check */
private static final Pattern B64_PATTERN =
Pattern.compile("^[0-9a-zA-Z~-]{" + MIN_DEST_LENGTH + ',' + MAX_DEST_LENGTH + "}={0,2}$");
/**
* Construct an AddressBook from the contents of the Map addresses.
*
* @param addresses
* A Map containing human readable addresses as keys, mapped to
* base64 i2p destinations.
*/
public AddressBook(Map<String, HostTxtEntry> addresses) {
this.addresses = addresses;
this.subFile = null;
this.location = null;
}
/*
* Construct an AddressBook from the contents of the file at url. If the
* remote file cannot be read, construct an empty AddressBook
*
* @param url
* A URL pointing at a file with lines in the format "key=value",
* where key is a human readable name, and value is a base64 i2p
* destination.
*/
/* unused
public AddressBook(String url, String proxyHost, int proxyPort) {
this.location = url;
EepGet get = new EepGet(I2PAppContext.getGlobalContext(), true,
proxyHost, proxyPort, 0, "addressbook.tmp", url, true,
null);
get.fetch();
try {
this.addresses = ConfigParser.parse(new File("addressbook.tmp"));
} catch (IOException exp) {
this.addresses = new HashMap();
}
new File("addressbook.tmp").delete();
}
*/
static final long MAX_SUB_SIZE = 5 * 1024 * 1024l; //about 8,000 hosts
/**
* Construct an AddressBook from the Subscription subscription. If the
* address book at subscription has not changed since the last time it was
* read or cannot be read, return an empty AddressBook.
* Set a maximum size of the remote book to make it a little harder for a malicious book-sender.
*
* Yes, the EepGet fetch() is done in this constructor.
*
* This stores the subscription in a temporary file and does not read the whole thing into memory.
* An AddressBook created with this constructor may not be modified or written using write().
* It may be a merge source (an parameter for another AddressBook's merge())
* but may not be a merge target (this.merge() will throw an exception).
*
* @param subscription
* A Subscription instance pointing at a remote address book.
* @param proxyHost hostname of proxy
* @param proxyPort port number of proxy
*/
public AddressBook(Subscription subscription, String proxyHost, int proxyPort) {
Map<String, HostTxtEntry> a = null;
File subf = null;
File tmp = null;
try {
tmp = SecureFile.createTempFile("addressbook", null, I2PAppContext.getGlobalContext().getTempDir());
// Apache 2.4 mod_deflate etag bug workaround
// strip -gzip from the etag
// Gitlab #454
String loc = subscription.getLocation();
String etag = subscription.getEtag();
if (loc.startsWith("http://i2p-projekt.i2p/") && etag != null && etag.endsWith("-gzip\""))
etag = etag.substring(0, etag.length() - 6) + '"';
EepGet get = new EepGet(I2PAppContext.getGlobalContext(), true,
proxyHost, proxyPort, 0, -1l, MAX_SUB_SIZE, tmp.getAbsolutePath(), null,
loc, true, etag, subscription.getLastModified(), null);
if (get.fetch()) {
subscription.setEtag(get.getETag());
subscription.setLastModified(get.getLastModified());
subscription.setLastFetched(I2PAppContext.getGlobalContext().clock().now());
subf = tmp;
} else {
a = Collections.emptyMap();
tmp.delete();
}
} catch (IOException ioe) {
if (tmp != null)
tmp.delete();
a = Collections.emptyMap();
}
this.addresses = a;
this.subFile = subf;
this.location = subscription.getLocation();
}
/**
* Construct an AddressBook from the contents of the file at file. If the
* file cannot be read, construct an empty AddressBook.
* This reads the entire file into memory.
* The resulting map is modifiable and may be a merge target.
*
* @param file
* A File pointing at a file with lines in the format
* "key=value", where key is a human readable name, and value is
* a base64 i2p destination.
*/
public AddressBook(File file) {
this.location = file.toString();
Map<String, HostTxtEntry> a;
try {
a = HostTxtParser.parse(file);
} catch (IOException exp) {
a = new HashMap<String, HostTxtEntry>();
}
this.addresses = a;
this.subFile = null;
}
/**
* Test only.
*
* @param testsubfile path to a file containing the simulated fetch of a subscription
* @since 0.9.26
*/
public AddressBook(String testsubfile) {
this.location = testsubfile;
this.addresses = null;
this.subFile = new File(testsubfile);
}
/**
* Return an iterator over the addresses in the AddressBook.
* @since 0.8.7
*/
public Iterator<Map.Entry<String, HostTxtEntry>> iterator() {
if (this.subFile != null) {
try {
return new HostTxtIterator(this.subFile);
} catch (IOException ioe) {
return new HostTxtIterator();
}
}
return this.addresses.entrySet().iterator();
}
/**
* Delete the temp file or clear the map.
* @since 0.8.7
*/
public void delete() {
if (this.subFile != null) {
this.subFile.delete();
} else if (this.addresses != null) {
try {
this.addresses.clear();
} catch (UnsupportedOperationException uoe) {}
}
}
/**
* Return the location of the file this AddressBook was constructed from.
*
* @return A String representing either an abstract path, or a url,
* depending on how the instance was constructed.
* Will be null if created with the Map constructor.
*/
public String getLocation() {
return this.location;
}
/**
* Return a string representation of the origin of the AddressBook.
*
* @return A String representing the origin of the AddressBook.
*/
@Override
public String toString() {
if (this.location != null)
return "Book from " + this.location;
return "Map containing " + this.addresses.size() + " entries";
}
/**
* Do basic validation of the address
* address was already converted to lower case by HostTxtParser.parse()
*/
public static boolean isValidKey(String host) {
return
host.endsWith(".i2p") &&
host.length() > 4 &&
host.length() <= 67 && // 63 + ".i2p"
(! host.startsWith(".")) &&
(! host.startsWith("-")) &&
host.indexOf(".-") < 0 &&
host.indexOf("-.") < 0 &&
host.indexOf("..") < 0 &&
// IDN - basic check, not complete validation
(host.indexOf("--") < 0 || host.startsWith("xn--") || host.indexOf(".xn--") > 0) &&
HOST_PATTERN.matcher(host).matches() &&
// Base32 spoofing (52chars.i2p)
// We didn't do it this way, we use a .b32.i2p suffix, but let's prohibit it anyway
(! (host.length() == 56 && B32_PATTERN.matcher(host.substring(0,52)).matches())) &&
// ... or maybe we do Base32 this way ...
(! host.equals("b32.i2p")) &&
(! host.endsWith(".b32.i2p")) &&
// some reserved names that may be used for local configuration someday
(! host.equals("proxy.i2p")) &&
(! host.equals("router.i2p")) &&
(! host.equals("console.i2p")) &&
(! host.endsWith(".proxy.i2p")) &&
(! host.endsWith(".router.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)) &&
// B64 comes in groups of 2, 3, or 4 chars, but never 1
((dest.length() % 4) != 1) &&
B64_PATTERN.matcher(dest).matches()
;
}
/**
* Merge this AddressBook with AddressBook other, writing messages about new
* addresses or conflicts to log. Addresses in AddressBook other that are
* not in this AddressBook are added to this AddressBook. In case of a
* conflict, addresses in this AddressBook take precedence
*
* @param other
* An AddressBook to merge with.
* @param overwrite True to overwrite
* @param log
* The log to write messages about new addresses or conflicts to. May be null.
*
* @throws IllegalStateException if this was created with the Subscription constructor.
*/
public void merge(AddressBook other, boolean overwrite, Log log) {
if (this.addresses == null)
throw new IllegalStateException();
Iterator<Map.Entry<String, HostTxtEntry>> iter = other.iterator();
try {
merge2(other, iter, overwrite, log);
} finally {
if (iter instanceof HostTxtIterator)
((HostTxtIterator) iter).close();
}
}
private void merge2(AddressBook other, Iterator<Map.Entry<String, HostTxtEntry>> iter, boolean overwrite, Log log) {
while(iter.hasNext()) {
Map.Entry<String, HostTxtEntry> entry = iter.next();
String otherKey = entry.getKey();
HostTxtEntry otherValue = entry.getValue();
if (isValidKey(otherKey) && isValidDest(otherValue.getDest())) {
if (this.addresses.containsKey(otherKey) && !overwrite) {
if (DEBUG && log != null &&
!this.addresses.get(otherKey).equals(otherValue.getDest())) {
log.append("Conflict for " + otherKey + " from "
+ other.location
+ ". Destination in remote address book is "
+ otherValue);
}
} else if (!this.addresses.containsKey(otherKey)
|| !this.addresses.get(otherKey).equals(otherValue)) {
this.addresses.put(otherKey, otherValue);
this.modified = true;
if (log != null) {
log.append("New address " + otherKey
+ " added to address book. From: " + other.location);
}
}
}
}
}
/**
* Write the contents of this AddressBook out to the File file. If the file
* cannot be writen to, this method will silently fail.
*
* @param file
* The file to write the contents of this AddressBook too.
*
* @throws IllegalStateException if this was created with the Subscription constructor.
*/
public void write(File file) {
if (this.addresses == null)
throw new IllegalStateException();
if (this.modified) {
try {
HostTxtParser.write(this.addresses, file);
} catch (IOException exp) {
System.err.println("Error writing addressbook " + file.getAbsolutePath() + " : " + exp.toString());
}
}
}
/**
* Write this AddressBook out to the file it was read from. Requires that
* AddressBook was constructed from a file on the local filesystem. If the
* file cannot be writen to, this method will silently fail.
*
* @throws IllegalStateException if this was not created with the File constructor.
*/
public void write() {
if (this.location == null || this.location.startsWith("http://"))
throw new IllegalStateException();
this.write(new File(this.location));
}
/****
public static void main(String[] args) {
String[] tests = { "foo.i2p",
"3bnipzzu67cdq2rcygyxz52xhvy6ylokn4zfrk36ywn6pixmaoza.b32.i2p",
"9rhEy4dT9fMlcSOhDzfWRxCV2aen4Zp4eSthOf5f9gVKMa4PtQJ-wEzm2KEYeDXkbM6wEDvMQ6ou4LIniSE6bSAwy7fokiXk5oabels-sJmftnQWRbZyyXEAsLc3gpJJvp9km7kDyZ0z0YGL5tf3S~OaWdptB5tSBOAOjm6ramcYZMWhyUqm~xSL1JyXUqWEHRYwhoDJNL6-L516VpDYVigMBpIwskjeFGcqK8BqWAe0bRwxIiFTPN6Ck8SDzQvS1l1Yj-zfzg3X3gOknzwR8nrHUkjsWtEB6nhbOr8AR21C9Hs0a7MUJvSe2NOuBoNTrtxT76jDruI78JcG5r~WKl6M12yM-SqeBNE9hQn2QCHeHAKju7FdRCbqaZ99IwyjfwvZbkiYYQVN1xlUuGaXrj98XDzK7GORYdH-PrVGfEbMXQ40KLHUWHz8w4tQXAOQrCHEichod0RIzuuxo3XltCWKrf1xGZhkAo9bk2qXi6digCijvYNaKmQdXZYWW~RtAAAA",
"6IZTYacjlXjSAxu-uXEO5oGsj-f4tfePHEvGjs5pu-AMXMwD7-xFdi8kdobDMJp9yRAl96U7yLl~0t9zHeqqYmNeZnDSkTmAcSC2PT45ZJDXBobKi1~a77zuqfPwnzEatYfW3GL1JQAEkAmiwNJoG7ThTZ3zT7W9ekVJpHi9mivpTbaI~rALLfuAg~Mvr60nntZHjqhEZuiU4dTXrmc5nykl~UaMnBdwHL4jKmoN5CotqHyLYZfp74fdD-Oq4SkhuBhU8wkBIM3lz3Ul1o6-s0lNUMdYJq1CyxnyP7jeekdfAlSx4P4sU4M0dPaYvPdOFWPWwBuEh0pCs5Mj01B2xeEBhpV~xSLn6ru5Vq98TrmaR33KHxd76OYYFsWwzVbBuMVSd800XpBghGFucGw01YHYsPh3Afb01sXbf8Nb1bkxCy~DsrmoH4Ww3bpx66JhRTWvg5al3oWlCX51CnJUqaaK~dPL-pBvAyLKIA5aYvl8ca66jtA7AFDxsOb2texBBQAEAAcAAA==",
"te9Ky7XvVcLLr5vQqvfmOasg915P3-ddP3iDqpMMk7v5ufFKobLAX~1k-E4WVsJVlkYvkHVOjxix-uT1IdewKmLd81s5wZtz0GQ3ZC6p0C3S2cOxz7kQqf7QYSR0BrhZC~2du3-GdQO9TqNmsnHrah5lOZf0LN2JFEFPqg8ZB5JNm3JjJeSqePBRk3zAUogNaNK3voB1MVI0ZROKopXAJM4XMERNqI8tIH4ngGtV41SEJJ5pUFrrTx~EiUPqmSEaEA6UDYZiqd23ZlewZ31ExXQj97zvkuhKCoS9A9MNkzZejJhP-TEXWF8~KHur9f51H--EhwZ42Aj69-3GuNjsMdTwglG5zyIfhd2OspxJrXzCPqIV2sXn80IbPgwxHu0CKIJ6X43B5vTyVu87QDI13MIRNGWNZY5KmM5pilGP7jPkOs4xQDo4NHzpuJR5igjWgJIBPU6fI9Pzq~BMzjLiZOMp8xNWey1zKC96L0eX4of1MG~oUvq0qmIHGNa1TlUwBQAEAAEAAA==",
"(*&(*&(*&(*",
"9rhEy4dT9fMlcSOhDzfWRxCV2aen4Zp4eSthOf5f9gVKMa4PtQJ-wEzm2KEYeDXkbM6wEDvMQ6ou4LIniSE6bSAwy7fokiXk5oabels-sJmftnQWRbZyyXEAsLc3gpJJvp9km7kDyZ0z0YGL5tf3S~OaWdptB5tSBOAOjm6ramcYZMWhyUqm~xSL1JyXUqWEHRYwhoDJNL6-L516VpDYVigMBpIwskjeFGcqK8BqWAe0bRwxIiFTPN6Ck8SDzQvS1l1Yj-zfzg3X3gOknzwR8nrHUkjsWtEB6nhbOr8AR21C9Hs0a7MUJvSe2NOuBoNTrtxT76jDruI78JcG5r~WKl6M12yM-SqeBNE9hQn2QCHeHAKju7FdRCbqaZ99IwyjfwvZbkiYYQVN1xlUuGaXrj98XDzK7GORYdH-PrVGfEbMXQ40KLHUWHz8w4tQXAOQrCHEichod0RIzuuxo3XltCWKrf1xGZhkAo9bk2qXi6digCijvYNaKmQdXZYWW~RtAAA",
"6IZTYacjlXjSAxu-uXEO5oGsj-f4tfePHEvGjs5pu-AMXMwD7-xFdi8kdobDMJp9yRAl96U7yLl~0t9zHeqqYmNeZnDSkTmAcSC2PT45ZJDXBobKi1~a77zuqfPwnzEatYfW3GL1JQAEkAmiwNJoG7ThTZ3zT7W9ekVJpHi9mivpTbaI~rALLfuAg~Mvr60nntZHjqhEZuiU4dTXrmc5nykl~UaMnBdwHL4jKmoN5CotqHyLYZfp74fdD-Oq4SkhuBhU8wkBIM3lz3Ul1o6-s0lNUMdYJq1CyxnyP7jeekdfAlSx4P4sU4M0dPaYvPdOFWPWwBuEh0pCs5Mj01B2xeEBhpV~xSLn6ru5Vq98TrmaR33KHxd76OYYFsWwzVbBuMVSd800XpBghGFucGw01YHYsPh3Afb01sXbf8Nb1bkxCy~DsrmoH4Ww3bpx66JhRTWvg5al3oWlCX51CnJUqaaK~dPL-pBvAyLKIA5aYvl8ca66jtA7AFDxsOb2texBBQAEAAcAAA===",
"!e9Ky7XvVcLLr5vQqvfmOasg915P3-ddP3iDqpMMk7v5ufFKobLAX~1k-E4WVsJVlkYvkHVOjxix-uT1IdewKmLd81s5wZtz0GQ3ZC6p0C3S2cOxz7kQqf7QYSR0BrhZC~2du3-GdQO9TqNmsnHrah5lOZf0LN2JFEFPqg8ZB5JNm3JjJeSqePBRk3zAUogNaNK3voB1MVI0ZROKopXAJM4XMERNqI8tIH4ngGtV41SEJJ5pUFrrTx~EiUPqmSEaEA6UDYZiqd23ZlewZ31ExXQj97zvkuhKCoS9A9MNkzZejJhP-TEXWF8~KHur9f51H--EhwZ42Aj69-3GuNjsMdTwglG5zyIfhd2OspxJrXzCPqIV2sXn80IbPgwxHu0CKIJ6X43B5vTyVu87QDI13MIRNGWNZY5KmM5pilGP7jPkOs4xQDo4NHzpuJR5igjWgJIBPU6fI9Pzq~BMzjLiZOMp8xNWey1zKC96L0eX4of1MG~oUvq0qmIHGNa1TlUwBQAEAAEAAA==",
"x"
};
for (String s : tests) {
test(s);
}
}
public static void test(String s) {
System.out.println(s + " valid host? " + isValidKey(s) + " valid dest? " + isValidDest(s));
}
****/
}
package net.i2p.addressbook;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.i2p.CoreVersion;
/**
* Simple command line access to various utilities.
* Not a public API. Subject to change.
* Apps and plugins should use specific classes.
*
* @since 0.9.55
*/
public class CommandLine extends net.i2p.util.CommandLine {
protected static final List<String> ACLASSES = Arrays.asList(new String[] {
"net.i2p.addressbook.HostTxtParser",
"net.i2p.router.naming.BlockfileNamingService",
"net.metanotion.io.block.BlockFile",
});
protected CommandLine() {}
public static void main(String args[]) {
List<String> classes = new ArrayList<String>(ACLASSES.size() + CLASSES.size());
classes.addAll(ACLASSES);
classes.addAll(CLASSES);
if (args.length > 0) {
exec(args, classes);
}
usage(classes);
System.exit(1);
}
private static void usage(List<String> classes) {
System.err.println("I2P Address book version " + CoreVersion.VERSION + '\n' +
"USAGE: java -jar /path/to/addressbook.jar command [args]");
printCommands(classes);
}
}
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
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 config file
* format, and subscription file format.
*
* TODO: switch to the DataHelper loadProps/storeProps methods?
*
* @author Ragnarok
*/
class ConfigParser {
private static final boolean isWindows = SystemVersion.isWindows();
/**
* Strip the comments from a String. Lines that begin with '#' and ';' are
* considered comments, as well as any part of a line after a '#'.
*
* @param inputLine
* A String to strip comments from.
* @return A String without comments, but otherwise identical to inputLine.
*/
public static String stripComments(String inputLine) {
if (inputLine.startsWith(";")) {
return "";
}
int hash = inputLine.indexOf('#');
if (hash >= 0) {
return inputLine.substring(0, hash);
} else {
return inputLine;
}
}
/**
* 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, String> parse(BufferedReader input) throws IOException {
try {
Map<String, String> result = new HashMap<String, String>();
String inputLine;
while ((inputLine = input.readLine()) != null) {
inputLine = stripComments(inputLine);
if (inputLine.length() == 0)
continue;
String[] splitLine = DataHelper.split(inputLine, "=", 2);
if (splitLine.length == 2) {
result.put(splitLine[0].trim().toLowerCase(Locale.US), splitLine[1].trim());
}
}
return result;
} finally {
try { input.close(); } catch (IOException ioe) {}
}
}
/**
* 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, String> parse(File file) throws IOException {
FileInputStream fileStream = null;
try {
fileStream = new FileInputStream(file);
BufferedReader input = new BufferedReader(new InputStreamReader(
fileStream, "UTF-8"));
Map<String, String> rv = parse(input);
return rv;
} finally {
if (fileStream != null) {
try {
fileStream.close();
} catch (IOException ioe) {}
}
}
}
/**
* Return a Map using the contents of the String string. See
* parseBufferedReader for details of the input format.
*
* @param string
* A String to parse.
* @return A Map containing the key, value pairs from string.
* @throws IOException
* if file cannot be read.
*/
/****
public static Map<String, String> parse(String string) throws IOException {
StringReader stringReader = new StringReader(string);
BufferedReader input = new BufferedReader(stringReader);
return parse(input);
}
****/
/**
* 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, String> parse(File file, Map<String, String> map) {
Map<String, String> result;
try {
result = parse(file);
// migrate
String local = result.remove("master_addressbook");
if (local != null)
result.put("local_addressbook", local);
for (Map.Entry<String, String> 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;
}
/**
* Return a List where each element is a line from the BufferedReader input.
*
* @param input
* A BufferedReader to parse.
* @return A List consisting of one element for each line in input.
* @throws IOException
* if input cannot be read.
*/
private static List<String> parseSubscriptions(BufferedReader input)
throws IOException {
try {
List<String> result = new ArrayList<String>(4);
String inputLine;
while ((inputLine = input.readLine()) != null) {
inputLine = stripComments(inputLine).trim();
if (inputLine.length() > 0) {
result.add(inputLine);
}
}
return result;
} finally {
try { input.close(); } catch (IOException ioe) {}
}
}
/**
* Return a List where each element is a line from the File file.
*
* @param file
* A File to parse.
* @return A List consisting of one element for each line in file.
* @throws IOException
* if file cannot be read.
*/
private static List<String> parseSubscriptions(File file) throws IOException {
FileInputStream fileStream = null;
try {
fileStream = new FileInputStream(file);
BufferedReader input = new BufferedReader(new InputStreamReader(
fileStream, "UTF-8"));
List<String> rv = parseSubscriptions(input);
return rv;
} finally {
if (fileStream != null) {
try {
fileStream.close();
} catch (IOException ioe) {}
}
}
}
/**
* Return a List where each element is a line from the String string.
*
* @param string
* A String to parse.
* @return A List consisting of one element for each line in string.
* @throws IOException
* if string cannot be read.
*/
/****
public static List<String> parseSubscriptions(String string) throws IOException {
StringReader stringReader = new StringReader(string);
BufferedReader input = new BufferedReader(stringReader);
return parseSubscriptions(input);
}
****/
/**
* Return a List using the contents of the File file. If file cannot be
* read, use list instead, and write the result to where file should have
* been.
*
* @param file
* A File to attempt to parse.
* @param list The default subscriptions to be saved and returned if the file cannot be read
* @return A List consisting of one element for each line in file, or if
* file cannot be read, list.
*/
public static List<String> parseSubscriptions(File file, List<String> list) {
List<String> result;
try {
result = parseSubscriptions(file);
// Fix up files that contain the old default
// which was changed in 0.9.11
if (result.remove(Daemon.OLD_DEFAULT_SUB)) {
for (String sub : list) {
if (!result.contains(sub))
result.add(sub);
}
try {
writeSubscriptions(result, file);
// TODO log
} catch (IOException ioe) {}
}
} catch (IOException exp) {
result = list;
try {
writeSubscriptions(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, String> map, BufferedWriter output) throws IOException {
try {
for (Map.Entry<String, String> entry : map.entrySet()) {
output.write(entry.getKey() + '=' + entry.getValue());
output.newLine();
}
} 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, String> 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")));
}
}
/**
* Write contents of List list to BufferedReader output. Output is written
* with each element of list on a new line.
*
* @param list
* A List to write to file.
* @param output
* A BufferedReader to write list to.
* @throws IOException
* if output cannot be written to.
*/
private static void writeSubscriptions(List<String> list, BufferedWriter output)
throws IOException {
try {
for (String s : list) {
output.write(s);
output.newLine();
}
} finally {
try { output.close(); } catch (IOException ioe) {}
}
}
/**
* Write contents of List list to File file. Output is written with each
* element of list on a new line.
*
* @param list
* A List to write to file.
* @param file
* A File to write list to.
* @throws IOException
* if output cannot be written to.
*/
private static void writeSubscriptions(List<String> list, File file)
throws IOException {
writeSubscriptions(list, new BufferedWriter(
new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8")));
}
}
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.i2p.addressbook;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import net.i2p.I2PAppContext;
import net.i2p.app.ClientAppManager;
import net.i2p.client.naming.HostTxtEntry;
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.PortMapper;
import net.i2p.util.SecureDirectory;
import net.i2p.util.SystemVersion;
import net.i2p.util.Translate;
/**
* Main class of addressbook. Performs updates, and runs the main loop.
* As of 0.9.30, package private, run with DaemonThread.
*
* @author Ragnarok
*
*/
class Daemon {
public static final String VERSION = "2.0.4";
private volatile boolean _running;
private static final boolean DEBUG = false;
// If you change this, change in SusiDNS SubscriptionBean also
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
* subscribed address books listed in subscriptions.
*
* @param local
* The local AddressBook. This address book is never
* overwritten, so it is safe for the user to write to.
* It is only merged to the published addressbook.
* May be null.
* @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 I2P Site so that others may subscribe to it.
* May be null.
* 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.
* May be null.
*/
public static void update(AddressBook local, AddressBook router,
File published, SubscriptionList subscriptions, Log log) {
for (AddressBook book : subscriptions) {
// yes, the EepGet fetch() is done in next()
router.merge(book, false, log);
}
router.write();
if (published != null) {
if (local != null)
router.merge(local, true, null);
router.write(published);
}
subscriptions.write();
}
/**
* Update the router and published address books using remote data from the
* subscribed address books listed in subscriptions.
* Merging of the "local" addressbook is NOT supported.
*
* @param router
* The NamingService to update, generally the root NamingService from the context.
* @param published
* The published AddressBook. This address book is published on
* the user's I2P Site so that others may subscribe to it.
* May be null.
* 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.
* May be null.
* @since 0.8.7
*/
public static void update(NamingService router, File published, SubscriptionList subscriptions, Log log) {
// If the NamingService is a database, we look up as we go.
// If it is a text file, we do things differently, to avoid O(n**2) behavior
// when scanning large subscription results (i.e. those that return the whole file, not just the new entries) -
// we load all the known hostnames into a Set one time.
// This also has the advantage of not flushing the NamingService's LRU cache.
String nsClass = router.getClass().getSimpleName();
boolean isTextFile = nsClass.equals("HostsTxtNamingService") || nsClass.equals("SingleFileNamingService");
Set<String> knownNames;
if (isTextFile) {
// load the hostname set
Properties opts = new Properties();
opts.setProperty("file", "hosts.txt");
knownNames = router.getNames(opts);
} else {
knownNames = null;
}
NamingService publishedNS;
if (published != null) {
publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath());
} else {
publishedNS = null;
}
Iterator<AddressBook> iter = subscriptions.iterator();
while (iter.hasNext()) {
// yes, the EepGet fetch() is done in next()
long start = System.currentTimeMillis();
AddressBook addressbook = iter.next();
// SubscriptionIterator puts in a dummy AddressBook with no location if no fetch is done
if (DEBUG && log != null && addressbook.getLocation() != null) {
long end = System.currentTimeMillis();
log.append("Fetch of " + addressbook.getLocation() + " took " + (end - start));
}
Iterator<Map.Entry<String, HostTxtEntry>> iter2 = addressbook.iterator();
try {
update(router, knownNames, publishedNS, addressbook, iter2, log);
} finally {
if (iter2 instanceof HostTxtIterator)
((HostTxtIterator) iter2).close();
addressbook.delete();
}
} // subscriptions
subscriptions.write();
}
/**
* @param knownNames only non-null if router book is a text file
* @param publishedNS only non-null if we have a published address book
* @since 0.9.33 split out from above
*/
private static void update(NamingService router, Set<String> knownNames,
NamingService publishedNS, AddressBook addressbook,
Iterator<Map.Entry<String, HostTxtEntry>> iter, Log log) {
long start = DEBUG ? System.currentTimeMillis() : 0;
int old = 0, nnew = 0, invalid = 0, conflict = 0, total = 0;
int deleted = 0;
while(iter.hasNext()) {
Map.Entry<String, HostTxtEntry> entry = iter.next();
total++;
// may be null for 'remove' entries
String key = entry.getKey();
boolean isKnown;
// NOT set for text file NamingService
Destination oldDest;
if (knownNames != null) {
oldDest = null;
isKnown = key != null ? knownNames.contains(key) : false;
} else {
oldDest = key != null ? router.lookup(key) : null;
isKnown = oldDest != null;
}
try {
HostTxtEntry he = entry.getValue();
Properties hprops = he.getProps();
boolean mustValidate = MUST_VALIDATE || hprops != null;
String action = hprops != null ? hprops.getProperty(HostTxtEntry.PROP_ACTION) : null;
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) {
log.append("Bad signature of action " + action + " for key " + key +
". From: " + addressbook.getLocation());
}
invalid++;
} else if (action != null || !isKnown) {
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");
}
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());
}
}
if (action != null) {
// Process commands. hprops is non-null.
// Must handle isKnown in each case.
if (action.equals(HostTxtEntry.ACTION_ADDDEST)) {
// Add an alternate destination (new crypto) for existing hostname
// Requires new NamingService support if the key exists
String polddest = hprops.getProperty(HostTxtEntry.PROP_OLDDEST);
if (polddest != null) {
Destination pod = new Destination(polddest);
List<Destination> pod2 = router.lookupAll(key);
if (pod2 == null) {
// we didn't know it before, so we'll add it
// check inner sig anyway
if (!he.hasValidInnerSig()) {
logInner(log, action, key, addressbook);
invalid++;
continue;
}
} else if (pod2.contains(dest)) {
// we knew it before, with the same dest
old++;
continue;
} else if (pod2.contains(pod)) {
// checks out, so verify the inner sig
if (!he.hasValidInnerSig()) {
logInner(log, action, key, addressbook);
invalid++;
continue;
}
// TODO Requires NamingService support
// if (isTextFile), do we replace or not? check sigType.isAvailable()
boolean success = router.addDestination(key, dest, props);
if (log != null) {
if (success)
log.append("Additional address for " + key +
" added to address book. From: " + addressbook.getLocation());
else
log.append("Failed to add additional address for " + key +
" From: " + addressbook.getLocation());
}
// now update the published addressbook
// ditto
if (publishedNS != null) {
// FIXME this fails, no support in SFNS
success = publishedNS.addDestination(key, dest, props);
if (log != null && !success)
log.append("Add to published address book " + publishedNS.getName() + " failed for " + key);
}
nnew++;
continue;
} else {
// mismatch, disallow
logMismatch(log, action, key, pod2, he.getDest(), addressbook);
invalid++;
continue;
}
} else {
logMissing(log, action, key, addressbook);
invalid++;
continue;
}
} else if (action.equals(HostTxtEntry.ACTION_ADDNAME)) {
// Add an alias for an existing hostname, same dest
if (isKnown) {
// could be same or different dest
old++;
continue;
}
String poldname = hprops.getProperty(HostTxtEntry.PROP_OLDNAME);
if (poldname != null) {
List<Destination> pod = router.lookupAll(poldname);
if (pod == null) {
// we didn't have the old one, so we'll add the new one
} else if (pod.contains(dest)) {
// checks out, so we'll add the new one
} else {
// mismatch, disallow
logMismatch(log, action, key, pod, he.getDest(), addressbook);
invalid++;
continue;
}
} else {
logMissing(log, action, key, addressbook);
invalid++;
continue;
}
} else if (action.equals(HostTxtEntry.ACTION_ADDSUBDOMAIN)) {
// add a subdomain with verification
if (isKnown) {
old++;
continue;
}
String polddest = hprops.getProperty(HostTxtEntry.PROP_OLDDEST);
String poldname = hprops.getProperty(HostTxtEntry.PROP_OLDNAME);
if (polddest != null && poldname != null) {
// check for valid subdomain
if (!AddressBook.isValidKey(poldname) ||
key.indexOf('.' + poldname) <= 0) {
if (log != null)
log.append("Action: " + action + " failed because" +
" old name " + poldname +
" is invalid" +
". From: " + addressbook.getLocation());
invalid++;
continue;
}
Destination pod = new Destination(polddest);
List<Destination> pod2 = router.lookupAll(poldname);
if (pod2 == null) {
// we didn't have the old name
// check inner sig anyway
if (!he.hasValidInnerSig()) {
logInner(log, action, key, addressbook);
invalid++;
continue;
}
} else if (pod2.contains(pod)) {
// checks out, so verify the inner sig
if (!he.hasValidInnerSig()) {
logInner(log, action, key, addressbook);
invalid++;
continue;
}
} else {
// mismatch, disallow
logMismatch(log, action, key, pod2, polddest, addressbook);
invalid++;
continue;
}
} else {
logMissing(log, action, key, addressbook);
invalid++;
continue;
}
} else if (action.equals(HostTxtEntry.ACTION_CHANGEDEST)) {
// change destination on an existing entry
// This removes all previous destinations under that hostname,
// is this what we want?
String polddest = hprops.getProperty(HostTxtEntry.PROP_OLDDEST);
if (polddest != null) {
Destination pod = new Destination(polddest);
List<Destination> pod2 = router.lookupAll(key);
if (pod2 == null) {
// we didn't have the old name
// check inner sig anyway
if (!he.hasValidInnerSig()) {
logInner(log, action, key, addressbook);
invalid++;
continue;
}
} else if (pod2.contains(dest)) {
// we already have the new dest
old++;
continue;
} else if (pod2.contains(pod)) {
// checks out, so verify the inner sig
if (!he.hasValidInnerSig()) {
logInner(log, action, key, addressbook);
invalid++;
continue;
}
if (log != null) {
if (pod2.size() == 1)
log.append("Changing destination for " + key +
". From: " + addressbook.getLocation());
else
log.append("Replacing " + pod2.size() + " destinations for " + key +
". From: " + addressbook.getLocation());
}
allowExistingKeyInPublished = true;
props.setProperty("m", Long.toString(I2PAppContext.getGlobalContext().clock().now()));
} else {
// mismatch, disallow
logMismatch(log, action, key, pod2, polddest, addressbook);
invalid++;
continue;
}
} else {
logMissing(log, action, key, addressbook);
invalid++;
continue;
}
} else if (action.equals(HostTxtEntry.ACTION_CHANGENAME)) {
// Delete old name, replace with new
// This removes all previous destinations under that hostname,
// is this what we want?
if (isKnown) {
old++;
continue;
}
String poldname = hprops.getProperty(HostTxtEntry.PROP_OLDNAME);
if (poldname != null) {
List<Destination> pod = router.lookupAll(poldname);
if (pod == null) {
// we didn't have the old name
} else if (pod.contains(dest)) {
// checks out, so we'll delete it
if (knownNames != null)
knownNames.remove(poldname);
boolean success = router.remove(poldname, dest);
if (success)
deleted++;
if (log != null) {
if (success)
log.append("Removed: " + poldname +
" to be replaced with " + key +
". From: " + addressbook.getLocation());
else
log.append("Remove failed for: " + poldname +
" to be replaced with " + key +
". From: " + addressbook.getLocation());
}
// now update the published addressbook
if (publishedNS != null) {
success = publishedNS.remove(poldname, dest);
if (log != null && !success)
log.append("Remove from published address book " + publishedNS.getName() + " failed for " + poldname);
}
} else {
// mismatch, disallow
logMismatch(log, action, key, pod, he.getDest(), addressbook);
continue;
}
} else {
logMissing(log, action, key, addressbook);
invalid++;
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;
props.setProperty("m", Long.toString(I2PAppContext.getGlobalContext().clock().now()));
}
} 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 (publishedNS != null) {
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 " + publishedNS.getName() + " failed for new key " + key);
}
}
if (knownNames != null) {
// 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) {
Destination pod = new Destination(polddest);
List<Destination> pod2 = router.lookupAll(poldname);
if (pod2 != null && pod2.contains(pod)) {
if (knownNames != null && pod2.size() == 1)
knownNames.remove(poldname);
boolean success = router.remove(poldname, pod);
if (success)
deleted++;
if (log != null) {
if (success)
log.append("Removed: " + poldname +
" as requested" +
". From: " + addressbook.getLocation());
else
log.append("Remove failed for: " + poldname +
" as requested" +
". From: " + addressbook.getLocation());
}
// now update the published addressbook
if (publishedNS != null) {
success = publishedNS.remove(poldname, pod);
if (log != null && !success)
log.append("Remove from published address book " + publishedNS.getName() + " failed for " + poldname);
}
} else if (pod2 != null) {
// mismatch, disallow
logMismatch(log, action, key, pod2, polddest, addressbook);
invalid++;
} else {
old++;
}
} else {
logMissing(log, action, "delete", addressbook);
invalid++;
}
} else if (action.equals(HostTxtEntry.ACTION_REMOVEALL)) {
// delete all entries with this destination
String polddest = hprops.getProperty(HostTxtEntry.PROP_DEST);
// oldname is optional, but nice because not all books support reverse lookup
if (polddest != null) {
Destination pod = new Destination(polddest);
String poldname = hprops.getProperty(HostTxtEntry.PROP_NAME);
if (poldname != null) {
List<Destination> pod2 = router.lookupAll(poldname);
if (pod2 != null && pod2.contains(pod)) {
if (knownNames != null)
knownNames.remove(poldname);
boolean success = router.remove(poldname, pod);
if (success)
deleted++;
if (log != null) {
if (success)
log.append("Removed: " + poldname +
" as requested" +
". From: " + addressbook.getLocation());
else
log.append("Remove failed for: " + poldname +
" as requested" +
". From: " + addressbook.getLocation());
}
// now update the published addressbook
if (publishedNS != null) {
success = publishedNS.remove(poldname, pod);
if (log != null && !success)
log.append("Remove from published address book " + publishedNS.getName() + " failed for " + poldname);
}
} else if (pod2 != null) {
// mismatch, disallow
logMismatch(log, action, key, pod2, polddest, addressbook);
invalid++;
} else {
old++;
}
}
// reverse lookup, delete all
List<String> revs = router.reverseLookupAll(pod);
if (revs != null) {
for (String rev : revs) {
if (knownNames != null)
knownNames.remove(rev);
boolean success = router.remove(rev, pod);
if (success)
deleted++;
if (log != null) {
if (success)
log.append("Removed: " + rev +
" as requested" +
". From: " + addressbook.getLocation());
else
log.append("Remove failed for: " + rev +
" as requested" +
". From: " + addressbook.getLocation());
}
// now update the published addressbook
if (publishedNS != null) {
success = publishedNS.remove(rev, pod);
if (log != null && !success)
log.append("Remove from published address book " + publishedNS.getName() + " failed for " + rev);
}
}
}
} else {
logMissing(log, action, "delete", addressbook);
invalid++;
}
} else {
if (log != null)
log.append("Action: " + action + " w/o name=dest unrecognized" +
". From: " + addressbook.getLocation());
invalid++;
}
continue;
} else {
if (log != null)
log.append("No action in command line" +
". From: " + addressbook.getLocation());
invalid++;
continue;
}
} else if (log != null) {
log.append("Bad hostname " + key + ". From: "
+ addressbook.getLocation());
invalid++;
}
/****
} else if (false && DEBUG && log != null) {
// lookup the conflict if we haven't yet (O(n**2) for text file)
if (isTextFile)
oldDest = router.lookup(key);
if (oldDest != null && !oldDest.toBase64().equals(entry.getValue())) {
log.append("Conflict for " + key + ". From: "
+ addressbook.getLocation()
+ ". Destination in remote address book is "
+ entry.getValue());
conflict++;
} else {
old++;
}
****/
} else {
old++;
}
} catch (DataFormatException dfe) {
if (log != null)
log.append("Invalid b64 for " + key + " From: " + addressbook.getLocation());
invalid++;
}
} // entries
if (DEBUG && log != null && total > 0) {
log.append("Merge of " + addressbook.getLocation() + " into " + router +
" took " + (System.currentTimeMillis() - start) + " ms with " +
total + " total, " +
nnew + " new, " +
old + " old, " +
deleted + " deleted, " +
invalid + " invalid, " +
conflict + " conflicts");
}
if (nnew > 0) {
ClientAppManager cmgr = I2PAppContext.getGlobalContext().clientAppManager();
if (cmgr != null) {
int nc = cmgr.getBubbleCount(PortMapper.SVC_SUSIDNS) + nnew;
String msg = ngettext("{0} new host", "{0} new hosts", nc);
cmgr.setBubble(PortMapper.SVC_SUSIDNS, nc, msg);
}
}
}
/**
* translate (ngettext) from the routerconsole bundle
* @since 0.9.66
*/
private static String ngettext(String s, String p, int n) {
return Translate.getString(n, s, p, I2PAppContext.getGlobalContext(), "net.i2p.router.web.messages");
}
/** @since 0.9.26 */
private static void logInner(Log log, String action, String name, AddressBook addressbook) {
if (log != null) {
log.append("Action: " + action + " failed because" +
" inner signature for key " + name +
" failed" +
". From: " + addressbook.getLocation());
}
}
/** @since 0.9.26 */
private static void logMissing(Log log, String action, String name, AddressBook addressbook) {
if (log != null) {
log.append("Action: " + action + " for " + name +
" failed, missing required parameters" +
". From: " + addressbook.getLocation());
}
}
/** @since 0.9.26 */
private static void logMismatch(Log log, String action, String name, List<Destination> dests,
String olddest, AddressBook addressbook) {
if (log != null) {
StringBuilder buf = new StringBuilder(16);
final int sz = dests.size();
for (int i = 0; i < sz; i++) {
buf.append(dests.get(i).toBase64(), 0, 6);
if (i != sz - 1)
buf.append(", ");
}
log.append("Action: " + action + " failed because" +
" destinations for " + name +
" (" + buf + ')' +
" do not include" +
" (" + olddest.substring(0, 6) + ')' +
". From: " + addressbook.getLocation());
}
}
/**
* Run an update, using the Map settings to provide the parameters.
*
* @param settings
* A Map containg the parameters needed by update.
* @param home
* The directory containing addressbook's configuration files.
*/
public static void update(Map<String, String> settings, String home) {
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"));
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"));
long delay;
try {
delay = Long.parseLong(settings.get("update_delay"));
} catch (NumberFormatException nfe) {
delay = 12;
}
delay *= 60 * 60 * 1000;
List<String> defaultSubs = new ArrayList<String>(4);
// defaultSubs.add("http://i2p/NF2RLVUxVulR3IqK0sGJR0dHQcGXAzwa6rEO4WAWYXOHw-DoZhKnlbf1nzHXwMEJoex5nFTyiNMqxJMWlY54cvU~UenZdkyQQeUSBZXyuSweflUXFqKN-y8xIoK2w9Ylq1k8IcrAFDsITyOzjUKoOPfVq34rKNDo7fYyis4kT5bAHy~2N1EVMs34pi2RFabATIOBk38Qhab57Umpa6yEoE~rbyR~suDRvD7gjBvBiIKFqhFueXsR2uSrPB-yzwAGofTXuklofK3DdKspciclTVzqbDjsk5UXfu2nTrC1agkhLyqlOfjhyqC~t1IXm-Vs2o7911k7KKLGjB4lmH508YJ7G9fLAUyjuB-wwwhejoWqvg7oWvqo4oIok8LG6ECR71C3dzCvIjY2QcrhoaazA9G4zcGMm6NKND-H4XY6tUWhpB~5GefB3YczOqMbHq4wi0O9MzBFrOJEOs3X4hwboKWANf7DT5PZKJZ5KorQPsYRSq0E3wSOsFCSsdVCKUGsAAAA/i2p/hosts.txt");
defaultSubs.add(DEFAULT_SUB);
SubscriptionList subscriptions = new SubscriptionList(subscriptionFile,
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
// Default false
if (Boolean.parseBoolean(settings.get("update_direct"))) {
// Direct hosts.txt access
File routerFile = new File(home, settings.get("router_addressbook"));
AddressBook local;
if (should_publish) {
File localFile = new File(home, settings.get("local_addressbook"));
local = new AddressBook(localFile);
} else {
local = null;
}
AddressBook router = new AddressBook(routerFile);
update(local, router, published, subscriptions, log);
} else {
// Naming service - no merging of local to router and published is supported.
update(getNamingService(settings.get("naming_service")), published, subscriptions, log);
}
}
/** depth-first search */
private static NamingService searchNamingService(NamingService ns, String srch)
{
String name = ns.getName();
if (name.equals(srch) || name.endsWith('/' + srch) || name.endsWith('\\' + srch))
return ns;
List<NamingService> list = ns.getNamingServices();
if (list != null) {
for (NamingService nss : list) {
NamingService rv = searchNamingService(nss, srch);
if (rv != null)
return rv;
}
}
return null;
}
/** @return the configured NamingService, or the root NamingService */
private static NamingService getNamingService(String srch)
{
NamingService root = I2PAppContext.getGlobalContext().namingService();
NamingService rv = searchNamingService(root, srch);
return rv != null ? rv : root;
}
/**
* Load the settings, set the proxy, then enter into the main loop. The main
* loop performs an immediate update, and then an update every number of
* hours, as configured in the settings file.
*
* @param args
* Command line arguments. If there are any arguments provided,
* the first is taken as addressbook's home directory, and the
* others are ignored.
*/
public static void main(String[] args) {
Daemon daemon = new Daemon();
if (args.length > 0 && args[0].equals("test"))
daemon.test(args);
else
daemon.run(args);
}
/** @since 0.9.26 */
public static void test(String[] args) {
Properties ctxProps = new Properties();
String PROP_FORCE = "i2p.naming.blockfile.writeInAppContext";
ctxProps.setProperty(PROP_FORCE, "true");
I2PAppContext ctx = new I2PAppContext(ctxProps);
NamingService ns = getNamingService("hosts.txt");
File published = new File("test-published.txt");
Log log = new Log(new File("test-log.txt"));
SubscriptionList subscriptions = new SubscriptionList("test-sub.txt");
update(ns, published, subscriptions, log);
ctx.logManager().flush();
}
/**
* @param args may be null
*/
public void run(String[] args) {
_running = true;
String settingsLocation = "config.txt";
File homeFile;
if (args != null && args.length > 0) {
homeFile = new SecureDirectory(args[0]);
if (!homeFile.isAbsolute())
homeFile = new SecureDirectory(I2PAppContext.getGlobalContext().getRouterDir(), args[0]);
} else {
homeFile = new SecureDirectory(System.getProperty("user.dir"));
}
Map<String, String> defaultSettings = new HashMap<String, String>();
defaultSettings.put("proxy_host", "127.0.0.1");
defaultSettings.put("proxy_port", "4444");
defaultSettings.put("local_addressbook", "../userhosts.txt");
defaultSettings.put("router_addressbook", "../hosts.txt");
defaultSettings.put("published_addressbook", "../eepsite/docroot/hosts.txt");
defaultSettings.put("should_publish", "false");
defaultSettings.put("log", "log.txt");
defaultSettings.put("subscriptions", "subscriptions.txt");
defaultSettings.put("etags", "etags");
defaultSettings.put("last_modified", "last_modified");
defaultSettings.put("last_fetched", "last_fetched");
defaultSettings.put("update_delay", "12");
defaultSettings.put("update_direct", "false");
defaultSettings.put("naming_service", "hosts.txt");
if (!homeFile.exists()) {
boolean created = homeFile.mkdirs();
if (!created)
System.out.println("ERROR: Addressbook directory " + homeFile.getAbsolutePath() + " could not be created");
//else
// System.out.println("INFO: Addressbook directory " + homeFile.getAbsolutePath() + " created");
}
File settingsFile = new File(homeFile, settingsLocation);
Map<String, String> settings = ConfigParser.parse(settingsFile, defaultSettings);
// wait
try {
Thread.sleep(5*60*1000 + I2PAppContext.getGlobalContext().random().nextLong(5*60*1000));
// Static method, and redundant Thread.currentThread().sleep(5*60*1000);
} catch (InterruptedException ie) {}
while (_running) {
long delay = Long.parseLong(settings.get("update_delay"));
if (delay < 1) {
delay = 1;
}
update(settings, homeFile.getAbsolutePath());
try {
synchronized (this) {
wait(delay * 60 * 60 * 1000);
}
} catch (InterruptedException exp) {
}
if (!_running)
break;
settings = ConfigParser.parse(settingsFile, defaultSettings);
}
}
/**
* Call this to get the addressbook to reread its config and
* refetch its subscriptions.
*/
public void wakeup() {
synchronized (this) {
notifyAll();
}
}
public void stop() {
_running = false;
wakeup();
}
}
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.i2p.addressbook;
import java.util.Properties;
import net.i2p.I2PAppContext;
import net.i2p.client.naming.NamingServiceUpdater;
import net.i2p.util.I2PAppThread;
/**
* A thread that waits five minutes, then runs the addressbook daemon.
*
* @author Ragnarok
*
*/
public class DaemonThread extends I2PAppThread implements NamingServiceUpdater {
private final String[] args;
private final Daemon daemon;
/**
* Construct a DaemonThread with the command line arguments args.
* @param args
* A String array to pass to Daemon.main().
*/
public DaemonThread(String[] args) {
this.args = args;
daemon = new Daemon();
}
/* (non-Javadoc)
* @see java.lang.Runnable#run()
*/
@Override
public void run() {
//try {
// Thread.sleep(5 * 60 * 1000);
//} catch (InterruptedException exp) {
//}
I2PAppContext.getGlobalContext().namingService().registerUpdater(this);
try {
if (args != null && args.length > 0 && args[0].equals("test"))
daemon.test(args);
else
daemon.run(args);
} finally {
I2PAppContext.getGlobalContext().namingService().unregisterUpdater(this);
}
}
public void halt() {
daemon.stop();
interrupt();
}
/**
* The NamingServiceUpdater interface.
* While this may be called directly, the recommended way
* is to call I2PAppContext.namingService().requestUpdate(Properties)
* which will call this.
*
* @param options ignored, may be null
* @since 0.8.7
*/
public void update(Properties options) {
interrupt();
}
}
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.i2p.addressbook;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import net.i2p.client.naming.HostTxtEntry;
import net.i2p.data.DataHelper;
/**
* A class to iterate through a hosts.txt or config file without
* reading the whole thing into memory.
* Keys are always converted to lower case.
*
* Callers should iterate all the way through or call close()
* to ensure the underlying stream is closed.
*
* This is not used for config files.
* It is only used for subscriptions.
*
* @since 0.8.7, renamed from ConfigIterator in 0.9.26
*/
class HostTxtIterator implements Iterator<Map.Entry<String, HostTxtEntry>>, Closeable {
private BufferedReader input;
private MapEntry next;
/**
* A dummy iterator in which hasNext() is always false.
*/
public HostTxtIterator() {}
/**
* An iterator over the key/value pairs in the file.
*/
public HostTxtIterator(File file) throws IOException {
FileInputStream fileStream = new FileInputStream(file);
input = new BufferedReader(new InputStreamReader(fileStream, "UTF-8"));
}
public boolean hasNext() {
if (input == null)
return false;
if (next != null)
return true;
try {
String inputLine;
while ((inputLine = input.readLine()) != null) {
HostTxtEntry he = HostTxtParser.parse(inputLine, true);
if (he == null)
continue;
next = new MapEntry(he.getName(), he);
return true;
}
} catch (IOException ioe) {}
try { input.close(); } catch (IOException ioe) {}
input = null;
next = null;
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();
Map.Entry<String, HostTxtEntry> rv = next;
next = null;
return rv;
}
public void remove() {
throw new UnsupportedOperationException();
}
public void close() {
if (input != null) {
try { input.close(); } catch (IOException ioe) {}
}
}
/**
* The object returned by the iterator.
*/
private static class MapEntry implements Map.Entry<String, HostTxtEntry> {
private final String key;
private final HostTxtEntry value;
public MapEntry(String k, HostTxtEntry v) {
key = k;
value = v;
}
public String getKey() {
return key;
}
public HostTxtEntry getValue() {
return value;
}
public HostTxtEntry setValue(HostTxtEntry v) {
throw new UnsupportedOperationException();
}
public int hashCode() {
return key.hashCode() ^ value.hashCode();
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
@SuppressWarnings("unchecked")
Map.Entry<Object, Object> e = (Map.Entry<Object, Object>) o;
return key.equals(e.getKey()) && value.equals(e.getValue());
}
}
}
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 java.util.Properties;
import net.i2p.client.naming.HostTxtEntry;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
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, public since 0.9.55 for CLI
*/
public 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.
*
* Returned map will not contain null ("remove") entries.
*
* @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, false);
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...]
* @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, boolean allowCommandOnly) {
if (inputLine.startsWith(";"))
return null;
int comment = inputLine.indexOf('#');
String kv;
String sprops;
if (comment >= 0) {
int shebang = inputLine.indexOf(HostTxtEntry.PROPS_SEPARATOR);
if (shebang == comment && shebang + 2 < inputLine.length()) {
if (comment == 0 && !allowCommandOnly)
return null;
sprops = inputLine.substring(shebang + 2);
} else {
if (comment == 0)
return null;
sprops = null;
}
kv = inputLine.substring(0, comment);
} else {
sprops = null;
kv = inputLine;
}
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 {
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 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.
* @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.
*
* Returned map will not contain null ("remove") entries.
*
* @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")));
}
}
/**
* Usage: HostTxtParser [-q] validate example.i2p=b64dest[#!key1=val1#key2=val2]
*/
public static void main(String[] args) throws Exception {
boolean quiet = false;
if (args.length > 0 && args[0].equals("-q")) {
quiet = true;
args = java.util.Arrays.copyOfRange(args, 1, args.length);
}
if (args.length != 2 || !args[0].equals("validate")) {
System.err.println("Usage: HostTxtParser validate example.i2p=b64dest[#!key1=val1#key2=val2]");
System.exit(1);
}
HostTxtEntry e = parse(args[1].trim(), false);
if (e == null) {
if (!quiet)
System.err.println("Bad format");
System.exit(2);
}
if (!e.hasValidSig()) {
if (!quiet) {
System.err.println("Bad signature for " + e.getName());
String dest = e.getDest();
try {
Destination d = new Destination(dest);
System.err.println(dest);
System.err.println(d.toString());
} catch (Exception ex) {
System.err.println("Invalid destination: " + dest);
}
Properties p = e.getProps();
if (p != null) {
for (Map.Entry<?,?> m : p.entrySet()) {
System.err.println(m.getKey() + "=" + m.getValue());
}
}
}
System.exit(3);
}
Properties p = e.getProps();
if (p != null) {
if (p.containsKey(HostTxtEntry.PROP_ACTION) ||
p.containsKey(HostTxtEntry.PROP_OLDDEST) ||
p.containsKey(HostTxtEntry.PROP_OLDNAME) ||
p.containsKey(HostTxtEntry.PROP_OLDSIG)) {
if (!e.hasValidSig()) {
if (!quiet) {
System.err.println("Bad inner signature for " + e.getName());
for (Map.Entry<?,?> m : p.entrySet()) {
System.err.println(m.getKey() + "=" + m.getValue());
}
}
System.exit(4);
}
}
}
if (!quiet) {
System.err.println("Good signature for " + e.getName());
try {
String dest = e.getDest();
Destination d = new Destination(dest);
System.err.println(dest);
System.err.println(d.toString());
} catch (Exception ex) {}
if (p != null) {
for (Map.Entry<?,?> m : p.entrySet()) {
System.err.println(m.getKey() + "=" + m.getValue());
}
}
}
System.exit(0);
}
/****
public static void test(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() +
"\nAction: " + (e.getProps() != null ? e.getProps().getProperty("action") : "(none)") +
"\nValid Inner? " + e.hasValidInnerSig() +
"\nValid? " + e.hasValidSig() +
'\n');
}
}
****/
}
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.i2p.addressbook;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.util.Date;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.util.SecureFileOutputStream;
/**
* A simple log with automatic time stamping.
*
* @author Ragnarok
*
*/
class Log {
private final File file;
/**
* Construct a Log instance that writes to the File file.
*
* @param file
* A File for the log to write to.
*/
public Log(File file) {
this.file = file;
}
/**
* Write entry to a new line in the log, with appropriate time stamp.
*
* @param entry
* A String containing a message to append to the log.
*/
public void append(String entry) {
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(this.file,
true), "UTF-8"));
String timestamp = DataHelper.formatTime(I2PAppContext.getGlobalContext().clock().now());
bw.write(timestamp + " -- " + entry);
bw.newLine();
} catch (IOException exp) {
} finally {
if (bw != null)
try { bw.close(); } catch (IOException ioe) {}
}
}
/**
* Return the File that the Log is writing to.
*
* @return The File that the log is writing to.
*/
/****
public File getFile() {
return this.file;
}
****/
}
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.i2p.addressbook;
/**
* A subscription to a remote address book.
*
* @author Ragnarok
*
*/
class Subscription {
private final String location;
private String etag;
private String lastModified;
private long lastFetched;
/**
* Construct a Subscription pointing to the address book at location, that
* was last read at the time represented by etag and lastModified.
*
* @param location
* A String representing a url to a remote address book. Non-null.
* @param etag
* The etag header that we received the last time we read this
* subscription. May be null.
* @param lastModified
* the last-modified header we received the last time we read
* this subscription. May be null.
* @param lastFetched when the subscription was last fetched (Java time, as a String).
* May be null.
*/
public Subscription(String location, String etag, String lastModified, String lastFetched) {
this.location = location;
this.etag = etag;
this.lastModified = lastModified;
if (lastFetched != null) {
try {
this.lastFetched = Long.parseLong(lastFetched);
} catch (NumberFormatException nfe) {}
}
}
/**
* Return the location this Subscription points at.
*
* @return A String representing a url to a remote address book.
*/
public String getLocation() {
return this.location;
}
/**
* Return the etag header that we received the last time we read this
* subscription.
*
* @return A String containing the etag header.
*/
public String getEtag() {
return this.etag;
}
/**
* Set the etag header.
*
* @param etag
* A String containing the etag header.
*/
public void setEtag(String etag) {
this.etag = etag;
}
/**
* Return the last-modified header that we received the last time we read
* this subscription.
*
* @return A String containing the last-modified header.
*/
public String getLastModified() {
return this.lastModified;
}
/**
* Set the last-modified header.
*
* @param lastModified
* A String containing the last-modified header.
*/
public void setLastModified(String lastModified) {
this.lastModified = lastModified;
}
/** @since 0.8.2 */
public long getLastFetched() {
return this.lastFetched;
}
/** @since 0.8.2 */
public void setLastFetched(long t) {
this.lastFetched = t;
}
}
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.i2p.addressbook;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import net.i2p.I2PAppContext;
import net.i2p.client.naming.HostTxtEntry;
import net.i2p.util.PortMapper;
/**
* An iterator over the subscriptions in a SubscriptionList. Note that this iterator
* returns AddressBook objects, and not Subscription objects.
* Yes, the EepGet fetch() is done in here in next().
*
* @author Ragnarok
*/
class SubscriptionIterator implements Iterator<AddressBook> {
private final Iterator<Subscription> subIterator;
private final String proxyHost;
private final int proxyPort;
private final long delay;
/**
* Construct a SubscriptionIterator using the Subscriprions in List subscriptions.
*
* @param subscriptions
* List of Subscription objects that represent address books.
* @param delay the minimum delay since last fetched for the iterator to actually fetch
* @param proxyHost proxy hostname
* @param proxyPort proxt port number
*/
public SubscriptionIterator(List<Subscription> subscriptions, long delay, String proxyHost, int proxyPort) {
this.subIterator = subscriptions.iterator();
this.delay = delay;
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
}
/* (non-Javadoc)
* @see java.util.Iterator#hasNext()
*/
public boolean hasNext() {
return this.subIterator.hasNext();
}
/**
* Yes, the EepGet fetch() is done in here in next().
*
* see java.util.Iterator#next()
* @return non-null AddressBook (empty if the minimum delay has not been met,
* or there is no proxy tunnel, or the fetch otherwise fails)
*/
public AddressBook next() {
Subscription sub = this.subIterator.next();
if (sub.getLocation().startsWith("file:")) {
// test only
return new AddressBook(sub.getLocation().substring(5));
} else if (sub.getLastFetched() + this.delay < I2PAppContext.getGlobalContext().clock().now() &&
I2PAppContext.getGlobalContext().portMapper().getPort(PortMapper.SVC_HTTP_PROXY) >= 0 &&
!I2PAppContext.getGlobalContext().getBooleanProperty("i2p.vmCommSystem")) {
//System.err.println("Fetching addressbook from " + sub.getLocation());
return new AddressBook(sub, this.proxyHost, this.proxyPort);
} else {
//System.err.println("Addressbook " + sub.getLocation() + " was last fetched " +
// DataHelper.formatDuration(I2PAppContext.getGlobalContext().clock().now() - sub.getLastFetched()) +
// " ago but the minimum delay is " +
// DataHelper.formatDuration(this.delay));
return new AddressBook(Collections.<String, HostTxtEntry>emptyMap());
}
}
/* (non-Javadoc)
* @see java.util.Iterator#remove()
*/
public void remove() {
throw new UnsupportedOperationException();
}
}
/*
* Copyright (c) 2004 Ragnarok
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package net.i2p.addressbook;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* A list of Subscriptions loaded from a file.
*
* @author Ragnarok
*
*/
class SubscriptionList implements Iterable<AddressBook> {
private final List<Subscription> subscriptions;
private final File etagsFile;
private final File lastModifiedFile;
private final File lastFetchedFile;
private final long delay;
private final String proxyHost;
private final int proxyPort;
/**
* Construct a SubscriptionList using the urls from locationsFile and, if
* available, the etags and last-modified headers loaded from etagsFile and
* lastModifiedFile.
*
* @param locationsFile
* A file containing one url on each line.
* @param etagsFile
* A file containg the etag headers used for conditional GET. The
* file is in the format "url=etag".
* @param lastModifiedFile
* A file containg the last-modified headers used for conditional
* GET. The file is in the format "url=leastmodified".
* @param delay the minimum delay since last fetched for the iterator to actually fetch
* @param defaultSubs default subscription file
* @param proxyHost proxy hostname
* @param proxyPort proxy port number
*/
public SubscriptionList(File locationsFile, File etagsFile,
File lastModifiedFile, File lastFetchedFile, long delay, List<String> defaultSubs, String proxyHost,
int proxyPort) {
this.subscriptions = new ArrayList<Subscription>(4);
this.etagsFile = etagsFile;
this.lastModifiedFile = lastModifiedFile;
this.lastFetchedFile = lastFetchedFile;
this.delay = delay;
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
Map<String, String> etags;
Map<String, String> lastModified;
Map<String, String> lastFetched;
List<String> locations = ConfigParser.parseSubscriptions(locationsFile,
defaultSubs);
try {
etags = ConfigParser.parse(etagsFile);
} catch (IOException exp) {
etags = Collections.<String, String>emptyMap();
}
try {
lastModified = ConfigParser.parse(lastModifiedFile);
} catch (IOException exp) {
lastModified = Collections.<String, String>emptyMap();
}
try {
lastFetched = ConfigParser.parse(lastFetchedFile);
} catch (IOException exp) {
lastFetched = Collections.<String, String>emptyMap();
}
for (String location : locations) {
this.subscriptions.add(new Subscription(location, etags.get(location),
lastModified.get(location),
lastFetched.get(location)));
}
}
/**
* Testing only.
*
* @param hoststxt path to a local file used as the test 'subscription' input
* @since 0.9.26
*/
public SubscriptionList(String hoststxt) {
File dummy = new File("/dev/null");
this.etagsFile = dummy;
this.lastModifiedFile = dummy;
this.lastFetchedFile = dummy;
this.delay = 0;
this.proxyHost = "127.0.0.1";
this.proxyPort = 4444;
Subscription sub = new Subscription("file:" + hoststxt, null, null, null);
this.subscriptions = Collections.singletonList(sub);
}
/**
* Return an iterator over the AddressBooks represented by the Subscriptions
* in this SubscriptionList.
*
* @return A SubscriptionIterator.
*/
public SubscriptionIterator iterator() {
return new SubscriptionIterator(this.subscriptions, this.delay, this.proxyHost,
this.proxyPort);
}
/**
* Write the etag and last-modified headers,
* and the last-fetched time, for each Subscription to files.
* BUG - If the subscription URL is a cgi containing an '=' the files
* won't be read back correctly; the '=' should be escaped.
*/
public void write() {
int sz = subscriptions.size();
Map<String, String> etags = new HashMap<String, String>(sz);
Map<String, String> lastModified = new HashMap<String, String>(sz);
Map<String, String> lastFetched = new HashMap<String, String>(sz);
for (Subscription sub : this.subscriptions) {
if (sub.getEtag() != null) {
etags.put(sub.getLocation(), sub.getEtag());
}
if (sub.getLastModified() != null) {
lastModified.put(sub.getLocation(), sub.getLastModified());
}
lastFetched.put(sub.getLocation(), Long.toString(sub.getLastFetched()));
}
try {
ConfigParser.write(etags, this.etagsFile);
} catch (IOException exp) {}
try {
ConfigParser.write(lastModified, this.lastModifiedFile);
} catch (IOException exp) {}
try {
ConfigParser.write(lastFetched, this.lastFetchedFile);
} catch (IOException exp) {}
}
}
<html>
<body>
<p>
The addressbook application, which fetches hosts.txt files from subscription URLs via
HTTP and adds new hosts to the local database.
While implemented as a webapp, this application contains no user interface.
May also be packaged as a jar, as is done for Android.
The webapp named 'addressbook' in the console is actually SusiDNS.
</p>
</body>
</html>
/*
* free (adj.): unencumbered; not under the control of others
* Written by mihi in 2004 and released into the public domain
* with no warranty of any kind, either expressed or implied.
* It probably won't make your computer catch on fire, or eat
* your children, but it might. Use at your own risk.
*/
package net.i2p.router.naming;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import net.i2p.I2PAppContext;
import net.i2p.client.naming.DummyNamingService;
import net.i2p.client.naming.HostsTxtNamingService;
import net.i2p.client.naming.NamingService;
import net.i2p.client.naming.NamingServiceListener;
import net.i2p.client.naming.SingleFileNamingService;
import net.i2p.crypto.SigType;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.LHMCache;
import net.i2p.util.Log;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.SystemVersion;
import net.i2p.util.VersionComparator;
import net.metanotion.io.RAIFile;
import net.metanotion.io.Serializer;
import net.metanotion.io.block.BlockFile;
import net.metanotion.io.data.IntBytes;
import net.metanotion.io.data.UTF8StringBytes;
import net.metanotion.util.skiplist.SkipIterator;
import net.metanotion.util.skiplist.SkipList;
/**
* A naming service using the net.metanotion BlockFile database.
*
* This database contains the following skiplists:
* <pre>
*
* "%%__INFO__%%" is the master database skiplist, containing one entry:
* "info": a Properties, serialized with DataHelper functions:
* "version": "4"
* "created": Java long time (ms)
* "upgraded": Java long time (ms) (as of database version 2)
* "lists": Comma-separated list of host databases, to be
* searched in-order for lookups
*
* "%%__REVERSE__%%" is the reverse lookup skiplist
* (as of database version 2):
* The skiplist keys are Integers, the first 4 bytes of the hash of the dest.
* The skiplist values are Properties.
* There may be multiple entries in the properties, each one is a reverse mapping,
* as there may be more than one hostname for a given destination,
* or there could be collisions with the same first 4 bytes of the hash.
* Each property key is a hostname.
* Each property value is the empty string.
*
* For each host database, there is a skiplist containing
* the hosts for that database.
* The keys/values in these skiplists are as follows:
* key: a UTF-8 String
* value: a DestEntry, which is:
* a one-byte count of the Properties/Destination pairs to follow
* (as of database version 4, otherwise one)
* that many pairs of:
* Properties (serialized with DataHelper)
* Destination (serialized as usual).
*
*
* The DestEntry Properties typically contains:
* "a": The time added (Java long time in ms)
* "s": The original source of the entry (typically a file name or subscription URL)
* others TBD
*
* </pre>
*
* All hostnames are converted to lower case.
*
* @since 0.8.7, moved from core to addressbook in 0.9.31
*/
public class BlockfileNamingService extends DummyNamingService {
private final BlockFile _bf;
private final RAIFile _raf;
private final List<String> _lists;
private final List<InvalidEntry> _invalid;
private final Map<String, String> _negativeCache;
private volatile boolean _isClosed;
private final boolean _readOnly;
private String _version = "0";
private volatile boolean _isVersion4;
private boolean _needsUpgrade;
private static final Serializer<Properties> _infoSerializer = new PropertiesSerializer();
private static final Serializer<String> _stringSerializer = new UTF8StringBytes();
private static final Serializer<DestEntry> _destSerializerV1 = new DestEntrySerializer();
private static final Serializer<DestEntry> _destSerializerV4 = new DestEntrySerializerV4();
// upgrade(), initExisting(), and initNew() will change this to _destSerializerV4
private volatile Serializer<DestEntry> _destSerializer = _destSerializerV1;
private static final Serializer<Integer> _hashIndexSerializer = new IntBytes();
private static final String HOSTS_DB = "hostsdb.blockfile";
private static final String FALLBACK_LIST = "hosts.txt";
private static final String PROP_FORCE = "i2p.naming.blockfile.writeInAppContext";
private static final String INFO_SKIPLIST = "%%__INFO__%%";
private static final String REVERSE_SKIPLIST = "%%__REVERSE__%%";
private static final String PROP_INFO = "info";
private static final String PROP_VERSION = "version";
private static final String PROP_LISTVERSION = "listversion";
private static final String PROP_LISTS = "lists";
private static final String PROP_CREATED = "created";
private static final String PROP_UPGRADED = "upgraded";
private static final String VERSION = "4";
private static final String PROP_ADDED = "a";
// See susidns
//private static final String PROP_MODDED = "m";
private static final String PROP_SOURCE = "s";
// See susidns
//private static final String PROP_VALIDATED = "v";
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
* one and imports entries from hosts.txt, userhosts.txt, and privatehosts.txt.
*
* If not in router context, the database will be opened read-only
* unless the property i2p.naming.blockfile.writeInAppContext is true.
* Not designed for multiple instantiations or simultaneous use by multple JVMs.
*
* @throws RuntimeException on fatal error
*/
public BlockfileNamingService(I2PAppContext context) {
super(context);
_lists = new ArrayList<String>();
_invalid = new ArrayList<InvalidEntry>();
_negativeCache = new LHMCache<String, String>(NEGATIVE_CACHE_SIZE);
BlockFile bf = null;
RAIFile raf = null;
boolean readOnly = false;
File f = new File(_context.getRouterDir(), HOSTS_DB);
if (f.exists()) {
try {
// closing a BlockFile does not close the underlying file,
// so we must create and retain a RAF so we may close it later
// *** Open readonly if not in router context (unless forced)
readOnly = (!f.canWrite()) ||
((!context.isRouterContext()) && (!context.getBooleanProperty(PROP_FORCE)));
raf = new RAIFile(f, true, !readOnly);
bf = initExisting(raf);
if (readOnly && context.isRouterContext())
_log.logAlways(Log.WARN, "Read-only hosts database in router context");
if (bf.wasMounted()) {
if (context.isRouterContext())
_log.logAlways(Log.WARN, "The hosts database was not closed cleanly or is still open by another process");
else
_log.logAlways(Log.WARN, "The hosts database is possibly in use by another process, perhaps the router? " +
"The database is not designed for simultaneous access by multiple processes.\n" +
"If you are using clients outside the router JVM, consider using the hosts.txt " +
"naming service with " +
"i2p.naming.impl=net.i2p.client.naming.HostsTxtNamingService");
}
} catch (IOException ioe) {
if (raf != null) {
try { raf.close(); } catch (IOException e) {}
}
File corrupt = new File(_context.getRouterDir(), HOSTS_DB + '.' + System.currentTimeMillis() + ".corrupt");
_log.log(Log.CRIT, "Corrupt, unsupported version, or unreadable database " +
f + ", moving to " + corrupt +
" and creating new database", ioe);
boolean success = f.renameTo(corrupt);
if (!success)
_log.log(Log.CRIT, "Failed to move corrupt database " + f + " to " + corrupt);
}
}
if (bf == null) {
try {
// closing a BlockFile does not close the underlying file,
// so we must create and retain a RAF so we may close it later
raf = new RAIFile(f, true, true);
SecureFileOutputStream.setPerms(f);
bf = initNew(raf);
} catch (IOException ioe) {
if (raf != null) {
try { raf.close(); } catch (IOException e) {}
}
_log.log(Log.CRIT, "Failed to initialize database", ioe);
throw new RuntimeException(ioe);
}
readOnly = false;
}
_bf = bf;
_raf = raf;
_readOnly = readOnly;
if (_needsUpgrade)
upgrade();
_context.addShutdownTask(new Shutdown());
}
/**
* Create a new database and initialize it from the local files
* privatehosts.txt, userhosts.txt, and hosts.txt,
* creating a skiplist in the database for each.
*/
private BlockFile initNew(RAIFile f) throws IOException {
long start = _context.clock().now();
_version = VERSION;
_destSerializer = _destSerializerV4;
_isVersion4 = true;
try {
BlockFile rv = new BlockFile(f, true);
SkipList<String, Properties> hdr = rv.makeIndex(INFO_SKIPLIST, _stringSerializer, _infoSerializer);
Properties info = new Properties();
info.setProperty(PROP_VERSION, VERSION);
info.setProperty(PROP_CREATED, Long.toString(_context.clock().now()));
String list = _context.getProperty(HostsTxtNamingService.PROP_HOSTS_FILE,
HostsTxtNamingService.DEFAULT_HOSTS_FILE);
info.setProperty(PROP_LISTS, list);
hdr.put(PROP_INFO, info);
rv.makeIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer);
int total = 0;
for (String hostsfile : getFilenames(list)) {
_lists.add(hostsfile);
File file = new File(_context.getRouterDir(), hostsfile);
if ((!file.exists()) || !(file.canRead()))
continue;
int count = 0;
BufferedReader in = null;
String sourceMsg = "Imported from " + hostsfile + " file";
try {
in = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"), 16*1024);
String line = null;
while ( (line = in.readLine()) != null) {
if (line.startsWith("#"))
continue;
int split = line.indexOf('=');
if (split <= 0)
continue;
String key = line.substring(0, split).toLowerCase(Locale.US);
if (line.indexOf('#') > 0) { // trim off any end of line comment
line = line.substring(0, line.indexOf('#')).trim();
if (line.length() < split + 1)
continue;
}
String b64 = line.substring(split+1).trim();
Destination d = lookupBase64(b64);
if (d != null) {
addEntry(rv, hostsfile, key, d, sourceMsg);
addReverseEntry(rv, key, d, _log);
count++;
} else {
_log.logAlways(Log.WARN, "Unable to import entry for " + key +
" from file " + file + " - bad Base 64: " + b64);
}
}
} catch (IOException ioe) {
_log.error("Failed to read hosts from " + file, ioe);
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
total += count;
_log.logAlways(Log.INFO, "Migrating " + count + " hosts from " + file + " to new hosts database");
}
if (_log.shouldLog(Log.INFO))
_log.info("DB init took " + DataHelper.formatDuration(_context.clock().now() - start));
if (total <= 0)
_log.logAlways(Log.WARN, "No hosts.txt files found, Initialized hosts database with zero entries");
return rv;
} catch (RuntimeException e) {
_log.error("Failed to initialize database", e);
throw new IOException(e.toString());
}
}
/**
* Read the info block of an existing database.
*/
private BlockFile initExisting(RAIFile raf) throws IOException {
long start = _context.clock().now();
try {
BlockFile bf = new BlockFile(raf, false);
// TODO all in one skiplist or separate?
SkipList<String, Properties> hdr = bf.getIndex(INFO_SKIPLIST, _stringSerializer, _infoSerializer);
if (hdr == null)
throw new IOException("No db header");
Properties info = hdr.get(PROP_INFO);
if (info == null)
throw new IOException("No header info");
String list = info.getProperty(PROP_LISTS);
if (list == null)
throw new IOException("No lists");
long createdOn = 0;
String created = info.getProperty(PROP_CREATED);
if (created != null) {
try {
createdOn = Long.parseLong(created);
} catch (NumberFormatException nfe) {}
}
String version = info.getProperty(PROP_VERSION);
if (version == null)
throw new IOException("No version");
if (VersionComparator.comp(version, VERSION) > 0)
throw new IOException("Database version is " + version +
" but this implementation only supports versions 1-" + VERSION +
" Did you downgrade I2P??");
_version = version;
if (VersionComparator.comp(version, "4") >= 0) {
_destSerializer = _destSerializerV4;
_isVersion4 = true;
}
_needsUpgrade = needsUpgrade(bf);
if (_needsUpgrade) {
if (_log.shouldLog(Log.WARN))
_log.warn("Upgrading database from version " + _version + " to " + VERSION +
", created " + (new Date(createdOn)).toString() +
" containing lists: " + list);
} else {
if (_log.shouldLog(Log.INFO))
_log.info("Found database version " + _version +
" created " + (new Date(createdOn)).toString() +
" containing lists: " + list);
}
List<String> skiplists = getFilenames(list);
if (skiplists.isEmpty())
skiplists.add(FALLBACK_LIST);
_lists.addAll(skiplists);
if (_log.shouldLog(Log.INFO))
_log.info("DB init took " + DataHelper.formatDuration(_context.clock().now() - start));
return bf;
} catch (RuntimeException e) {
_log.error("Failed to initialize database", e);
throw new IOException(e.toString());
}
}
/**
* @return true if needs an upgrade
* @throws IOE on bad version
* @since 0.8.9
*/
private boolean needsUpgrade(BlockFile bf) throws IOException {
if (VersionComparator.comp(_version, VERSION) >= 0)
return false;
if (!bf.file.canWrite()) {
_log.logAlways(Log.WARN, "Not upgrading read-only database version " + _version);
return false;
}
return true;
}
/**
* Blockfile must be writable of course.
*
* Version 1->2: Add reverse skiplist and populate
* Version 2->3: Re-populate reverse skiplist as version 2 didn't keep it updated
* after the upgrade. No change to format.
* Version 3->4: Change format to support multiple destinations per hostname
*
* @return true if upgraded successfully
* @since 0.8.9
*/
private boolean upgrade() {
try {
// version 1 -> version 2
// Add reverse skiplist
if (VersionComparator.comp(_version, "2") < 0) {
SkipList<Integer, Properties> rev = _bf.getIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer);
if (rev == null) {
rev = _bf.makeIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer);
if (_log.shouldLog(Log.WARN))
_log.warn("Created reverse index");
}
setVersion("2");
}
// version 2 -> version 3
// no change in format, just regenerate skiplist
if (VersionComparator.comp(_version, "3") < 0) {
Map<String, Destination> entries = getEntries();
int i = 0;
for (Map.Entry<String, Destination> entry : entries.entrySet()) {
addReverseEntry(entry.getKey(), entry.getValue());
i++;
}
// i may be greater than skiplist keys if there are dups
if (_log.shouldLog(Log.WARN))
_log.warn("Updated reverse index with " + i + " entries");
setVersion("3");
}
// version 3 -> version 4
// support multiple destinations per hostname
if (VersionComparator.comp(_version, "4") < 0) {
// Upgrade of 4K entry DB on RPi 2 is over 2 1/2 minutes, probably worse on Android, disable for now
if (SystemVersion.isAndroid()) {
if (_log.shouldWarn())
_log.warn("Deferring upgrade to version 4 on Android");
return true;
}
SkipList<String, Properties> hdr = _bf.getIndex(INFO_SKIPLIST, _stringSerializer, _infoSerializer);
if (hdr == null)
throw new IOException("No db header");
Properties info = hdr.get(PROP_INFO);
if (info == null)
throw new IOException("No header info");
for (String list : _lists) {
try {
// so that we can handle an aborted upgrade,
// we keep track of the version of each list
String vprop = PROP_LISTVERSION + '_' + list;
String listVersion = info.getProperty(vprop);
if (listVersion == null || VersionComparator.comp(listVersion, "4") < 0) {
if (_log.shouldWarn())
_log.warn("Upgrading " + list + " from database version 3 to 4");
_bf.reformatIndex(list, _stringSerializer, _destSerializerV1,
_stringSerializer, _destSerializerV4);
info.setProperty(vprop, "4");
hdr.put(PROP_INFO, info);
} else {
if (_log.shouldWarn())
_log.warn("Partial upgrade, " + list + " already at version " + listVersion);
}
} catch (IOException ioe) {
_log.error("Failed upgrade of list " + list + " to version 4", ioe);
}
}
_destSerializer = _destSerializerV4;
_isVersion4 = true;
setVersion("4");
}
return true;
} catch (IOException ioe) {
_log.error("Error upgrading DB", ioe);
} catch (RuntimeException e) {
_log.error("Error upgrading DB", e);
}
return false;
}
/**
* Save new version number in blockfile after upgrade.
* Blockfile must be writable, of course.
* Side effect: sets _version field
*
* Caller must synchronize
* @since 0.9.26 pulled out of upgrade()
*/
private void setVersion(String version) throws IOException {
SkipList<String, Properties> hdr = _bf.getIndex(INFO_SKIPLIST, _stringSerializer, _infoSerializer);
if (hdr == null)
throw new IOException("No db header");
Properties info = hdr.get(PROP_INFO);
if (info == null)
throw new IOException("No header info");
info.setProperty(PROP_VERSION, version);
info.setProperty(PROP_UPGRADED, Long.toString(_context.clock().now()));
hdr.put(PROP_INFO, info);
if (_log.shouldLog(Log.WARN))
_log.warn("Upgraded database from version " + _version + " to version " + version);
_version = version;
}
/**
* For either v1 or v4.
* Caller must synchronize
* @return entry or null, or throws ioe
*/
private DestEntry getEntry(String listname, String key) throws IOException {
try {
SkipList<String, DestEntry> sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null)
return null;
DestEntry rv = sl.get(key);
return rv;
} catch (IOException ioe) {
_log.error("DB Lookup error", ioe);
// delete index??
throw ioe;
} catch (RuntimeException e) {
_log.error("DB Lookup error", e);
throw new IOException(e.toString());
}
}
/**
* Caller must synchronize
* @param source may be null
*/
private void addEntry(BlockFile bf, String listname, String key, Destination dest, String source) throws IOException {
try {
// catch IOE and delete index??
SkipList<String, DestEntry> sl = bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null) {
//_log.info("Making new skiplist " + listname);
sl = bf.makeIndex(listname, _stringSerializer, _destSerializer);
}
Properties props = new Properties();
props.setProperty(PROP_ADDED, Long.toString(_context.clock().now()));
if (source != null)
props.setProperty(PROP_SOURCE, source);
addEntry(sl, key, dest, props);
} catch (IOException ioe) {
_log.error("DB add error", ioe);
// delete index??
throw ioe;
} catch (RuntimeException e) {
_log.error("DB add error", e);
throw new IOException(e.toString());
}
}
/**
* Caller must synchronize
* @param source may be null
* @throws RuntimeException
*/
/****
private void addEntry(SkipList sl, String key, Destination dest, String source) {
Properties props = new Properties();
props.setProperty(PROP_ADDED, Long.toString(_context.clock().now()));
if (source != null)
props.setProperty(PROP_SOURCE, source);
addEntry(sl, key, dest, props);
}
****/
/**
* Single dest version.
* Caller must synchronize
*
* @param props may be null
* @throws RuntimeException
*/
private static void addEntry(SkipList<String, DestEntry> sl, String key, Destination dest, Properties props) {
DestEntry de = new DestEntry();
de.dest = dest;
de.props = props;
sl.put(key, de);
}
/**
* Multiple dests version.
* DB MUST be version 4.
* Caller must synchronize
*
* @param propsList may be null, or entries may be null
* @throws RuntimeException
* @since 0.9.26
*/
private static void addEntry(SkipList<String, DestEntry> sl, String key, List<Destination> dests, List<Properties> propsList) {
DestEntry de = new DestEntry();
de.destList = dests;
de.dest = dests.get(0);
de.propsList = propsList;
if (propsList != null)
de.props = propsList.get(0);
sl.put(key, de);
}
private static List<String> getFilenames(String list) {
StringTokenizer tok = new StringTokenizer(list, ",");
List<String> rv = new ArrayList<String>(tok.countTokens());
while (tok.hasMoreTokens())
rv.add(tok.nextToken());
return rv;
}
/**
* Caller must synchronize
* @return removed object or null
* @throws RuntimeException
*/
private static <V> V removeEntry(SkipList<String, V> sl, String key) {
return sl.remove(key);
}
///// Reverse index methods
/**
* Caller must synchronize.
* @return null without exception on error (logs only)
* @since 0.8.9
*/
/****
private String getReverseEntry(Destination dest) {
return getReverseEntry(dest.calculateHash());
}
****/
/**
* Caller must synchronize.
* Returns null without exception on error (logs only).
* Returns without logging if no reverse skiplist (version 1).
*
* @return all found if more than one
* @since 0.9.26 from getReverseEntry() 0.8.9
*/
private List<String> getReverseEntries(Hash hash) {
try {
SkipList<Integer, Properties> rev = _bf.getIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer);
if (rev == null)
return null;
Integer idx = getReverseKey(hash);
//_log.info("Get reverse " + idx + ' ' + hash);
Properties props = rev.get(idx);
if (props == null)
return null;
List<String> rv = new ArrayList<String>(props.size());
for (String key : props.stringPropertyNames()) {
// now do the forward lookup to verify (using the cache)
List<Destination> ld = lookupAll(key);
if (ld != null) {
for (Destination d : ld) {
if (d.calculateHash().equals(hash)) {
rv.add(key);
break;
}
}
}
}
if (!rv.isEmpty())
return rv;
} catch (IOException ioe) {
_log.error("DB get reverse error", ioe);
} catch (RuntimeException e) {
_log.error("DB get reverse error", e);
}
return null;
}
/**
* Caller must synchronize.
* Fails without exception on error (logs only)
* @since 0.8.9
*/
private void addReverseEntry(String key, Destination dest) {
addReverseEntry(_bf, key, dest, _log);
}
/**
* Caller must synchronize.
* Fails without exception on error (logs only).
* Returns without logging if no reverse skiplist (version 1).
*
* We store one or more hostnames for a given hash.
* The skiplist key is a signed Integer, the first 4 bytes of the dest hash.
* For convenience (since we have a serializer already) we use
* a Properties as the value, with a null string as the value for each hostname property.
* We could in the future use the property value for something.
* @since 0.8.9
*/
private static void addReverseEntry(BlockFile bf, String key, Destination dest, Log log) {
//log.info("Add reverse " + key);
try {
SkipList<Integer, Properties> rev = bf.getIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer);
if (rev == null)
return;
Integer idx = getReverseKey(dest);
Properties props = rev.get(idx);
if (props != null) {
if (props.getProperty(key) != null)
return;
} else {
props = new Properties();
}
props.put(key, "");
rev.put(idx, props);
} catch (IOException ioe) {
log.error("DB add reverse error", ioe);
} catch (RuntimeException e) {
log.error("DB add reverse error", e);
}
}
/**
* Caller must synchronize.
* Fails without exception on error (logs only)
* @since 0.8.9
*/
private void removeReverseEntry(String key, Destination dest) {
//_log.info("Remove reverse " + key);
try {
SkipList<Integer, Properties> rev = _bf.getIndex(REVERSE_SKIPLIST, _hashIndexSerializer, _infoSerializer);
if (rev == null)
return;
Integer idx = getReverseKey(dest);
Properties props = rev.get(idx);
if (props == null || props.remove(key) == null)
return;
if (props.isEmpty())
rev.remove(idx);
else
rev.put(idx, props);
} catch (IOException ioe) {
_log.error("DB remove reverse error", ioe);
} catch (RuntimeException e) {
_log.error("DB remove reverse error", e);
}
}
/**
* @since 0.8.9
*/
private static Integer getReverseKey(Destination dest) {
return getReverseKey(dest.calculateHash());
}
/**
* @since 0.8.9
*/
private static Integer getReverseKey(Hash hash) {
byte[] hashBytes = hash.getData();
int i = (int) DataHelper.fromLong(hashBytes, 0, 4);
return Integer.valueOf(i);
}
////////// Start NamingService API
/*
*
* Will strip a "www." prefix and retry if lookup fails
*
* @param hostname upper/lower case ok
* @param options If non-null and contains the key "list", lookup in
* that list only, otherwise all lists
*/
@Override
public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) {
if (hostname.endsWith(".i2p.alt")) {
// RFC 9476
hostname = hostname.substring(0, hostname.length() - 4);
}
Destination rv = lookup2(hostname, lookupOptions, storedOptions);
if (rv == null) {
// if hostname starts with "www.", strip and try again
// but not for www.i2p
hostname = hostname.toLowerCase(Locale.US);
if (hostname.startsWith("www.") && hostname.length() > 7) {
hostname = hostname.substring(4);
rv = lookup2(hostname, lookupOptions, storedOptions);
}
}
return rv;
}
/*
* Single dest version.
*
* @param lookupOptions If non-null and contains the key "list", lookup in
* that list only, otherwise all lists
*/
private Destination lookup2(String hostname, Properties lookupOptions, Properties storedOptions) {
String listname = null;
if (lookupOptions != null)
listname = lookupOptions.getProperty("list");
Destination d = null;
// only use cache if we aren't retreiving options or specifying the list
if (listname == null && storedOptions == null) {
d = super.lookup(hostname, null, null);
if (d != null)
return d;
// Base32 failed?
if (hostname.length() >= BASE32_HASH_LENGTH + 8 && hostname.toLowerCase(Locale.US).endsWith(".b32.i2p"))
return null;
}
String key = hostname.toLowerCase(Locale.US);
synchronized(_negativeCache) {
if (_negativeCache.get(key) != null)
return null;
}
synchronized(_bf) {
if (_isClosed)
return null;
for (String list : _lists) {
if (listname != null && !list.equals(listname))
continue;
try {
DestEntry de = getEntry(list, key);
if (de != null) {
if (!validate(key, de, listname))
continue;
d = de.dest;
if (storedOptions != null && de.props != null)
storedOptions.putAll(de.props);
break;
}
} catch (IOException ioe) {
break;
}
}
deleteInvalid();
}
if (d != null) {
putCache(hostname, d);
} else {
synchronized(_negativeCache) {
_negativeCache.put(key, DUMMY);
}
}
return d;
}
/*
* Multiple dests version.
* DB MUST be version 4.
*
* @param lookupOptions If non-null and contains the key "list", lookup in
* that list only, otherwise all lists
* @since 0.9.26
*/
private List<Destination> lookupAll2(String hostname, Properties lookupOptions, List<Properties> storedOptions) {
// only use cache for b32
if (hostname.length() >= BASE32_HASH_LENGTH + 8 && hostname.toLowerCase(Locale.US).endsWith(".b32.i2p")) {
Destination d = super.lookup(hostname, null, null);
if (d != null) {
if (storedOptions != null)
storedOptions.add(null);
return Collections.singletonList(d);
}
// Base32 failed?
return null;
}
String key = hostname.toLowerCase(Locale.US);
synchronized(_negativeCache) {
if (_negativeCache.get(key) != null)
return null;
}
String listname = null;
if (lookupOptions != null)
listname = lookupOptions.getProperty("list");
List<Destination> rv = null;
synchronized(_bf) {
if (_isClosed)
return null;
for (String list : _lists) {
if (listname != null && !list.equals(listname))
continue;
try {
DestEntry de = getEntry(list, key);
if (de != null) {
if (!validate(key, de, listname))
continue;
if (de.destList != null) {
rv = de.destList;
if (storedOptions != null)
storedOptions.addAll(de.propsList);
} else {
rv = Collections.singletonList(de.dest);
if (storedOptions != null)
storedOptions.add(de.props);
}
break;
}
} catch (IOException ioe) {
break;
}
}
deleteInvalid();
}
if (rv != null) {
putCache(hostname, rv.get(0));
} else {
synchronized(_negativeCache) {
_negativeCache.put(key, DUMMY);
}
}
return rv;
}
/**
* @param options If non-null and contains the key "list", add to that list
* (default "hosts.txt")
* Use the key "s" for the source
*/
@Override
public boolean put(String hostname, Destination d, Properties options) {
return put(hostname, d, options, false);
}
/**
* @param options If non-null and contains the key "list", add to that list
* (default "hosts.txt")
* Use the key "s" for the source.
* Key "a" will be added with the current time, unless
* "a" is present in options.
*/
@Override
public boolean putIfAbsent(String hostname, Destination d, Properties options) {
return put(hostname, d, options, true);
}
/**
* Single dest version
* This does not prevent adding b32. Caller must check.
*
* @param checkExisting if true, fail if entry already exists
*/
private boolean put(String hostname, Destination d, Properties options, boolean checkExisting) {
if (_readOnly) {
_log.error("Add entry failed, read-only hosts database");
return false;
}
String key = hostname.toLowerCase(Locale.US);
synchronized(_negativeCache) {
_negativeCache.remove(key);
}
String listname = FALLBACK_LIST;
Properties props = new Properties();
props.setProperty(PROP_ADDED, Long.toString(_context.clock().now()));
if (options != null) {
props.putAll(options);
String list = options.getProperty("list");
if (list != null) {
listname = list;
props.remove("list");
}
}
synchronized(_bf) {
if (_isClosed)
return false;
try {
SkipList<String, DestEntry> sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null)
sl = _bf.makeIndex(listname, _stringSerializer, _destSerializer);
boolean changed = (checkExisting || !_listeners.isEmpty()) && sl.get(key) != null;
if (changed && checkExisting)
return false;
addEntry(sl, key, d, props);
if (changed) {
removeCache(hostname);
// removeReverseEntry(key, oldDest) ???
}
addReverseEntry(key, d);
for (NamingServiceListener nsl : _listeners) {
if (changed)
nsl.entryChanged(this, hostname, d, options);
else
nsl.entryAdded(this, hostname, d, options);
}
return true;
} catch (IOException ioe) {
_log.error("DB add error", ioe);
return false;
} catch (RuntimeException re) {
_log.error("DB add error", re);
return false;
}
}
}
/**
* Multiple dests version.
* DB MUST be version 4.
* This does not prevent adding b32. Caller must check.
*
* @param propsList may be null, or entries may be null
* @param checkExisting if true, fail if entry already exists
* @since 0.9.26
*/
private boolean put(String hostname, List<Destination> dests, List<Properties> propsList, boolean checkExisting) {
int sz = dests.size();
if (sz <= 0)
throw new IllegalArgumentException();
if (sz == 1)
return put(hostname, dests.get(0), propsList != null ? propsList.get(0) : null, checkExisting);
if (_readOnly) {
_log.error("Add entry failed, read-only hosts database");
return false;
}
String key = hostname.toLowerCase(Locale.US);
synchronized(_negativeCache) {
_negativeCache.remove(key);
}
String listname = FALLBACK_LIST;
String date = Long.toString(_context.clock().now());
List<Properties> outProps = new ArrayList<Properties>(propsList.size());
for (Properties options : propsList) {
Properties props = new Properties();
props.setProperty(PROP_ADDED, date);
if (options != null) {
props.putAll(options);
String list = options.getProperty("list");
if (list != null) {
listname = list;
props.remove("list");
}
}
outProps.add(props);
}
synchronized(_bf) {
if (_isClosed)
return false;
try {
SkipList<String, DestEntry> sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null)
sl = _bf.makeIndex(listname, _stringSerializer, _destSerializer);
boolean changed = (checkExisting || !_listeners.isEmpty()) && sl.get(key) != null;
if (changed && checkExisting)
return false;
addEntry(sl, key, dests, outProps);
if (changed) {
removeCache(hostname);
// removeReverseEntry(key, oldDest) ???
}
for (int i = 0; i < dests.size(); i++) {
Destination d = dests.get(i);
Properties options = propsList.get(i);
addReverseEntry(key, d);
for (NamingServiceListener nsl : _listeners) {
if (changed)
nsl.entryChanged(this, hostname, d, options);
else
nsl.entryAdded(this, hostname, d, options);
}
}
return true;
} catch (IOException ioe) {
_log.error("DB add error", ioe);
return false;
} catch (RuntimeException re) {
_log.error("DB add error", re);
return false;
}
}
}
/**
* @param options If non-null and contains the key "list", remove
* from that list (default "hosts.txt", NOT all lists)
*/
@Override
public boolean remove(String hostname, Properties options) {
if (_readOnly) {
_log.error("Remove entry failed, read-only hosts database");
return false;
}
String key = hostname.toLowerCase(Locale.US);
String listname = FALLBACK_LIST;
if (options != null) {
String list = options.getProperty("list");
if (list != null) {
listname = list;
}
}
synchronized(_bf) {
if (_isClosed)
return false;
try {
SkipList<String, DestEntry> sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null)
return false;
DestEntry removed = removeEntry(sl, key);
boolean rv = removed != null;
if (rv) {
removeCache(hostname);
try {
removeReverseEntry(key, removed.dest);
} catch (ClassCastException cce) {
_log.error("DB reverse remove error", cce);
}
for (NamingServiceListener nsl : _listeners) {
nsl.entryRemoved(this, key);
}
}
return rv;
} catch (IOException ioe) {
_log.error("DB remove error", ioe);
return false;
} catch (RuntimeException re) {
_log.error("DB remove error", re);
return false;
}
}
}
/**
* @param options If non-null and contains the key "list", get
* from that list (default "hosts.txt", NOT all lists)
* Key "skip": skip that many entries
* Key "limit": max number to return
* Key "search": return only those matching substring
* Key "startsWith": return only those starting with
* ("[0-9]" allowed)
* Key "beginWith": start here in the iteration
* Don't use both startsWith and beginWith.
* Search, startsWith, and beginWith values must be lower case.
*/
@Override
public Map<String, Destination> getEntries(Properties options) {
String listname = FALLBACK_LIST;
String search = null;
String startsWith = null;
String beginWith = null;
int limit = Integer.MAX_VALUE;
int skip = 0;
if (options != null) {
String ln = options.getProperty("list");
if (ln != null)
listname = ln;
search = options.getProperty("search");
startsWith = options.getProperty("startsWith");
beginWith = options.getProperty("beginWith");
if (beginWith == null && startsWith != null) {
if (startsWith.equals("[0-9]"))
beginWith = "0";
else
beginWith = startsWith;
}
String lim = options.getProperty("limit");
try {
limit = Integer.parseInt(lim);
} catch (NumberFormatException nfe) {}
String sk = options.getProperty("skip");
try {
skip = Integer.parseInt(sk);
} catch (NumberFormatException nfe) {}
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Searching " + listname + " beginning with " + beginWith +
" starting with " + startsWith + " search string " + search +
" limit=" + limit + " skip=" + skip);
synchronized(_bf) {
if (_isClosed)
return Collections.emptyMap();
try {
SkipList<String, DestEntry> sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("No skiplist found for lookup in " + listname);
return Collections.emptyMap();
}
SkipIterator<String, DestEntry> iter;
if (beginWith != null)
iter = sl.find(beginWith);
else
iter = sl.iterator();
Map<String, Destination> rv = new TreeMap<String, Destination>();
for (int i = 0; i < skip && iter.hasNext(); i++) {
// don't bother validating here
iter.next();
}
for (int i = 0; i < limit && iter.hasNext(); ) {
String key = iter.nextKey();
if (startsWith != null) {
if (startsWith.equals("[0-9]")) {
if (key.charAt(0) > '9')
break;
} else if (!key.startsWith(startsWith)) {
break;
}
}
DestEntry de = iter.next();
if (!validate(key, de, listname))
continue;
if (search != null && key.indexOf(search) < 0)
continue;
rv.put(key, de.dest);
i++;
}
return rv;
} catch (IOException ioe) {
_log.error("DB lookup error", ioe);
return Collections.emptyMap();
} catch (RuntimeException re) {
_log.error("DB lookup error", re);
return Collections.emptyMap();
} finally {
deleteInvalid();
}
}
}
/**
* @param options If non-null and contains the key "list", get
* from that list (default "hosts.txt", NOT all lists)
* Key "skip": skip that many entries
* Key "limit": max number to return
* Key "search": return only those matching substring
* Key "startsWith": return only those starting with
* ("[0-9]" allowed)
* Key "beginWith": start here in the iteration
* Don't use both startsWith and beginWith.
* Search, startsWith, and beginWith values must be lower case.
* @since 0.9.20
*/
@Override
public Map<String, String> getBase64Entries(Properties options) {
String listname = FALLBACK_LIST;
String search = null;
String startsWith = null;
String beginWith = null;
int limit = Integer.MAX_VALUE;
int skip = 0;
if (options != null) {
String ln = options.getProperty("list");
if (ln != null)
listname = ln;
search = options.getProperty("search");
startsWith = options.getProperty("startsWith");
beginWith = options.getProperty("beginWith");
if (beginWith == null && startsWith != null) {
if (startsWith.equals("[0-9]"))
beginWith = "0";
else
beginWith = startsWith;
}
String lim = options.getProperty("limit");
try {
limit = Integer.parseInt(lim);
} catch (NumberFormatException nfe) {}
String sk = options.getProperty("skip");
try {
skip = Integer.parseInt(sk);
} catch (NumberFormatException nfe) {}
}
synchronized(_bf) {
if (_isClosed)
return Collections.emptyMap();
try {
SkipList<String, DestEntry> sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("No skiplist found for lookup in " + listname);
return Collections.emptyMap();
}
SkipIterator<String, DestEntry> iter;
if (beginWith != null)
iter = sl.find(beginWith);
else
iter = sl.iterator();
Map<String, String> rv = new TreeMap<String, String>();
for (int i = 0; i < skip && iter.hasNext(); i++) {
// don't bother validating here
iter.next();
}
for (int i = 0; i < limit && iter.hasNext(); ) {
String key = iter.nextKey();
if (startsWith != null) {
if (startsWith.equals("[0-9]")) {
if (key.charAt(0) > '9')
break;
} else if (!key.startsWith(startsWith)) {
break;
}
}
DestEntry de = iter.next();
if (!validate(key, de, listname))
continue;
if (search != null && key.indexOf(search) < 0)
continue;
rv.put(key, de.dest.toBase64());
i++;
}
return rv;
} catch (IOException ioe) {
_log.error("DB lookup error", ioe);
return Collections.emptyMap();
} catch (RuntimeException re) {
_log.error("DB lookup error", re);
return Collections.emptyMap();
} finally {
deleteInvalid();
}
}
}
/**
* Export in a hosts.txt format.
* Output is sorted.
* Caller must close writer.
*
* @param options If non-null and contains the key "list", get
* from that list (default "hosts.txt", NOT all lists)
* Key "search": return only those matching substring
* Key "startsWith": return only those starting with
* ("[0-9]" allowed)
* Key "beginWith": start here in the iteration
* @since 0.9.30 override NamingService to add stored authentication strings
*/
@Override
public void export(Writer out, Properties options) throws IOException {
String listname = FALLBACK_LIST;
String search = null;
String startsWith = null;
String beginWith = null;
if (options != null) {
String ln = options.getProperty("list");
if (ln != null)
listname = ln;
search = options.getProperty("search");
startsWith = options.getProperty("startsWith");
beginWith = options.getProperty("beginWith");
if (beginWith == null && startsWith != null) {
if (startsWith.equals("[0-9]"))
beginWith = "0";
else
beginWith = startsWith;
}
}
out.write("# Address book: ");
out.write(getName());
out.write(" (" + listname + ')');
final String nl = System.getProperty("line.separator", "\n");
out.write(nl);
out.write("# Exported: ");
out.write((new Date()).toString());
out.write(nl);
synchronized(_bf) {
if (_isClosed)
return;
try {
SkipList<String, DestEntry> sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("No skiplist found for lookup in " + listname);
return;
}
if (beginWith == null && search == null) {
int sz = sl.size();
if (sz <= 0) {
out.write("# No entries");
out.write(nl);
return;
}
if (sz > 1) {
// actually not right due to multidest
out.write("# " + sz + " entries");
out.write(nl);
}
}
SkipIterator<String, DestEntry> iter;
if (beginWith != null)
iter = sl.find(beginWith);
else
iter = sl.iterator();
int cnt = 0;
while (iter.hasNext()) {
String key = iter.nextKey();
if (startsWith != null) {
if (startsWith.equals("[0-9]")) {
if (key.charAt(0) > '9')
break;
} else if (!key.startsWith(startsWith)) {
break;
}
}
DestEntry de = iter.next();
if (!validate(key, de, listname))
continue;
if (search != null && key.indexOf(search) < 0)
continue;
int dsz = de.destList != null ? de.destList.size() : 1;
// new non-DSA dest is put first, so put in reverse
// order so importers will see the older dest first
for (int i = dsz - 1; i >= 0; i--) {
Properties p;
Destination d;
if (i == 0) {
p = de.props;
d = de.dest;
} else {
p = de.propsList.get(i);
d = de.destList.get(i);
}
out.write("# ");
out.write(key);
out.write(": ");
out.write(d.toBase32());
out.write(nl);
out.write(key);
out.write('=');
out.write(d.toBase64());
if (p != null)
SingleFileNamingService.writeOptions(p, out);
out.write(nl);
cnt++;
}
}
if (beginWith != null || search != null) {
if (cnt <= 0) {
out.write("# No entries");
out.write(nl);
return;
}
if (cnt > 1) {
out.write("# " + cnt + " entries");
out.write(nl);
}
}
} catch (RuntimeException re) {
throw new IOException("DB lookup error", re);
} finally {
deleteInvalid();
}
}
}
/**
* Broken prior to 0.9.62, would only return one name.
*
* @param options If non-null and contains the key "list", get
* from that list (default "hosts.txt", NOT all lists)
* Key "skip": skip that many entries
* Key "limit": max number to return
* Key "search": return only those matching substring
* Key "startsWith": return only those starting with
* ("[0-9]" allowed)
* Key "beginWith": start here in the iteration
* Don't use both startsWith and beginWith.
* Search, startsWith, and beginWith values must be lower case.
* @since 0.9.20
*/
@Override
public Set<String> getNames(Properties options) {
String listname = FALLBACK_LIST;
String search = null;
String startsWith = null;
String beginWith = null;
int limit = Integer.MAX_VALUE;
int skip = 0;
if (options != null) {
String ln = options.getProperty("list");
if (ln != null)
listname = ln;
search = options.getProperty("search");
startsWith = options.getProperty("startsWith");
beginWith = options.getProperty("beginWith");
if (beginWith == null && startsWith != null) {
if (startsWith.equals("[0-9]"))
beginWith = "0";
else
beginWith = startsWith;
}
String lim = options.getProperty("limit");
try {
limit = Integer.parseInt(lim);
} catch (NumberFormatException nfe) {}
String sk = options.getProperty("skip");
try {
skip = Integer.parseInt(sk);
} catch (NumberFormatException nfe) {}
}
synchronized(_bf) {
if (_isClosed)
return Collections.emptySet();
try {
SkipList<String, DestEntry> sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null) {
if (_log.shouldLog(Log.WARN))
_log.warn("No skiplist found for lookup in " + listname);
return Collections.emptySet();
}
SkipIterator<String, DestEntry> iter;
if (beginWith != null)
iter = sl.find(beginWith);
else
iter = sl.iterator();
Set<String> rv = new HashSet<String>();
for (int i = 0; i < skip && iter.hasNext(); i++) {
iter.next();
}
for (int i = 0; i < limit && iter.hasNext(); ) {
String key = iter.nextKey();
if (startsWith != null) {
if (startsWith.equals("[0-9]")) {
if (key.charAt(0) > '9')
break;
} else if (!key.startsWith(startsWith)) {
break;
}
}
iter.next();
if (search != null && key.indexOf(search) < 0)
continue;
rv.add(key);
i++;
}
return rv;
} catch (IOException ioe) {
_log.error("DB lookup error", ioe);
return Collections.emptySet();
} catch (RuntimeException re) {
_log.error("DB lookup error", re);
return Collections.emptySet();
}
}
}
/**
* @param options ignored
* @since 0.8.9
*/
@Override
public String reverseLookup(Destination d, Properties options) {
return reverseLookup(d.calculateHash());
}
/**
* @since 0.8.9
*/
@Override
public String reverseLookup(Hash h) {
List<String> ls;
synchronized(_bf) {
if (_isClosed)
return null;
ls = getReverseEntries(h);
}
return (ls != null) ? ls.get(0) : null;
}
/**
* @param options ignored
* @since 0.9.26
*/
@Override
public List<String> reverseLookupAll(Destination d, Properties options) {
return reverseLookupAll(d.calculateHash());
}
/**
* @since 0.9.26
*/
@Override
public List<String> reverseLookupAll(Hash h) {
synchronized(_bf) {
if (_isClosed)
return null;
return getReverseEntries(h);
}
}
/**
* @param options If non-null and contains the key "list", return the
* size of that list (default "hosts.txt", NOT all lists)
*/
@Override
public int size(Properties options) {
String listname = FALLBACK_LIST;
if (options != null) {
String list = options.getProperty("list");
if (list != null) {
listname = list;
}
}
synchronized(_bf) {
if (_isClosed)
return 0;
try {
SkipList<String, DestEntry> sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
if (sl == null)
return 0;
return sl.size();
} catch (IOException ioe) {
_log.error("DB size error", ioe);
return 0;
} catch (RuntimeException re) {
_log.error("DB size error", re);
return 0;
}
}
}
public void shutdown() {
close();
}
////////// End NamingService API
//// Begin new API for multiple Destinations
/**
* Return all of the entries found in the first list found, or in the list
* specified in lookupOptions. Does not aggregate all destinations found
* in all lists.
*
* If storedOptions is non-null, it must be a List that supports null entries.
* If the returned value (the List of Destinations) is non-null,
* the same number of Properties objects will be added to storedOptions.
* If no properties were found for a given Destination, the corresponding
* entry in the storedOptions list will be null.
*
* @param lookupOptions input parameter, NamingService-specific, may be null
* @param storedOptions output parameter, NamingService-specific, any stored properties will be added if non-null
* @return non-empty List of Destinations, or null if nothing found
* @since 0.9.26
*/
@Override
public List<Destination> lookupAll(String hostname, Properties lookupOptions, List<Properties> storedOptions) {
if (!_isVersion4)
return super.lookupAll(hostname, lookupOptions, storedOptions);
List<Destination> rv = lookupAll2(hostname, lookupOptions, storedOptions);
if (rv == null) {
// if hostname starts with "www.", strip and try again
// but not for www.i2p
hostname = hostname.toLowerCase(Locale.US);
if (hostname.startsWith("www.") && hostname.length() > 7) {
hostname = hostname.substring(4);
rv = lookupAll2(hostname, lookupOptions, storedOptions);
}
}
// we sort the destinations in addDestination(),
// which is a lot easier than sorting them here
return rv;
}
/**
* Add a Destination to an existing hostname's entry in the addressbook.
*
* This does not prevent adding b32. Caller must check.
*
* @param options NamingService-specific, may be null
* @return success
* @since 0.9.26
*/
@Override
public boolean addDestination(String hostname, Destination d, Properties options) {
if (!_isVersion4)
return putIfAbsent(hostname, d, options);
List<Properties> storedOptions = new ArrayList<Properties>(4);
synchronized(_bf) {
// 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 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.
// For now, non-DSA at the front, DSA at the end
SigType type = d.getSigningPublicKey().getType();
if (type != SigType.DSA_SHA1 && type.isAvailable()) {
newDests.add(0, d);
storedOptions.add(0, options);
} else {
newDests.add(d);
storedOptions.add(options);
}
return put(hostname, newDests, storedOptions, false);
}
}
/**
* Remove a hostname's entry only if it contains the Destination d.
* If the NamingService supports multiple Destinations per hostname,
* and this is the only Destination, removes the entire entry.
* If aditional Destinations remain, it only removes the
* specified Destination from the entry.
*
* @param options NamingService-specific, may be null
* @return true if entry containing d was successfully removed.
* @since 0.9.26
*/
@Override
public boolean remove(String hostname, Destination d, Properties options) {
if (!_isVersion4) {
// super does a get-test-remove, so lock around that
synchronized(_bf) {
return super.remove(hostname, d, options);
}
}
List<Properties> storedOptions = new ArrayList<Properties>(4);
synchronized(_bf) {
// 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++) {
Destination dd = dests.get(i);
if (dd.equals(d)) {
// Found it. Remove and return.
if (dests.size() == 1)
return remove(hostname, options);
List<Destination> newDests = new ArrayList<Destination>(dests.size() - 1);
for (int j = 0; j < dests.size(); j++) {
if (j != i)
newDests.add(dests.get(j));
}
storedOptions.remove(i);
removeReverseEntry(hostname, d);
if (options != null) {
String list = options.getProperty("list");
if (list != null)
storedOptions.get(0).setProperty("list", list);
}
return put(hostname, newDests, storedOptions, false);
}
}
}
return false;
}
//// End new API for multiple Destinations
/**
* Continuously validate anything we read in.
* Queue anything invalid to be removed at the end of the operation.
* Caller must sync!
* @return valid
*/
private boolean validate(String key, DestEntry de, String listname) {
if (key == null)
return false;
// de.props may be null
// publickey check is a quick proxy to detect dest deserialization failure
boolean rv = key.length() > 0 &&
de != null &&
de.dest != null &&
de.dest.getPublicKey() != null;
if (_isVersion4 && rv && de.destList != null) {
// additional checks for multi-dest
rv = de.propsList != null &&
de.destList.size() == de.propsList.size() &&
!de.destList.contains(null);
}
if ((!rv) && (!_readOnly))
_invalid.add(new InvalidEntry(key, listname));
return rv;
}
/**
* Remove and log all invalid entries queued by validate()
* while scanning in lookup() or getEntries().
* We delete in the order detected, as an error may be corrupting later entries in the skiplist.
* Caller must sync!
*/
private void deleteInvalid() {
if (_invalid.isEmpty())
return;
_log.error("Removing " + _invalid.size() + " corrupt entries from database");
for (InvalidEntry ie : _invalid) {
String key = ie.key;
String list = ie.list;
try {
SkipList<String, DestEntry> sl = _bf.getIndex(list, _stringSerializer, _destSerializer);
if (sl == null) {
_log.error("No list found to remove corrupt \"" + key + "\" from database " + list);
continue;
}
// this will often return null since it was corrupt
boolean success = removeEntry(sl, key) != null;
if (success)
_log.error("Removed corrupt \"" + key + "\" from database " + list);
else
_log.error("May have Failed to remove corrupt \"" + key + "\" from database " + list);
} catch (RuntimeException re) {
_log.error("Error while removing corrupt \"" + key + "\" from database " + list, re);
} catch (IOException ioe) {
_log.error("Error while removing corrput \"" + key + "\" from database " + list, ioe);
}
}
_invalid.clear();
}
/****
private void dumpDB() {
synchronized(_bf) {
if (_isClosed)
_log.error("Database is closed");
for (String list : _lists) {
try {
SkipList sl = _bf.getIndex(list, _stringSerializer, _destSerializer);
if (sl == null) {
_log.error("No list found for " + list);
continue;
}
int i = 0;
for (SkipIterator iter = sl.iterator(); iter.hasNext(); ) {
String key = (String) iter.nextKey();
DestEntry de = (DestEntry) iter.next();
if (!validate(key, de, list))
continue;
_log.error("DB " + list + " key " + key + " val " + de);
i++;
}
_log.error(i + " entries found for " + list);
} catch (IOException ioe) {
_log.error("Fail", ioe);
break;
}
}
deleteInvalid();
}
}
****/
private void close() {
synchronized(_bf) {
try {
_bf.close();
} catch (IOException ioe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error closing", ioe);
} catch (RuntimeException e) {
if (_log.shouldLog(Log.WARN))
_log.warn("Error closing", e);
}
try {
_raf.close();
} catch (IOException ioe) {
}
_isClosed = true;
}
synchronized(_negativeCache) {
_negativeCache.clear();
}
clearCache();
}
/** for logging errors in the static serializers below */
private static void logError(String msg, Throwable t) {
I2PAppContext.getGlobalContext().logManager().getLog(BlockfileNamingService.class).error(msg, t);
}
private class Shutdown implements Runnable {
public void run() {
close();
}
}
/**
* Used for the values in the header skiplist
* Take care not to throw on any error.
* This means that some things will fail with no indication other than the log,
* but if we threw a RuntimeException we would prevent access to entries later in
* the SkipSpan.
*/
private static class PropertiesSerializer implements Serializer<Properties> {
/**
* A format error on the properties is non-fatal (returns an empty properties)
*/
public byte[] getBytes(Properties p) {
try {
return DataHelper.toProperties(p);
} catch (DataFormatException dfe) {
logError("DB Write Fail - properties too big?", dfe);
// null properties is a two-byte length of 0.
return new byte[2];
}
}
/** returns null on error */
public Properties construct(byte[] b) {
Properties rv = new Properties();
try {
DataHelper.fromProperties(b, 0, rv);
} catch (DataFormatException dfe) {
logError("DB Read Fail", dfe);
return null;
}
return rv;
}
}
/**
* A DestEntry contains Properties and a Destination,
* and is serialized in that order.
*/
private static class DestEntry {
/** May be null.
* If more than one dest, contains the first props.
*/
public Properties props;
/** May not be null.
* If more than one dest, contains the first dest.
*/
public Destination dest;
/** May be null - v4 only - same size as destList - may contain null entries
* Only non-null if more than one dest.
* First entry always equal to props.
*/
public List<Properties> propsList;
/** May be null - v4 only - same size as propsList
* Only non-null if more than one dest.
* First entry always equal to dest.
*/
public List<Destination> destList;
@Override
public String toString() {
return "DestEntry (" + DataHelper.toString(props) +
") " + dest.toString();
}
}
/**
* Used for the values in the addressbook skiplists
* Take care not to throw on any error.
* This means that some things will fail with no indication other than the log,
* but if we threw a RuntimeException we would prevent access to entries later in
* the SkipSpan.
*/
private static class DestEntrySerializer implements Serializer<DestEntry> {
/**
* A format error on the properties is non-fatal (only the properties are lost)
* A format error on the destination is fatal
*/
public byte[] getBytes(DestEntry de) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
try {
try {
DataHelper.writeProperties(baos, de.props, true, false); // UTF-8, unsorted
} catch (DataFormatException dfe) {
logError("DB Write Fail - properties too big?", dfe);
// null properties is a two-byte length of 0.
baos.write(new byte[2]);
}
de.dest.writeBytes(baos);
} catch (IOException ioe) {
logError("DB Write Fail", ioe);
} catch (DataFormatException dfe) {
logError("DB Write Fail", dfe);
}
return baos.toByteArray();
}
/** returns null on error */
public DestEntry construct(byte[] b) {
DestEntry rv = new DestEntry();
ByteArrayInputStream bais = new ByteArrayInputStream(b);
try {
rv.props = DataHelper.readProperties(bais);
//dest.readBytes(bais);
// Will this flush the dest cache too much?
rv.dest = Destination.create(bais);
} catch (IOException ioe) {
logError("DB Read Fail", ioe);
return null;
} catch (DataFormatException dfe) {
logError("DB Read Fail", dfe);
return null;
}
return rv;
}
}
/**
* For multiple destinations per hostname
* @since 0.9.26
*/
private static class DestEntrySerializerV4 implements Serializer<DestEntry> {
public byte[] getBytes(DestEntry de) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
int sz = de.destList != null ? de.destList.size() : 1;
try {
baos.write((byte) sz);
for (int i = 0; i < sz; i++) {
Properties p;
Destination d;
if (i == 0) {
p = de.props;
d = de.dest;
} else {
p = de.propsList.get(i);
d = de.destList.get(i);
}
try {
writeProperties(baos, p);
} catch (DataFormatException dfe) {
logError("DB Write Fail - properties too big?", dfe);
baos.write(new byte[2]);
}
d.writeBytes(baos);
}
} catch (IOException ioe) {
logError("DB Write Fail", ioe);
} catch (DataFormatException dfe) {
logError("DB Write Fail", dfe);
}
return baos.toByteArray();
}
/** returns null on error */
public DestEntry construct(byte[] b) {
DestEntry rv = new DestEntry();
ByteArrayInputStream bais = new ByteArrayInputStream(b);
try {
int sz = bais.read() & 0xff;
if (sz <= 0)
throw new DataFormatException("bad dest count " + sz);
rv.props = readProperties(bais);
rv.dest = Destination.create(bais);
if (sz > 1) {
rv.propsList = new ArrayList<Properties>(sz);
rv.destList = new ArrayList<Destination>(sz);
rv.propsList.add(rv.props);
rv.destList.add(rv.dest);
for (int i = 1; i < sz; i++) {
rv.propsList.add(readProperties(bais));
rv.destList.add(Destination.create(bais));
}
}
} catch (IOException ioe) {
logError("DB Read Fail", ioe);
return null;
} catch (DataFormatException dfe) {
logError("DB Read Fail", dfe);
return null;
}
return rv;
}
}
/**
* Same as DataHelper.writeProperties, UTF-8, unsorted,
* except that values may up to 4K bytes.
*
* @param props source may be null
* @throws DataFormatException if any key string is over 255 bytes long,
* if any value string is over 4096 bytes long, or if the total length
* (not including the two length bytes) is greater than 65535 bytes.
* @since 0.9.26
*/
private static void writeProperties(ByteArrayOutputStream rawStream, Properties p)
throws DataFormatException, IOException {
if (p != null && !p.isEmpty()) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(p.size() * 32);
for (Map.Entry<Object, Object> entry : p.entrySet()) {
String key = (String) entry.getKey();
String val = (String) entry.getValue();
DataHelper.writeStringUTF8(baos, key);
baos.write('=');
writeLongStringUTF8(baos, val);
baos.write(';');
}
if (baos.size() > 65535)
throw new DataFormatException("Properties too big (65535 max): " + baos.size());
DataHelper.writeLong(rawStream, 2, baos.size());
baos.writeTo(rawStream);
} else {
DataHelper.writeLong(rawStream, 2, 0);
}
}
/**
* Same as DataHelper.readProperties, UTF-8, unsorted,
* except that values may up to 4K bytes.
*
* Throws DataFormatException on duplicate key
*
* @param in stream to read the mapping from
* @throws DataFormatException if the format is invalid
* @throws IOException if there is a problem reading the data
* @return a Properties
* @since 0.9.26
*/
public static Properties readProperties(ByteArrayInputStream in)
throws DataFormatException, IOException {
Properties props = new Properties();
int size = (int) DataHelper.readLong(in, 2);
// this doesn't prevent reading past the end on corruption
int ignore = in.available() - size;
while (in.available() > ignore) {
String key = DataHelper.readString(in);
int b = in.read();
if (b != '=')
throw new DataFormatException("Bad key " + b);
String val = readLongString(in);
b = in.read();
if (b != ';')
throw new DataFormatException("Bad value");
Object old = props.put(key, val);
if (old != null)
throw new DataFormatException("Duplicate key " + key);
}
return props;
}
/**
* Same as DataHelper.writeStringUTF8, except that
* strings up to 4K bytes are allowed.
* Format is: one-byte length + data, or 0xff + two-byte length + data
*
* @param out stream to write string
* @param string to write out: null strings are valid, but strings of excess length will
* cause a DataFormatException to be thrown
* @throws DataFormatException if the string is not valid
* @throws IOException if there is an IO error writing the string
*/
private static void writeLongStringUTF8(ByteArrayOutputStream out, String string)
throws DataFormatException, IOException {
if (string == null) {
out.write(0);
} else {
byte[] raw = string.getBytes("UTF-8");
int len = raw.length;
if (len >= 255) {
if (len > MAX_VALUE_LENGTH)
throw new DataFormatException(MAX_VALUE_LENGTH + " max, but this is "
+ len + " [" + string + "]");
out.write(0xff);
DataHelper.writeLong(out, 2, len);
} else {
out.write(len);
}
out.write(raw);
}
}
/**
* Same as DataHelper.readString, except that
* strings up to 4K bytes are allowed.
* Format is: one-byte length + data, or 0xff + two-byte length + data
*
* @param in stream to read from
* @throws DataFormatException if the stream doesn't contain a validly formatted string
* @throws EOFException if there aren't enough bytes to read the string
* @throws IOException if there is an IO error reading the string
* @return UTF-8 string
*/
private static String readLongString(ByteArrayInputStream in) throws DataFormatException, IOException {
int size = in.read();
if (size < 0)
throw new EOFException("EOF reading string");
if (size == 0xff) {
size = (int) DataHelper.readLong(in, 2);
if (size > MAX_VALUE_LENGTH)
throw new DataFormatException(MAX_VALUE_LENGTH + " max, but this is " + size);
}
if (size == 0)
return "";
byte raw[] = new byte[size];
int read = DataHelper.read(in, raw);
if (read != size)
throw new EOFException("EOF reading string");
return new String(raw, "UTF-8");
}
/**
* Used to store entries that need deleting
*/
private static class InvalidEntry {
public final String key;
public final String list;
public InvalidEntry(String k, String l) {
key = k;
list = l;
}
}
/**
* BlockfileNamingService [force]
* force = force writable
*/
public static void main(String[] args) {
Properties ctxProps = new Properties();
if (args.length > 0 && args[0].equals("force"))
ctxProps.setProperty(PROP_FORCE, "true");
I2PAppContext ctx = new I2PAppContext(ctxProps);
BlockfileNamingService bns = new BlockfileNamingService(ctx);
Properties sprops = new Properties();
String lname = "privatehosts.txt";
sprops.setProperty("list", lname);
System.out.println("List " + lname + " contains " + bns.size(sprops));
lname = "userhosts.txt";
sprops.setProperty("list", lname);
System.out.println("List " + lname + " contains " + bns.size(sprops));
lname = "hosts.txt";
sprops.setProperty("list", lname);
System.out.println("List " + lname + " contains " + bns.size(sprops));
/****
List<String> names = null;
Properties props = new Properties();
try {
DataHelper.loadProps(props, new File("hosts.txt"), true);
names = new ArrayList(props.keySet());
Collections.shuffle(names);
} catch (IOException ioe) {
System.out.println("No hosts.txt to test with");
bns.close();
return;
}
System.out.println("size() reports " + bns.size());
System.out.println("getEntries() returns " + bns.getEntries().size());
System.out.println("Testing with " + names.size() + " hostnames");
int found = 0;
int notfound = 0;
int rfound = 0;
int rnotfound = 0;
long start = System.currentTimeMillis();
for (String name : names) {
Destination dest = bns.lookup(name);
if (dest != null) {
found++;
String reverse = bns.reverseLookup(dest);
if (reverse != null)
rfound++;
else
rnotfound++;
} else {
notfound++;
}
}
System.out.println("BFNS took " + DataHelper.formatDuration(System.currentTimeMillis() - start));
System.out.println("found " + found + " notfound " + notfound);
System.out.println("reverse found " + rfound + " notfound " + rnotfound);
//if (true) return;
System.out.println("Removing all " + names.size() + " hostnames");
found = 0;
notfound = 0;
Collections.shuffle(names);
start = System.currentTimeMillis();
for (String name : names) {
if (bns.remove(name))
found++;
else
notfound++;
}
System.out.println("BFNS took " + DataHelper.formatDuration(System.currentTimeMillis() - start));
System.out.println("removed " + found + " not removed " + notfound);
System.out.println("Adding back " + names.size() + " hostnames");
found = 0;
notfound = 0;
Collections.shuffle(names);
start = System.currentTimeMillis();
for (String name : names) {
try {
if (bns.put(name, new Destination(props.getProperty(name))))
found++;
else
notfound++;
} catch (DataFormatException dfe) {}
}
System.out.println("BFNS took " + DataHelper.formatDuration(System.currentTimeMillis() - start));
System.out.println("Added " + found + " not added " + notfound);
System.out.println("size() reports " + bns.size());
//bns.dumpDB();
****/
bns.close();
ctx.logManager().flush();
System.out.flush();
/****
if (true) return;
HostsTxtNamingService htns = new HostsTxtNamingService(I2PAppContext.getGlobalContext());
found = 0;
notfound = 0;
start = System.currentTimeMillis();
for (String name : names) {
Destination dest = htns.lookup(name);
if (dest != null)
found++;
else
notfound++;
}
System.out.println("HTNS took " + DataHelper.formatDuration(System.currentTimeMillis() - start));
System.out.println("found " + found + " notfound " + notfound);
****/
}
}
<html>
<body>
<p>
The BlockfileNamingService based on the Metanotion BlockFile Database.
This is the default NamingService for the router.
</p>
</body>
</html>
package net.metanotion;
/**
* Exists only to enable package.html to be included in javadoc.
* http://java.sun.com/j2se/javadoc/faq/index.html#packagewithoutjavafiles
*/
abstract class Dummy {}
Version 0.1.1 from http://www.metanotion.net/software/sandbox/block.html
License: See any source file.
This is a partial list of changes for I2P. For details, see history.txt
and the source control logs.
- BSkipList has an option to not keep everything in memory.
When this option is enabled, we use the new IBSkipSpan instead of
BSkipSpan. IBSkipSpan has the following changes:
* Only the first key in the span, and no values, are stored in memory
* put() and remove() read the span keys and values in from disk first
* flush() nulls out the keys and values after flushing to disk
* get() does a linear search through the keys on disk
- The metaIndex is stored in-memory. All "user" skiplists are not
stored in-memory.
- Default span size changed from 127 to 16
- Use I2P random source
- Return the previous SkipList if still open from a call to getIndex()
- Add a closeIndex() method
- Commented out some System.out.println()
- Convert Errors without message or cause to RuntimeExceptions with a message and cause
- Add unique on-disk headers to each page type for robustness;
change blockfile magic number as these headers are required
- Automatically repair some types of corruption;
report some other corruption types without failing
- Support read-only databases
- Lots and lots of bug fixes
TODO:
- More catching and repair of corruption
- Change PAGESIZE from default 1024 to 4096? No, wastes too much disk.
/*
Copyright (c) 2006, Matthew Estes
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Metanotion Software nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.metanotion.io;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RAIFile implements RandomAccessInterface, DataInput, DataOutput {
private final File f;
private final RandomAccessFile delegate;
private final boolean r, w;
public RAIFile(RandomAccessFile file) throws FileNotFoundException {
this.f = null;
this.delegate = file;
this.r = true;
// fake, we don't really know
this.w = true;
}
/** @param read must be true */
public RAIFile(File file, boolean read, boolean write) throws FileNotFoundException {
this.f = file;
this.r = read;
this.w = write;
String mode = "";
if(this.r) { mode += "r"; }
if(this.w) { mode += "w"; }
this.delegate = new RandomAccessFile(file, mode);
}
/**
* I2P is the file writable?
* Only valid if the File constructor was used, not the RAF constructor
* @since 0.8.8
*/
public boolean canWrite() {
return this.w;
}
/**
* @since 0.8.8
*/
@Override
public String toString() {
if (this.f != null)
return this.f.getAbsolutePath();
return this.delegate.toString();
}
public long getFilePointer() throws IOException { return delegate.getFilePointer(); }
public long length() throws IOException { return delegate.length(); }
public int read() throws IOException { return delegate.read(); }
public int read(byte[] b) throws IOException { return delegate.read(b); }
public int read(byte[] b, int off, int len) throws IOException { return delegate.read(b,off,len); }
public void seek(long pos) throws IOException { delegate.seek(pos); }
public void setLength(long newLength) throws IOException { delegate.setLength(newLength); }
// Closeable Methods
// TODO May need to change.
public void close() throws IOException { delegate.close(); }
// DataInput Methods
public boolean readBoolean() throws IOException { return delegate.readBoolean(); }
public byte readByte() throws IOException { return delegate.readByte(); }
public char readChar() throws IOException { return delegate.readChar(); }
public double readDouble() throws IOException { return delegate.readDouble(); }
public float readFloat() throws IOException { return delegate.readFloat(); }
public void readFully(byte[] b) throws IOException { delegate.readFully(b); }
public void readFully(byte[] b, int off, int len) throws IOException { delegate.readFully(b,off,len); }
public int readInt() throws IOException { return delegate.readInt(); }
public String readLine() throws IOException { return delegate.readLine(); }
public long readLong() throws IOException { return delegate.readLong(); }
public short readShort() throws IOException { return delegate.readShort(); }
public int readUnsignedByte() throws IOException { return delegate.readUnsignedByte(); }
public int readUnsignedShort() throws IOException { return delegate.readUnsignedShort(); }
/**
* I2P
* @throws IOException if the read value is negative
*/
public int readUnsignedInt() throws IOException {
int rv = readInt();
if (rv < 0)
throw new IOException("Negative value for unsigned int: " + rv);
return rv;
}
/** Read a UTF encoded string
I would delegate here. But Java's read/writeUTF combo suck.
A signed 2 byte length is not enough.
This reads a 4 byte length.
The upper byte MUST be zero, if its not, then its not this method and has used an
extensible length encoding.
This is followed by the bytes of the UTF encoded string, as
returned by String.getBytes("UTF-8");
*/
public String readUTF() throws IOException {
int len = delegate.readInt();
if((len < 0) || (len >= 16777216)) { throw new IOException("Bad Length Encoding"); }
byte[] bytes = new byte[len];
int l = delegate.read(bytes);
if(l==-1) { throw new IOException("EOF while reading String"); }
String s = new String(bytes, "UTF-8");
return s;
}
public int skipBytes(int n) throws IOException { return delegate.skipBytes(n); }
// DataOutput Methods
public void write(int b) throws IOException { delegate.write(b); }
public void write(byte[] b) throws IOException { delegate.write(b); }
public void write(byte[] b, int off, int len) throws IOException { delegate.write(b,off,len); }
public void writeBoolean(boolean v) throws IOException { delegate.writeBoolean(v); }
public void writeByte(int v) throws IOException { delegate.writeByte(v); }
public void writeShort(int v) throws IOException { delegate.writeShort(v); }
public void writeChar(int v) throws IOException { delegate.writeChar(v); }
public void writeInt(int v) throws IOException { delegate.writeInt(v); }
public void writeLong(long v) throws IOException { delegate.writeLong(v); }
public void writeFloat(float v) throws IOException { delegate.writeFloat(v); }
public void writeDouble(double v) throws IOException { delegate.writeDouble(v); }
public void writeBytes(String s) throws IOException { delegate.writeBytes(s); }
public void writeChars(String s) throws IOException { delegate.writeChars(s); }
/** Write a UTF encoded string
I would delegate here. But Java's read/writeUTF combo suck.
A signed 2 byte length is not enough.
This writes a 4 byte length.
The upper byte MUST be zero, if its not, then its not this method and has used an
extensible length encoding.
This is followed by the bytes of the UTF encoded string, as
returned by String.getBytes("UTF-8");
*/
public void writeUTF(String str) throws IOException {
byte[] string = str.getBytes("UTF-8");
if(string.length >= 16777216) { throw new IOException("String to long for encoding type"); }
delegate.writeInt(string.length);
delegate.write(string);
}
}
/*
Copyright (c) 2006, Matthew Estes
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Metanotion Software nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.metanotion.io;
import java.io.Closeable;
import java.io.IOException;
public interface RandomAccessInterface extends Closeable {
public long getFilePointer() throws IOException;
public long length() throws IOException;
public int read() throws IOException;
public int read(byte[] b) throws IOException;
public int read(byte[] b, int off, int len) throws IOException;
public void seek(long pos) throws IOException;
public void setLength(long newLength) throws IOException;
/**
* I2P is the file writable?
* Only valid if the File constructor was used, not the RAF constructor
* @since 0.8.8
*/
public boolean canWrite();
// Closeable Methods
public void close() throws IOException;
// DataInput Methods
public boolean readBoolean() throws IOException;
public byte readByte() throws IOException;
public char readChar() throws IOException;
public double readDouble() throws IOException;
public float readFloat() throws IOException;
public void readFully(byte[] b) throws IOException;
public void readFully(byte[] b, int off, int len) throws IOException;
public int readInt() throws IOException;
public String readLine() throws IOException;
public long readLong() throws IOException;
public short readShort() throws IOException;
public int readUnsignedByte() throws IOException;
public int readUnsignedShort() throws IOException;
// I2P
public int readUnsignedInt() throws IOException;
public String readUTF() throws IOException;
public int skipBytes(int n) throws IOException;
// DataOutput Methods
public void write(int b) throws IOException;
public void write(byte[] b) throws IOException;
public void write(byte[] b, int off, int len) throws IOException;
public void writeBoolean(boolean v) throws IOException;
public void writeByte(int v) throws IOException;
public void writeShort(int v) throws IOException;
public void writeChar(int v) throws IOException;
public void writeInt(int v) throws IOException;
public void writeLong(long v) throws IOException;
public void writeFloat(float v) throws IOException;
public void writeDouble(double v) throws IOException;
public void writeBytes(String s) throws IOException;
public void writeChars(String s) throws IOException;
public void writeUTF(String str) throws IOException;
}
/*
Copyright (c) 2006, Matthew Estes
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Metanotion Software nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.metanotion.io;
public interface Serializer<T> {
public byte[] getBytes(T o);
public T construct(byte[] b);
}