/* * This file is part of susidns project, see http://susi.i2p/ * * Copyright (C) 2005 <susi23@mail.i2p> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ package i2p.susi.dns; import java.io.IOException; import java.io.Writer; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.SortedMap; import net.i2p.client.naming.NamingService; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; /** * Talk to the NamingService API instead of modifying the hosts.txt files directly, * except for the 'published' addressbook. * * @since 0.8.7 */ public class NamingServiceBean extends AddressbookBean { private static final String DEFAULT_NS = "BlockfileNamingService"; private String detail; 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) && getNamingService().getName().equals(DEFAULT_NS); } @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() { if (isDirect()) return super.isNotEmpty(); return totalSize() > 0; } @Override public String getFileName() { if (isDirect()) return super.getFileName(); loadConfig(); String filename = properties.getProperty( getBook() + "_addressbook" ); return basename(filename); } @Override public String getDisplayName() { if (isDirect()) return super.getDisplayName(); loadConfig(); return _t("{0} address book in {1} database", getFileName(), getNamingService().getName()); } /** depth-first search */ private static NamingService searchNamingService(NamingService ns, String srch) { String name = ns.getName(); if (name.equals(srch) || basename(name).equals(srch) || name.equals(DEFAULT_NS)) 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; } private static String basename(String filename) { int slash = filename.lastIndexOf('/'); if (slash >= 0) filename = filename.substring(slash + 1); return filename; } /** @return the NamingService for the current file name, or the root NamingService */ private NamingService getNamingService() { NamingService root = _context.namingService(); NamingService rv = searchNamingService(root, getFileName()); return rv != null ? rv : root; } /** * 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("Searching within " + service + " with filename=" + getFileName() + " and with filter=" + filter + " and with search=" + search); String message = ""; try { LinkedList<AddressBean> list = new LinkedList<AddressBean>(); Map<String, Destination> results; Properties searchProps = new Properties(); // only blockfile needs this searchProps.setProperty("list", getFileName()); if (filter != null) { String startsAt = filter.equals("0-9") ? "[0-9]" : filter; searchProps.setProperty("startsWith", startsAt); } 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(Locale.US)); results = service.getEntries(searchProps); debug("Result count: " + results.size()); for (Map.Entry<String, Destination> entry : results.entrySet()) { String name = entry.getKey(); if( filter != null && filter.length() > 0 ) { if (filter.equals("0-9")) { char first = name.charAt(0); if( first < '0' || first > '9' ) continue; } else if( ! name.toLowerCase(Locale.US).startsWith( filter.toLowerCase(Locale.US) ) ) { continue; } } if( search != null && search.length() > 0 ) { if( name.indexOf( search ) == -1 ) { continue; } } String destination = entry.getValue().toBase64(); if (destination != null) { list.addLast( new AddressBean( name, destination ) ); } else { // delete it too? System.err.println("Bad entry " + name + " in database " + service.getName()); } } AddressBean array[] = list.toArray(new AddressBean[list.size()]); if (!(results instanceof SortedMap)) Arrays.sort( array, sorter ); entries = array; message = generateLoadMessage(); } catch (RuntimeException e) { warn(e); } if( message.length() > 0 ) message = "<p>" + message + "</p>"; return message; } /** Perform actions, returning messages about this. */ @Override public String getMessages() { if (isDirect()) return super.getMessages(); // Loading config and addressbook moved into getLoadBookMessages() String message = ""; if( action != null ) { Properties nsOptions = new Properties(); // only blockfile needs this nsOptions.setProperty("list", getFileName()); if (_context.getBooleanProperty(PROP_PW_ENABLE) || (serial != null && serial.equals(lastSerial))) { boolean changed = false; if (action.equals(_t("Add")) || action.equals(_t("Replace")) || action.equals(_t("Add Alternate"))) { if(hostname != null && destination != null) { try { // throws IAE with translated message String host = AddressBean.toASCII(hostname); String displayHost = host.equals(hostname) ? hostname : hostname + " (" + host + ')'; Properties outProperties= new Properties(); Destination oldDest = getNamingService().lookup(host, nsOptions, outProperties); if (oldDest != null && destination.equals(oldDest.toBase64())) { message = _t("Host name {0} is already in address book, unchanged.", displayHost); } else if (oldDest == null && action.equals(_t("Add Alternate"))) { message = _t("Host name {0} is not in the address book.", displayHost); } else if (oldDest != null && action.equals(_t("Add"))) { message = _t("Host name {0} is already in address book with a different destination. Click \"Replace\" to overwrite.", displayHost); } else { try { Destination dest = new Destination(destination); if (oldDest != null) { nsOptions.putAll(outProperties); String now = Long.toString(_context.clock().now()); if (action.equals(_t("Add Alternate"))) nsOptions.setProperty("a", now); else nsOptions.setProperty("m", now); } nsOptions.setProperty("s", _t("Manually added via SusiDNS")); boolean success; if (action.equals(_t("Add Alternate"))) { // check all for dups List<Destination> all = getNamingService().lookupAll(host); if (all == null || !all.contains(dest)) { success = getNamingService().addDestination(host, dest, nsOptions); } else { // will get generic message below success = false; } } else { success = getNamingService().put(host, dest, nsOptions); } if (success) { changed = true; if (oldDest == null || action.equals(_t("Add Alternate"))) message = _t("Destination added for {0}.", displayHost); else message = _t("Destination changed for {0}.", displayHost); if (!host.endsWith(".i2p")) message += "<br>" + _t("Warning - host name does not end with \".i2p\""); // clear form hostname = null; destination = null; } else { message = _t("Failed to add Destination for {0} to naming service {1}", displayHost, getNamingService().getName()) + "<br>"; } } catch (DataFormatException dfe) { message = _t("Invalid Base 64 destination."); } } } catch (IllegalArgumentException iae) { message = iae.getMessage(); if (message == null) message = _t("Invalid host name \"{0}\".", hostname); } } else { message = _t("Please enter a host name and destination"); } // clear search when adding search = null; } else if (action.equals(_t("Delete Selected")) || action.equals(_t("Delete Entry"))) { String name = null; int deleted = 0; Destination matchDest = null; if (action.equals(_t("Delete Entry"))) { // remove specified dest only in case there is more than one if (destination != null) { try { matchDest = new Destination(destination); } catch (DataFormatException dfe) {} } } for (String n : deletionMarks) { boolean success; if (matchDest != null) success = getNamingService().remove(n, matchDest, nsOptions); else success = getNamingService().remove(n, nsOptions); String uni = AddressBean.toUnicode(n); String displayHost = uni.equals(n) ? n : uni + " (" + n + ')'; if (!success) { message += _t("Failed to delete Destination for {0} from naming service {1}", displayHost, getNamingService().getName()) + "<br>"; } else if (deleted++ == 0) { changed = true; name = displayHost; } } if( changed ) { if (deleted == 1) // parameter is a host name message += _t("Destination {0} deleted.", name); else // parameter will always be >= 2 message = ngettext("1 destination deleted.", "{0} destinations deleted.", deleted); } else { message = _t("No entries selected to delete."); } // clear search when deleting if (action.equals(_t("Delete Entry"))) search = null; } if( changed ) { message += "<br>" + _t("Address book saved."); } } else { message = _t("Invalid form submission, probably because you used the \"back\" or \"reload\" button on your browser. Please resubmit.") + ' ' + _t("If the problem persists, verify that you have cookies enabled in your browser."); } } action = null; if( message.length() > 0 ) message = "<p class=\"messages\">" + message + "</p>"; return message; } public void setH(String h) { this.detail = DataHelper.stripHTML(h); } public AddressBean getLookup() { if (this.detail == null) return null; if (isDirect()) { // go to some trouble to make this work for the published addressbook this.filter = this.detail.substring(0, 1); this.search = this.detail; // we don't want the messages, we just want to populate entries super.getLoadBookMessages(); for (int i = 0; i < this.entries.length; i++) { if (this.search.equals(this.entries[i].getName())) return this.entries[i]; } return null; } Properties nsOptions = new Properties(); Properties outProps = new Properties(); nsOptions.setProperty("list", getFileName()); Destination dest = getNamingService().lookup(this.detail, nsOptions, outProps); if (dest == null) return null; AddressBean rv = new AddressBean(this.detail, dest.toBase64()); rv.setProperties(outProps); return rv; } /** * @since 0.9.26 */ public List<AddressBean> getLookupAll() { if (this.detail == null) return null; if (isDirect()) { // won't work for the published addressbook AddressBean ab = getLookup(); if (ab != null) return Collections.singletonList(ab); return null; } Properties nsOptions = new Properties(); List<Properties> propsList = new ArrayList<Properties>(4); nsOptions.setProperty("list", getFileName()); List<Destination> dests = getNamingService().lookupAll(this.detail, nsOptions, propsList); if (dests == null) return null; List<AddressBean> rv = new ArrayList<AddressBean>(dests.size()); for (int i = 0; i < dests.size(); i++) { AddressBean ab = new AddressBean(this.detail, dests.get(i).toBase64()); ab.setProperties(propsList.get(i)); rv.add(ab); } return rv; } /** * @since 0.9.20 */ public void export(Writer out) throws IOException { Properties searchProps = new Properties(); // only blockfile needs this searchProps.setProperty("list", getFileName()); if (filter != null) { String startsAt = filter.equals("0-9") ? "[0-9]" : filter; searchProps.setProperty("startsWith", startsAt); } if (search != null && search.length() > 0) searchProps.setProperty("search", search.toLowerCase(Locale.US)); getNamingService().export(out, searchProps); // No post-filtering for hosts.txt naming services. It is what it is. } }