diff --git a/apps/BOB/src/net/i2p/BOB/DoCMDS.java b/apps/BOB/src/net/i2p/BOB/DoCMDS.java index b36360339..9894530d8 100644 --- a/apps/BOB/src/net/i2p/BOB/DoCMDS.java +++ b/apps/BOB/src/net/i2p/BOB/DoCMDS.java @@ -34,7 +34,9 @@ import java.util.StringTokenizer; import java.util.concurrent.atomic.AtomicBoolean; import net.i2p.I2PException; import net.i2p.client.I2PClientFactory; +import net.i2p.data.DataFormatException; import net.i2p.data.Destination; +import net.i2p.i2ptunnel.I2PTunnel; import net.i2p.util.Log; // needed only for debugging. // import java.util.logging.Level; @@ -50,7 +52,7 @@ public class DoCMDS implements Runnable { // FIX ME // I need a better way to do versioning, but this will do for now. - public static final String BMAJ = "00", BMIN = "00", BREV = "0D", BEXT = ""; + public static final String BMAJ = "00", BMIN = "00", BREV = "0E", BEXT = ""; public static final String BOBversion = BMAJ + "." + BMIN + "." + BREV + BEXT; private Socket server; private Properties props; @@ -86,6 +88,7 @@ public class DoCMDS implements Runnable { private static final String C_inhost = "inhost"; private static final String C_inport = "inport"; private static final String C_list = "list"; + private static final String C_lookup = "lookup"; private static final String C_newkeys = "newkeys"; private static final String C_option = "option"; private static final String C_outhost = "outhost"; @@ -113,6 +116,7 @@ public class DoCMDS implements Runnable { {C_inhost, C_inhost + " hostname | IP * Set the inbound hostname or IP."}, {C_inport, C_inport + " port_number * Set the inbound port number nickname listens on."}, {C_list, C_list + " * List all tunnels."}, + {C_lookup, C_lookup + " * Lookup an i2p address."}, {C_newkeys, C_newkeys + " * Generate a new keypair for the current nickname."}, {C_option, C_option + " I2CPoption=something * Set an I2CP option. NOTE: Don't use any spaces."}, {C_outhost, C_outhost + " hostname | IP * Set the outbound hostname or IP."}, @@ -138,6 +142,7 @@ public class DoCMDS implements Runnable { C_inhost + " " + C_inport + " " + C_list + " " + + C_lookup + " " + C_newkeys + " " + C_option + " " + C_outhost + " " + @@ -446,6 +451,25 @@ public class DoCMDS implements Runnable { } else if (Command.equals(C_visit)) { visitAllThreads(); out.println("OK "); + } else if (Command.equals(C_lookup)) { + Destination dest = null; + String reply = null; + if (Arg.endsWith(".i2p")) { + try { + try { + dest = I2PTunnel.destFromName(Arg); + } catch (DataFormatException ex) { + } + reply = dest.toBase64(); + } catch (NullPointerException npe) { + // Could not find the destination!? + } + } + if (reply == null) { + out.println("ERROR Address Not found."); + } else { + out.println("OK " + reply); + } } else if (Command.equals(C_getdest)) { if (ns) { if (dk) { diff --git a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java index be74de990..6c95e2e19 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/ConfigParser.java @@ -25,9 +25,9 @@ import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; -import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; +import java.io.OutputStreamWriter; import java.io.StringReader; import java.util.HashMap; import java.util.Iterator; @@ -35,6 +35,8 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import net.i2p.util.SecureFileOutputStream; + /** * Utility class providing methods to parse and write files in config file * format, and subscription file format. @@ -277,7 +279,7 @@ public class ConfigParser { */ public static void write(Map map, File file) throws IOException { ConfigParser - .write(map, new BufferedWriter(new FileWriter(file, false))); + .write(map, new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8"))); } /** @@ -316,7 +318,7 @@ public class ConfigParser { public static void writeSubscriptions(List list, File file) throws IOException { ConfigParser.writeSubscriptions(list, new BufferedWriter( - new FileWriter(file, false))); + new OutputStreamWriter(new SecureFileOutputStream(file), "UTF-8"))); } } diff --git a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java index 077acdb7f..258801011 100644 --- a/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java +++ b/apps/addressbook/java/src/net/i2p/addressbook/Daemon.java @@ -29,6 +29,7 @@ import java.util.List; import java.util.Map; import net.i2p.I2PAppContext; +import net.i2p.util.SecureDirectory; /** * Main class of addressbook. Performs updates, and runs the main loop. @@ -131,11 +132,11 @@ public class Daemon { String settingsLocation = "config.txt"; File homeFile; if (args.length > 0) { - homeFile = new File(args[0]); + homeFile = new SecureDirectory(args[0]); if (!homeFile.isAbsolute()) - homeFile = new File(I2PAppContext.getGlobalContext().getRouterDir(), args[0]); + homeFile = new SecureDirectory(I2PAppContext.getGlobalContext().getRouterDir(), args[0]); } else { - homeFile = new File(System.getProperty("user.dir")); + homeFile = new SecureDirectory(System.getProperty("user.dir")); } Map defaultSettings = new HashMap(); diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 127a2bb19..6f17a810a 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -26,6 +26,7 @@ import net.i2p.util.ConcurrentHashSet; import net.i2p.util.EepGet; import net.i2p.util.FileUtil; import net.i2p.util.Log; +import net.i2p.util.SecureDirectory; import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; import net.i2p.util.Translate; @@ -77,7 +78,7 @@ public class I2PSnarkUtil { // This is used for both announce replies and .torrent file downloads, // so it must be available even if not connected to I2CP. // so much for multiple instances - _tmpDir = new File(ctx.getTempDir(), "i2psnark"); + _tmpDir = new SecureDirectory(ctx.getTempDir(), "i2psnark"); FileUtil.rmdir(_tmpDir, false); _tmpDir.mkdirs(); } @@ -196,11 +197,14 @@ public class I2PSnarkUtil { /** connect to the given destination */ I2PSocket connect(PeerID peer) throws IOException { - Hash dest = peer.getAddress().calculateHash(); + Destination addr = peer.getAddress(); + if (addr == null) + throw new IOException("Null address"); + Hash dest = addr.calculateHash(); if (_shitlist.contains(dest)) throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are shitlisted"); try { - I2PSocket rv = _manager.connect(peer.getAddress()); + I2PSocket rv = _manager.connect(addr); if (rv != null) _shitlist.remove(dest); return rv; @@ -224,7 +228,8 @@ public class I2PSnarkUtil { public File get(String url, boolean rewrite) { return get(url, rewrite, 0); } public File get(String url, int retries) { return get(url, true, retries); } public File get(String url, boolean rewrite, int retries) { - _log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Fetching [" + url + "] proxy=" + _proxyHost + ":" + _proxyPort + ": " + _shouldProxy); File out = null; try { // we could use the system tmp dir but deleteOnExit() doesn't seem to work on all platforms... @@ -248,10 +253,12 @@ public class I2PSnarkUtil { } EepGet get = new I2PSocketEepGet(_context, _manager, retries, out.getAbsolutePath(), fetchURL); if (get.fetch()) { - _log.debug("Fetch successful [" + url + "]: size=" + out.length()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Fetch successful [" + url + "]: size=" + out.length()); return out; } else { - _log.warn("Fetch failed [" + url + "]"); + if (_log.shouldLog(Log.WARN)) + _log.warn("Fetch failed [" + url + "]"); out.delete(); return null; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java index f8c8feaed..140d3cb46 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java @@ -109,7 +109,8 @@ public class MetaInfo */ public MetaInfo(Map m) throws InvalidBEncodingException { - _log.debug("Creating a metaInfo: " + m, new Exception("source")); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Creating a metaInfo: " + m, new Exception("source")); BEValue val = (BEValue)m.get("announce"); if (val == null) throw new InvalidBEncodingException("Missing announce string"); @@ -446,14 +447,16 @@ public class MetaInfo else buf.append(val.toString()); } - _log.debug(buf.toString()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug(buf.toString()); byte[] infoBytes = BEncoder.bencode(info); //_log.debug("info bencoded: [" + Base64.encode(infoBytes, true) + "]"); try { MessageDigest digest = MessageDigest.getInstance("SHA"); byte hash[] = digest.digest(infoBytes); - _log.debug("info hash: [" + net.i2p.data.Base64.encode(hash) + "]"); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("info hash: [" + net.i2p.data.Base64.encode(hash) + "]"); return hash; } catch(NoSuchAlgorithmException nsa) diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java index e0ae6e1f2..da1003aab 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java @@ -92,7 +92,8 @@ public class Peer implements Comparable byte[] id = handshake(in, out); this.peerID = new PeerID(id, sock.getPeerDestination()); _id = ++__id; - _log.debug("Creating a new peer with " + peerID.getAddress().calculateHash().toBase64(), new Exception("creating " + _id)); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Creating a new peer with " + peerID.getAddress().calculateHash().toBase64(), new Exception("creating " + _id)); } /** @@ -129,7 +130,7 @@ public class Peer implements Comparable @Override public int hashCode() { - return peerID.hashCode() ^ (2 << _id); + return peerID.hashCode() ^ (7777 * (int)_id); } /** @@ -150,6 +151,7 @@ public class Peer implements Comparable /** * Compares the PeerIDs. + * @deprecated unused? */ public int compareTo(Object o) { @@ -181,14 +183,16 @@ public class Peer implements Comparable if (state != null) throw new IllegalStateException("Peer already started"); - _log.debug("Running connection to " + peerID.getAddress().calculateHash().toBase64(), new Exception("connecting")); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Running connection to " + peerID.getAddress().calculateHash().toBase64(), new Exception("connecting")); try { // Do we need to handshake? if (din == null) { sock = util.connect(peerID); - _log.debug("Connected to " + peerID + ": " + sock); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Connected to " + peerID + ": " + sock); if ((sock == null) || (sock.isClosed())) { throw new IOException("Unable to reach " + peerID); } @@ -207,14 +211,20 @@ public class Peer implements Comparable // = new BufferedOutputStream(sock.getOutputStream()); byte [] id = handshake(in, out); //handshake(bis, bos); byte [] expected_id = peerID.getID(); - if (!Arrays.equals(expected_id, id)) - throw new IOException("Unexpected peerID '" + if (expected_id == null) { + peerID.setID(id); + } else if (Arrays.equals(expected_id, id)) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Handshake got matching IDs with " + toString()); + } else { + throw new IOException("Unexpected peerID '" + PeerID.idencode(id) + "' expected '" + PeerID.idencode(expected_id) + "'"); - _log.debug("Handshake got matching IDs with " + toString()); + } } else { - _log.debug("Already have din [" + sock + "] with " + toString()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Already have din [" + sock + "] with " + toString()); } PeerConnectionIn in = new PeerConnectionIn(this, din); @@ -229,7 +239,8 @@ public class Peer implements Comparable state = s; listener.connected(this); - _log.debug("Start running the reader with " + toString()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Start running the reader with " + toString()); // Use this thread for running the incomming connection. // The outgoing connection creates its own Thread. out.startup(); @@ -279,7 +290,8 @@ public class Peer implements Comparable dout.write(my_id); dout.flush(); - _log.debug("Wrote my shared hash and ID to " + toString()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Wrote my shared hash and ID to " + toString()); // Handshake read - header byte b = din.readByte(); @@ -306,7 +318,8 @@ public class Peer implements Comparable // Handshake read - peer id din.readFully(bs); - _log.debug("Read the remote side's hash and peerID fully from " + toString()); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Read the remote side's hash and peerID fully from " + toString()); return bs; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java index c86938437..58ef3ae2d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java @@ -76,10 +76,12 @@ public class PeerAcceptor // is this working right? try { peerInfoHash = readHash(in); - _log.info("infohash read from " + socket.getPeerDestination().calculateHash().toBase64() - + ": " + Base64.encode(peerInfoHash)); + if (_log.shouldLog(Log.INFO)) + _log.info("infohash read from " + socket.getPeerDestination().calculateHash().toBase64() + + ": " + Base64.encode(peerInfoHash)); } catch (IOException ioe) { - _log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64()); + if (_log.shouldLog(Log.INFO)) + _log.info("Unable to read the infohash from " + socket.getPeerDestination().calculateHash().toBase64()); throw ioe; } in = new SequenceInputStream(new ByteArrayInputStream(peerInfoHash), in); diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java index 6d3895036..8f24c864a 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java @@ -375,7 +375,8 @@ public class PeerCoordinator implements PeerListener if (need_more) { - _log.debug("Adding a peer " + peer.getPeerID().getAddress().calculateHash().toBase64() + " for " + metainfo.getName(), new Exception("add/run")); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + metainfo.getName(), new Exception("add/run")); // Run the peer with us as listener and the current bitfield. final PeerListener listener = this; diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerID.java b/apps/i2psnark/java/src/org/klomp/snark/PeerID.java index c4a4e9194..37cf1a9b6 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerID.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerID.java @@ -24,19 +24,33 @@ import java.io.IOException; import java.net.UnknownHostException; import java.util.Map; +import net.i2p.I2PAppContext; +import net.i2p.data.Base32; import net.i2p.data.Base64; +import net.i2p.data.DataHelper; import net.i2p.data.Destination; import org.klomp.snark.bencode.BDecoder; import org.klomp.snark.bencode.BEValue; import org.klomp.snark.bencode.InvalidBEncodingException; +/** + * Store the address information about a peer. + * Prior to 0.8.1, an instantiation required a peer ID, and full Destination address. + * Starting with 0.8.1, to support compact tracker responses, + * a PeerID can be instantiated with a Destination Hash alone. + * The full destination lookup is deferred until getAddress() is called, + * and the PeerID is not required. + * Equality is now determined solely by the dest hash. + */ public class PeerID implements Comparable { - private final byte[] id; - private final Destination address; + private byte[] id; + private Destination address; private final int port; - + private byte[] destHash; + /** whether we have tried to get the dest from the hash - only do once */ + private boolean triedDestLookup; private final int hash; public PeerID(byte[] id, Destination address) @@ -44,7 +58,7 @@ public class PeerID implements Comparable this.id = id; this.address = address; this.port = 6881; - + this.destHash = address.calculateHash().getData(); hash = calculateHash(); } @@ -77,17 +91,49 @@ public class PeerID implements Comparable throw new InvalidBEncodingException("Invalid destination [" + bevalue.getString() + "]"); port = 6881; - + this.destHash = address.calculateHash().getData(); hash = calculateHash(); } + /** + * Creates a PeerID from a destHash + * @since 0.8.1 + */ + public PeerID(byte[] dest_hash) throws InvalidBEncodingException + { + // id and address remain null + port = 6881; + if (dest_hash.length != 32) + throw new InvalidBEncodingException("bad hash length"); + destHash = dest_hash; + hash = DataHelper.hashCode(dest_hash); + } + public byte[] getID() { return id; } - public Destination getAddress() + /** for connecting out to peer based on desthash @since 0.8.1 */ + public void setID(byte[] xid) { + id = xid; + } + + /** + * Get the destination. + * If this PeerId was instantiated with a destHash, + * and we have not yet done so, lookup the full destination, which may take + * up to 10 seconds. + * @return Dest or null if unknown + */ + public synchronized Destination getAddress() + { + if (address == null && destHash != null && !triedDestLookup) { + String b32 = Base32.encode(destHash) + ".b32.i2p"; + address = I2PAppContext.getGlobalContext().namingService().lookup(b32); + triedDestLookup = true; + } return address; } @@ -96,16 +142,19 @@ public class PeerID implements Comparable return port; } + /** @since 0.8.1 */ + public byte[] getDestHash() + { + return destHash; + } + private int calculateHash() { - int b = 0; - for (int i = 0; i < id.length; i++) - b ^= id[i]; - return (b ^ address.hashCode()) ^ port; + return DataHelper.hashCode(destHash); } /** - * The hash code of a PeerID is the exclusive or of all id bytes. + * The hash code of a PeerID is the hashcode of the desthash */ @Override public int hashCode() @@ -115,18 +164,15 @@ public class PeerID implements Comparable /** * Returns true if and only if this peerID and the given peerID have - * the same 20 bytes as ID. + * the same destination hash */ public boolean sameID(PeerID pid) { - boolean equal = true; - for (int i = 0; equal && i < id.length; i++) - equal = id[i] == pid.id[i]; - return equal; + return DataHelper.eq(destHash, pid.getDestHash()); } /** - * Two PeerIDs are equal when they have the same id, address and port. + * Two PeerIDs are equal when they have the same dest hash */ @Override public boolean equals(Object o) @@ -135,9 +181,7 @@ public class PeerID implements Comparable { PeerID pid = (PeerID)o; - return port == pid.port - && address.equals(pid.address) - && sameID(pid); + return sameID(pid); } else return false; @@ -145,6 +189,7 @@ public class PeerID implements Comparable /** * Compares port, address and id. + * @deprecated unused? and will NPE now that address can be null? */ public int compareTo(Object o) { @@ -176,6 +221,8 @@ public class PeerID implements Comparable @Override public String toString() { + if (id == null || address == null) + return "unkn@" + Base64.encode(destHash).substring(0, 6); int nonZero = 0; for (int i = 0; i < id.length; i++) { if (id[i] != 0) { diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index c2afec346..9d6cd9460 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -437,7 +437,7 @@ public class Snark try { storage.close(); } catch (IOException ioee) { ioee.printStackTrace(); } - fatal("Could not create storage", ioe); + fatal("Could not check or create storage", ioe); } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 59ab86a12..b0fe6d1ba 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -1,857 +1,880 @@ -package org.klomp.snark; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FilenameFilter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -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.data.Base64; -import net.i2p.data.DataHelper; -import net.i2p.util.I2PAppThread; -import net.i2p.util.Log; -import net.i2p.util.OrderedProperties; - -/** - * Manage multiple snarks - */ -public class SnarkManager implements Snark.CompleteListener { - private static SnarkManager _instance = new SnarkManager(); - public static SnarkManager instance() { return _instance; } - - /** map of (canonical) filename of the .torrent file to Snark instance (unsynchronized) */ - private final Map _snarks; - private final Object _addSnarkLock; - private /* FIXME final FIXME */ File _configFile; - private Properties _config; - private I2PAppContext _context; - private Log _log; - private final List _messages; - private I2PSnarkUtil _util; - private PeerCoordinatorSet _peerCoordinatorSet; - private ConnectionAcceptor _connectionAcceptor; - private Thread _monitor; - private boolean _running; - - public static final String PROP_I2CP_HOST = "i2psnark.i2cpHost"; - public static final String PROP_I2CP_PORT = "i2psnark.i2cpPort"; - public static final String PROP_I2CP_OPTS = "i2psnark.i2cpOptions"; - //public static final String PROP_EEP_HOST = "i2psnark.eepHost"; - //public static final String PROP_EEP_PORT = "i2psnark.eepPort"; - public static final String PROP_UPLOADERS_TOTAL = "i2psnark.uploaders.total"; - public static final String PROP_UPBW_MAX = "i2psnark.upbw.max"; - public static final String PROP_DIR = "i2psnark.dir"; - public static final String PROP_META_PREFIX = "i2psnark.zmeta."; - public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield"; - - private static final String CONFIG_FILE = "i2psnark.config"; - public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops - public static final String DEFAULT_AUTO_START = "false"; - public static final String PROP_LINK_PREFIX = "i2psnark.linkPrefix"; - public static final String DEFAULT_LINK_PREFIX = "file:///"; - public static final String PROP_STARTUP_DELAY = "i2psnark.startupDelay"; - - public static final int MIN_UP_BW = 2; - public static final int DEFAULT_MAX_UP_BW = 10; - public static final int DEFAULT_STARTUP_DELAY = 3; - private SnarkManager() { - _snarks = new HashMap(); - _addSnarkLock = new Object(); - _context = I2PAppContext.getGlobalContext(); - _log = _context.logManager().getLog(SnarkManager.class); - _messages = new ArrayList(16); - _util = new I2PSnarkUtil(_context); - _configFile = new File(CONFIG_FILE); - if (!_configFile.isAbsolute()) - _configFile = new File(_context.getConfigDir(), CONFIG_FILE); - loadConfig(null); - } - - /** Caller _must_ call loadConfig(file) before this if setting new values - * for i2cp host/port or i2psnark.dir - */ - public void start() { - _running = true; - _peerCoordinatorSet = new PeerCoordinatorSet(); - _connectionAcceptor = new ConnectionAcceptor(_util); - int minutes = getStartupDelayMinutes(); - _messages.add(_("Adding torrents in {0} minutes", minutes)); - _monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true); - _monitor.start(); - _context.addShutdownTask(new SnarkManagerShutdown()); - } - - public void stop() { - _running = false; - _monitor.interrupt(); - _connectionAcceptor.halt(); - (new SnarkManagerShutdown()).run(); - } - - /** hook to I2PSnarkUtil for the servlet */ - public I2PSnarkUtil util() { return _util; } - - private static final int MAX_MESSAGES = 5; - public void addMessage(String message) { - synchronized (_messages) { - _messages.add(message); - while (_messages.size() > MAX_MESSAGES) - _messages.remove(0); - } - if (_log.shouldLog(Log.INFO)) - _log.info("MSG: " + message); - } - - /** newest last */ - public List getMessages() { - synchronized (_messages) { - return new ArrayList(_messages); - } - } - - public boolean shouldAutoStart() { - return Boolean.valueOf(_config.getProperty(PROP_AUTO_START, DEFAULT_AUTO_START+"")).booleanValue(); - } - public String linkPrefix() { - return _config.getProperty(PROP_LINK_PREFIX, DEFAULT_LINK_PREFIX + getDataDir().getAbsolutePath() + File.separatorChar); - } - private int getStartupDelayMinutes() { - return Integer.valueOf(_config.getProperty(PROP_STARTUP_DELAY)).intValue(); - } - public File getDataDir() { - String dir = _config.getProperty(PROP_DIR, "i2psnark"); - File f = new File(dir); - if (!f.isAbsolute()) - f = new File(_context.getAppDir(), dir); - return f; - } - - /** null to set initial defaults */ - public void loadConfig(String filename) { - if (_config == null) - _config = new OrderedProperties(); - if (filename != null) { - File cfg = new File(filename); - if (!cfg.isAbsolute()) - cfg = new File(_context.getConfigDir(), filename); - _configFile = cfg; - if (cfg.exists()) { - try { - DataHelper.loadProps(_config, cfg); - } catch (IOException ioe) { - _log.error("Error loading I2PSnark config '" + filename + "'", ioe); - } - } - } - // now add sane defaults - if (!_config.containsKey(PROP_I2CP_HOST)) - _config.setProperty(PROP_I2CP_HOST, "127.0.0.1"); - if (!_config.containsKey(PROP_I2CP_PORT)) - _config.setProperty(PROP_I2CP_PORT, "7654"); - if (!_config.containsKey(PROP_I2CP_OPTS)) - _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0 inbound.quantity=3 outbound.quantity=3"); - //if (!_config.containsKey(PROP_EEP_HOST)) - // _config.setProperty(PROP_EEP_HOST, "127.0.0.1"); - //if (!_config.containsKey(PROP_EEP_PORT)) - // _config.setProperty(PROP_EEP_PORT, "4444"); - if (!_config.containsKey(PROP_UPLOADERS_TOTAL)) - _config.setProperty(PROP_UPLOADERS_TOTAL, "" + Snark.MAX_TOTAL_UPLOADERS); - if (!_config.containsKey(PROP_DIR)) - _config.setProperty(PROP_DIR, "i2psnark"); - if (!_config.containsKey(PROP_AUTO_START)) - _config.setProperty(PROP_AUTO_START, DEFAULT_AUTO_START); - if (!_config.containsKey(PROP_STARTUP_DELAY)) - _config.setProperty(PROP_STARTUP_DELAY, "" + DEFAULT_STARTUP_DELAY); - - updateConfig(); - } - - /** call from DirMonitor since loadConfig() is called before router I2CP is up */ - private void getBWLimit() { - if (!_config.containsKey(PROP_UPBW_MAX)) { - int[] limits = BWLimits.getBWLimits(_util.getI2CPHost(), _util.getI2CPPort()); - if (limits != null && limits[1] > 0) - _util.setMaxUpBW(limits[1]); - } - } - - private void updateConfig() { - String i2cpHost = _config.getProperty(PROP_I2CP_HOST); - int i2cpPort = getInt(PROP_I2CP_PORT, 7654); - String opts = _config.getProperty(PROP_I2CP_OPTS); - Map i2cpOpts = new HashMap(); - if (opts != null) { - StringTokenizer tok = new StringTokenizer(opts, " "); - while (tok.hasMoreTokens()) { - String pair = tok.nextToken(); - int split = pair.indexOf('='); - if (split > 0) - i2cpOpts.put(pair.substring(0, split), pair.substring(split+1)); - } - } - if (i2cpHost != null) { - _util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts); - _log.debug("Configuring with I2CP options " + i2cpOpts); - } - //I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties()); - //String eepHost = _config.getProperty(PROP_EEP_HOST); - //int eepPort = getInt(PROP_EEP_PORT, 4444); - //if (eepHost != null) - // _util.setProxy(eepHost, eepPort); - _util.setMaxUploaders(getInt(PROP_UPLOADERS_TOTAL, Snark.MAX_TOTAL_UPLOADERS)); - _util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW)); - _util.setStartupDelay(getInt(PROP_STARTUP_DELAY, DEFAULT_STARTUP_DELAY)); - String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS); - if (ot != null) - _util.setOpenTrackerString(ot); - // FIXME set util use open trackers property somehow - getDataDir().mkdirs(); - } - - private int getInt(String prop, int defaultVal) { - String p = _config.getProperty(prop); - try { - if ( (p != null) && (p.trim().length() > 0) ) - return Integer.parseInt(p.trim()); - } catch (NumberFormatException nfe) { - // ignore - } - return defaultVal; - } - - public void updateConfig(String dataDir, boolean autoStart, String startDelay, String seedPct, String eepHost, - String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts, - String upLimit, String upBW, boolean useOpenTrackers, String openTrackers) { - boolean changed = false; - //if (eepHost != null) { - // // unused, we use socket eepget - // int port = _util.getEepProxyPort(); - // try { port = Integer.parseInt(eepPort); } catch (NumberFormatException nfe) {} - // String host = _util.getEepProxyHost(); - // if ( (eepHost.trim().length() > 0) && (port > 0) && - // ((!host.equals(eepHost) || (port != _util.getEepProxyPort()) )) ) { - // _util.setProxy(eepHost, port); - // changed = true; - // _config.setProperty(PROP_EEP_HOST, eepHost); - // _config.setProperty(PROP_EEP_PORT, eepPort+""); - // addMessage("EepProxy location changed to " + eepHost + ":" + port); - // } - //} - if (upLimit != null) { - int limit = _util.getMaxUploaders(); - try { limit = Integer.parseInt(upLimit); } catch (NumberFormatException nfe) {} - if ( limit != _util.getMaxUploaders()) { - if ( limit >= Snark.MIN_TOTAL_UPLOADERS ) { - _util.setMaxUploaders(limit); - changed = true; - _config.setProperty(PROP_UPLOADERS_TOTAL, "" + limit); - addMessage(_("Total uploaders limit changed to {0}", limit)); - } else { - addMessage(_("Minimum total uploaders limit is {0}", Snark.MIN_TOTAL_UPLOADERS)); - } - } - } - if (upBW != null) { - int limit = _util.getMaxUpBW(); - try { limit = Integer.parseInt(upBW); } catch (NumberFormatException nfe) {} - if ( limit != _util.getMaxUpBW()) { - if ( limit >= MIN_UP_BW ) { - _util.setMaxUpBW(limit); - changed = true; - _config.setProperty(PROP_UPBW_MAX, "" + limit); - addMessage(_("Up BW limit changed to {0}KBps", limit)); - } else { - addMessage(_("Minimum up bandwidth limit is {0}KBps", MIN_UP_BW)); - } - } - } - - if (startDelay != null){ - int minutes = _util.getStartupDelay(); - try { minutes = Integer.parseInt(startDelay); } catch (NumberFormatException nfe) {} - if ( minutes != _util.getStartupDelay()) { - _util.setStartupDelay(minutes); - changed = true; - _config.setProperty(PROP_STARTUP_DELAY, "" + minutes); - addMessage(_("Startup delay limit changed to {0} minutes", minutes)); - } - - } - if (i2cpHost != null) { - int oldI2CPPort = _util.getI2CPPort(); - String oldI2CPHost = _util.getI2CPHost(); - int port = oldI2CPPort; - try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {} - String host = oldI2CPHost; - Map opts = new HashMap(); - if (i2cpOpts == null) i2cpOpts = ""; - StringTokenizer tok = new StringTokenizer(i2cpOpts, " \t\n"); - while (tok.hasMoreTokens()) { - String pair = tok.nextToken(); - int split = pair.indexOf('='); - if (split > 0) - opts.put(pair.substring(0, split), pair.substring(split+1)); - } - Map oldOpts = new HashMap(); - String oldI2CPOpts = _config.getProperty(PROP_I2CP_OPTS); - if (oldI2CPOpts == null) oldI2CPOpts = ""; - tok = new StringTokenizer(oldI2CPOpts, " \t\n"); - while (tok.hasMoreTokens()) { - String pair = tok.nextToken(); - int split = pair.indexOf('='); - if (split > 0) - oldOpts.put(pair.substring(0, split), pair.substring(split+1)); - } - - if ( (i2cpHost.trim().length() > 0) && (port > 0) && - ((!host.equals(i2cpHost) || - (port != _util.getI2CPPort()) || - (!oldOpts.equals(opts)))) ) { - boolean snarksActive = false; - Set names = listTorrentFiles(); - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - Snark snark = getTorrent((String)iter.next()); - if ( (snark != null) && (!snark.stopped) ) { - snarksActive = true; - break; - } - } - if (snarksActive) { - Properties p = new Properties(); - p.putAll(opts); - _util.setI2CPConfig(i2cpHost, port, p); - addMessage(_("I2CP and tunnel changes will take effect after stopping all torrents")); - _log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts - + "] oldOpts [" + oldOpts + "]"); - } else { - if (_util.connected()) { - _util.disconnect(); - addMessage(_("Disconnecting old I2CP destination")); - } - Properties p = new Properties(); - p.putAll(opts); - addMessage(_("I2CP settings changed to {0}", i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")")); - _util.setI2CPConfig(i2cpHost, port, p); - boolean ok = _util.connect(); - if (!ok) { - addMessage(_("Unable to connect with the new settings, reverting to the old I2CP settings")); - _util.setI2CPConfig(oldI2CPHost, oldI2CPPort, oldOpts); - ok = _util.connect(); - if (!ok) - addMessage(_("Unable to reconnect with the old settings!")); - } else { - addMessage(_("Reconnected on the new I2CP destination")); - _config.setProperty(PROP_I2CP_HOST, i2cpHost.trim()); - _config.setProperty(PROP_I2CP_PORT, "" + port); - _config.setProperty(PROP_I2CP_OPTS, i2cpOpts.trim()); - changed = true; - // no PeerAcceptors/I2PServerSockets to deal with, since all snarks are inactive - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - Snark snark = getTorrent(name); - if ( (snark != null) && (snark.acceptor != null) ) { - snark.acceptor.restart(); - addMessage(_("I2CP listener restarted for \"{0}\"", snark.meta.getName())); - } - } - } - } - changed = true; - } - } - if (shouldAutoStart() != autoStart) { - _config.setProperty(PROP_AUTO_START, autoStart + ""); - if (autoStart) - addMessage(_("Enabled autostart")); - else - addMessage(_("Disabled autostart")); - changed = true; - } - if (_util.shouldUseOpenTrackers() != useOpenTrackers) { - _config.setProperty(I2PSnarkUtil.PROP_USE_OPENTRACKERS, useOpenTrackers + ""); - if (useOpenTrackers) - addMessage(_("Enabled open trackers - torrent restart required to take effect.")); - else - addMessage(_("Disabled open trackers - torrent restart required to take effect.")); - changed = true; - } - if (openTrackers != null) { - if (openTrackers.trim().length() > 0 && !openTrackers.trim().equals(_util.getOpenTrackerString())) { - _config.setProperty(I2PSnarkUtil.PROP_OPENTRACKERS, openTrackers.trim()); - _util.setOpenTrackerString(openTrackers); - addMessage(_("Open Tracker list changed - torrent restart required to take effect.")); - changed = true; - } - } - if (changed) { - saveConfig(); - } else { - addMessage(_("Configuration unchanged.")); - } - } - - public void saveConfig() { - try { - synchronized (_configFile) { - DataHelper.storeProps(_config, _configFile); - } - } catch (IOException ioe) { - addMessage(_("Unable to save the config to {0}", _configFile.getAbsolutePath())); - } - } - - public Properties getConfig() { return _config; } - - /** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */ - private static final int MAX_FILES_PER_TORRENT = 512; - - /** set of canonical .torrent filenames that we are dealing with */ - public Set listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } } - - /** - * Grab the torrent given the (canonical) filename of the .torrent file - * @return Snark or null - */ - public Snark getTorrent(String filename) { synchronized (_snarks) { return (Snark)_snarks.get(filename); } } - - /** - * Grab the torrent given the base name of the storage - * @return Snark or null - * @since 0.7.14 - */ - public Snark getTorrentByBaseName(String filename) { - synchronized (_snarks) { - for (Snark s : _snarks.values()) { - if (s.storage.getBaseName().equals(filename)) - return s; - } - } - return null; - } - - public void addTorrent(String filename) { addTorrent(filename, false); } - public void addTorrent(String filename, boolean dontAutoStart) { - if ((!dontAutoStart) && !_util.connected()) { - addMessage(_("Connecting to I2P")); - boolean ok = _util.connect(); - if (!ok) { - addMessage(_("Error connecting to I2P - check your I2CP settings!")); - return; - } - } - File sfile = new File(filename); - try { - filename = sfile.getCanonicalPath(); - } catch (IOException ioe) { - _log.error("Unable to add the torrent " + filename, ioe); - addMessage(_("Error: Could not add the torrent {0}", filename) + ": " + ioe.getMessage()); - return; - } - File dataDir = getDataDir(); - Snark torrent = null; - synchronized (_snarks) { - torrent = (Snark)_snarks.get(filename); - } - // don't hold the _snarks lock while verifying the torrent - if (torrent == null) { - synchronized (_addSnarkLock) { - // double-check - synchronized (_snarks) { - if(_snarks.get(filename) != null) - return; - } - - FileInputStream fis = null; - try { - fis = new FileInputStream(sfile); - } catch (IOException ioe) { - // catch this here so we don't try do delete it below - addMessage(_("Cannot open \"{0}\"", sfile.getName()) + ": " + ioe.getMessage()); - return; - } - - try { - MetaInfo info = new MetaInfo(fis); - try { - fis.close(); - fis = null; - } catch (IOException e) {} - - if (!TrackerClient.isValidAnnounce(info.getAnnounce())) { - if (_util.shouldUseOpenTrackers() && _util.getOpenTrackers() != null) { - addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", will announce to i2p open trackers only", info.getName())); - } else { - addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", and open trackers are disabled, you must enable open trackers before starting the torrent!", info.getName())); - dontAutoStart = true; - } - } - String rejectMessage = locked_validateTorrent(info); - if (rejectMessage != null) { - sfile.delete(); - addMessage(rejectMessage); - return; - } else { - torrent = new Snark(_util, filename, null, -1, null, null, this, - _peerCoordinatorSet, _connectionAcceptor, - false, dataDir.getPath()); - torrent.completeListener = this; - synchronized (_snarks) { - _snarks.put(filename, torrent); - } - } - } catch (IOException ioe) { - addMessage(_("Torrent in \"{0}\" is invalid", sfile.getName()) + ": " + ioe.getMessage()); - if (sfile.exists()) - sfile.delete(); - return; - } finally { - if (fis != null) try { fis.close(); } catch (IOException ioe) {} - } - } - } else { - return; - } - // ok, snark created, now lets start it up or configure it further - File f = new File(filename); - if (!dontAutoStart && shouldAutoStart()) { - torrent.startTorrent(); - addMessage(_("Torrent added and started: \"{0}\"", f.getName())); - } else { - addMessage(_("Torrent added: \"{0}\"", f.getName())); - } - } - - /** - * Get the timestamp for a torrent from the config file - */ - public long getSavedTorrentTime(Snark snark) { - MetaInfo metainfo = snark.meta; - byte[] ih = metainfo.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); - if (time == null) - return 0; - int comma = time.indexOf(','); - if (comma <= 0) - return 0; - time = time.substring(0, comma); - try { return Long.parseLong(time); } catch (NumberFormatException nfe) {} - return 0; - } - - /** - * Get the saved bitfield for a torrent from the config file. - * Convert "." to a full bitfield. - */ - public BitField getSavedTorrentBitField(Snark snark) { - MetaInfo metainfo = snark.meta; - byte[] ih = metainfo.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); - if (bf == null) - return null; - int comma = bf.indexOf(','); - if (comma <= 0) - return null; - bf = bf.substring(comma + 1).trim(); - int len = metainfo.getPieces(); - if (bf.equals(".")) { - BitField bitfield = new BitField(len); - for (int i = 0; i < len; i++) - bitfield.set(i); - return bitfield; - } - byte[] bitfield = Base64.decode(bf); - if (bitfield == null) - return null; - if (bitfield.length * 8 < len) - return null; - return new BitField(bitfield, len); - } - - /** - * Save the completion status of a torrent and the current time in the config file - * in the form "i2psnark.zmeta.$base64infohash=$time,$base64bitfield". - * The config file property key is appended with the Base64 of the infohash, - * with the '=' changed to '$' since a key can't contain '='. - * The time is a standard long converted to string. - * The status is either a bitfield converted to Base64 or "." for a completed - * torrent to save space in the config file and in memory. - */ - public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield) { - byte[] ih = metainfo.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - String now = "" + System.currentTimeMillis(); - String bfs; - if (bitfield.complete()) { - bfs = "."; - } else { - byte[] bf = bitfield.getFieldBytes(); - bfs = Base64.encode(bf); - } - _config.setProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX, now + "," + bfs); - saveConfig(); - } - - /** - * Remove the status of a torrent from the config file. - * This may help the config file from growing too big. - */ - public void removeTorrentStatus(MetaInfo metainfo) { - byte[] ih = metainfo.getInfoHash(); - String infohash = Base64.encode(ih); - infohash = infohash.replace('=', '$'); - _config.remove(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); - saveConfig(); - } - - /** - * Warning - does not validate announce URL - use TrackerClient.isValidAnnounce() - */ - private String locked_validateTorrent(MetaInfo info) throws IOException { - List files = info.getFiles(); - if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) { - return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size()); - } else if ( (files == null) && (info.getName().endsWith(".torrent")) ) { - return _("Torrent file \"{0}\" cannot end in \".torrent\", deleting it!", info.getName()); - } else if (info.getPieces() <= 0) { - return _("No pieces in \"{0}\", deleting it!", info.getName()); - } else if (info.getPieces() > Storage.MAX_PIECES) { - return _("Too many pieces in \"{0}\", limit is {1}, deleting it!", info.getName(), Storage.MAX_PIECES); - } else if (info.getPieceLength(0) > Storage.MAX_PIECE_SIZE) { - return _("Pieces are too large in \"{0}\" ({1}B), deleting it.", info.getName(), DataHelper.formatSize2(info.getPieceLength(0))) + ' ' + - _("Limit is {0}B", DataHelper.formatSize2(Storage.MAX_PIECE_SIZE)); - } else if (info.getTotalLength() > Storage.MAX_TOTAL_SIZE) { - System.out.println("torrent info: " + info.toString()); - List lengths = info.getLengths(); - if (lengths != null) - for (int i = 0; i < lengths.size(); i++) - System.out.println("File " + i + " is " + lengths.get(i) + " long."); - - return _("Torrents larger than {0}B are not supported yet, deleting \"{1}\"", Storage.MAX_TOTAL_SIZE, info.getName()); - } else { - // ok - return null; - } - } - - /** - * Stop the torrent, leaving it on the list of torrents unless told to remove it - */ - public Snark stopTorrent(String filename, boolean shouldRemove) { - File sfile = new File(filename); - try { - filename = sfile.getCanonicalPath(); - } catch (IOException ioe) { - _log.error("Unable to remove the torrent " + filename, ioe); - addMessage(_("Error: Could not remove the torrent {0}", filename) + ": " + ioe.getMessage()); - return null; - } - int remaining = 0; - Snark torrent = null; - synchronized (_snarks) { - if (shouldRemove) - torrent = (Snark)_snarks.remove(filename); - else - torrent = (Snark)_snarks.get(filename); - remaining = _snarks.size(); - } - if (torrent != null) { - boolean wasStopped = torrent.stopped; - torrent.stopTorrent(); - if (remaining == 0) { - // should we disconnect/reconnect here (taking care to deal with the other thread's - // I2PServerSocket.accept() call properly?) - ////_util. - } - if (!wasStopped) - addMessage(_("Torrent stopped: \"{0}\"", sfile.getName())); - } - return torrent; - } - /** - * Stop the torrent and delete the torrent file itself, but leaving the data - * behind. - */ - public void removeTorrent(String filename) { - Snark torrent = stopTorrent(filename, true); - if (torrent != null) { - File torrentFile = new File(filename); - torrentFile.delete(); - if (torrent.storage != null) - removeTorrentStatus(torrent.storage.getMetaInfo()); - addMessage(_("Torrent removed: \"{0}\"", torrentFile.getName())); - } - } - - private class DirMonitor implements Runnable { - public void run() { - try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {} - // the first message was a "We are starting up in 1m" - synchronized (_messages) { - if (_messages.size() == 1) - _messages.remove(0); - } - - // here because we need to delay until I2CP is up - // although the user will see the default until then - getBWLimit(); - while (true) { - File dir = getDataDir(); - _log.debug("Directory Monitor loop over " + dir.getAbsolutePath()); - try { - monitorTorrents(dir); - } catch (Exception e) { - _log.error("Error in the DirectoryMonitor", e); - } - try { Thread.sleep(60*1000); } catch (InterruptedException ie) {} - } - } - } - - /** two listeners */ - public void torrentComplete(Snark snark) { - File f = new File(snark.torrent); - long len = snark.meta.getTotalLength(); - addMessage(_("Download finished: \"{0}\"", f.getName()) + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')'); - updateStatus(snark); - } - - public void updateStatus(Snark snark) { - saveTorrentStatus(snark.meta, snark.storage.getBitField()); - } - - private void monitorTorrents(File dir) { - String fileNames[] = dir.list(TorrentFilenameFilter.instance()); - List foundNames = new ArrayList(0); - if (fileNames != null) { - for (int i = 0; i < fileNames.length; i++) { - try { - foundNames.add(new File(dir, fileNames[i]).getCanonicalPath()); - } catch (IOException ioe) { - _log.error("Error resolving '" + fileNames[i] + "' in '" + dir, ioe); - } - } - } - - Set existingNames = listTorrentFiles(); - // lets find new ones first... - for (int i = 0; i < foundNames.size(); i++) { - if (existingNames.contains(foundNames.get(i))) { - // already known. noop - } else { - if (shouldAutoStart() && !_util.connect()) - addMessage(_("Unable to connect to I2P!")); - addTorrent((String)foundNames.get(i), !shouldAutoStart()); - } - } - // now lets see which ones have been removed... - for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) { - String name = (String)iter.next(); - if (foundNames.contains(name)) { - // known and still there. noop - } else { - // known, but removed. drop it - stopTorrent(name, true); - } - } - } - - /** translate */ - private String _(String s) { - return _util.getString(s); - } - - /** translate */ - private String _(String s, Object o) { - return _util.getString(s, o); - } - - /** translate */ - private String _(String s, Object o, Object o2) { - return _util.getString(s, o, o2); - } - - /** - * "name", "announceURL=websiteURL" pairs - */ - private static final String DEFAULT_TRACKERS[] = { -// "Postman", "http://YRgrgTLGnbTq2aZOZDJQ~o6Uk5k6TK-OZtx0St9pb0G-5EGYURZioxqYG8AQt~LgyyI~NCj6aYWpPO-150RcEvsfgXLR~CxkkZcVpgt6pns8SRc3Bi-QSAkXpJtloapRGcQfzTtwllokbdC-aMGpeDOjYLd8b5V9Im8wdCHYy7LRFxhEtGb~RL55DA8aYOgEXcTpr6RPPywbV~Qf3q5UK55el6Kex-6VCxreUnPEe4hmTAbqZNR7Fm0hpCiHKGoToRcygafpFqDw5frLXToYiqs9d4liyVB-BcOb0ihORbo0nS3CLmAwZGvdAP8BZ7cIYE3Z9IU9D1G8JCMxWarfKX1pix~6pIA-sp1gKlL1HhYhPMxwyxvuSqx34o3BqU7vdTYwWiLpGM~zU1~j9rHL7x60pVuYaXcFQDR4-QVy26b6Pt6BlAZoFmHhPcAuWfu-SFhjyZYsqzmEmHeYdAwa~HojSbofg0TMUgESRXMw6YThK1KXWeeJVeztGTz25sL8AAAA.i2p/announce.php=http://tracker.postman.i2p/" -// , "eBook", "http://E71FRom6PZNEqTN2Lr8P-sr23b7HJVC32KoGnVQjaX6zJiXwhJy2HsXob36Qmj81TYFZdewFZa9mSJ533UZgGyQkXo2ahctg82JKYZfDe5uDxAn1E9YPjxZCWJaFJh0S~UwSs~9AZ7UcauSJIoNtpxrtbmRNVFLqnkEDdLZi26TeucfOmiFmIWnVblLniWv3tG1boE9Abd-6j3FmYVrRucYuepAILYt6katmVNOk6sXmno1Eynrp~~MBuFq0Ko6~jsc2E2CRVYXDhGHEMdt-j6JUz5D7S2RIVzDRqQyAZLKJ7OdQDmI31przzmne1vOqqqLC~1xUumZVIvF~yOeJUGNjJ1Vx0J8i2BQIusn1pQJ6UCB~ZtZZLQtEb8EPVCfpeRi2ri1M5CyOuxN0V5ekmPHrYIBNevuTCRC26NP7ZS5VDgx1~NaC3A-CzJAE6f1QXi0wMI9aywNG5KGzOPifcsih8eyGyytvgLtrZtV7ykzYpPCS-rDfITncpn5hliPUAAAA.i2p/pub/bt/announce.php=http://de-ebook-archiv.i2p/pub/bt/" -// , "Gaytorrents", "http://uxPWHbK1OIj9HxquaXuhMiIvi21iK0~ZiG9d8G0840ZXIg0r6CbiV71xlsqmdnU6wm0T2LySriM0doW2gUigo-5BNkUquHwOjLROiETnB3ZR0Ml4IGa6QBPn1aAq2d9~g1r1nVjLE~pcFnXB~cNNS7kIhX1d6nLgYVZf0C2cZopEow2iWVUggGGnAA9mHjE86zLEnTvAyhbAMTqDQJhEuLa0ZYSORqzJDMkQt90MV4YMjX1ICY6RfUSFmxEqu0yWTrkHsTtRw48l~dz9wpIgc0a0T9C~eeWvmBFTqlJPtQZwntpNeH~jF7nlYzB58olgV2HHFYpVYD87DYNzTnmNWxCJ5AfDorm6AIUCV2qaE7tZtI1h6fbmGpGlPyW~Kw5GXrRfJwNvr6ajwAVi~bPVnrBwDZezHkfW4slOO8FACPR28EQvaTu9nwhAbqESxV2hCTq6vQSGjuxHeOuzBOEvRWkLKOHWTC09t2DbJ94FSqETmZopTB1ukEmaxRWbKSIaAAAA.i2p/announce.php=http://gaytorrents.i2p/" -// , "NickyB", "http://9On6d3cZ27JjwYCtyJJbowe054d5tFnfMjv4PHsYs-EQn4Y4mk2zRixatvuAyXz2MmRfXG-NAUfhKr0KCxRNZbvHmlckYfT-WBzwwpiMAl0wDFY~Pl8cqXuhfikSG5WrqdPfDNNIBuuznS0dqaczf~OyVaoEOpvuP3qV6wKqbSSLpjOwwAaQPHjlRtNIW8-EtUZp-I0LT45HSoowp~6b7zYmpIyoATvIP~sT0g0MTrczWhbVTUZnEkZeLhOR0Duw1-IRXI2KHPbA24wLO9LdpKKUXed05RTz0QklW5ROgR6TYv7aXFufX8kC0-DaKvQ5JKG~h8lcoHvm1RCzNqVE-2aiZnO2xH08H-iCWoLNJE-Td2kT-Tsc~3QdQcnEUcL5BF-VT~QYRld2--9r0gfGl-yDrJZrlrihHGr5J7ImahelNn9PpkVp6eIyABRmJHf2iicrk3CtjeG1j9OgTSwaNmEpUpn4aN7Kx0zNLdH7z6uTgCGD9Kmh1MFYrsoNlTp4AAAA.i2p/bittorrent/announce.php=http://nickyb.i2p/bittorrent/" -// , "Orion", "http://gKik1lMlRmuroXVGTZ~7v4Vez3L3ZSpddrGZBrxVriosCQf7iHu6CIk8t15BKsj~P0JJpxrofeuxtm7SCUAJEr0AIYSYw8XOmp35UfcRPQWyb1LsxUkMT4WqxAT3s1ClIICWlBu5An~q-Mm0VFlrYLIPBWlUFnfPR7jZ9uP5ZMSzTKSMYUWao3ejiykr~mtEmyls6g-ZbgKZawa9II4zjOy-hdxHgP-eXMDseFsrym4Gpxvy~3Fv9TuiSqhpgm~UeTo5YBfxn6~TahKtE~~sdCiSydqmKBhxAQ7uT9lda7xt96SS09OYMsIWxLeQUWhns-C~FjJPp1D~IuTrUpAFcVEGVL-BRMmdWbfOJEcWPZ~CBCQSO~VkuN1ebvIOr9JBerFMZSxZtFl8JwcrjCIBxeKPBmfh~xYh16BJm1BBBmN1fp2DKmZ2jBNkAmnUbjQOqWvUcehrykWk5lZbE7bjJMDFH48v3SXwRuDBiHZmSbsTY6zhGY~GkMQHNGxPMMSIAAAA.i2p/bt/announce.php=http://orion.i2p/bt/" -// , "anonymity", "http://8EoJZIKrWgGuDrxA3nRJs1jsPfiGwmFWL91hBrf0HA7oKhEvAna4Ocx47VLUR9retVEYBAyWFK-eZTPcvhnz9XffBEiJQQ~kFSCqb1fV6IfPiV3HySqi9U5Caf6~hC46fRd~vYnxmaBLICT3N160cxBETqH3v2rdxdJpvYt8q4nMk9LUeVXq7zqCTFLLG5ig1uKgNzBGe58iNcsvTEYlnbYcE930ABmrzj8G1qQSgSwJ6wx3tUQNl1z~4wSOUMan~raZQD60lRK70GISjoX0-D0Po9WmPveN3ES3g72TIET3zc3WPdK2~lgmKGIs8GgNLES1cXTolvbPhdZK1gxddRMbJl6Y6IPFyQ9o4-6Rt3Lp-RMRWZ2TG7j2OMcNSiOmATUhKEFBDfv-~SODDyopGBmfeLw16F4NnYednvn4qP10dyMHcUASU6Zag4mfc2-WivrOqeWhD16fVAh8MoDpIIT~0r9XmwdaVFyLcjbXObabJczxCAW3fodQUnvuSkwzAAAA.i2p/anonymityTracker/announce.php=http://anonymityweb.i2p/anonymityTracker/" -// , "The freak's tracker", "http://mHKva9x24E5Ygfey2llR1KyQHv5f8hhMpDMwJDg1U-hABpJ2NrQJd6azirdfaR0OKt4jDlmP2o4Qx0H598~AteyD~RJU~xcWYdcOE0dmJ2e9Y8-HY51ie0B1yD9FtIV72ZI-V3TzFDcs6nkdX9b81DwrAwwFzx0EfNvK1GLVWl59Ow85muoRTBA1q8SsZImxdyZ-TApTVlMYIQbdI4iQRwU9OmmtefrCe~ZOf4UBS9-KvNIqUL0XeBSqm0OU1jq-D10Ykg6KfqvuPnBYT1BYHFDQJXW5DdPKwcaQE4MtAdSGmj1epDoaEBUa9btQlFsM2l9Cyn1hzxqNWXELmx8dRlomQLlV4b586dRzW~fLlOPIGC13ntPXogvYvHVyEyptXkv890jC7DZNHyxZd5cyrKC36r9huKvhQAmNABT2Y~pOGwVrb~RpPwT0tBuPZ3lHYhBFYmD8y~AOhhNHKMLzea1rfwTvovBMByDdFps54gMN1mX4MbCGT4w70vIopS9yAAAA.i2p/bytemonsoon/announce.php" -// , "mastertracker", "http://VzXD~stRKbL3MOmeTn1iaCQ0CFyTmuFHiKYyo0Rd~dFPZFCYH-22rT8JD7i-C2xzYFa4jT5U2aqHzHI-Jre4HL3Ri5hFtZrLk2ax3ji7Qfb6qPnuYkuiF2E2UDmKUOppI8d9Ye7tjdhQVCy0izn55tBaB-U7UWdcvSK2i85sauyw3G0Gfads1Rvy5-CAe2paqyYATcDmGjpUNLoxbfv9KH1KmwRTNH6k1v4PyWYYnhbT39WfKMbBjSxVQRdi19cyJrULSWhjxaQfJHeWx5Z8Ev4bSPByBeQBFl2~4vqy0S5RypINsRSa3MZdbiAAyn5tr5slWR6QdoqY3qBQgBJFZppy-3iWkFqqKgSxCPundF8gdDLC5ddizl~KYcYKl42y9SGFHIukH-TZs8~em0~iahzsqWVRks3zRG~tlBcX2U3M2~OJs~C33-NKhyfZT7-XFBREvb8Szmd~p66jDxrwOnKaku-G6DyoQipJqIz4VHmY9-y5T8RrUcJcM-5lVoMpAAAA.i2p/announce.php=http://tracker.mastertracker.i2p/" -// , "Galen", "http://5jpwQMI5FT303YwKa5Rd38PYSX04pbIKgTaKQsWbqoWjIfoancFdWCShXHLI5G5ofOb0Xu11vl2VEMyPsg1jUFYSVnu4-VfMe3y4TKTR6DTpetWrnmEK6m2UXh91J5DZJAKlgmO7UdsFlBkQfR2rY853-DfbJtQIFl91tbsmjcA5CGQi4VxMFyIkBzv-pCsuLQiZqOwWasTlnzey8GcDAPG1LDcvfflGV~6F5no9mnuisZPteZKlrv~~TDoXTj74QjByWc4EOYlwqK8sbU9aOvz~s31XzErbPTfwiawiaZ0RUI-IDrKgyvmj0neuFTWgjRGVTH8bz7cBZIc3viy6ioD-eMQOrXaQL0TCWZUelRwHRvgdPiQrxdYQs7ixkajeHzxi-Pq0EMm5Vbh3j3Q9kfUFW3JjFDA-MLB4g6XnjCbM5J1rC0oOBDCIEfhQkszru5cyLjHiZ5yeA0VThgu~c7xKHybv~OMXION7V8pBKOgET7ZgAkw1xgYe3Kkyq5syAAAA.i2p/tr/announce.php=http://galen.i2p/tr/" - "POSTMAN", "http://tracker2.postman.i2p/announce.php=http://tracker2.postman.i2p/" - ,"WELTERDE", "http://tracker.welterde.i2p/a=http://tracker.welterde.i2p/stats?mode=top5" - , "CRSTRACK", "http://b4G9sCdtfvccMAXh~SaZrPqVQNyGQbhbYMbw6supq2XGzbjU4NcOmjFI0vxQ8w1L05twmkOvg5QERcX6Mi8NQrWnR0stLExu2LucUXg1aYjnggxIR8TIOGygZVIMV3STKH4UQXD--wz0BUrqaLxPhrm2Eh9Hwc8TdB6Na4ShQUq5Xm8D4elzNUVdpM~RtChEyJWuQvoGAHY3ppX-EJJLkiSr1t77neS4Lc-KofMVmgI9a2tSSpNAagBiNI6Ak9L1T0F9uxeDfEG9bBSQPNMOSUbAoEcNxtt7xOW~cNOAyMyGydwPMnrQ5kIYPY8Pd3XudEko970vE0D6gO19yoBMJpKx6Dh50DGgybLQ9CpRaynh2zPULTHxm8rneOGRcQo8D3mE7FQ92m54~SvfjXjD2TwAVGI~ae~n9HDxt8uxOecAAvjjJ3TD4XM63Q9TmB38RmGNzNLDBQMEmJFpqQU8YeuhnS54IVdUoVQFqui5SfDeLXlSkh4vYoMU66pvBfWbAAAA.i2p/tracker/announce.php=http://crstrack.i2p/tracker/" - }; - - /** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */ - public static final String PROP_TRACKERS = "i2psnark.trackers"; - private static Map trackerMap = null; - /** sorted map of name to announceURL=baseURL */ - public Map getTrackers() { - if (trackerMap != null) // only do this once, can't be updated while running - return trackerMap; - Map rv = new TreeMap(); - String trackers = _config.getProperty(PROP_TRACKERS); - if ( (trackers == null) || (trackers.trim().length() <= 0) ) - trackers = _context.getProperty(PROP_TRACKERS); - if ( (trackers == null) || (trackers.trim().length() <= 0) ) { - for (int i = 0; i < DEFAULT_TRACKERS.length; i += 2) - rv.put(DEFAULT_TRACKERS[i], DEFAULT_TRACKERS[i+1]); - } else { - StringTokenizer tok = new StringTokenizer(trackers, ","); - while (tok.hasMoreTokens()) { - String pair = tok.nextToken(); - int split = pair.indexOf('='); - if (split <= 0) - continue; - String name = pair.substring(0, split).trim(); - String url = pair.substring(split+1).trim(); - if ( (name.length() > 0) && (url.length() > 0) ) - rv.put(name, url); - } - } - - trackerMap = rv; - return trackerMap; - } - - private static class TorrentFilenameFilter implements FilenameFilter { - private static final TorrentFilenameFilter _filter = new TorrentFilenameFilter(); - public static TorrentFilenameFilter instance() { return _filter; } - public boolean accept(File dir, String name) { - return (name != null) && (name.endsWith(".torrent")); - } - } - - public class SnarkManagerShutdown extends I2PAppThread { - @Override - public void run() { - Set names = listTorrentFiles(); - for (Iterator iter = names.iterator(); iter.hasNext(); ) { - Snark snark = getTorrent((String)iter.next()); - if ( (snark != null) && (!snark.stopped) ) - snark.stopTorrent(); - } - } - } -} +package org.klomp.snark; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +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.data.Base64; +import net.i2p.data.DataHelper; +import net.i2p.util.I2PAppThread; +import net.i2p.util.Log; +import net.i2p.util.OrderedProperties; +import net.i2p.util.SecureDirectory; + +/** + * Manage multiple snarks + */ +public class SnarkManager implements Snark.CompleteListener { + private static SnarkManager _instance = new SnarkManager(); + public static SnarkManager instance() { return _instance; } + + /** map of (canonical) filename of the .torrent file to Snark instance (unsynchronized) */ + private final Map _snarks; + private final Object _addSnarkLock; + private /* FIXME final FIXME */ File _configFile; + private Properties _config; + private I2PAppContext _context; + private Log _log; + private final List _messages; + private I2PSnarkUtil _util; + private PeerCoordinatorSet _peerCoordinatorSet; + private ConnectionAcceptor _connectionAcceptor; + private Thread _monitor; + private boolean _running; + + public static final String PROP_I2CP_HOST = "i2psnark.i2cpHost"; + public static final String PROP_I2CP_PORT = "i2psnark.i2cpPort"; + public static final String PROP_I2CP_OPTS = "i2psnark.i2cpOptions"; + //public static final String PROP_EEP_HOST = "i2psnark.eepHost"; + //public static final String PROP_EEP_PORT = "i2psnark.eepPort"; + public static final String PROP_UPLOADERS_TOTAL = "i2psnark.uploaders.total"; + public static final String PROP_UPBW_MAX = "i2psnark.upbw.max"; + public static final String PROP_DIR = "i2psnark.dir"; + public static final String PROP_META_PREFIX = "i2psnark.zmeta."; + public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield"; + + private static final String CONFIG_FILE = "i2psnark.config"; + public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops + public static final String DEFAULT_AUTO_START = "false"; + public static final String PROP_LINK_PREFIX = "i2psnark.linkPrefix"; + public static final String DEFAULT_LINK_PREFIX = "file:///"; + public static final String PROP_STARTUP_DELAY = "i2psnark.startupDelay"; + + public static final int MIN_UP_BW = 2; + public static final int DEFAULT_MAX_UP_BW = 10; + public static final int DEFAULT_STARTUP_DELAY = 3; + private SnarkManager() { + _snarks = new HashMap(); + _addSnarkLock = new Object(); + _context = I2PAppContext.getGlobalContext(); + _log = _context.logManager().getLog(SnarkManager.class); + _messages = new ArrayList(16); + _util = new I2PSnarkUtil(_context); + _configFile = new File(CONFIG_FILE); + if (!_configFile.isAbsolute()) + _configFile = new File(_context.getConfigDir(), CONFIG_FILE); + loadConfig(null); + } + + /** Caller _must_ call loadConfig(file) before this if setting new values + * for i2cp host/port or i2psnark.dir + */ + public void start() { + _running = true; + _peerCoordinatorSet = new PeerCoordinatorSet(); + _connectionAcceptor = new ConnectionAcceptor(_util); + int minutes = getStartupDelayMinutes(); + _messages.add(_("Adding torrents in {0} minutes", minutes)); + _monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true); + _monitor.start(); + _context.addShutdownTask(new SnarkManagerShutdown()); + } + + public void stop() { + _running = false; + _monitor.interrupt(); + _connectionAcceptor.halt(); + (new SnarkManagerShutdown()).run(); + } + + /** hook to I2PSnarkUtil for the servlet */ + public I2PSnarkUtil util() { return _util; } + + private static final int MAX_MESSAGES = 5; + public void addMessage(String message) { + synchronized (_messages) { + _messages.add(message); + while (_messages.size() > MAX_MESSAGES) + _messages.remove(0); + } + if (_log.shouldLog(Log.INFO)) + _log.info("MSG: " + message); + } + + /** newest last */ + public List getMessages() { + synchronized (_messages) { + return new ArrayList(_messages); + } + } + + public boolean shouldAutoStart() { + return Boolean.valueOf(_config.getProperty(PROP_AUTO_START, DEFAULT_AUTO_START+"")).booleanValue(); + } + public String linkPrefix() { + return _config.getProperty(PROP_LINK_PREFIX, DEFAULT_LINK_PREFIX + getDataDir().getAbsolutePath() + File.separatorChar); + } + private int getStartupDelayMinutes() { + return Integer.valueOf(_config.getProperty(PROP_STARTUP_DELAY)).intValue(); + } + public File getDataDir() { + String dir = _config.getProperty(PROP_DIR, "i2psnark"); + File f = new SecureDirectory(dir); + if (!f.isAbsolute()) + f = new SecureDirectory(_context.getAppDir(), dir); + return f; + } + + /** null to set initial defaults */ + public void loadConfig(String filename) { + if (_config == null) + _config = new OrderedProperties(); + if (filename != null) { + File cfg = new File(filename); + if (!cfg.isAbsolute()) + cfg = new File(_context.getConfigDir(), filename); + _configFile = cfg; + if (cfg.exists()) { + try { + DataHelper.loadProps(_config, cfg); + } catch (IOException ioe) { + _log.error("Error loading I2PSnark config '" + filename + "'", ioe); + } + } + } + // now add sane defaults + if (!_config.containsKey(PROP_I2CP_HOST)) + _config.setProperty(PROP_I2CP_HOST, "127.0.0.1"); + if (!_config.containsKey(PROP_I2CP_PORT)) + _config.setProperty(PROP_I2CP_PORT, "7654"); + if (!_config.containsKey(PROP_I2CP_OPTS)) + _config.setProperty(PROP_I2CP_OPTS, "inbound.length=2 inbound.lengthVariance=0 outbound.length=2 outbound.lengthVariance=0 inbound.quantity=3 outbound.quantity=3"); + //if (!_config.containsKey(PROP_EEP_HOST)) + // _config.setProperty(PROP_EEP_HOST, "127.0.0.1"); + //if (!_config.containsKey(PROP_EEP_PORT)) + // _config.setProperty(PROP_EEP_PORT, "4444"); + if (!_config.containsKey(PROP_UPLOADERS_TOTAL)) + _config.setProperty(PROP_UPLOADERS_TOTAL, "" + Snark.MAX_TOTAL_UPLOADERS); + if (!_config.containsKey(PROP_DIR)) + _config.setProperty(PROP_DIR, "i2psnark"); + if (!_config.containsKey(PROP_AUTO_START)) + _config.setProperty(PROP_AUTO_START, DEFAULT_AUTO_START); + if (!_config.containsKey(PROP_STARTUP_DELAY)) + _config.setProperty(PROP_STARTUP_DELAY, "" + DEFAULT_STARTUP_DELAY); + + updateConfig(); + } + + /** call from DirMonitor since loadConfig() is called before router I2CP is up */ + private void getBWLimit() { + if (!_config.containsKey(PROP_UPBW_MAX)) { + int[] limits = BWLimits.getBWLimits(_util.getI2CPHost(), _util.getI2CPPort()); + if (limits != null && limits[1] > 0) + _util.setMaxUpBW(limits[1]); + } + } + + private void updateConfig() { + String i2cpHost = _config.getProperty(PROP_I2CP_HOST); + int i2cpPort = getInt(PROP_I2CP_PORT, 7654); + String opts = _config.getProperty(PROP_I2CP_OPTS); + Map i2cpOpts = new HashMap(); + if (opts != null) { + StringTokenizer tok = new StringTokenizer(opts, " "); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split > 0) + i2cpOpts.put(pair.substring(0, split), pair.substring(split+1)); + } + } + if (i2cpHost != null) { + _util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Configuring with I2CP options " + i2cpOpts); + } + //I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties()); + //String eepHost = _config.getProperty(PROP_EEP_HOST); + //int eepPort = getInt(PROP_EEP_PORT, 4444); + //if (eepHost != null) + // _util.setProxy(eepHost, eepPort); + _util.setMaxUploaders(getInt(PROP_UPLOADERS_TOTAL, Snark.MAX_TOTAL_UPLOADERS)); + _util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW)); + _util.setStartupDelay(getInt(PROP_STARTUP_DELAY, DEFAULT_STARTUP_DELAY)); + String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS); + if (ot != null) + _util.setOpenTrackerString(ot); + // FIXME set util use open trackers property somehow + getDataDir().mkdirs(); + } + + private int getInt(String prop, int defaultVal) { + String p = _config.getProperty(prop); + try { + if ( (p != null) && (p.trim().length() > 0) ) + return Integer.parseInt(p.trim()); + } catch (NumberFormatException nfe) { + // ignore + } + return defaultVal; + } + + public void updateConfig(String dataDir, boolean autoStart, String startDelay, String seedPct, String eepHost, + String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts, + String upLimit, String upBW, boolean useOpenTrackers, String openTrackers) { + boolean changed = false; + //if (eepHost != null) { + // // unused, we use socket eepget + // int port = _util.getEepProxyPort(); + // try { port = Integer.parseInt(eepPort); } catch (NumberFormatException nfe) {} + // String host = _util.getEepProxyHost(); + // if ( (eepHost.trim().length() > 0) && (port > 0) && + // ((!host.equals(eepHost) || (port != _util.getEepProxyPort()) )) ) { + // _util.setProxy(eepHost, port); + // changed = true; + // _config.setProperty(PROP_EEP_HOST, eepHost); + // _config.setProperty(PROP_EEP_PORT, eepPort+""); + // addMessage("EepProxy location changed to " + eepHost + ":" + port); + // } + //} + if (upLimit != null) { + int limit = _util.getMaxUploaders(); + try { limit = Integer.parseInt(upLimit); } catch (NumberFormatException nfe) {} + if ( limit != _util.getMaxUploaders()) { + if ( limit >= Snark.MIN_TOTAL_UPLOADERS ) { + _util.setMaxUploaders(limit); + changed = true; + _config.setProperty(PROP_UPLOADERS_TOTAL, "" + limit); + addMessage(_("Total uploaders limit changed to {0}", limit)); + } else { + addMessage(_("Minimum total uploaders limit is {0}", Snark.MIN_TOTAL_UPLOADERS)); + } + } + } + if (upBW != null) { + int limit = _util.getMaxUpBW(); + try { limit = Integer.parseInt(upBW); } catch (NumberFormatException nfe) {} + if ( limit != _util.getMaxUpBW()) { + if ( limit >= MIN_UP_BW ) { + _util.setMaxUpBW(limit); + changed = true; + _config.setProperty(PROP_UPBW_MAX, "" + limit); + addMessage(_("Up BW limit changed to {0}KBps", limit)); + } else { + addMessage(_("Minimum up bandwidth limit is {0}KBps", MIN_UP_BW)); + } + } + } + + if (startDelay != null){ + int minutes = _util.getStartupDelay(); + try { minutes = Integer.parseInt(startDelay); } catch (NumberFormatException nfe) {} + if ( minutes != _util.getStartupDelay()) { + _util.setStartupDelay(minutes); + changed = true; + _config.setProperty(PROP_STARTUP_DELAY, "" + minutes); + addMessage(_("Startup delay limit changed to {0} minutes", minutes)); + } + + } + if (i2cpHost != null) { + int oldI2CPPort = _util.getI2CPPort(); + String oldI2CPHost = _util.getI2CPHost(); + int port = oldI2CPPort; + try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {} + String host = oldI2CPHost; + Map opts = new HashMap(); + if (i2cpOpts == null) i2cpOpts = ""; + StringTokenizer tok = new StringTokenizer(i2cpOpts, " \t\n"); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split > 0) + opts.put(pair.substring(0, split), pair.substring(split+1)); + } + Map oldOpts = new HashMap(); + String oldI2CPOpts = _config.getProperty(PROP_I2CP_OPTS); + if (oldI2CPOpts == null) oldI2CPOpts = ""; + tok = new StringTokenizer(oldI2CPOpts, " \t\n"); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split > 0) + oldOpts.put(pair.substring(0, split), pair.substring(split+1)); + } + + if ( (i2cpHost.trim().length() > 0) && (port > 0) && + ((!host.equals(i2cpHost) || + (port != _util.getI2CPPort()) || + (!oldOpts.equals(opts)))) ) { + boolean snarksActive = false; + Set names = listTorrentFiles(); + for (Iterator iter = names.iterator(); iter.hasNext(); ) { + Snark snark = getTorrent((String)iter.next()); + if ( (snark != null) && (!snark.stopped) ) { + snarksActive = true; + break; + } + } + if (snarksActive) { + Properties p = new Properties(); + p.putAll(opts); + _util.setI2CPConfig(i2cpHost, port, p); + addMessage(_("I2CP and tunnel changes will take effect after stopping all torrents")); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts + + "] oldOpts [" + oldOpts + "]"); + } else { + if (_util.connected()) { + _util.disconnect(); + addMessage(_("Disconnecting old I2CP destination")); + } + Properties p = new Properties(); + p.putAll(opts); + addMessage(_("I2CP settings changed to {0}", i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")")); + _util.setI2CPConfig(i2cpHost, port, p); + boolean ok = _util.connect(); + if (!ok) { + addMessage(_("Unable to connect with the new settings, reverting to the old I2CP settings")); + _util.setI2CPConfig(oldI2CPHost, oldI2CPPort, oldOpts); + ok = _util.connect(); + if (!ok) + addMessage(_("Unable to reconnect with the old settings!")); + } else { + addMessage(_("Reconnected on the new I2CP destination")); + _config.setProperty(PROP_I2CP_HOST, i2cpHost.trim()); + _config.setProperty(PROP_I2CP_PORT, "" + port); + _config.setProperty(PROP_I2CP_OPTS, i2cpOpts.trim()); + changed = true; + // no PeerAcceptors/I2PServerSockets to deal with, since all snarks are inactive + for (Iterator iter = names.iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + Snark snark = getTorrent(name); + if ( (snark != null) && (snark.acceptor != null) ) { + snark.acceptor.restart(); + addMessage(_("I2CP listener restarted for \"{0}\"", snark.meta.getName())); + } + } + } + } + changed = true; + } + } + if (shouldAutoStart() != autoStart) { + _config.setProperty(PROP_AUTO_START, autoStart + ""); + if (autoStart) + addMessage(_("Enabled autostart")); + else + addMessage(_("Disabled autostart")); + changed = true; + } + if (_util.shouldUseOpenTrackers() != useOpenTrackers) { + _config.setProperty(I2PSnarkUtil.PROP_USE_OPENTRACKERS, useOpenTrackers + ""); + if (useOpenTrackers) + addMessage(_("Enabled open trackers - torrent restart required to take effect.")); + else + addMessage(_("Disabled open trackers - torrent restart required to take effect.")); + changed = true; + } + if (openTrackers != null) { + if (openTrackers.trim().length() > 0 && !openTrackers.trim().equals(_util.getOpenTrackerString())) { + _config.setProperty(I2PSnarkUtil.PROP_OPENTRACKERS, openTrackers.trim()); + _util.setOpenTrackerString(openTrackers); + addMessage(_("Open Tracker list changed - torrent restart required to take effect.")); + changed = true; + } + } + if (changed) { + saveConfig(); + } else { + addMessage(_("Configuration unchanged.")); + } + } + + public void saveConfig() { + try { + synchronized (_configFile) { + DataHelper.storeProps(_config, _configFile); + } + } catch (IOException ioe) { + addMessage(_("Unable to save the config to {0}", _configFile.getAbsolutePath())); + } + } + + public Properties getConfig() { return _config; } + + /** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */ + private static final int MAX_FILES_PER_TORRENT = 512; + + /** set of canonical .torrent filenames that we are dealing with */ + public Set listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } } + + /** + * Grab the torrent given the (canonical) filename of the .torrent file + * @return Snark or null + */ + public Snark getTorrent(String filename) { synchronized (_snarks) { return (Snark)_snarks.get(filename); } } + + /** + * Grab the torrent given the base name of the storage + * @return Snark or null + * @since 0.7.14 + */ + public Snark getTorrentByBaseName(String filename) { + synchronized (_snarks) { + for (Snark s : _snarks.values()) { + if (s.storage.getBaseName().equals(filename)) + return s; + } + } + return null; + } + + /** @throws RuntimeException via Snark.fatal() */ + public void addTorrent(String filename) { addTorrent(filename, false); } + + /** @throws RuntimeException via Snark.fatal() */ + public void addTorrent(String filename, boolean dontAutoStart) { + if ((!dontAutoStart) && !_util.connected()) { + addMessage(_("Connecting to I2P")); + boolean ok = _util.connect(); + if (!ok) { + addMessage(_("Error connecting to I2P - check your I2CP settings!")); + return; + } + } + File sfile = new File(filename); + try { + filename = sfile.getCanonicalPath(); + } catch (IOException ioe) { + _log.error("Unable to add the torrent " + filename, ioe); + addMessage(_("Error: Could not add the torrent {0}", filename) + ": " + ioe.getMessage()); + return; + } + File dataDir = getDataDir(); + Snark torrent = null; + synchronized (_snarks) { + torrent = (Snark)_snarks.get(filename); + } + // don't hold the _snarks lock while verifying the torrent + if (torrent == null) { + synchronized (_addSnarkLock) { + // double-check + synchronized (_snarks) { + if(_snarks.get(filename) != null) + return; + } + + FileInputStream fis = null; + try { + fis = new FileInputStream(sfile); + } catch (IOException ioe) { + // catch this here so we don't try do delete it below + addMessage(_("Cannot open \"{0}\"", sfile.getName()) + ": " + ioe.getMessage()); + return; + } + + try { + MetaInfo info = new MetaInfo(fis); + try { + fis.close(); + fis = null; + } catch (IOException e) {} + + if (!TrackerClient.isValidAnnounce(info.getAnnounce())) { + if (_util.shouldUseOpenTrackers() && _util.getOpenTrackers() != null) { + addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", will announce to i2p open trackers only", info.getName())); + } else { + addMessage(_("Warning - Ignoring non-i2p tracker in \"{0}\", and open trackers are disabled, you must enable open trackers before starting the torrent!", info.getName())); + dontAutoStart = true; + } + } + String rejectMessage = locked_validateTorrent(info); + if (rejectMessage != null) { + sfile.delete(); + addMessage(rejectMessage); + return; + } else { + torrent = new Snark(_util, filename, null, -1, null, null, this, + _peerCoordinatorSet, _connectionAcceptor, + false, dataDir.getPath()); + torrent.completeListener = this; + synchronized (_snarks) { + _snarks.put(filename, torrent); + } + } + } catch (IOException ioe) { + addMessage(_("Torrent in \"{0}\" is invalid", sfile.getName()) + ": " + ioe.getMessage()); + if (sfile.exists()) + sfile.delete(); + return; + } finally { + if (fis != null) try { fis.close(); } catch (IOException ioe) {} + } + } + } else { + return; + } + // ok, snark created, now lets start it up or configure it further + File f = new File(filename); + if (!dontAutoStart && shouldAutoStart()) { + torrent.startTorrent(); + addMessage(_("Torrent added and started: \"{0}\"", f.getName())); + } else { + addMessage(_("Torrent added: \"{0}\"", f.getName())); + } + } + + /** + * Get the timestamp for a torrent from the config file + */ + public long getSavedTorrentTime(Snark snark) { + MetaInfo metainfo = snark.meta; + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + if (time == null) + return 0; + int comma = time.indexOf(','); + if (comma <= 0) + return 0; + time = time.substring(0, comma); + try { return Long.parseLong(time); } catch (NumberFormatException nfe) {} + return 0; + } + + /** + * Get the saved bitfield for a torrent from the config file. + * Convert "." to a full bitfield. + */ + public BitField getSavedTorrentBitField(Snark snark) { + MetaInfo metainfo = snark.meta; + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + if (bf == null) + return null; + int comma = bf.indexOf(','); + if (comma <= 0) + return null; + bf = bf.substring(comma + 1).trim(); + int len = metainfo.getPieces(); + if (bf.equals(".")) { + BitField bitfield = new BitField(len); + for (int i = 0; i < len; i++) + bitfield.set(i); + return bitfield; + } + byte[] bitfield = Base64.decode(bf); + if (bitfield == null) + return null; + if (bitfield.length * 8 < len) + return null; + return new BitField(bitfield, len); + } + + /** + * Save the completion status of a torrent and the current time in the config file + * in the form "i2psnark.zmeta.$base64infohash=$time,$base64bitfield". + * The config file property key is appended with the Base64 of the infohash, + * with the '=' changed to '$' since a key can't contain '='. + * The time is a standard long converted to string. + * The status is either a bitfield converted to Base64 or "." for a completed + * torrent to save space in the config file and in memory. + */ + public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield) { + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + String now = "" + System.currentTimeMillis(); + String bfs; + if (bitfield.complete()) { + bfs = "."; + } else { + byte[] bf = bitfield.getFieldBytes(); + bfs = Base64.encode(bf); + } + _config.setProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX, now + "," + bfs); + saveConfig(); + } + + /** + * Remove the status of a torrent from the config file. + * This may help the config file from growing too big. + */ + public void removeTorrentStatus(MetaInfo metainfo) { + byte[] ih = metainfo.getInfoHash(); + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + _config.remove(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); + saveConfig(); + } + + /** + * Warning - does not validate announce URL - use TrackerClient.isValidAnnounce() + */ + private String locked_validateTorrent(MetaInfo info) throws IOException { + List files = info.getFiles(); + if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) { + return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size()); + } else if ( (files == null) && (info.getName().endsWith(".torrent")) ) { + return _("Torrent file \"{0}\" cannot end in \".torrent\", deleting it!", info.getName()); + } else if (info.getPieces() <= 0) { + return _("No pieces in \"{0}\", deleting it!", info.getName()); + } else if (info.getPieces() > Storage.MAX_PIECES) { + return _("Too many pieces in \"{0}\", limit is {1}, deleting it!", info.getName(), Storage.MAX_PIECES); + } else if (info.getPieceLength(0) > Storage.MAX_PIECE_SIZE) { + return _("Pieces are too large in \"{0}\" ({1}B), deleting it.", info.getName(), DataHelper.formatSize2(info.getPieceLength(0))) + ' ' + + _("Limit is {0}B", DataHelper.formatSize2(Storage.MAX_PIECE_SIZE)); + } else if (info.getTotalLength() > Storage.MAX_TOTAL_SIZE) { + System.out.println("torrent info: " + info.toString()); + List lengths = info.getLengths(); + if (lengths != null) + for (int i = 0; i < lengths.size(); i++) + System.out.println("File " + i + " is " + lengths.get(i) + " long."); + + return _("Torrents larger than {0}B are not supported yet, deleting \"{1}\"", Storage.MAX_TOTAL_SIZE, info.getName()); + } else { + // ok + return null; + } + } + + /** + * Stop the torrent, leaving it on the list of torrents unless told to remove it + */ + public Snark stopTorrent(String filename, boolean shouldRemove) { + File sfile = new File(filename); + try { + filename = sfile.getCanonicalPath(); + } catch (IOException ioe) { + _log.error("Unable to remove the torrent " + filename, ioe); + addMessage(_("Error: Could not remove the torrent {0}", filename) + ": " + ioe.getMessage()); + return null; + } + int remaining = 0; + Snark torrent = null; + synchronized (_snarks) { + if (shouldRemove) + torrent = (Snark)_snarks.remove(filename); + else + torrent = (Snark)_snarks.get(filename); + remaining = _snarks.size(); + } + if (torrent != null) { + boolean wasStopped = torrent.stopped; + torrent.stopTorrent(); + if (remaining == 0) { + // should we disconnect/reconnect here (taking care to deal with the other thread's + // I2PServerSocket.accept() call properly?) + ////_util. + } + if (!wasStopped) + addMessage(_("Torrent stopped: \"{0}\"", sfile.getName())); + } + return torrent; + } + /** + * Stop the torrent and delete the torrent file itself, but leaving the data + * behind. + */ + public void removeTorrent(String filename) { + Snark torrent = stopTorrent(filename, true); + if (torrent != null) { + File torrentFile = new File(filename); + torrentFile.delete(); + if (torrent.storage != null) + removeTorrentStatus(torrent.storage.getMetaInfo()); + addMessage(_("Torrent removed: \"{0}\"", torrentFile.getName())); + } + } + + private class DirMonitor implements Runnable { + public void run() { + try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {} + // the first message was a "We are starting up in 1m" + synchronized (_messages) { + if (_messages.size() == 1) + _messages.remove(0); + } + + // here because we need to delay until I2CP is up + // although the user will see the default until then + getBWLimit(); + while (true) { + File dir = getDataDir(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Directory Monitor loop over " + dir.getAbsolutePath()); + try { + monitorTorrents(dir); + } catch (Exception e) { + _log.error("Error in the DirectoryMonitor", e); + } + try { Thread.sleep(60*1000); } catch (InterruptedException ie) {} + } + } + } + + /** two listeners */ + public void torrentComplete(Snark snark) { + StringBuilder buf = new StringBuilder(256); + buf.append("").append(snark.storage.getBaseName()).append(""); + long len = snark.meta.getTotalLength(); + addMessage(_("Download finished: {0}", buf.toString()) + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')'); + updateStatus(snark); + } + + public void updateStatus(Snark snark) { + saveTorrentStatus(snark.meta, snark.storage.getBitField()); + } + + private void monitorTorrents(File dir) { + String fileNames[] = dir.list(TorrentFilenameFilter.instance()); + List foundNames = new ArrayList(0); + if (fileNames != null) { + for (int i = 0; i < fileNames.length; i++) { + try { + foundNames.add(new File(dir, fileNames[i]).getCanonicalPath()); + } catch (IOException ioe) { + _log.error("Error resolving '" + fileNames[i] + "' in '" + dir, ioe); + } + } + } + + Set existingNames = listTorrentFiles(); + // lets find new ones first... + for (int i = 0; i < foundNames.size(); i++) { + if (existingNames.contains(foundNames.get(i))) { + // already known. noop + } else { + if (shouldAutoStart() && !_util.connect()) + addMessage(_("Unable to connect to I2P!")); + try { + // Snark.fatal() throws a RuntimeException + // don't let one bad torrent kill the whole loop + addTorrent(foundNames.get(i), !shouldAutoStart()); + } catch (Exception e) { + addMessage(_("Unable to add {0}", foundNames.get(i)) + ": " + e); + } + } + } + // now lets see which ones have been removed... + for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) { + String name = (String)iter.next(); + if (foundNames.contains(name)) { + // known and still there. noop + } else { + // known, but removed. drop it + try { + // Snark.fatal() throws a RuntimeException + // don't let one bad torrent kill the whole loop + stopTorrent(name, true); + } catch (Exception e) { + // don't bother with message + } + } + } + } + + /** translate */ + private String _(String s) { + return _util.getString(s); + } + + /** translate */ + private String _(String s, Object o) { + return _util.getString(s, o); + } + + /** translate */ + private String _(String s, Object o, Object o2) { + return _util.getString(s, o, o2); + } + + /** + * "name", "announceURL=websiteURL" pairs + */ + private static final String DEFAULT_TRACKERS[] = { +// "Postman", "http://YRgrgTLGnbTq2aZOZDJQ~o6Uk5k6TK-OZtx0St9pb0G-5EGYURZioxqYG8AQt~LgyyI~NCj6aYWpPO-150RcEvsfgXLR~CxkkZcVpgt6pns8SRc3Bi-QSAkXpJtloapRGcQfzTtwllokbdC-aMGpeDOjYLd8b5V9Im8wdCHYy7LRFxhEtGb~RL55DA8aYOgEXcTpr6RPPywbV~Qf3q5UK55el6Kex-6VCxreUnPEe4hmTAbqZNR7Fm0hpCiHKGoToRcygafpFqDw5frLXToYiqs9d4liyVB-BcOb0ihORbo0nS3CLmAwZGvdAP8BZ7cIYE3Z9IU9D1G8JCMxWarfKX1pix~6pIA-sp1gKlL1HhYhPMxwyxvuSqx34o3BqU7vdTYwWiLpGM~zU1~j9rHL7x60pVuYaXcFQDR4-QVy26b6Pt6BlAZoFmHhPcAuWfu-SFhjyZYsqzmEmHeYdAwa~HojSbofg0TMUgESRXMw6YThK1KXWeeJVeztGTz25sL8AAAA.i2p/announce.php=http://tracker.postman.i2p/" +// , "eBook", "http://E71FRom6PZNEqTN2Lr8P-sr23b7HJVC32KoGnVQjaX6zJiXwhJy2HsXob36Qmj81TYFZdewFZa9mSJ533UZgGyQkXo2ahctg82JKYZfDe5uDxAn1E9YPjxZCWJaFJh0S~UwSs~9AZ7UcauSJIoNtpxrtbmRNVFLqnkEDdLZi26TeucfOmiFmIWnVblLniWv3tG1boE9Abd-6j3FmYVrRucYuepAILYt6katmVNOk6sXmno1Eynrp~~MBuFq0Ko6~jsc2E2CRVYXDhGHEMdt-j6JUz5D7S2RIVzDRqQyAZLKJ7OdQDmI31przzmne1vOqqqLC~1xUumZVIvF~yOeJUGNjJ1Vx0J8i2BQIusn1pQJ6UCB~ZtZZLQtEb8EPVCfpeRi2ri1M5CyOuxN0V5ekmPHrYIBNevuTCRC26NP7ZS5VDgx1~NaC3A-CzJAE6f1QXi0wMI9aywNG5KGzOPifcsih8eyGyytvgLtrZtV7ykzYpPCS-rDfITncpn5hliPUAAAA.i2p/pub/bt/announce.php=http://de-ebook-archiv.i2p/pub/bt/" +// , "Gaytorrents", "http://uxPWHbK1OIj9HxquaXuhMiIvi21iK0~ZiG9d8G0840ZXIg0r6CbiV71xlsqmdnU6wm0T2LySriM0doW2gUigo-5BNkUquHwOjLROiETnB3ZR0Ml4IGa6QBPn1aAq2d9~g1r1nVjLE~pcFnXB~cNNS7kIhX1d6nLgYVZf0C2cZopEow2iWVUggGGnAA9mHjE86zLEnTvAyhbAMTqDQJhEuLa0ZYSORqzJDMkQt90MV4YMjX1ICY6RfUSFmxEqu0yWTrkHsTtRw48l~dz9wpIgc0a0T9C~eeWvmBFTqlJPtQZwntpNeH~jF7nlYzB58olgV2HHFYpVYD87DYNzTnmNWxCJ5AfDorm6AIUCV2qaE7tZtI1h6fbmGpGlPyW~Kw5GXrRfJwNvr6ajwAVi~bPVnrBwDZezHkfW4slOO8FACPR28EQvaTu9nwhAbqESxV2hCTq6vQSGjuxHeOuzBOEvRWkLKOHWTC09t2DbJ94FSqETmZopTB1ukEmaxRWbKSIaAAAA.i2p/announce.php=http://gaytorrents.i2p/" +// , "NickyB", "http://9On6d3cZ27JjwYCtyJJbowe054d5tFnfMjv4PHsYs-EQn4Y4mk2zRixatvuAyXz2MmRfXG-NAUfhKr0KCxRNZbvHmlckYfT-WBzwwpiMAl0wDFY~Pl8cqXuhfikSG5WrqdPfDNNIBuuznS0dqaczf~OyVaoEOpvuP3qV6wKqbSSLpjOwwAaQPHjlRtNIW8-EtUZp-I0LT45HSoowp~6b7zYmpIyoATvIP~sT0g0MTrczWhbVTUZnEkZeLhOR0Duw1-IRXI2KHPbA24wLO9LdpKKUXed05RTz0QklW5ROgR6TYv7aXFufX8kC0-DaKvQ5JKG~h8lcoHvm1RCzNqVE-2aiZnO2xH08H-iCWoLNJE-Td2kT-Tsc~3QdQcnEUcL5BF-VT~QYRld2--9r0gfGl-yDrJZrlrihHGr5J7ImahelNn9PpkVp6eIyABRmJHf2iicrk3CtjeG1j9OgTSwaNmEpUpn4aN7Kx0zNLdH7z6uTgCGD9Kmh1MFYrsoNlTp4AAAA.i2p/bittorrent/announce.php=http://nickyb.i2p/bittorrent/" +// , "Orion", "http://gKik1lMlRmuroXVGTZ~7v4Vez3L3ZSpddrGZBrxVriosCQf7iHu6CIk8t15BKsj~P0JJpxrofeuxtm7SCUAJEr0AIYSYw8XOmp35UfcRPQWyb1LsxUkMT4WqxAT3s1ClIICWlBu5An~q-Mm0VFlrYLIPBWlUFnfPR7jZ9uP5ZMSzTKSMYUWao3ejiykr~mtEmyls6g-ZbgKZawa9II4zjOy-hdxHgP-eXMDseFsrym4Gpxvy~3Fv9TuiSqhpgm~UeTo5YBfxn6~TahKtE~~sdCiSydqmKBhxAQ7uT9lda7xt96SS09OYMsIWxLeQUWhns-C~FjJPp1D~IuTrUpAFcVEGVL-BRMmdWbfOJEcWPZ~CBCQSO~VkuN1ebvIOr9JBerFMZSxZtFl8JwcrjCIBxeKPBmfh~xYh16BJm1BBBmN1fp2DKmZ2jBNkAmnUbjQOqWvUcehrykWk5lZbE7bjJMDFH48v3SXwRuDBiHZmSbsTY6zhGY~GkMQHNGxPMMSIAAAA.i2p/bt/announce.php=http://orion.i2p/bt/" +// , "anonymity", "http://8EoJZIKrWgGuDrxA3nRJs1jsPfiGwmFWL91hBrf0HA7oKhEvAna4Ocx47VLUR9retVEYBAyWFK-eZTPcvhnz9XffBEiJQQ~kFSCqb1fV6IfPiV3HySqi9U5Caf6~hC46fRd~vYnxmaBLICT3N160cxBETqH3v2rdxdJpvYt8q4nMk9LUeVXq7zqCTFLLG5ig1uKgNzBGe58iNcsvTEYlnbYcE930ABmrzj8G1qQSgSwJ6wx3tUQNl1z~4wSOUMan~raZQD60lRK70GISjoX0-D0Po9WmPveN3ES3g72TIET3zc3WPdK2~lgmKGIs8GgNLES1cXTolvbPhdZK1gxddRMbJl6Y6IPFyQ9o4-6Rt3Lp-RMRWZ2TG7j2OMcNSiOmATUhKEFBDfv-~SODDyopGBmfeLw16F4NnYednvn4qP10dyMHcUASU6Zag4mfc2-WivrOqeWhD16fVAh8MoDpIIT~0r9XmwdaVFyLcjbXObabJczxCAW3fodQUnvuSkwzAAAA.i2p/anonymityTracker/announce.php=http://anonymityweb.i2p/anonymityTracker/" +// , "The freak's tracker", "http://mHKva9x24E5Ygfey2llR1KyQHv5f8hhMpDMwJDg1U-hABpJ2NrQJd6azirdfaR0OKt4jDlmP2o4Qx0H598~AteyD~RJU~xcWYdcOE0dmJ2e9Y8-HY51ie0B1yD9FtIV72ZI-V3TzFDcs6nkdX9b81DwrAwwFzx0EfNvK1GLVWl59Ow85muoRTBA1q8SsZImxdyZ-TApTVlMYIQbdI4iQRwU9OmmtefrCe~ZOf4UBS9-KvNIqUL0XeBSqm0OU1jq-D10Ykg6KfqvuPnBYT1BYHFDQJXW5DdPKwcaQE4MtAdSGmj1epDoaEBUa9btQlFsM2l9Cyn1hzxqNWXELmx8dRlomQLlV4b586dRzW~fLlOPIGC13ntPXogvYvHVyEyptXkv890jC7DZNHyxZd5cyrKC36r9huKvhQAmNABT2Y~pOGwVrb~RpPwT0tBuPZ3lHYhBFYmD8y~AOhhNHKMLzea1rfwTvovBMByDdFps54gMN1mX4MbCGT4w70vIopS9yAAAA.i2p/bytemonsoon/announce.php" +// , "mastertracker", "http://VzXD~stRKbL3MOmeTn1iaCQ0CFyTmuFHiKYyo0Rd~dFPZFCYH-22rT8JD7i-C2xzYFa4jT5U2aqHzHI-Jre4HL3Ri5hFtZrLk2ax3ji7Qfb6qPnuYkuiF2E2UDmKUOppI8d9Ye7tjdhQVCy0izn55tBaB-U7UWdcvSK2i85sauyw3G0Gfads1Rvy5-CAe2paqyYATcDmGjpUNLoxbfv9KH1KmwRTNH6k1v4PyWYYnhbT39WfKMbBjSxVQRdi19cyJrULSWhjxaQfJHeWx5Z8Ev4bSPByBeQBFl2~4vqy0S5RypINsRSa3MZdbiAAyn5tr5slWR6QdoqY3qBQgBJFZppy-3iWkFqqKgSxCPundF8gdDLC5ddizl~KYcYKl42y9SGFHIukH-TZs8~em0~iahzsqWVRks3zRG~tlBcX2U3M2~OJs~C33-NKhyfZT7-XFBREvb8Szmd~p66jDxrwOnKaku-G6DyoQipJqIz4VHmY9-y5T8RrUcJcM-5lVoMpAAAA.i2p/announce.php=http://tracker.mastertracker.i2p/" +// , "Galen", "http://5jpwQMI5FT303YwKa5Rd38PYSX04pbIKgTaKQsWbqoWjIfoancFdWCShXHLI5G5ofOb0Xu11vl2VEMyPsg1jUFYSVnu4-VfMe3y4TKTR6DTpetWrnmEK6m2UXh91J5DZJAKlgmO7UdsFlBkQfR2rY853-DfbJtQIFl91tbsmjcA5CGQi4VxMFyIkBzv-pCsuLQiZqOwWasTlnzey8GcDAPG1LDcvfflGV~6F5no9mnuisZPteZKlrv~~TDoXTj74QjByWc4EOYlwqK8sbU9aOvz~s31XzErbPTfwiawiaZ0RUI-IDrKgyvmj0neuFTWgjRGVTH8bz7cBZIc3viy6ioD-eMQOrXaQL0TCWZUelRwHRvgdPiQrxdYQs7ixkajeHzxi-Pq0EMm5Vbh3j3Q9kfUFW3JjFDA-MLB4g6XnjCbM5J1rC0oOBDCIEfhQkszru5cyLjHiZ5yeA0VThgu~c7xKHybv~OMXION7V8pBKOgET7ZgAkw1xgYe3Kkyq5syAAAA.i2p/tr/announce.php=http://galen.i2p/tr/" + "POSTMAN", "http://tracker2.postman.i2p/announce.php=http://tracker2.postman.i2p/" + ,"WELTERDE", "http://tracker.welterde.i2p/a=http://tracker.welterde.i2p/stats?mode=top5" + , "CRSTRACK", "http://b4G9sCdtfvccMAXh~SaZrPqVQNyGQbhbYMbw6supq2XGzbjU4NcOmjFI0vxQ8w1L05twmkOvg5QERcX6Mi8NQrWnR0stLExu2LucUXg1aYjnggxIR8TIOGygZVIMV3STKH4UQXD--wz0BUrqaLxPhrm2Eh9Hwc8TdB6Na4ShQUq5Xm8D4elzNUVdpM~RtChEyJWuQvoGAHY3ppX-EJJLkiSr1t77neS4Lc-KofMVmgI9a2tSSpNAagBiNI6Ak9L1T0F9uxeDfEG9bBSQPNMOSUbAoEcNxtt7xOW~cNOAyMyGydwPMnrQ5kIYPY8Pd3XudEko970vE0D6gO19yoBMJpKx6Dh50DGgybLQ9CpRaynh2zPULTHxm8rneOGRcQo8D3mE7FQ92m54~SvfjXjD2TwAVGI~ae~n9HDxt8uxOecAAvjjJ3TD4XM63Q9TmB38RmGNzNLDBQMEmJFpqQU8YeuhnS54IVdUoVQFqui5SfDeLXlSkh4vYoMU66pvBfWbAAAA.i2p/tracker/announce.php=http://crstrack.i2p/tracker/" + }; + + /** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */ + public static final String PROP_TRACKERS = "i2psnark.trackers"; + private static Map trackerMap = null; + /** sorted map of name to announceURL=baseURL */ + public Map getTrackers() { + if (trackerMap != null) // only do this once, can't be updated while running + return trackerMap; + Map rv = new TreeMap(); + String trackers = _config.getProperty(PROP_TRACKERS); + if ( (trackers == null) || (trackers.trim().length() <= 0) ) + trackers = _context.getProperty(PROP_TRACKERS); + if ( (trackers == null) || (trackers.trim().length() <= 0) ) { + for (int i = 0; i < DEFAULT_TRACKERS.length; i += 2) + rv.put(DEFAULT_TRACKERS[i], DEFAULT_TRACKERS[i+1]); + } else { + StringTokenizer tok = new StringTokenizer(trackers, ","); + while (tok.hasMoreTokens()) { + String pair = tok.nextToken(); + int split = pair.indexOf('='); + if (split <= 0) + continue; + String name = pair.substring(0, split).trim(); + String url = pair.substring(split+1).trim(); + if ( (name.length() > 0) && (url.length() > 0) ) + rv.put(name, url); + } + } + + trackerMap = rv; + return trackerMap; + } + + private static class TorrentFilenameFilter implements FilenameFilter { + private static final TorrentFilenameFilter _filter = new TorrentFilenameFilter(); + public static TorrentFilenameFilter instance() { return _filter; } + public boolean accept(File dir, String name) { + return (name != null) && (name.endsWith(".torrent")); + } + } + + public class SnarkManagerShutdown extends I2PAppThread { + @Override + public void run() { + Set names = listTorrentFiles(); + for (Iterator iter = names.iterator(); iter.hasNext(); ) { + Snark snark = getTorrent((String)iter.next()); + if ( (snark != null) && (!snark.stopped) ) + snark.stopTorrent(); + } + } + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index 065e70880..05df351c6 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -357,6 +357,7 @@ public class TrackerClient extends I2PAppThread + "&uploaded=" + uploaded + "&downloaded=" + downloaded + "&left=" + left + + "&compact" + ((! event.equals(NO_EVENT)) ? ("&event=" + event) : ""); if (left <= 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers()) s += "&numwant=0"; diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java index 84198f12f..360a4f47e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java @@ -24,7 +24,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.HashSet; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -33,11 +32,17 @@ import org.klomp.snark.bencode.BDecoder; import org.klomp.snark.bencode.BEValue; import org.klomp.snark.bencode.InvalidBEncodingException; +/** + * The data structure for the tracker response. + * Handles both traditional and compact formats. + * Compact format 1 - a list of hashes - early format for testing + * Compact format 2 - One big string of concatenated hashes - official format + */ public class TrackerInfo { private final String failure_reason; private final int interval; - private final Set peers; + private final Set peers; private int complete; private int incomplete; @@ -73,10 +78,19 @@ public class TrackerInfo interval = beInterval.getInt(); BEValue bePeers = (BEValue)m.get("peers"); - if (bePeers == null) + if (bePeers == null) { peers = Collections.EMPTY_SET; - else - peers = getPeers(bePeers.getList(), my_id, metainfo); + } else { + Set p; + try { + // One big string (the official compact format) + p = getPeers(bePeers.getBytes(), my_id, metainfo); + } catch (InvalidBEncodingException ibe) { + // List of Dictionaries or List of Strings + p = getPeers(bePeers.getList(), my_id, metainfo); + } + peers = p; + } BEValue bev = (BEValue)m.get("complete"); if (bev != null) try { @@ -94,32 +108,68 @@ public class TrackerInfo } } - public static Set getPeers(InputStream in, byte[] my_id, MetaInfo metainfo) +/****** + public static Set getPeers(InputStream in, byte[] my_id, MetaInfo metainfo) throws IOException { return getPeers(new BDecoder(in), my_id, metainfo); } - public static Set getPeers(BDecoder be, byte[] my_id, MetaInfo metainfo) + public static Set getPeers(BDecoder be, byte[] my_id, MetaInfo metainfo) throws IOException { return getPeers(be.bdecodeList().getList(), my_id, metainfo); } +******/ - public static Set getPeers(List l, byte[] my_id, MetaInfo metainfo) + /** List of Dictionaries or List of Strings */ + private static Set getPeers(List l, byte[] my_id, MetaInfo metainfo) throws IOException { - Set peers = new HashSet(l.size()); + Set peers = new HashSet(l.size()); - Iterator it = l.iterator(); - while (it.hasNext()) - { + for (BEValue bev : l) { PeerID peerID; try { - peerID = new PeerID(((BEValue)it.next()).getMap()); + // Case 1 - non-compact - A list of dictionaries (maps) + peerID = new PeerID(bev.getMap()); } catch (InvalidBEncodingException ibe) { - // don't let one bad entry spoil the whole list - //Snark.debug("Discarding peer from list: " + ibe, Snark.ERROR); + try { + // Case 2 - compact - A list of 32-byte binary strings (hashes) + // This was just for testing and is not the official format + peerID = new PeerID(bev.getBytes()); + } catch (InvalidBEncodingException ibe2) { + // don't let one bad entry spoil the whole list + //Snark.debug("Discarding peer from list: " + ibe, Snark.ERROR); + continue; + } + } + peers.add(new Peer(peerID, my_id, metainfo)); + } + + return peers; + } + + private static final int HASH_LENGTH = 32; + + /** + * One big string of concatenated 32-byte hashes + * @since 0.8.1 + */ + private static Set getPeers(byte[] l, byte[] my_id, MetaInfo metainfo) + throws IOException + { + int count = l.length / HASH_LENGTH; + Set peers = new HashSet(count); + + for (int i = 0; i < count; i++) { + PeerID peerID; + byte[] hash = new byte[HASH_LENGTH]; + System.arraycopy(l, i * HASH_LENGTH, hash, 0, HASH_LENGTH); + try { + peerID = new PeerID(hash); + } catch (InvalidBEncodingException ibe) { + // won't happen continue; } peers.add(new Peer(peerID, my_id, metainfo)); @@ -128,7 +178,7 @@ public class TrackerInfo return peers; } - public Set getPeers() + public Set getPeers() { return peers; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java index cba6b973f..c1733cb13 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java +++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java @@ -140,7 +140,7 @@ public class BEValue * succeeds when the BEValue is actually a List, otherwise it will * throw a InvalidBEncodingException. */ - public List getList() throws InvalidBEncodingException + public List getList() throws InvalidBEncodingException { try { @@ -157,7 +157,7 @@ public class BEValue * values. This operation only succeeds when the BEValue is actually * a Map, otherwise it will throw a InvalidBEncodingException. */ - public Map getMap() throws InvalidBEncodingException + public Map getMap() throws InvalidBEncodingException { try { diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index 9a291c89f..36266de1b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -8,6 +8,7 @@ import java.io.PrintWriter; import java.text.Collator; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.Iterator; @@ -821,10 +822,10 @@ public class I2PSnarkServlet extends Default { out.write("\n\n"); if(showPeers && isRunning && curPeers > 0) { - List peers = snark.coordinator.peerList(); - Iterator it = peers.iterator(); - while (it.hasNext()) { - Peer peer = (Peer)it.next(); + List peers = snark.coordinator.peerList(); + if (!showDebug) + Collections.sort(peers, new PeerComparator()); + for (Peer peer : peers) { if (!peer.isConnected()) continue; out.write(""); @@ -908,6 +909,19 @@ public class I2PSnarkServlet extends Default { } } + /** + * Sort by completeness (seeds first), then by ID + * @since 0.8.1 + */ + private static class PeerComparator implements Comparator { + public int compare(Peer l, Peer r) { + int diff = r.completed() - l.completed(); // reverse + if (diff != 0) + return diff; + return l.toString().substring(5, 9).compareTo(r.toString().substring(5, 9)); + } + } + private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException { String uri = req.getRequestURI(); String newURL = req.getParameter("newURL"); diff --git a/apps/i2psnark/locale/messages_zh.po b/apps/i2psnark/locale/messages_zh.po index e39ae0970..b649d8c58 100644 --- a/apps/i2psnark/locale/messages_zh.po +++ b/apps/i2psnark/locale/messages_zh.po @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: I2P i2psnark\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-07-01 04:52+0000\n" -"PO-Revision-Date: 2010-07-01 12:53+0800\n" +"POT-Creation-Date: 2010-10-04 02:45+0000\n" +"PO-Revision-Date: 2010-10-04 12:00+0800\n" "Last-Translator: walking \n" "Language-Team: foo \n" "MIME-Version: 1.0\n" @@ -224,7 +224,6 @@ msgid "Torrents" msgstr "种子" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:187 -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:193 #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:837 msgid "I2PSnark" msgstr "" @@ -233,63 +232,65 @@ msgstr "" msgid "Refresh page" msgstr "刷新页面" +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:193 +msgid " I2PSnark" +msgstr "" + #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:195 msgid "Forum" msgstr "论坛" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:240 -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1289 -msgid "Status" -msgstr "状态" +msgid "Status" +msgstr "状态" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:246 -msgid "Hide Peers" -msgstr "隐藏用户" +msgid "\"Hide" +msgstr "\"隐藏节点\"" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:249 -msgid "Show Peers" -msgstr "显示用户" +msgid "\"Show" +msgstr "\"显示节点\"" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:254 -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1271 -msgid "Torrent" -msgstr "种子" +msgid "Torrent" +msgstr "种子" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:256 -msgid "ETA" -msgstr "预计剩余时间" +msgid "ETA" +msgstr "预计剩余时间" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:258 -msgid "Downloaded" -msgstr "已下载" +msgid "RX" +msgstr "下载" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:260 -msgid "Uploaded" -msgstr "已上传" +msgid "TX" +msgstr "上传" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:262 -msgid "Down Rate" -msgstr "下载速度" +msgid "Rate" +msgstr "下载速度" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:264 -msgid "Up Rate" -msgstr "上传速度" +msgid "Rate" +msgstr "上传速度" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:271 msgid "Stop all torrents and the I2P tunnel" msgstr "停止全部种子及I2P隧道" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:273 -msgid "Stop All" -msgstr "停止全部" +msgid "\"Stop" +msgstr "\"全部停止\"" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:278 msgid "Start all torrents and the I2P tunnel" msgstr "启动全部种子及I2P隧道" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:280 -msgid "Start All" -msgstr "启动全部" +msgid "\"Start" +msgstr "\"全部开始\"" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:297 msgid "No torrents loaded." @@ -317,13 +318,13 @@ msgid "Torrent file {0} does not exist" msgstr "种子文件{0}不存在" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:346 -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1476 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1481 #, java-format msgid "Torrent already running: {0}" msgstr "种子已启动:{0}" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:348 -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1478 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1483 #, java-format msgid "Torrent already in the queue: {0}" msgstr "种子排队中:{0}" @@ -450,7 +451,7 @@ msgid "Seeding" msgstr "正做种" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:682 -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1327 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1317 msgid "Complete" msgstr "完成" @@ -486,24 +487,24 @@ msgid "Tracker" msgstr "Tracker服务器" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:751 -msgid "Details" -msgstr "详情" +msgid "" +msgstr "" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:785 msgid "Stop the torrent" msgstr "停止种子" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:787 -msgid "Stop" -msgstr "停止" +msgid "\"Stop\"" +msgstr "\"停止\"" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:793 msgid "Start the torrent" msgstr "启动种子" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:795 -msgid "Start" -msgstr "启动" +msgid "\"Start\"" +msgstr "\"开始\"" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:800 msgid "Remove the torrent from the active list, deleting the .torrent file" @@ -518,8 +519,8 @@ msgid "Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded msgstr "您确定要删除文件“{0}.torrent”(下载的数据文件不会被删除)?" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:807 -msgid "Remove" -msgstr "移除" +msgid "\"Remove\"" +msgstr "\"删除种子\"" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:811 msgid "Delete the .torrent file and the associated data file(s)" @@ -534,8 +535,8 @@ msgid "Are you sure you want to delete the torrent \\''{0}\\'' and all downloade msgstr "您确定要删除种子“{0}”(下载的数据文件会一并被删除)?" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:818 -msgid "Delete" -msgstr "删除" +msgid "\"Delete\"" +msgstr "\"删除种子" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:861 msgid "Seed" @@ -558,8 +559,8 @@ msgid "Choking (We are not allowing the peer to request pieces)" msgstr "拒绝请求" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:927 -msgid "Add Torrent" -msgstr "添加种子" +msgid "Add Torrent" +msgstr "添加种子" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:929 msgid "From URL" @@ -571,16 +572,16 @@ msgstr "添加种子" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:937 #, java-format -msgid "Alternately, you can copy .torrent files to the directory {0}." -msgstr "或者您可以将.torrent文件复制到以下目录{0}." +msgid "You can also copy .torrent files to: {0}" +msgstr "或者您可以将.torrent文件复制到{0}." #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:939 -msgid "Removing a .torrent file will cause the torrent to stop." -msgstr "删除种子文件将导致中止该下载任务。" +msgid "Removing a .torrent will cause it to stop." +msgstr "删除种子文件将导致该下载任务中止。" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:958 -msgid "Create Torrent" -msgstr "创建种子" +msgid "Create Torrent" +msgstr "创建种子" #. out.write("From file:
\n"); #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:961 @@ -596,8 +597,8 @@ msgid "Select a tracker" msgstr "选择一个Tracker" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:982 -msgid "or" -msgstr "或" +msgid "or " +msgstr "或 " #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:985 msgid "Specify custom tracker announce URL" @@ -609,8 +610,8 @@ msgstr "创建种子" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1006 #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1133 -msgid "Configuration" -msgstr "设置" +msgid "Configuration" +msgstr "设置" #: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1009 msgid "Data directory" @@ -709,53 +710,103 @@ msgid "1 tunnel" msgid_plural "{0} tunnels" msgstr[0] "{0}隧道" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1283 -msgid "Up to higher level directory" -msgstr "上一层文件夹" +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1271 +msgid "Torrent" +msgstr "种子" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1288 -msgid "File" -msgstr "文件" +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1278 +msgid "\"File\" " +msgstr "\"文件\" " -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1288 -msgid "Size" -msgstr "大小" +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1278 +msgid "\"FileSize\"Size" +msgstr "\"文件大小\"大小" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1311 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1279 +msgid "Status" +msgstr "状态" + +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1301 msgid "Directory" msgstr "文件夹" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1316 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1306 msgid "Torrent not found?" msgstr "种子未找到" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1324 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1314 msgid "File not found in torrent?" msgstr "种子中没有发现文件?" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1330 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1320 msgid "complete" msgstr "完成" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1331 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1321 msgid "bytes remaining" msgstr "剩余字节数" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1456 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1373 +msgid "Up to higher level directory" +msgstr "上一层文件夹" + +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1461 #, java-format msgid "Torrent fetched from {0}" msgstr "从{0}获取种子成功" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1484 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1489 #, java-format msgid "Torrent at {0} was not valid" msgstr "{0}的种子中有错误" -#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1489 +#: ../java/src/org/klomp/snark/web/I2PSnarkServlet.java:1494 #, java-format msgid "Torrent was not retrieved from {0}" msgstr "从{0}获得种子失败" +#~ msgid "Status" +#~ msgstr "状态" +#~ msgid "Hide Peers" +#~ msgstr "隐藏用户" +#~ msgid "Show Peers" +#~ msgstr "显示用户" +#~ msgid "ETA" +#~ msgstr "预计剩余时间" +#~ msgid "Downloaded" +#~ msgstr "已下载" +#~ msgid "Uploaded" +#~ msgstr "已上传" +#~ msgid "Down Rate" +#~ msgstr "下载速度" +#~ msgid "Up Rate" +#~ msgstr "上传速度" +#~ msgid "Stop All" +#~ msgstr "停止全部" +#~ msgid "Start All" +#~ msgstr "启动全部" +#~ msgid "Details" +#~ msgstr "详情" +#~ msgid "Stop" +#~ msgstr "停止" +#~ msgid "Start" +#~ msgstr "启动" +#~ msgid "Remove" +#~ msgstr "移除" +#~ msgid "Delete" +#~ msgstr "删除" +#~ msgid "Add Torrent" +#~ msgstr "添加种子" +#~ msgid "Create Torrent" +#~ msgstr "创建种子" +#~ msgid "or" +#~ msgstr "或" +#~ msgid "Configuration" +#~ msgstr "设置" +#~ msgid "File" +#~ msgstr "文件" +#~ msgid "Size" +#~ msgstr "大小" #~ msgid "Cannot change the I2CP settings while torrents are active" #~ msgstr "正在下载/上传,无法更改I2CP设置" #~ msgid "{0} torrents" diff --git a/apps/i2psnark/web.xml b/apps/i2psnark/web.xml index 71260245f..2925ff00e 100644 --- a/apps/i2psnark/web.xml +++ b/apps/i2psnark/web.xml @@ -22,4 +22,68 @@ 30 + + + + + + mkv + video/x-matroska + + + + wmv + video/x-ms-wmv + + + + flv + video/x-flv + + + + mp4 + video/mp4 + + + + rar + application/rar + + + + 7z + application/x-7z-compressed + + + + iso + application/x-iso9660-image + + + + ico + image/x-icon + + + + exe + application/x-msdos-program + + + + flac + audio/flac + + + + m4a + audio/mpeg + + + + wma + audio/x-ms-wma + + diff --git a/apps/i2ptunnel/java/build.xml b/apps/i2ptunnel/java/build.xml index 7b66b6882..b17e8d9f3 100644 --- a/apps/i2ptunnel/java/build.xml +++ b/apps/i2ptunnel/java/build.xml @@ -81,7 +81,7 @@ + basedir="../jsp/" excludes="web.xml, web-fragment.xml, web-out.xml, **/*.java, *.jsp"> diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java index af4049a21..326ebb644 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/HTTPResponseOutputStream.java @@ -24,6 +24,9 @@ import net.i2p.util.I2PAppThread; import net.i2p.util.Log; /** + * This does the transparent gzip decompression on the client side. + * Extended in I2PTunnelHTTPServer to do the compression on the server side. + * * Simple stream for delivering an HTTP response to * the client, trivially filtered to make sure "Connection: close" * is always in the response. Perhaps add transparent handling of the @@ -33,29 +36,27 @@ import net.i2p.util.Log; * */ class HTTPResponseOutputStream extends FilterOutputStream { - private I2PAppContext _context; - private Log _log; - private ByteCache _cache; + private final I2PAppContext _context; + private final Log _log; protected ByteArray _headerBuffer; private boolean _headerWritten; - private byte _buf1[]; + private final byte _buf1[]; protected boolean _gzip; private long _dataWritten; private InternalGZIPInputStream _in; private static final int CACHE_SIZE = 8*1024; + private static final ByteCache _cache = ByteCache.getInstance(8, CACHE_SIZE); + // OOM DOS prevention + private static final int MAX_HEADER_SIZE = 64*1024; public HTTPResponseOutputStream(OutputStream raw) { super(raw); _context = I2PAppContext.getGlobalContext(); - _context.statManager().createRateStat("i2ptunnel.httpCompressionRatio", "ratio of compressed size to decompressed size after transfer", "I2PTunnel", new long[] { 60*1000, 30*60*1000 }); - _context.statManager().createRateStat("i2ptunnel.httpCompressed", "compressed size transferred", "I2PTunnel", new long[] { 60*1000, 30*60*1000 }); - _context.statManager().createRateStat("i2ptunnel.httpExpanded", "size transferred after expansion", "I2PTunnel", new long[] { 60*1000, 30*60*1000 }); + _context.statManager().createRateStat("i2ptunnel.httpCompressionRatio", "ratio of compressed size to decompressed size after transfer", "I2PTunnel", new long[] { 60*60*1000 }); + _context.statManager().createRateStat("i2ptunnel.httpCompressed", "compressed size transferred", "I2PTunnel", new long[] { 60*60*1000 }); + _context.statManager().createRateStat("i2ptunnel.httpExpanded", "size transferred after expansion", "I2PTunnel", new long[] { 60*60*1000 }); _log = _context.logManager().getLog(getClass()); - _cache = ByteCache.getInstance(8, CACHE_SIZE); _headerBuffer = _cache.acquire(); - _headerWritten = false; - _gzip = false; - _dataWritten = 0; _buf1 = new byte[1]; } @@ -96,14 +97,20 @@ class HTTPResponseOutputStream extends FilterOutputStream { } } - /** grow (and free) the buffer as necessary */ - private void ensureCapacity() { + /** + * grow (and free) the buffer as necessary + * @throws IOException if the headers are too big + */ + private void ensureCapacity() throws IOException { + if (_headerBuffer.getValid() >= MAX_HEADER_SIZE) + throw new IOException("Max header size exceeded: " + MAX_HEADER_SIZE); if (_headerBuffer.getValid() + 1 >= _headerBuffer.getData().length) { int newSize = (int)(_headerBuffer.getData().length * 1.5); ByteArray newBuf = new ByteArray(new byte[newSize]); System.arraycopy(_headerBuffer.getData(), 0, newBuf.getData(), 0, _headerBuffer.getValid()); newBuf.setValid(_headerBuffer.getValid()); newBuf.setOffset(0); + // if we changed the ByteArray size, don't put it back in the cache if (_headerBuffer.getData().length == CACHE_SIZE) _cache.release(_headerBuffer); _headerBuffer = newBuf; @@ -219,7 +226,7 @@ class HTTPResponseOutputStream extends FilterOutputStream { //out.flush(); PipedInputStream pi = new PipedInputStream(); PipedOutputStream po = new PipedOutputStream(pi); - new I2PAppThread(new Pusher(pi, out), "HTTP decompresser").start(); + new I2PAppThread(new Pusher(pi, out), "HTTP decompressor").start(); out = po; } @@ -231,13 +238,13 @@ class HTTPResponseOutputStream extends FilterOutputStream { _out = out; } public void run() { - OutputStream to = null; _in = null; - long start = System.currentTimeMillis(); long written = 0; + ByteArray ba = null; try { _in = new InternalGZIPInputStream(_inRaw); - byte buf[] = new byte[8192]; + ba = _cache.acquire(); + byte buf[] = ba.getData(); int read = -1; while ( (read = _in.read(buf)) != -1) { if (_log.shouldLog(Log.DEBUG)) @@ -251,6 +258,8 @@ class HTTPResponseOutputStream extends FilterOutputStream { } catch (IOException ioe) { if (_log.shouldLog(Log.WARN)) _log.warn("Error decompressing: " + written + ", " + (_in != null ? _in.getTotalRead() + "/" + _in.getTotalExpanded() : ""), ioe); + } catch (OutOfMemoryError oom) { + _log.error("OOM in HTTP Decompressor", oom); } finally { if (_log.shouldLog(Log.WARN) && (_in != null)) _log.warn("After decompression, written=" + written + @@ -259,23 +268,26 @@ class HTTPResponseOutputStream extends FilterOutputStream { + ", expanded=" + _in.getTotalExpanded() + ", remaining=" + _in.getRemaining() + ", finished=" + _in.getFinished() : "")); + if (ba != null) + _cache.release(ba); if (_out != null) try { _out.close(); } catch (IOException ioe) {} } - long end = System.currentTimeMillis(); + double compressed = (_in != null ? _in.getTotalRead() : 0); double expanded = (_in != null ? _in.getTotalExpanded() : 0); - double ratio = 0; - if (expanded > 0) - ratio = compressed/expanded; - - _context.statManager().addRateData("i2ptunnel.httpCompressionRatio", (int)(100d*ratio), end-start); - _context.statManager().addRateData("i2ptunnel.httpCompressed", (long)compressed, end-start); - _context.statManager().addRateData("i2ptunnel.httpExpanded", (long)expanded, end-start); + if (compressed > 0 && expanded > 0) { + // only update the stats if we did something + double ratio = compressed/expanded; + _context.statManager().addRateData("i2ptunnel.httpCompressionRatio", (int)(100d*ratio), 0); + _context.statManager().addRateData("i2ptunnel.httpCompressed", (long)compressed, 0); + _context.statManager().addRateData("i2ptunnel.httpExpanded", (long)expanded, 0); + } } } + /** just a wrapper to provide stats for debugging */ private static class InternalGZIPInputStream extends GZIPInputStream { public InternalGZIPInputStream(InputStream in) throws IOException { super(in); @@ -294,6 +306,12 @@ class HTTPResponseOutputStream extends FilterOutputStream { return 0; } } + + /** + * From Inflater javadoc: + * Returns the total number of bytes remaining in the input buffer. This can be used to find out + * what bytes still remain in the input buffer after decompression has finished. + */ public long getRemaining() { try { return super.inf.getRemaining(); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java index d5b88d4d3..435b30947 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelClient.java @@ -8,7 +8,6 @@ import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; -import net.i2p.I2PAppContext; import net.i2p.client.streaming.I2PSocket; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; @@ -110,7 +109,7 @@ public class I2PTunnelClient extends I2PTunnelClientBase { } if (size == 1) // skip the rand in the most common case return dests.get(0); - int index = I2PAppContext.getGlobalContext().random().nextInt(size); + int index = _context.random().nextInt(size); return dests.get(index); } } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java index fe92e94b6..d9afdc5f2 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelConnectClient.java @@ -146,7 +146,7 @@ public class I2PTunnelConnectClient extends I2PTunnelClientBase implements Runna int size = _proxyList.size(); if (size <= 0) return null; - int index = I2PAppContext.getGlobalContext().random().nextInt(size); + int index = _context.random().nextInt(size); return _proxyList.get(index); } } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java index b6c72821e..f34ceb1a5 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java @@ -205,7 +205,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable l.log("Proxy list is empty - no outproxy available"); return null; } - int index = I2PAppContext.getGlobalContext().random().nextInt(size); + int index = _context.random().nextInt(size); String proxy = (String)proxyList.get(index); return proxy; } @@ -626,6 +626,14 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable //line = "From: i2p"; line = null; continue; // completely strip the line + } else if (lowercaseLine.startsWith("authorization: ntlm ")) { + // Block Windows NTLM after 401 + line = null; + continue; + } else if (lowercaseLine.startsWith("proxy-authorization: ntlm ")) { + // Block Windows NTLM after 407 + line = null; + continue; } } @@ -808,7 +816,7 @@ public class I2PTunnelHTTPClient extends I2PTunnelClientBase implements Runnable * @return non-null */ private byte[] getErrorPage(String base, byte[] backup) { - return getErrorPage(getTunnel().getContext(), base, backup); + return getErrorPage(_context, base, backup); } private static byte[] getErrorPage(I2PAppContext ctx, String base, byte[] backup) { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java index 728d3537c..ca568ab92 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelHTTPServer.java @@ -290,6 +290,7 @@ public class I2PTunnelHTTPServer extends I2PTunnelServer { } } + /** just a wrapper to provide stats for debugging */ private static class InternalGZIPOutputStream extends GZIPOutputStream { public InternalGZIPOutputStream(OutputStream target) throws IOException { super(target); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java index 4d8b85a81..d0dc227ec 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelIRCClient.java @@ -9,7 +9,6 @@ import java.util.ArrayList; import java.util.List; import java.util.StringTokenizer; -import net.i2p.I2PAppContext; import net.i2p.client.streaming.I2PSocket; import net.i2p.data.DataFormatException; import net.i2p.data.Destination; @@ -124,7 +123,7 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable } if (size == 1) // skip the rand in the most common case return dests.get(0); - int index = I2PAppContext.getGlobalContext().random().nextInt(size); + int index = _context.random().nextInt(size); return dests.get(index); } @@ -182,6 +181,8 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable } outmsg=outmsg+"\r\n"; // rfc1459 sec. 2.3 output.write(outmsg.getBytes("ISO-8859-1")); + // probably doesn't do much but can't hurt + output.flush(); } else { if (_log.shouldLog(Log.WARN)) _log.warn("inbound BLOCKED: "+inmsg); @@ -257,6 +258,8 @@ public class I2PTunnelIRCClient extends I2PTunnelClientBase implements Runnable } outmsg=outmsg+"\r\n"; // rfc1459 sec. 2.3 output.write(outmsg.getBytes("ISO-8859-1")); + // save 250 ms in streaming + output.flush(); } else { if (_log.shouldLog(Log.WARN)) _log.warn("outbound BLOCKED: "+"\""+inmsg+"\""); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java index c2a014020..0e1c9049f 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/I2PTunnelRunner.java @@ -129,7 +129,14 @@ public class I2PTunnelRunner extends I2PAppThread implements I2PSocket.SocketErr // do NOT flush here, it will block and then onTimeout.run() won't happen on fail. // But if we don't flush, then we have to wait for the connectDelay timer to fire // in i2p socket? To be researched and/or fixed. - //i2pout.flush(); + // + // AS OF 0.8.1, MessageOutputStream.flush() is fixed to only wait for accept, + // not for "completion" (i.e. an ACK from the far end). + // So we now get a fast return from flush(), and can do it here to save 250 ms. + // To make sure we are under the initial window size and don't hang waiting for accept, + // only flush if it fits in one message. + if (initialI2PData.length <= 1730) // ConnectionOptions.DEFAULT_MAX_MESSAGE_SIZE + i2pout.flush(); } } if (initialSocketData != null) { diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java index f0470c5a3..02777dafb 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelController.java @@ -18,6 +18,7 @@ import net.i2p.data.Base32; import net.i2p.data.Destination; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; +import net.i2p.util.SecureFileOutputStream; /** * Coordinate the runtime operation and configuration of a tunnel. @@ -89,7 +90,7 @@ public class TunnelController implements Logging { } FileOutputStream fos = null; try { - fos = new FileOutputStream(keyFile); + fos = new SecureFileOutputStream(keyFile); Destination dest = client.createDestination(fos); String destStr = dest.toBase64(); log("Private key created and saved in " + keyFile.getAbsolutePath()); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java index 169f56954..618b035f7 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java @@ -20,6 +20,7 @@ import net.i2p.client.I2PSessionException; import net.i2p.data.DataHelper; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; +import net.i2p.util.SecureFileOutputStream; /** * Coordinate a set of tunnels within the JVM, loading and storing their config @@ -255,7 +256,7 @@ public class TunnelControllerGroup { FileOutputStream fos = null; try { - fos = new FileOutputStream(cfgFile); + fos = new SecureFileOutputStream(cfgFile); fos.write(buf.toString().getBytes("UTF-8")); if (_log.shouldLog(Log.INFO)) _log.info("Config written to " + cfgFile.getPath()); diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java index 38bc8b4db..f6c76d7f6 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java @@ -13,6 +13,11 @@ import java.util.List; import java.util.Properties; import java.util.StringTokenizer; +import net.i2p.data.Base64; +import net.i2p.data.Destination; +import net.i2p.data.PrivateKeyFile; +import net.i2p.data.Signature; +import net.i2p.data.SigningPrivateKey; import net.i2p.i2ptunnel.TunnelController; import net.i2p.i2ptunnel.TunnelControllerGroup; @@ -68,6 +73,31 @@ public class EditBean extends IndexBean { return "i2ptunnel" + tunnel + "-privKeys.dat"; } + public String getNameSignature(int tunnel) { + String spoof = getSpoofedHost(tunnel); + if (spoof.length() <= 0) + return ""; + TunnelController tun = getController(tunnel); + if (tun == null) + return ""; + String keyFile = tun.getPrivKeyFile(); + if (keyFile != null && keyFile.trim().length() > 0) { + PrivateKeyFile pkf = new PrivateKeyFile(keyFile); + try { + Destination d = pkf.getDestination(); + if (d == null) + return ""; + SigningPrivateKey privKey = pkf.getSigningPrivKey(); + if (privKey == null) + return ""; + //System.err.println("Signing " + spoof + " with " + Base64.encode(privKey.getData())); + Signature sig = _context.dsa().sign(spoof.getBytes("UTF-8"), privKey); + return Base64.encode(sig.getData()); + } catch (Exception e) {} + } + return ""; + } + public boolean startAutomatically(int tunnel) { TunnelController tun = getController(tunnel); if (tun != null) diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java index 2a77b9be8..d91a7900d 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -9,6 +9,7 @@ package net.i2p.i2ptunnel.web; */ import java.util.concurrent.ConcurrentHashMap; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -41,10 +42,10 @@ public class IndexBean { protected TunnelControllerGroup _group; private String _action; private int _tunnel; - private long _prevNonce; - private long _prevNonce2; - private long _curNonce; - private long _nextNonce; + //private long _prevNonce; + //private long _prevNonce2; + private String _curNonce; + //private long _nextNonce; private String _type; private String _name; @@ -85,10 +86,14 @@ public class IndexBean { /** deprecated unimplemented, now using routerconsole realm */ //public static final String PROP_TUNNEL_PASSPHRASE = "i2ptunnel.passphrase"; public static final String PROP_TUNNEL_PASSPHRASE = "consolePassword"; - static final String PROP_NONCE = IndexBean.class.getName() + ".nonce"; - static final String PROP_NONCE_OLD = PROP_NONCE + '2'; + //static final String PROP_NONCE = IndexBean.class.getName() + ".nonce"; + //static final String PROP_NONCE_OLD = PROP_NONCE + '2'; + /** 3 wasn't enough for some browsers. They are reloading the page for some reason - maybe HEAD? @since 0.8.1 */ + private static final int MAX_NONCES = 5; + /** store nonces in a static FIFO instead of in System Properties @since 0.8.1 */ + private static final List _nonces = new ArrayList(MAX_NONCES + 1); + static final String CLIENT_NICKNAME = "shared clients"; - public static final String PROP_THEME_NAME = "routerconsole.theme"; public static final String DEFAULT_THEME = "light"; public static final String PROP_CSS_DISABLED = "routerconsole.css.disabled"; @@ -98,34 +103,39 @@ public class IndexBean { _context = I2PAppContext.getGlobalContext(); _log = _context.logManager().getLog(IndexBean.class); _group = TunnelControllerGroup.getInstance(); - _action = null; _tunnel = -1; - _curNonce = -1; - _prevNonce = -1; - _prevNonce2 = -1; - try { - String nonce2 = System.getProperty(PROP_NONCE_OLD); - if (nonce2 != null) - _prevNonce2 = Long.parseLong(nonce2); - String nonce = System.getProperty(PROP_NONCE); - if (nonce != null) { - _prevNonce = Long.parseLong(nonce); - System.setProperty(PROP_NONCE_OLD, nonce); - } - } catch (NumberFormatException nfe) {} - _nextNonce = _context.random().nextLong(); - System.setProperty(PROP_NONCE, Long.toString(_nextNonce)); + _curNonce = "-1"; + addNonce(); _booleanOptions = new ConcurrentHashSet(4); _otherOptions = new ConcurrentHashMap(4); } - public long getNextNonce() { return _nextNonce; } + public static String getNextNonce() { + synchronized (_nonces) { + return _nonces.get(0); + } + } + public void setNonce(String nonce) { if ( (nonce == null) || (nonce.trim().length() <= 0) ) return; - try { - _curNonce = Long.parseLong(nonce); - } catch (NumberFormatException nfe) { - _curNonce = -1; + _curNonce = nonce; + } + + /** add a random nonce to the head of the queue @since 0.8.1 */ + private void addNonce() { + String nextNonce = Long.toString(_context.random().nextLong()); + synchronized (_nonces) { + _nonces.add(0, nextNonce); + int sz = _nonces.size(); + if (sz > MAX_NONCES) + _nonces.remove(sz - 1); + } + } + + /** do we know this nonce? @since 0.8.1 */ + private static boolean haveNonce(String nonce) { + synchronized (_nonces) { + return _nonces.contains(nonce); } } @@ -155,7 +165,7 @@ public class IndexBean { private String processAction() { if ( (_action == null) || (_action.trim().length() <= 0) || ("Cancel".equals(_action))) return ""; - if ( (_prevNonce != _curNonce) && (_prevNonce2 != _curNonce) && (!validPassphrase()) ) + if ( (!haveNonce(_curNonce)) && (!validPassphrase()) ) return "Invalid form submission, probably because you used the 'back' or 'reload' button on your browser. Please resubmit."; if ("Stop all".equals(_action)) return stopAll(); diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp index 939ccf520..97a0a93c9 100644 --- a/apps/i2ptunnel/jsp/editServer.jsp +++ b/apps/i2ptunnel/jsp/editServer.jsp @@ -196,7 +196,16 @@ <%=intl._("Add to local addressbook")%> <% } %> - + + <% if (("httpserver".equals(tunnelType)) || ("httpbidirserver".equals(tunnelType))) { + %>
+ + +
+ <% } %> + diff --git a/apps/i2ptunnel/locale/messages_zh.po b/apps/i2ptunnel/locale/messages_zh.po index 0955d4e0c..f6efbfd3b 100644 --- a/apps/i2ptunnel/locale/messages_zh.po +++ b/apps/i2ptunnel/locale/messages_zh.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: I2P i2ptunnel\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2010-07-01 04:52+0000\n" +"POT-Creation-Date: 2010-10-04 02:45+0000\n" "PO-Revision-Date: 2010-05-29 10:57+0800\n" "Last-Translator: walking \n" "Language-Team: foo \n" @@ -17,7 +17,7 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "X-Poedit-Language: Chinese\n" -#: ../java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java:493 +#: ../java/src/net/i2p/i2ptunnel/I2PTunnelHTTPClient.java:492 #, java-format msgid "" "To visit the destination in your host database, click here + basedir="../jsp/" excludes="web.xml, *.css, **/*.java, *.jsp, *.jsi, web-fragment.xml, web-out.xml"> diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHandler.java index 347cfdab7..69013f60a 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHandler.java @@ -17,6 +17,8 @@ public class ConfigLoggingHandler extends FormHandler { private String _recordFormat; private String _dateFormat; private String _fileSize; + private String _newLogClass; + private String _newLogLevel = "WARN"; @Override protected void processForm() { @@ -26,7 +28,7 @@ public class ConfigLoggingHandler extends FormHandler { // noop } } - + public void setShouldsave(String moo) { _shouldSave = true; } public void setLevels(String levels) { @@ -47,6 +49,18 @@ public class ConfigLoggingHandler extends FormHandler { public void setLogfilesize(String size) { _fileSize = (size != null ? size.trim() : null); } + + /** @since 0.8.1 */ + public void setNewlogclass(String s) { + if (s != null && s.length() > 0) + _newLogClass = s; + } + + /** @since 0.8.1 */ + public void setNewloglevel(String s) { + if (s != null) + _newLogLevel = s; + } /** * The user made changes to the config and wants to save them, so @@ -56,14 +70,18 @@ public class ConfigLoggingHandler extends FormHandler { private void saveChanges() { boolean shouldSave = false; - if (_levels != null) { + if (_levels != null || _newLogClass != null) { try { Properties props = new Properties(); - props.load(new ByteArrayInputStream(_levels.getBytes())); + if (_levels != null) + props.load(new ByteArrayInputStream(_levels.getBytes())); + if (_newLogClass != null) + props.setProperty(_newLogClass, _newLogLevel); _context.logManager().setLimits(props); shouldSave = true; - addFormNotice("Log limits updated"); + addFormNotice(_("Log overrides updated")); } catch (IOException ioe) { + // shouldn't ever happen (BAIS shouldnt cause an IOE) _context.logManager().getLog(ConfigLoggingHandler.class).error("Error reading from the props?", ioe); addFormError("Error updating the log limits - levels not valid"); } @@ -83,7 +101,7 @@ public class ConfigLoggingHandler extends FormHandler { } } - if (_dateFormat != null) { + if (_dateFormat != null && !_dateFormat.equals(_context.logManager().getDateFormatPattern())) { boolean valid = _context.logManager().setDateFormat(_dateFormat); if (valid) { shouldSave = true; @@ -139,7 +157,7 @@ public class ConfigLoggingHandler extends FormHandler { boolean saved = _context.logManager().saveConfig(); if (saved) - addFormNotice("Log configuration saved and applied successfully"); + addFormNotice(_("Log configuration saved")); else addFormNotice("Error saving the configuration (applied but not saved) - please see the error logs"); } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java index bde9b0893..ce41b1e12 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigLoggingHelper.java @@ -1,9 +1,13 @@ package net.i2p.router.web; +import java.util.List; import java.util.Iterator; import java.util.Properties; +import java.util.Set; import java.util.TreeSet; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; public class ConfigLoggingHelper extends HelperBase { public ConfigLoggingHelper() {} @@ -19,18 +23,13 @@ public class ConfigLoggingHelper extends HelperBase { } public String getMaxFileSize() { int bytes = _context.logManager().getFileSize(); - if (bytes == 0) return "1m"; - if (bytes > 1024*1024*1024) - return (bytes/(1024*1024*1024)) + "g"; - else if (bytes > 1024*1024) - return (bytes/(1024*1024)) + "m"; - else - return (bytes/(1024)) + "k"; + if (bytes <= 0) return "1.00 MB"; + return DataHelper.formatSize2(bytes) + 'B'; } public String getLogLevelTable() { StringBuilder buf = new StringBuilder(32*1024); Properties limits = _context.logManager().getLimits(); - TreeSet sortedLogs = new TreeSet(); + TreeSet sortedLogs = new TreeSet(); for (Iterator iter = limits.keySet().iterator(); iter.hasNext(); ) { String prefix = (String)iter.next(); sortedLogs.add(prefix); @@ -46,6 +45,20 @@ public class ConfigLoggingHelper extends HelperBase { buf.append("" + _("Add additional logging statements above. Example: net.i2p.router.tunnel=WARN") + "
"); buf.append("" + _("Or put entries in the logger.config file. Example: logger.record.net.i2p.router.tunnel=WARN") + "
"); buf.append("" + _("Valid levels are DEBUG, INFO, WARN, ERROR, CRIT") + "\n"); + + /**** + // this is too big and ugly + if (limits.size() <= 0) + return ""; + buf.append(""); + for (String prefix : sortedLogs) { + buf.append(""); + } + buf.append("
").append(prefix).append(""); + String level = limits.getProperty(prefix); + buf.append(getLogLevelBox("level-" + prefix, level, true)).append("
"); + ****/ + return buf.toString(); } @@ -53,18 +66,69 @@ public class ConfigLoggingHelper extends HelperBase { public String getDefaultLogLevelBox() { String cur = _context.logManager().getDefaultLimit(); + return getLogLevelBox("defaultloglevel", cur, false); + } + + private String getLogLevelBox(String name, String cur, boolean showRemove) { StringBuilder buf = new StringBuilder(128); - buf.append("\n"); for (int i = 0; i < levels.length; i++) { String l = levels[i]; - buf.append("\n"); } + if (showRemove) + buf.append(""); buf.append("\n"); return buf.toString(); } + + /** + * All the classes the log manager knows about, except ones that + * already have overrides + * @since 0.8.1 + */ + public String getNewClassBox() { + List logs = _context.logManager().getLogs(); + Set limits = _context.logManager().getLimits().keySet(); + TreeSet sortedLogs = new TreeSet(); + + for (Log log : logs) { + String name = log.getName(); + if (!limits.contains(name)) + sortedLogs.add(name); + + // add higher classes of length 3 or more + int dots = 0; + int lastdot = -1; + int nextdot = 0; + while ((nextdot = name.indexOf('.', lastdot + 1)) > 0) { + if (++dots >= 3) { + String subst = name.substring(0, nextdot); + if (!limits.contains(subst)) + sortedLogs.add(subst); + } + lastdot = nextdot; + } + } + + StringBuilder buf = new StringBuilder(65536); + buf.append("\n"); + buf.append(getLogLevelBox("newloglevel", "WARN", false)); + return buf.toString(); + } } diff --git a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java index d6eb2edd1..f499b2ea4 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/LogsHelper.java @@ -44,13 +44,16 @@ public class LogsHelper extends HelperBase { } ******/ - private String formatMessages(List msgs) { + /** formats in reverse order */ + private String formatMessages(List msgs) { + if (msgs.isEmpty()) + return "

" + _("No log messages") + "

"; boolean colorize = Boolean.valueOf(_context.getProperty("routerconsole.logs.color")).booleanValue(); StringBuilder buf = new StringBuilder(16*1024); buf.append("