* Naming services, addressbook, susidns:

- Fix search capability
    - Fix result count and view within results
    - Fix published address book
    - Fix ngettext
    - Cache size
    - Fix 0-9 filter
    - Addressbook updates via API, except for published
This commit is contained in:
zzz
2011-03-15 21:52:48 +00:00
parent 8b737b4adb
commit 12c5b9c21c
7 changed files with 338 additions and 76 deletions

View File

@ -166,10 +166,10 @@ class AddressBook {
private static final int MAX_DEST_LENGTH = MIN_DEST_LENGTH + 100; // longer than any known cert type for now
/**
* Do basic validation of the hostname and dest
* Do basic validation of the hostname
* hostname was already converted to lower case by ConfigParser.parse()
*/
private static boolean valid(String host, String dest) {
public static boolean isValidKey(String host) {
return
host.endsWith(".i2p") &&
host.length() > 4 &&
@ -193,8 +193,15 @@ class AddressBook {
(! host.equals("console.i2p")) &&
(! host.endsWith(".proxy.i2p")) &&
(! host.endsWith(".router.i2p")) &&
(! host.endsWith(".console.i2p")) &&
(! host.endsWith(".console.i2p"))
;
}
/**
* Do basic validation of the b64 dest, without bothering to instantiate it
*/
private static boolean isValidDest(String dest) {
return
// null cert ends with AAAA but other zero-length certs would be AA
((dest.length() == MIN_DEST_LENGTH && dest.endsWith("AA")) ||
(dest.length() > MIN_DEST_LENGTH && dest.length() <= MAX_DEST_LENGTH)) &&
@ -221,7 +228,7 @@ class AddressBook {
String otherKey = entry.getKey();
String otherValue = entry.getValue();
if (valid(otherKey, otherValue)) {
if (isValidKey(otherKey) && isValidDest(otherValue)) {
if (this.addresses.containsKey(otherKey) && !overwrite) {
if (!this.addresses.get(otherKey).equals(otherValue)
&& log != null) {

View File

@ -29,6 +29,10 @@ import java.util.List;
import java.util.Map;
import net.i2p.I2PAppContext;
import net.i2p.client.naming.NamingService;
import net.i2p.client.naming.SingleFileNamingService;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.util.SecureDirectory;
/**
@ -55,6 +59,7 @@ public class Daemon {
* @param published
* The published AddressBook. This address book is published on
* the user's eepsite so that others may subscribe to it.
* If non-null, overwrite with the new addressbook.
* @param subscriptions
* A SubscriptionList listing the remote address books to update
* from.
@ -75,6 +80,71 @@ public class Daemon {
subscriptions.write();
}
/**
* Update the router and published address books using remote data from the
* subscribed address books listed in subscriptions.
*
* @param router
* The router AddressBook. This is the address book read by
* client applications.
* @param published
* The published AddressBook. This address book is published on
* the user's eepsite so that others may subscribe to it.
* If non-null, overwrite with the new addressbook.
* @param subscriptions
* A SubscriptionList listing the remote address books to update
* from.
* @param log
* The log to write changes and conflicts to.
* @since 0.8.6
*/
public static void update(NamingService router, File published, SubscriptionList subscriptions, Log log) {
NamingService publishedNS = null;
Iterator<AddressBook> iter = subscriptions.iterator();
while (iter.hasNext()) {
// yes, the EepGet fetch() is done in next()
AddressBook sub = iter.next();
for (Map.Entry<String, String> entry : sub.getAddresses().entrySet()) {
String key = entry.getKey();
Destination oldDest = router.lookup(key);
try {
if (oldDest == null) {
if (AddressBook.isValidKey(key)) {
Destination dest = new Destination(entry.getValue());
boolean success = router.put(key, dest);
if (log != null) {
if (success)
log.append("New address " + key +
" added to address book. From: " + sub.getLocation());
else
log.append("Save to naming service " + router + " failed for new key " + key);
}
// now update the published addressbook
if (published != null) {
if (publishedNS == null)
publishedNS = new SingleFileNamingService(I2PAppContext.getGlobalContext(), published.getAbsolutePath());
success = publishedNS.putIfAbsent(key, dest);
if (!success)
log.append("Save to published addressbook " + published.getAbsolutePath() + " failed for new key " + key);
}
} else if (log != null) {
log.append("Bad hostname " + key + " from "
+ sub.getLocation());
}
} else if (!oldDest.toBase64().equals(entry.getValue()) && log != null) {
log.append("Conflict for " + key + " from "
+ sub.getLocation()
+ ". Destination in remote address book is "
+ entry.getValue());
}
} catch (DataFormatException dfe) {
if (log != null)
log.append("Invalid b64 for" + key + " From: " + sub.getLocation());
}
}
}
}
/**
* Run an update, using the Map settings to provide the parameters.
*
@ -120,7 +190,35 @@ public class Daemon {
.get("proxy_host"), Integer.parseInt(settings.get("proxy_port")));
Log log = new Log(logFile);
update(master, router, published, subscriptions, log);
if (true)
update(getNamingService(), published, subscriptions, log);
else
update(master, router, published, subscriptions, log);
}
/** depth-first search */
private static NamingService searchNamingService(NamingService ns, String srch)
{
String name = ns.getName();
if (name == srch)
return ns;
List<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 NamingService for the current file name, or the root NamingService */
private static NamingService getNamingService()
{
NamingService root = I2PAppContext.getGlobalContext().namingService();
NamingService rv = searchNamingService(root, "hosts.txt");
return rv != null ? rv : root;
}
/**

View File

@ -79,6 +79,7 @@ public class AddressbookBean
{
return addressbook != null && !addressbook.isEmpty();
}
public AddressbookBean()
{
properties = new Properties();
@ -86,9 +87,11 @@ public class AddressbookBean
beginIndex = 0;
endIndex = DISPLAY_SIZE - 1;
}
private long configLastLoaded = 0;
private static final String PRIVATE_BOOK = "private_addressbook";
private static final String DEFAULT_PRIVATE_BOOK = "../privatehosts.txt";
protected void loadConfig()
{
long currentTime = System.currentTimeMillis();
@ -113,6 +116,7 @@ public class AddressbookBean
try { fis.close(); } catch (IOException ioe) {}
}
}
public String getFileName()
{
loadConfig();
@ -144,7 +148,7 @@ public class AddressbookBean
book.compareToIgnoreCase( "router" ) != 0 &&
book.compareToIgnoreCase( "private" ) != 0 &&
book.compareToIgnoreCase( "published" ) != 0 ))
book = "master";
book = "router";
return book;
}
@ -215,40 +219,49 @@ public class AddressbookBean
* addressbook.jsp catches the case where the whole book is empty.
*/
protected String generateLoadMessage() {
String message = "";
String message;
String filterArg = "";
if( search != null && search.length() > 0 ) {
message = _("Search") + ' ';
}
int resultCount = resultSize();
if( filter != null && filter.length() > 0 ) {
if( search != null && search.length() > 0 )
message = _("Search within filtered list") + ' ';
message = ngettext("One result for search within filtered list.",
"{0} results for search within filtered list.",
resultCount);
else
message = _("Filtered list") + ' ';
message = ngettext("Filtered list contains 1 entry.",
"Fltered list contains {0} entries.",
resultCount);
filterArg = "&amp;filter=" + filter;
}
if (entries.length == 0) {
message += "- " + _("no matches") + '.';
} else if (getBeginInt() == 0 && getEndInt() == entries.length - 1) {
if (message.length() == 0)
message = _("Addressbook") + ' ';
if (entries.length <= 0)
message += _("contains no entries");
} else if( search != null && search.length() > 0 ) {
message = ngettext("One result for search.",
"{0} results for search.",
resultCount);
} else {
if (resultCount <= 0)
// covered in jsp
//message = _("This addressbook is empty.");
message = "";
else
message += _(entries.length, "contains 1 entry", "contains {0} entries");
message += '.';
message = ngettext("Addressbook contains 1 entry.",
"Addressbook contains {0} entries.",
resultCount);
}
if (resultCount <= 0) {
// nothing to display
} else if (getBeginInt() == 0 && getEndInt() == resultCount - 1) {
// nothing to display
} else {
if (getBeginInt() > 0) {
int newBegin = Math.max(0, getBeginInt() - DISPLAY_SIZE);
int newEnd = Math.max(0, getBeginInt() - 1);
message += "<a href=\"addressbook.jsp?book=" + getBook() + filterArg +
message += " <a href=\"addressbook.jsp?book=" + getBook() + filterArg +
"&amp;begin=" + newBegin + "&amp;end=" + newEnd + "\">" + newBegin +
'-' + newEnd + "</a> | ";
}
message += _("Showing {0} of {1}", "" + getBegin() + '-' + getEnd(), entries.length);
if (getEndInt() < entries.length - 1) {
int newBegin = Math.min(entries.length - 1, getEndInt() + 1);
int newEnd = Math.min(entries.length, getEndInt() + DISPLAY_SIZE);
message += ' ' + _("Showing {0} of {1}", "" + getBegin() + '-' + getEnd(), Integer.valueOf(resultCount));
if (getEndInt() < resultCount - 1) {
int newBegin = Math.min(resultCount - 1, getEndInt() + 1);
int newEnd = Math.min(resultCount, getEndInt() + DISPLAY_SIZE);
message += " | <a href=\"addressbook.jsp?book=" + getBook() + filterArg +
"&amp;begin=" + newBegin + "&amp;end=" + newEnd + "\">" + newBegin +
'-' + newEnd + "</a>";
@ -313,7 +326,8 @@ public class AddressbookBean
if (deleted == 1)
message = _("Destination {0} deleted.", name);
else
message = _("{0} destinations deleted.", deleted);
// parameter will always be >= 2
message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted);
}
}
if( changed ) {
@ -394,29 +408,76 @@ public class AddressbookBean
public void setHostname(String hostname) {
this.hostname = DataHelper.stripHTML(hostname).trim(); // XSS
}
protected int getBeginInt() {
return Math.max(0, Math.min(entries.length - 1, beginIndex));
return Math.max(0, Math.min(resultSize() - 1, beginIndex));
}
public String getBegin() {
return "" + getBeginInt();
}
/**
* @return beginning index into results
* @since 0.8.6
*/
public String getResultBegin() {
return isPrefiltered() ? "0" : Integer.toString(getBeginInt());
}
public void setBegin(String s) {
try {
beginIndex = Integer.parseInt(s);
} catch (NumberFormatException nfe) {}
}
protected int getEndInt() {
return Math.max(0, Math.max(getBeginInt(), Math.min(entries.length - 1, endIndex)));
return Math.max(0, Math.max(getBeginInt(), Math.min(resultSize() - 1, endIndex)));
}
public String getEnd() {
return "" + getEndInt();
}
/**
* @return ending index into results
* @since 0.8.6
*/
public String getResultEnd() {
return Integer.toString(isPrefiltered() ? resultSize() - 1 : getEndInt());
}
public void setEnd(String s) {
try {
endIndex = Integer.parseInt(s);
} catch (NumberFormatException nfe) {}
}
/**
* Does the entries map contain only the lookup result,
* or must we index into it?
* @since 0.8.6
*/
protected boolean isPrefiltered() {
return false;
}
/**
* @return the size of the lookup result
* @since 0.8.6
*/
protected int resultSize() {
return entries.length;
}
/**
* @return the total size of the address book
* @since 0.8.6
*/
protected int totalSize() {
return entries.length;
}
/** translate */
protected static String _(String s) {
return Messages.getString(s);
@ -433,7 +494,7 @@ public class AddressbookBean
}
/** translate (ngettext) @since 0.8.6 */
protected static String _(int n, String s, String p) {
protected static String ngettext(String s, String p, int n) {
return Messages.getString(n, s, p);
}
}

View File

@ -37,24 +37,58 @@ import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
/**
* Talk to the NamingService API instead of modifying the hosts.txt files directly
* Talk to the NamingService API instead of modifying the hosts.txt files directly,
* except for the 'published' addressbook.
*
* @since 0.8.5
* @since 0.8.6
*/
public class NamingServiceBean extends AddressbookBean
{
private static final String DEFAULT_NS = "BlockfileNamingService";
private boolean isDirect() {
return getBook().equals("published");
}
@Override
protected boolean isPrefiltered() {
if (isDirect())
return super.isPrefiltered();
return (search == null || search.length() <= 0) &&
(filter == null || filter.length() <= 0);
// and right naming service...
}
@Override
protected int resultSize() {
if (isDirect())
return super.resultSize();
return isPrefiltered() ? totalSize() : entries.length;
}
@Override
protected int totalSize() {
if (isDirect())
return super.totalSize();
// only blockfile needs the list property
Properties props = new Properties();
props.setProperty("list", getFileName());
return getNamingService().size(props);
}
@Override
public boolean isNotEmpty()
{
return getNamingService().size() > 0;
if (isDirect())
return super.isNotEmpty();
return totalSize() > 0;
}
@Override
public String getFileName()
{
if (isDirect())
return super.getFileName();
loadConfig();
String filename = properties.getProperty( getBook() + "_addressbook" );
int slash = filename.lastIndexOf('/');
@ -64,7 +98,7 @@ public class NamingServiceBean extends AddressbookBean
}
/** depth-first search */
private NamingService searchNamingService(NamingService ns, String srch)
private static NamingService searchNamingService(NamingService ns, String srch)
{
String name = ns.getName();
if (name == srch || name == DEFAULT_NS)
@ -88,10 +122,16 @@ public class NamingServiceBean extends AddressbookBean
return rv != null ? rv : root;
}
/** Load addressbook and apply filter, returning messages about this. */
/**
* Load addressbook and apply filter, returning messages about this.
* To control memory, don't load the whole addressbook if we can help it...
* only load what is searched for.
*/
@Override
public String getLoadBookMessages()
{
if (isDirect())
return super.getLoadBookMessages();
NamingService service = getNamingService();
Debug.debug("Searching within " + service + " with filename=" + getFileName() + " and with filter=" + filter + " and with search=" + search);
String message = "";
@ -100,16 +140,22 @@ public class NamingServiceBean extends AddressbookBean
Map<String, Destination> results;
Properties searchProps = new Properties();
// only blockfile needs this
searchProps.setProperty("list", getFileName());
searchProps.setProperty("list", getFileName());
if (filter != null) {
String startsAt = filter == "0-9" ? "0" : filter;
String startsAt = filter.equals("0-9") ? "[0-9]" : filter;
searchProps.setProperty("startsWith", startsAt);
}
if (beginIndex > 0)
searchProps.setProperty("skip", Integer.toString(beginIndex));
int limit = 1 + endIndex - beginIndex;
if (limit > 0)
searchProps.setProperty("limit", Integer.toString(limit));
if (isPrefiltered()) {
// Only limit if we not searching or filtering, so we will
// know the total number of results
if (beginIndex > 0)
searchProps.setProperty("skip", Integer.toString(beginIndex));
int limit = 1 + endIndex - beginIndex;
if (limit > 0)
searchProps.setProperty("limit", Integer.toString(limit));
}
if (search != null && search.length() > 0)
searchProps.setProperty("search", search.toLowerCase());
results = service.getEntries(searchProps);
Debug.debug("Result count: " + results.size());
@ -151,6 +197,8 @@ public class NamingServiceBean extends AddressbookBean
@Override
public String getMessages()
{
if (isDirect())
return super.getMessages();
// Loading config and addressbook moved into getLoadBookMessages()
String message = "";
@ -168,23 +216,22 @@ public class NamingServiceBean extends AddressbookBean
} else if (oldDest != null && !action.equals(_("Replace"))) {
message = _("Host name {0} is already in addressbook with a different destination. Click \"Replace\" to overwrite.", hostname);
} else {
boolean valid = true;
try {
Destination dest = new Destination(destination);
getNamingService().put(hostname, dest, nsOptions);
boolean success = getNamingService().put(hostname, dest, nsOptions);
if (success) {
changed = true;
if (oldDest == null)
message = _("Destination added for {0}.", hostname);
else
message = _("Destination changed for {0}.", hostname);
// clear form
hostname = null;
destination = null;
} else {
message = _("Failed to add Destination for {0} to naming service {1}", hostname, getNamingService()) + "<br>";
}
} catch (DataFormatException dfe) {
valid = false;
}
if (valid) {
changed = true;
if (oldDest == null)
message = _("Destination added for {0}.", hostname);
else
message = _("Destination changed for {0}.", hostname);
// clear form
hostname = null;
destination = null;
} else {
message = _("Invalid Base 64 destination.");
}
}
@ -197,17 +244,20 @@ public class NamingServiceBean extends AddressbookBean
String name = null;
int deleted = 0;
for (String n : deletionMarks) {
getNamingService().remove(n, nsOptions);
if (deleted++ == 0) {
boolean success = getNamingService().remove(n, nsOptions);
if (!success) {
message += _("Failed to delete Destination for {0} from naming service {1}", name, getNamingService()) + "<br>";
} else if (deleted++ == 0) {
changed = true;
name = n;
}
}
if( changed ) {
if (deleted == 1)
message = _("Destination {0} deleted.", name);
message += _("Destination {0} deleted.", name);
else
message = _("{0} destinations deleted.", deleted);
// parameter will always be >= 2
message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted);
}
}
if( changed ) {

View File

@ -144,7 +144,7 @@ ${book.loadBookMessages}
<th><%=intl._("Destination")%></th>
</tr>
<!-- limit iterator, or "Form too large" may result on submit, and is a huge web page if we don't -->
<c:forEach items="${book.entries}" var="addr" begin="${book.begin}" end="${book.end}">
<c:forEach items="${book.entries}" var="addr" begin="${book.resultBegin}" end="${book.resultEnd}">
<tr class="list${book.trClass}">
<c:if test="${book.master || book.router || book.published || book.private}">
<td class="checkbox"><input type="checkbox" name="checked" value="${addr.name}" title="<%=intl._("Mark for deletion")%>"></td>

View File

@ -476,13 +476,17 @@ public class BlockfileNamingService extends DummyNamingService {
* 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
* 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;
@ -491,10 +495,15 @@ public class BlockfileNamingService extends DummyNamingService {
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)
beginWith = startsWith;
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);
@ -505,7 +514,9 @@ public class BlockfileNamingService extends DummyNamingService {
} catch (NumberFormatException nfe) {}
}
if (_log.shouldLog(Log.DEBUG))
_log.debug("Searching " + listname + " beginning with " + beginWith + " starting with " + startsWith + " limit=" + limit + " skip=" + skip);
_log.debug("Searching " + listname + " beginning with " + beginWith +
" starting with " + startsWith + " search string " + search +
" limit=" + limit + " skip=" + skip);
synchronized(_bf) {
try {
SkipList sl = _bf.getIndex(listname, _stringSerializer, _destSerializer);
@ -523,12 +534,21 @@ public class BlockfileNamingService extends DummyNamingService {
for (int i = 0; i < skip && iter.hasNext(); i++) {
iter.next();
}
for (int i = 0; i < limit && iter.hasNext(); i++) {
String key = (String) iter.nextKey();
if (startsWith != null && !key.startsWith(startsWith))
break;
DestEntry de = (DestEntry) iter.next();
rv.put(key, de.dest);
for (int i = 0; i < limit && iter.hasNext(); ) {
String key = (String) iter.nextKey();
if (startsWith != null) {
if (startsWith.equals("[0-9]")) {
if (key.charAt(0) > '9')
break;
} else if (!key.startsWith(startsWith)) {
break;
}
}
DestEntry de = (DestEntry) iter.next();
if (search != null && key.indexOf(search) < 0)
continue;
rv.put(key, de.dest);
i++;
}
return rv;
} catch (IOException ioe) {

View File

@ -53,6 +53,10 @@ public class SingleFileNamingService extends NamingService {
private final static Log _log = new Log(SingleFileNamingService.class);
private final File _file;
private final ReentrantReadWriteLock _fileLock;
/** cached number of entries */
private int _size;
/** last write time */
private long _lastWrite;
public SingleFileNamingService(I2PAppContext context, String filename) {
super(context);
@ -292,14 +296,18 @@ public class SingleFileNamingService extends NamingService {
/**
* @param options As follows:
* Key "search": return only those matching substring
* Key "startsWith": return only those starting with
* ("[0-9]" allowed)
*/
@Override
public Map<String, Destination> getEntries(Properties options) {
if (!_file.exists())
return Collections.EMPTY_MAP;
String startsWith = "";
String searchOpt = null;
String startsWith = null;
if (options != null) {
searchOpt = options.getProperty("search");
startsWith = options.getProperty("startsWith");
}
BufferedReader in = null;
@ -307,11 +315,19 @@ public class SingleFileNamingService extends NamingService {
try {
in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024);
String line = null;
String search = startsWith + '=';
String search = startsWith == null ? null : startsWith + '=';
Map<String, Destination> rv = new HashMap();
while ( (line = in.readLine()) != null) {
if ((!startsWith.equals("")) && !line.startsWith(search))
if (line.length() <= 0)
continue;
if (search != null) {
if (startsWith.equals("[0-9]")) {
if (line.charAt(0) < '0' || line.charAt(0) > '9')
continue;
} else if (!line.startsWith(search)) {
continue;
}
}
if (line.startsWith("#"))
continue;
if (line.indexOf('#') > 0) // trim off any end of line comment
@ -320,12 +336,18 @@ public class SingleFileNamingService extends NamingService {
if (split <= 0)
continue;
String key = line.substring(split);
if (searchOpt != null && key.indexOf(searchOpt) < 0)
continue;
String b64 = line.substring(split+1); //.trim() ??????????????
try {
Destination dest = new Destination(b64);
rv.put(key, dest);
} catch (DataFormatException dfe) {}
}
if (searchOpt == null && startsWith == null) {
_lastWrite = _file.lastModified();
_size = rv.size();
}
return rv;
} catch (IOException ioe) {
_log.error("getEntries error", ioe);
@ -346,14 +368,18 @@ public class SingleFileNamingService extends NamingService {
BufferedReader in = null;
getReadLock();
try {
if (_file.lastModified() <= _lastWrite)
return _size;
in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024);
String line = null;
int rv = 0;
while ( (line = in.readLine()) != null) {
if (line.startsWith("#"))
if (line.startsWith("#") || line.length() <= 0)
continue;
rv++;
}
_lastWrite = _file.lastModified();
_size = rv;
return rv;
} catch (IOException ioe) {
_log.error("size() error", ioe);