/* * 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.client.naming; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantReadWriteLock; import net.i2p.I2PAppContext; import net.i2p.data.DataFormatException; import net.i2p.data.DataHelper; import net.i2p.data.Destination; import net.i2p.util.FileUtil; import net.i2p.util.Log; import net.i2p.util.SecureFile; import net.i2p.util.SecureFileOutputStream; /** * A naming service based on a single file using the "hosts.txt" format. * Supports adds, removes, and listeners. * * All methods here are case-sensitive. * Conversion to lower case is done in HostsTxtNamingService. * * This does NOT provide .b32.i2p or {b64} resolution. * It also does not do any caching. * Use from HostsTxtNamingService or chain with another NamingService * via MetaNamingService if you need those features. * * @since 0.8.7 */ public class SingleFileNamingService extends NamingService { private final File _file; private final ReentrantReadWriteLock _fileLock; /** cached number of entries */ private int _size; /** last write time */ private long _lastWrite; private volatile boolean _isClosed; public SingleFileNamingService(I2PAppContext context, String filename) { super(context); File file = new File(filename); if (!file.isAbsolute()) file = new File(context.getRouterDir(), filename); _file = file; _fileLock = new ReentrantReadWriteLock(true); } /** * @return the file's absolute path */ @Override public String getName() { return _file.getAbsolutePath(); } /** * Will strip a "www." prefix and retry if lookup fails * * @param hostname case-sensitive; caller should convert to lower case * @param lookupOptions ignored * @param storedOptions ignored */ @Override public Destination lookup(String hostname, Properties lookupOptions, Properties storedOptions) { try { String key = getKey(hostname); if (key == null && hostname.startsWith("www.") && hostname.length() > 7) key = getKey(hostname.substring(4)); if (key != null) return lookupBase64(key); } catch (IOException ioe) { if (_file.exists()) _log.error("Error loading hosts file " + _file, ioe); else if (_log.shouldLog(Log.WARN)) _log.warn("Error loading hosts file " + _file, ioe); } return null; } /** * @param options ignored */ @Override public String reverseLookup(Destination dest, Properties options) { String destkey = dest.toBase64(); BufferedReader in = null; getReadLock(); 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; if (line.indexOf('#') > 0) // trim off any end of line comment line = line.substring(0, line.indexOf('#')).trim(); int split = line.indexOf('='); if (split <= 0) continue; if (destkey.equals(line.substring(split + 1))) return line.substring(0, split); } return null; } catch (IOException ioe) { if (_file.exists()) _log.error("Error loading hosts file " + _file, ioe); else if (_log.shouldLog(Log.WARN)) _log.warn("Error loading hosts file " + _file, ioe); return null; } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} releaseReadLock(); } } /** * Better than DataHelper.loadProps(), doesn't load the whole file into memory, * and stops when it finds a match. * * @param host case-sensitive; caller should convert to lower case */ private String getKey(String host) throws IOException { BufferedReader in = null; getReadLock(); try { in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024); String line = null; String search = host + '='; while ( (line = in.readLine()) != null) { if (!line.startsWith(search)) continue; if (line.indexOf('#') > 0) // trim off any end of line comment line = line.substring(0, line.indexOf('#')).trim(); int split = line.indexOf('='); return line.substring(split+1); //.trim() ?????????????? } } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} releaseReadLock(); } return null; } /** * @param hostname case-sensitive; caller should convert to lower case * @param options if non-null, any prefixed with '=' will be appended * in subscription format */ @Override public boolean put(String hostname, Destination d, Properties options) { // try easy way first, most adds are not replaces if (putIfAbsent(hostname, d, options)) return true; if (!getWriteLock()) return false; BufferedReader in = null; BufferedWriter out = null; try { if (_isClosed) return false; File tmp = SecureFile.createTempFile("temp-", ".tmp", _file.getAbsoluteFile().getParentFile()); out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(tmp), "UTF-8")); if (_file.exists()) { in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024); String line = null; String search = hostname + '='; while ( (line = in.readLine()) != null) { if (line.startsWith(search)) continue; out.write(line); out.newLine(); } in.close(); } out.write(hostname); out.write('='); out.write(d.toBase64()); // subscription options if (options != null) writeOptions(options, out); out.newLine(); out.close(); boolean success = FileUtil.rename(tmp, _file); if (success) { for (NamingServiceListener nsl : _listeners) { nsl.entryChanged(this, hostname, d, options); } } return success; } catch (IOException ioe) { if (in != null) try { in.close(); } catch (IOException e) {} if (out != null) try { out.close(); } catch (IOException e) {} _log.error("Error adding " + hostname, ioe); return false; } finally { releaseWriteLock(); } } /** * @param hostname case-sensitive; caller should convert to lower case * @param options if non-null, any prefixed with '=' will be appended * in subscription format */ @Override public boolean putIfAbsent(String hostname, Destination d, Properties options) { BufferedWriter out = null; if (!getWriteLock()) return false; try { if (_isClosed) return false; // simply check if present, and if not, append try { if (getKey(hostname) != null) return false; } catch (IOException ioe) { if (_file.exists()) { _log.error("Error adding " + hostname, ioe); return false; } // else new file } out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(_file, true), "UTF-8")); // FIXME fails if previous last line didn't have a trailing \n out.write(hostname); out.write('='); out.write(d.toBase64()); // subscription options if (options != null) writeOptions(options, out); out.write('\n'); out.close(); for (NamingServiceListener nsl : _listeners) { nsl.entryAdded(this, hostname, d, options); } return true; } catch (IOException ioe) { if (out != null) try { out.close(); } catch (IOException e) {} _log.error("Error adding " + hostname, ioe); return false; } finally { releaseWriteLock(); } } /** * Write the subscription options part of the line (including the #!). * Only options starting with '=' (if any) are written (with the '=' stripped). * Does not write a newline. * * @param options non-null * @since 0.9.26, package private since 0.9.30, public since 0.9.31 */ public static void writeOptions(Properties options, Writer out) throws IOException { boolean started = false; for (Map.Entry<Object, Object> e : options.entrySet()) { String k = (String) e.getKey(); if (!k.startsWith("=")) continue; k = k.substring(1); String v = (String) e.getValue(); if (started) { out.write(HostTxtEntry.PROP_SEPARATOR); } else { started = true; out.write(HostTxtEntry.PROPS_SEPARATOR); } out.write(k); out.write('='); out.write(v); } } /** * @param hostname case-sensitive; caller should convert to lower case * @param options ignored */ @Override public boolean remove(String hostname, Properties options) { BufferedReader in = null; BufferedWriter out = null; if (!getWriteLock()) return false; try { if (!_file.exists()) return false; if (_isClosed) return false; in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024); File tmp = SecureFile.createTempFile("temp-", ".tmp", _file.getAbsoluteFile().getParentFile()); out = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(tmp), "UTF-8")); String line = null; String search = hostname + '='; boolean success = false; while ( (line = in.readLine()) != null) { if (line.startsWith(search)) { success = true; continue; } out.write(line); out.newLine(); } in.close(); out.close(); if (!success) { tmp.delete(); return false; } success = FileUtil.rename(tmp, _file); if (success) { for (NamingServiceListener nsl : _listeners) { nsl.entryRemoved(this, hostname); } } return success; } catch (IOException ioe) { if (in != null) try { in.close(); } catch (IOException e) {} if (out != null) try { out.close(); } catch (IOException e) {} _log.error("Error removing " + hostname, ioe); return false; } finally { releaseWriteLock(); } } /** * @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.emptyMap(); String searchOpt = null; String startsWith = null; if (options != null) { searchOpt = options.getProperty("search"); startsWith = options.getProperty("startsWith"); } if (_log.shouldLog(Log.DEBUG)) _log.debug("Searching " + " starting with " + startsWith + " search string " + searchOpt); BufferedReader in = null; getReadLock(); try { in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024); String line = null; Map<String, Destination> rv = new HashMap<String, Destination>(); while ( (line = in.readLine()) != null) { if (line.length() <= 0) continue; if (startsWith != null) { if (startsWith.equals("[0-9]")) { if (line.charAt(0) < '0' || line.charAt(0) > '9') continue; } else if (!line.startsWith(startsWith)) { continue; } } if (line.startsWith("#")) continue; if (line.indexOf('#') > 0) // trim off any end of line comment line = line.substring(0, line.indexOf('#')).trim(); int split = line.indexOf('='); if (split <= 0) continue; String key = line.substring(0, 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); return Collections.emptyMap(); } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} releaseReadLock(); } } /** * Overridden since we store base64 natively. * * @param options As follows: * Key "search": return only those matching substring * Key "startsWith": return only those starting with * ("[0-9]" allowed) * @return all mappings (matching the options if non-null) * or empty Map if none. * Returned Map is not sorted. * @since 0.9.20 */ public Map<String, String> getBase64Entries(Properties options) { if (!_file.exists()) return Collections.emptyMap(); String searchOpt = null; String startsWith = null; if (options != null) { searchOpt = options.getProperty("search"); startsWith = options.getProperty("startsWith"); } BufferedReader in = null; getReadLock(); try { in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024); String line = null; Map<String, String> rv = new HashMap<String, String>(); while ( (line = in.readLine()) != null) { if (line.length() <= 0) continue; if (startsWith != null) { if (startsWith.equals("[0-9]")) { if (line.charAt(0) < '0' || line.charAt(0) > '9') continue; } else if (!line.startsWith(startsWith)) { continue; } } if (line.startsWith("#")) continue; if (line.indexOf('#') > 0) // trim off any end of line comment line = line.substring(0, line.indexOf('#')).trim(); int split = line.indexOf('='); if (split <= 0) continue; String key = line.substring(0, split); if (searchOpt != null && key.indexOf(searchOpt) < 0) continue; String b64 = line.substring(split+1); //.trim() ?????????????? if (b64.length() < 387) continue; rv.put(key, b64); } if (searchOpt == null && startsWith == null) { _lastWrite = _file.lastModified(); _size = rv.size(); } return rv; } catch (IOException ioe) { _log.error("getEntries error", ioe); return Collections.emptyMap(); } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} releaseReadLock(); } } /** * Overridden for efficiency. * Output is not sorted. * * @param options ignored * @since 0.9.20 */ public void export(Writer out, Properties options) throws IOException { out.write("# Address book: "); out.write(getName()); final String nl = System.getProperty("line.separator", "\n"); out.write(nl); out.write("# Exported: "); out.write((new Date()).toString()); out.write(nl); BufferedReader in = null; getReadLock(); try { in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024); String line = null; while ( (line = in.readLine()) != null) { out.write(line); out.write(nl); } } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} releaseReadLock(); } } /** * @param options ignored * @return all known host names, unsorted */ public Set<String> getNames(Properties options) { if (!_file.exists()) return Collections.emptySet(); BufferedReader in = null; getReadLock(); try { in = new BufferedReader(new InputStreamReader(new FileInputStream(_file), "UTF-8"), 16*1024); String line = null; Set<String> rv = new HashSet<String>(); while ( (line = in.readLine()) != null) { if (line.length() <= 0) continue; if (line.startsWith("#")) continue; int split = line.indexOf('='); if (split <= 0) continue; String key = line.substring(0, split); rv.add(key); } return rv; } catch (IOException ioe) { _log.error("getNames error", ioe); return Collections.emptySet(); } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} releaseReadLock(); } } /** * @param options ignored */ @Override public int size(Properties options) { if (!_file.exists()) return 0; 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("#") || line.length() <= 0) continue; rv++; } _lastWrite = _file.lastModified(); _size = rv; return rv; } catch (IOException ioe) { _log.error("size() error", ioe); return -1; } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} releaseReadLock(); } } public void shutdown() { if (!getWriteLock()) return; try { _isClosed = true; } finally { releaseWriteLock(); } } private void getReadLock() { _fileLock.readLock().lock(); } private void releaseReadLock() { _fileLock.readLock().unlock(); } /** @return true if the lock was acquired */ private boolean getWriteLock() { try { boolean rv = _fileLock.writeLock().tryLock(10000, TimeUnit.MILLISECONDS); if ((!rv) && _log.shouldLog(Log.WARN)) _log.warn("no lock, size is: " + _fileLock.getQueueLength(), new Exception("rats")); return rv; } catch (InterruptedException ie) {} return false; } private void releaseWriteLock() { _fileLock.writeLock().unlock(); } /**** public static void main(String[] args) { NamingService ns = new SingleFileNamingService(I2PAppContext.getGlobalContext(), "hosts.txt"); Destination d = new Destination(); try { d.readBytes(new byte[387], 0); } catch (DataFormatException dfe) {} boolean b = ns.put("aaaaa", d); System.out.println("Test 1 pass? " + b); b = ns.put("bbbbb", d); System.out.println("Test 2 pass? " + b); b = ns.remove("aaaaa"); System.out.println("Test 3 pass? " + b); b = ns.lookup("aaaaa") == null; System.out.println("Test 4 pass? " + b); b = ns.lookup("bbbbb") != null; System.out.println("Test 5 pass? " + b); b = !ns.putIfAbsent("bbbbb", d); System.out.println("Test 6 pass? " + b); } ****/ }