diff --git a/apps/i2psnark/_icons/magnet.png b/apps/i2psnark/_icons/magnet.png new file mode 100644 index 0000000000000000000000000000000000000000..3430f2e5a9c08050cb90248f42315876fd04cb6a Binary files /dev/null and b/apps/i2psnark/_icons/magnet.png differ diff --git a/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java index 52099d1d2e67bc5e657260ccb598a55369bf35a0..b4ba0f2996acd956d728a8e2d792d7a47dd16024 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java +++ b/apps/i2psnark/java/src/org/klomp/snark/ConnectionAcceptor.java @@ -137,6 +137,11 @@ public class ConnectionAcceptor implements Runnable } } } else { + if (socket.getPeerDestination().equals(_util.getMyDestination())) { + _util.debug("Incoming connection from myself", Snark.ERROR); + try { socket.close(); } catch (IOException ioe) {} + continue; + } Thread t = new I2PAppThread(new Handler(socket), "I2PSnark incoming connection"); t.start(); } @@ -174,11 +179,8 @@ public class ConnectionAcceptor implements Runnable try { InputStream in = _socket.getInputStream(); OutputStream out = _socket.getOutputStream(); - - if (true) { - in = new BufferedInputStream(in); - //out = new BufferedOutputStream(out); - } + // this is for the readahead in PeerAcceptor.connection() + in = new BufferedInputStream(in); if (_log.shouldLog(Log.DEBUG)) _log.debug("Handling socket from " + _socket.getPeerDestination().calculateHash().toBase64()); peeracceptor.connection(_socket, in, out); diff --git a/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java index a86afa781b6c32d7cd64bbd4d1d6920153c8aaa0..478c17bb507832aa2bdcd12a799513e870714380 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java +++ b/apps/i2psnark/java/src/org/klomp/snark/CoordinatorListener.java @@ -31,6 +31,12 @@ public interface CoordinatorListener */ void peerChange(PeerCoordinator coordinator, Peer peer); + /** + * Called when the PeerCoordinator got the MetaInfo via magnet. + * @since 0.8.4 + */ + void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo); + public boolean overUploadLimit(int uploaders); public boolean overUpBWLimit(); public boolean overUpBWLimit(long total); diff --git a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..b3770070f4e99436f747a1ca8aee559b64bc51f5 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandler.java @@ -0,0 +1,361 @@ +package org.klomp.snark; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.util.Log; + +import org.klomp.snark.bencode.BDecoder; +import org.klomp.snark.bencode.BEncoder; +import org.klomp.snark.bencode.BEValue; +import org.klomp.snark.bencode.InvalidBEncodingException; + +/** + * REF: BEP 10 Extension Protocol + * @since 0.8.2 + * @author zzz + */ +abstract class ExtensionHandler { + + private static final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(ExtensionHandler.class); + + public static final int ID_HANDSHAKE = 0; + public static final int ID_METADATA = 1; + public static final String TYPE_METADATA = "ut_metadata"; + public static final int ID_PEX = 2; + /** not ut_pex since the compact format is different */ + public static final String TYPE_PEX = "i2p_pex"; + /** Pieces * SHA1 Hash length, + 25% extra for file names, benconding overhead, etc */ + private static final int MAX_METADATA_SIZE = Storage.MAX_PIECES * 20 * 5 / 4; + private static final int PARALLEL_REQUESTS = 3; + + + /** + * @param metasize -1 if unknown + * @return bencoded outgoing handshake message + */ + public static byte[] getHandshake(int metasize) { + Map<String, Object> handshake = new HashMap(); + Map<String, Integer> m = new HashMap(); + m.put(TYPE_METADATA, Integer.valueOf(ID_METADATA)); + m.put(TYPE_PEX, Integer.valueOf(ID_PEX)); + if (metasize >= 0) + handshake.put("metadata_size", Integer.valueOf(metasize)); + handshake.put("m", m); + handshake.put("p", Integer.valueOf(6881)); + handshake.put("v", "I2PSnark"); + handshake.put("reqq", Integer.valueOf(5)); + return BEncoder.bencode(handshake); + } + + public static void handleMessage(Peer peer, PeerListener listener, int id, byte[] bs) { + if (_log.shouldLog(Log.INFO)) + _log.info("Got extension msg " + id + " length " + bs.length + " from " + peer); + if (id == ID_HANDSHAKE) + handleHandshake(peer, listener, bs); + else if (id == ID_METADATA) + handleMetadata(peer, listener, bs); + else if (id == ID_PEX) + handlePEX(peer, listener, bs); + else if (_log.shouldLog(Log.INFO)) + _log.info("Unknown extension msg " + id + " from " + peer); + } + + private static void handleHandshake(Peer peer, PeerListener listener, byte[] bs) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Got handshake msg from " + peer); + try { + // this throws NPE on missing keys + InputStream is = new ByteArrayInputStream(bs); + BDecoder dec = new BDecoder(is); + BEValue bev = dec.bdecodeMap(); + Map<String, BEValue> map = bev.getMap(); + peer.setHandshakeMap(map); + Map<String, BEValue> msgmap = map.get("m").getMap(); + + if (msgmap.get(TYPE_PEX) != null) { + if (_log.shouldLog(Log.WARN)) + _log.debug("Peer supports PEX extension: " + peer); + // peer state calls peer listener calls sendPEX() + } + + MagnetState state = peer.getMagnetState(); + + if (msgmap.get(TYPE_METADATA) == null) { + if (_log.shouldLog(Log.WARN)) + _log.debug("Peer does not support metadata extension: " + peer); + // drop if we need metainfo and we haven't found anybody yet + synchronized(state) { + if (!state.isInitialized()) { + _log.debug("Dropping peer, we need metadata! " + peer); + peer.disconnect(); + } + } + return; + } + + BEValue msize = map.get("metadata_size"); + if (msize == null) { + if (_log.shouldLog(Log.WARN)) + _log.debug("Peer does not have the metainfo size yet: " + peer); + // drop if we need metainfo and we haven't found anybody yet + synchronized(state) { + if (!state.isInitialized()) { + _log.debug("Dropping peer, we need metadata! " + peer); + peer.disconnect(); + } + } + return; + } + int metaSize = msize.getInt(); + if (_log.shouldLog(Log.WARN)) + _log.debug("Got the metainfo size: " + metaSize); + + int remaining; + synchronized(state) { + if (state.isComplete()) + return; + + if (state.isInitialized()) { + if (state.getSize() != metaSize) { + if (_log.shouldLog(Log.WARN)) + _log.debug("Wrong metainfo size " + metaSize + " from: " + peer); + peer.disconnect(); + return; + } + } else { + // initialize it + if (metaSize > MAX_METADATA_SIZE) { + if (_log.shouldLog(Log.WARN)) + _log.debug("Huge metainfo size " + metaSize + " from: " + peer); + peer.disconnect(false); + return; + } + if (_log.shouldLog(Log.INFO)) + _log.info("Initialized state, metadata size = " + metaSize + " from " + peer); + state.initialize(metaSize); + } + remaining = state.chunksRemaining(); + } + + // send requests for chunks + int count = Math.min(remaining, PARALLEL_REQUESTS); + for (int i = 0; i < count; i++) { + int chk; + synchronized(state) { + chk = state.getNextRequest(); + } + if (_log.shouldLog(Log.INFO)) + _log.info("Request chunk " + chk + " from " + peer); + sendRequest(peer, chk); + } + } catch (Exception e) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Handshake exception from " + peer, e); + } + } + + private static final int TYPE_REQUEST = 0; + private static final int TYPE_DATA = 1; + private static final int TYPE_REJECT = 2; + + private static final int CHUNK_SIZE = 16*1024; + + /** + * REF: BEP 9 + * @since 0.8.4 + */ + private static void handleMetadata(Peer peer, PeerListener listener, byte[] bs) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Got metadata msg from " + peer); + try { + InputStream is = new ByteArrayInputStream(bs); + BDecoder dec = new BDecoder(is); + BEValue bev = dec.bdecodeMap(); + Map<String, BEValue> map = bev.getMap(); + int type = map.get("msg_type").getInt(); + int piece = map.get("piece").getInt(); + + MagnetState state = peer.getMagnetState(); + if (type == TYPE_REQUEST) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Got request for " + piece + " from: " + peer); + byte[] pc; + synchronized(state) { + pc = state.getChunk(piece); + } + sendPiece(peer, piece, pc); + // Do this here because PeerConnectionOut only reports for PIECE messages + peer.uploaded(pc.length); + listener.uploaded(peer, pc.length); + } else if (type == TYPE_DATA) { + int size = map.get("total_size").getInt(); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Got data for " + piece + " length " + size + " from: " + peer); + boolean done; + int chk = -1; + synchronized(state) { + if (state.isComplete()) + return; + int len = is.available(); + if (len != size) { + // probably fatal + if (_log.shouldLog(Log.WARN)) + _log.warn("total_size " + size + " but avail data " + len); + } + peer.downloaded(len); + listener.downloaded(peer, len); + done = state.saveChunk(piece, bs, bs.length - len, len); + if (_log.shouldLog(Log.INFO)) + _log.info("Got chunk " + piece + " from " + peer); + if (!done) + chk = state.getNextRequest(); + } + // out of the lock + if (done) { + // Done! + // PeerState will call the listener (peer coord), who will + // check to see if the MagnetState has it + if (_log.shouldLog(Log.WARN)) + _log.warn("Got last chunk from " + peer); + } else { + // get the next chunk + if (_log.shouldLog(Log.INFO)) + _log.info("Request chunk " + chk + " from " + peer); + sendRequest(peer, chk); + } + } else if (type == TYPE_REJECT) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Got reject msg from " + peer); + peer.disconnect(false); + } else { + if (_log.shouldLog(Log.WARN)) + _log.warn("Got unknown metadata msg from " + peer); + peer.disconnect(false); + } + } catch (Exception e) { + if (_log.shouldLog(Log.WARN)) + _log.info("Metadata ext. msg. exception from " + peer, e); + // fatal ? + peer.disconnect(false); + } + } + + private static void sendRequest(Peer peer, int piece) { + sendMessage(peer, TYPE_REQUEST, piece); + } + + private static void sendReject(Peer peer, int piece) { + sendMessage(peer, TYPE_REJECT, piece); + } + + /** REQUEST and REJECT are the same except for message type */ + private static void sendMessage(Peer peer, int type, int piece) { + Map<String, Object> map = new HashMap(); + map.put("msg_type", Integer.valueOf(type)); + map.put("piece", Integer.valueOf(piece)); + byte[] payload = BEncoder.bencode(map); + try { + int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_METADATA).getInt(); + peer.sendExtension(hisMsgCode, payload); + } catch (Exception e) { + // NPE, no metadata capability + if (_log.shouldLog(Log.WARN)) + _log.info("Metadata send req msg exception to " + peer, e); + } + } + + private static void sendPiece(Peer peer, int piece, byte[] data) { + Map<String, Object> map = new HashMap(); + map.put("msg_type", Integer.valueOf(TYPE_DATA)); + map.put("piece", Integer.valueOf(piece)); + map.put("total_size", Integer.valueOf(data.length)); + byte[] dict = BEncoder.bencode(map); + byte[] payload = new byte[dict.length + data.length]; + System.arraycopy(dict, 0, payload, 0, dict.length); + System.arraycopy(data, 0, payload, dict.length, data.length); + try { + int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_METADATA).getInt(); + peer.sendExtension(hisMsgCode, payload); + } catch (Exception e) { + // NPE, no metadata caps + if (_log.shouldLog(Log.WARN)) + _log.info("Metadata send piece msg exception to " + peer, e); + } + } + + private static final int HASH_LENGTH = 32; + + /** + * Can't find a published standard for this anywhere. + * See the libtorrent code. + * Here we use the "added" key as a single string of concatenated + * 32-byte peer hashes. + * added.f and dropped unsupported + * @since 0.8.4 + */ + private static void handlePEX(Peer peer, PeerListener listener, byte[] bs) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Got PEX msg from " + peer); + try { + InputStream is = new ByteArrayInputStream(bs); + BDecoder dec = new BDecoder(is); + BEValue bev = dec.bdecodeMap(); + Map<String, BEValue> map = bev.getMap(); + byte[] ids = map.get("added").getBytes(); + if (ids.length < HASH_LENGTH) + return; + int len = Math.min(ids.length, (I2PSnarkUtil.MAX_CONNECTIONS - 1) * HASH_LENGTH); + List<PeerID> peers = new ArrayList(len / HASH_LENGTH); + for (int off = 0; off < len; off += HASH_LENGTH) { + byte[] hash = new byte[HASH_LENGTH]; + System.arraycopy(ids, off, hash, 0, HASH_LENGTH); + if (DataHelper.eq(hash, peer.getPeerID().getDestHash())) + continue; + PeerID pID = new PeerID(hash); + peers.add(pID); + } + // could include ourselves, listener must remove + listener.gotPeers(peer, peers); + } catch (Exception e) { + if (_log.shouldLog(Log.WARN)) + _log.info("PEX msg exception from " + peer, e); + //peer.disconnect(false); + } + } + + /** + * added.f and dropped unsupported + * @param pList non-null + * @since 0.8.4 + */ + public static void sendPEX(Peer peer, List<Peer> pList) { + if (pList.isEmpty()) + return; + Map<String, Object> map = new HashMap(); + byte[] peers = new byte[HASH_LENGTH * pList.size()]; + int off = 0; + for (Peer p : pList) { + System.arraycopy(p.getPeerID().getDestHash(), 0, peers, off, HASH_LENGTH); + off += HASH_LENGTH; + } + map.put("added", peers); + byte[] payload = BEncoder.bencode(map); + try { + int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_PEX).getInt(); + peer.sendExtension(hisMsgCode, payload); + } catch (Exception e) { + // NPE, no PEX caps + if (_log.shouldLog(Log.WARN)) + _log.info("PEX msg exception to " + peer, e); + } + } + +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandshake.java b/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandshake.java deleted file mode 100644 index fb69b044d284a7414f74b3e58a0b5773fd1c8419..0000000000000000000000000000000000000000 --- a/apps/i2psnark/java/src/org/klomp/snark/ExtensionHandshake.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.klomp.snark; - -import java.util.HashMap; -import java.util.Map; - -import org.klomp.snark.bencode.BEncoder; -import org.klomp.snark.bencode.BEValue; - -/** - * REF: BEP 10 Extension Protocol - * @since 0.8.2 - */ -class ExtensionHandshake { - - private static final byte[] _payload = buildPayload(); - - /** - * @return bencoded data - */ - static byte[] getPayload() { - return _payload; - } - - /** just a test for now */ - private static byte[] buildPayload() { - Map<String, Object> handshake = new HashMap(); - Map<String, Integer> m = new HashMap(); - m.put("foo", Integer.valueOf(99)); - m.put("bar", Integer.valueOf(101)); - handshake.put("m", m); - handshake.put("p", Integer.valueOf(6881)); - handshake.put("v", "I2PSnark"); - handshake.put("reqq", Integer.valueOf(5)); - return BEncoder.bencode(handshake); - } -} diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index b36c0fdcbf493cb682fb3bc28d5d56f7b744027f..13bda2e5575013d204b5e9f88fd856c51abfdb62 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -34,6 +34,9 @@ import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; import net.i2p.util.Translate; +import org.klomp.snark.dht.DHT; +//import org.klomp.snark.dht.KRPC; + /** * I2P specific helpers for I2PSnark * We use this class as a sort of context for i2psnark @@ -58,6 +61,8 @@ public class I2PSnarkUtil { private int _maxConnections; private File _tmpDir; private int _startupDelay; + private boolean _shouldUseOT; + private DHT _dht; public static final int DEFAULT_STARTUP_DELAY = 3; public static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers"; @@ -66,6 +71,9 @@ public class I2PSnarkUtil { public static final String DEFAULT_OPENTRACKERS = "http://tracker.welterde.i2p/a"; public static final int DEFAULT_MAX_UP_BW = 8; //KBps public static final int MAX_CONNECTIONS = 16; // per torrent + private static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond"; + //private static final boolean ENABLE_DHT = true; + public I2PSnarkUtil(I2PAppContext ctx) { _context = ctx; _log = _context.logManager().getLog(Snark.class); @@ -78,6 +86,7 @@ public class I2PSnarkUtil { _maxUpBW = DEFAULT_MAX_UP_BW; _maxConnections = MAX_CONNECTIONS; _startupDelay = DEFAULT_STARTUP_DELAY; + _shouldUseOT = DEFAULT_USE_OPENTRACKERS; // 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 @@ -124,9 +133,21 @@ public class I2PSnarkUtil { _configured = true; } + /** + * @param KBps + */ public void setMaxUpBW(int limit) { _maxUpBW = limit; + _opts.put(PROP_MAX_BW, Integer.toString(limit * (1024 * 6 / 5))); // add a little for overhead _configured = true; + if (_manager != null) { + I2PSession sess = _manager.getSession(); + if (sess != null) { + Properties newProps = new Properties(); + newProps.putAll(_opts); + sess.updateOptions(newProps); + } + } } public void setMaxConnections(int limit) { @@ -146,6 +167,10 @@ public class I2PSnarkUtil { public int getEepProxyPort() { return _proxyPort; } public boolean getEepProxySet() { return _shouldProxy; } public int getMaxUploaders() { return _maxUploaders; } + + /** + * @return KBps + */ public int getMaxUpBW() { return _maxUpBW; } public int getMaxConnections() { return _maxConnections; } public int getStartupDelay() { return _startupDelay; } @@ -158,7 +183,7 @@ public class I2PSnarkUtil { // try to find why reconnecting after stop if (_log.shouldLog(Log.DEBUG)) _log.debug("Connecting to I2P", new Exception("I did it")); - Properties opts = new Properties(); + Properties opts = _context.getProperties(); if (_opts != null) { for (Iterator iter = _opts.keySet().iterator(); iter.hasNext(); ) { String key = (String)iter.next(); @@ -187,10 +212,20 @@ public class I2PSnarkUtil { // opts.setProperty("i2p.streaming.readTimeout", "120000"); _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts); } + // FIXME this only instantiates krpc once, left stuck with old manager + //if (ENABLE_DHT && _manager != null && _dht == null) + // _dht = new KRPC(_context, _manager.getSession()); return (_manager != null); } + /** + * @return null if disabled or not started + * @since 0.8.4 + */ + public DHT getDHT() { return _dht; } + public boolean connected() { return _manager != null; } + /** * Destroy the destination itself */ @@ -214,6 +249,8 @@ public class I2PSnarkUtil { Destination addr = peer.getAddress(); if (addr == null) throw new IOException("Null address"); + if (addr.equals(getMyDestination())) + throw new IOException("Attempt to connect to myself"); Hash dest = addr.calculateHash(); if (_shitlist.contains(dest)) throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are shitlisted"); @@ -287,15 +324,23 @@ public class I2PSnarkUtil { } String getOurIPString() { + Destination dest = getMyDestination(); + if (dest != null) + return dest.toBase64(); + return "unknown"; + } + + /** + * @return dest or null + * @since 0.8.4 + */ + Destination getMyDestination() { if (_manager == null) - return "unknown"; + return null; I2PSession sess = _manager.getSession(); - if (sess != null) { - Destination dest = sess.getMyDestination(); - if (dest != null) - return dest.toBase64(); - } - return "unknown"; + if (sess != null) + return sess.getMyDestination(); + return null; } /** Base64 only - static (no naming service) */ @@ -400,10 +445,10 @@ public class I2PSnarkUtil { /** comma delimited list open trackers to use as backups */ /** sorted map of name to announceURL=baseURL */ - public List getOpenTrackers() { + public List<String> getOpenTrackers() { if (!shouldUseOpenTrackers()) return null; - List rv = new ArrayList(1); + List<String> rv = new ArrayList(1); String trackers = getOpenTrackerString(); StringTokenizer tok = new StringTokenizer(trackers, ", "); while (tok.hasMoreTokens()) @@ -414,11 +459,27 @@ public class I2PSnarkUtil { return rv; } + public void setUseOpenTrackers(boolean yes) { + _shouldUseOT = yes; + } + public boolean shouldUseOpenTrackers() { - String rv = (String) _opts.get(PROP_USE_OPENTRACKERS); - if (rv == null) - return DEFAULT_USE_OPENTRACKERS; - return Boolean.valueOf(rv).booleanValue(); + return _shouldUseOT; + } + + /** + * Like DataHelper.toHexString but ensures no loss of leading zero bytes + * @since 0.8.4 + */ + public static String toHex(byte[] b) { + StringBuilder buf = new StringBuilder(40); + for (int i = 0; i < b.length; i++) { + int bi = b[i] & 0xff; + if (bi < 16) + buf.append('0'); + buf.append(Integer.toHexString(bi)); + } + return buf.toString(); } /** hook between snark's logger and an i2p log */ diff --git a/apps/i2psnark/java/src/org/klomp/snark/MagnetState.java b/apps/i2psnark/java/src/org/klomp/snark/MagnetState.java new file mode 100644 index 0000000000000000000000000000000000000000..e9eb5638963a309ccf12c123341bd1abee19c5e1 --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/MagnetState.java @@ -0,0 +1,204 @@ +package org.klomp.snark; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; + +import org.klomp.snark.bencode.BDecoder; +import org.klomp.snark.bencode.BEValue; + +/** + * Simple state for the download of the metainfo, shared between + * Peer and ExtensionHandler. + * + * Nothing is synchronized here! + * Caller must synchronize on this for everything! + * + * Reference: BEP 9 + * + * @since 0.8.4 + * author zzz + */ +class MagnetState { + public static final int CHUNK_SIZE = 16*1024; + private static final Random random = I2PAppContext.getGlobalContext().random(); + + private final byte[] infohash; + private boolean complete; + /** if false, nothing below is valid */ + private boolean isInitialized; + + private int metaSize; + private int totalChunks; + /** bitfield for the metainfo chunks - will remain null if we start out complete */ + private BitField requested; + private BitField have; + /** bitfield for the metainfo */ + private byte[] metainfoBytes; + /** only valid when finished */ + private MetaInfo metainfo; + + /** + * @param meta null for new magnet + */ + public MagnetState(byte[] iHash, MetaInfo meta) { + infohash = iHash; + if (meta != null) { + metainfo = meta; + initialize(meta.getInfoBytes().length); + complete = true; + } + } + + /** + * @param call this for a new magnet when you have the size + * @throws IllegalArgumentException + */ + public void initialize(int size) { + if (isInitialized) + throw new IllegalArgumentException("already set"); + isInitialized = true; + metaSize = size; + totalChunks = (size + (CHUNK_SIZE - 1)) / CHUNK_SIZE; + if (metainfo != null) { + metainfoBytes = metainfo.getInfoBytes(); + } else { + // we don't need these if complete + have = new BitField(totalChunks); + requested = new BitField(totalChunks); + metainfoBytes = new byte[metaSize]; + } + } + + /** + * @param Call this for a new magnet when the download is complete. + * @throws IllegalArgumentException + */ + public void setMetaInfo(MetaInfo meta) { + metainfo = meta; + } + + /** + * @throws IllegalArgumentException + */ + public MetaInfo getMetaInfo() { + if (!complete) + throw new IllegalArgumentException("not complete"); + return metainfo; + } + + /** + * @throws IllegalArgumentException + */ + public int getSize() { + if (!isInitialized) + throw new IllegalArgumentException("not initialized"); + return metaSize; + } + + public boolean isInitialized() { + return isInitialized; + } + + public boolean isComplete() { + return complete; + } + + public int chunkSize(int chunk) { + return Math.min(CHUNK_SIZE, metaSize - (chunk * CHUNK_SIZE)); + } + + /** @return chunk count */ + public int chunksRemaining() { + if (!isInitialized) + throw new IllegalArgumentException("not initialized"); + if (complete) + return 0; + return totalChunks - have.count(); + } + + /** @return chunk number */ + public int getNextRequest() { + if (!isInitialized) + throw new IllegalArgumentException("not initialized"); + if (complete) + throw new IllegalArgumentException("complete"); + int rand = random.nextInt(totalChunks); + for (int i = 0; i < totalChunks; i++) { + int chk = (i + rand) % totalChunks; + if (!(have.get(chk) || requested.get(chk))) { + requested.set(chk); + return chk; + } + } + // all requested - end game + for (int i = 0; i < totalChunks; i++) { + int chk = (i + rand) % totalChunks; + if (!have.get(chk)) + return chk; + } + throw new IllegalArgumentException("complete"); + } + + /** + * @throws IllegalArgumentException + */ + public byte[] getChunk(int chunk) { + if (!complete) + throw new IllegalArgumentException("not complete"); + if (chunk < 0 || chunk >= totalChunks) + throw new IllegalArgumentException("bad chunk number"); + int size = chunkSize(chunk); + byte[] rv = new byte[size]; + System.arraycopy(metainfoBytes, chunk * CHUNK_SIZE, rv, 0, size); + // use meta.getInfoBytes() so we don't save it in memory + return rv; + } + + /** + * @return true if this was the last piece + * @throws NPE, IllegalArgumentException, IOException, ... + */ + public boolean saveChunk(int chunk, byte[] data, int off, int length) throws Exception { + if (!isInitialized) + throw new IllegalArgumentException("not initialized"); + if (chunk < 0 || chunk >= totalChunks) + throw new IllegalArgumentException("bad chunk number"); + if (have.get(chunk)) + return false; // shouldn't happen if synced + int size = chunkSize(chunk); + if (size != length) + throw new IllegalArgumentException("bad chunk length"); + System.arraycopy(data, off, metainfoBytes, chunk * CHUNK_SIZE, size); + have.set(chunk); + boolean done = have.complete(); + if (done) { + metainfo = buildMetaInfo(); + complete = true; + } + return done; + } + + /** + * @return true if this was the last piece + * @throws NPE, IllegalArgumentException, IOException, ... + */ + public MetaInfo buildMetaInfo() throws Exception { + // top map has nothing in it but the info map (no announce) + Map<String, Object> map = new HashMap(); + InputStream is = new ByteArrayInputStream(metainfoBytes); + BDecoder dec = new BDecoder(is); + BEValue bev = dec.bdecodeMap(); + map.put("info", bev); + MetaInfo newmeta = new MetaInfo(map); + if (!DataHelper.eq(newmeta.getInfoHash(), infohash)) + throw new IOException("info hash mismatch"); + return newmeta; + } +} diff --git a/apps/i2psnark/java/src/org/klomp/snark/Message.java b/apps/i2psnark/java/src/org/klomp/snark/Message.java index a9d1e23f2532508d0f5a904d14f45253328e4497..b4344f3ba588a68049fd08aa52bda99da4d3e3b6 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Message.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Message.java @@ -53,6 +53,7 @@ class Message // Used for HAVE, REQUEST, PIECE and CANCEL messages. // low byte used for EXTENSION message + // low two bytes used for PORT message int piece; // Used for REQUEST, PIECE and CANCEL messages. @@ -67,7 +68,8 @@ class Message // Used to do deferred fetch of data DataLoader dataLoader; - SimpleTimer.TimedEvent expireEvent; + // now unused + //SimpleTimer.TimedEvent expireEvent; /** Utility method for sending a message through a DataStream. */ void sendMessage(DataOutputStream dos) throws IOException @@ -103,10 +105,13 @@ class Message if (type == REQUEST || type == CANCEL) datalen += 4; - // length is 1 byte + // msg type is 1 byte if (type == EXTENSION) datalen += 1; + if (type == PORT) + datalen += 2; + // add length of data for piece or bitfield array. if (type == BITFIELD || type == PIECE || type == EXTENSION) datalen += len; @@ -130,6 +135,9 @@ class Message if (type == EXTENSION) dos.writeByte((byte) piece & 0xff); + if (type == PORT) + dos.writeShort(piece & 0xffff); + // Send actual data if (type == BITFIELD || type == PIECE || type == EXTENSION) dos.write(data, off, len); @@ -160,6 +168,8 @@ class Message return "PIECE(" + piece + "," + begin + "," + length + ")"; case CANCEL: return "CANCEL(" + piece + "," + begin + "," + length + ")"; + case PORT: + return "PORT(" + piece + ")"; case EXTENSION: return "EXTENSION(" + piece + ',' + data.length + ')'; default: diff --git a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java index ad2680045f19256d440de1db1860d482c925892c..3e13c7a421ee81606da5a67181ec3c3494a707b7 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/MetaInfo.java @@ -25,6 +25,7 @@ import java.io.InputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -53,37 +54,43 @@ public class MetaInfo private final byte[] info_hash; private final String name; private final String name_utf8; - private final List files; - private final List files_utf8; - private final List lengths; + private final List<List<String>> files; + private final List<List<String>> files_utf8; + private final List<Long> lengths; private final int piece_length; private final byte[] piece_hashes; private final long length; - private final Map infoMap; + private Map<String, BEValue> infoMap; - private byte[] torrentdata; - - MetaInfo(String announce, String name, String name_utf8, List files, List lengths, + /** + * Called by Storage when creating a new torrent from local data + * + * @param announce may be null + * @param files null for single-file torrent + * @param lengths null for single-file torrent + */ + MetaInfo(String announce, String name, String name_utf8, List<List<String>> files, List<Long> lengths, int piece_length, byte[] piece_hashes, long length) { this.announce = announce; this.name = name; this.name_utf8 = name_utf8; - this.files = files; + this.files = files == null ? null : Collections.unmodifiableList(files); this.files_utf8 = null; - this.lengths = lengths; + this.lengths = lengths == null ? null : Collections.unmodifiableList(lengths); this.piece_length = piece_length; this.piece_hashes = piece_hashes; this.length = length; this.info_hash = calculateInfoHash(); - infoMap = null; + //infoMap = null; } /** * Creates a new MetaInfo from the given InputStream. The * InputStream must start with a correctly bencoded dictonary * describing the torrent. + * Caller must close the stream. */ public MetaInfo(InputStream in) throws IOException { @@ -104,23 +111,29 @@ public class MetaInfo * Creates a new MetaInfo from a Map of BEValues and the SHA1 over * the original bencoded info dictonary (this is a hack, we could * reconstruct the bencoded stream and recalculate the hash). Will - * throw a InvalidBEncodingException if the given map does not - * contain a valid announce string or info dictonary. + * NOT throw a InvalidBEncodingException if the given map does not + * contain a valid announce string. + * WILL throw a InvalidBEncodingException if the given map does not + * contain a valid info dictionary. */ public MetaInfo(Map m) throws InvalidBEncodingException { 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"); - this.announce = val.getString(); + // Disabled check, we can get info from a magnet now + if (val == null) { + //throw new InvalidBEncodingException("Missing announce string"); + this.announce = null; + } else { + this.announce = val.getString(); + } val = (BEValue)m.get("info"); if (val == null) throw new InvalidBEncodingException("Missing info map"); Map info = val.getMap(); - infoMap = info; + infoMap = Collections.unmodifiableMap(info); val = (BEValue)info.get("name"); if (val == null) @@ -160,39 +173,39 @@ public class MetaInfo throw new InvalidBEncodingException ("Missing length number and/or files list"); - List list = val.getList(); + List<BEValue> list = val.getList(); int size = list.size(); if (size == 0) throw new InvalidBEncodingException("zero size files list"); - files = new ArrayList(size); - files_utf8 = new ArrayList(size); - lengths = new ArrayList(size); + List<List<String>> m_files = new ArrayList(size); + List<List<String>> m_files_utf8 = new ArrayList(size); + List<Long> m_lengths = new ArrayList(size); long l = 0; for (int i = 0; i < list.size(); i++) { - Map desc = ((BEValue)list.get(i)).getMap(); - val = (BEValue)desc.get("length"); + Map<String, BEValue> desc = list.get(i).getMap(); + val = desc.get("length"); if (val == null) throw new InvalidBEncodingException("Missing length number"); long len = val.getLong(); - lengths.add(new Long(len)); + m_lengths.add(Long.valueOf(len)); l += len; val = (BEValue)desc.get("path"); if (val == null) throw new InvalidBEncodingException("Missing path list"); - List path_list = val.getList(); + List<BEValue> path_list = val.getList(); int path_length = path_list.size(); if (path_length == 0) throw new InvalidBEncodingException("zero size file path list"); - List file = new ArrayList(path_length); - Iterator it = path_list.iterator(); + List<String> file = new ArrayList(path_length); + Iterator<BEValue> it = path_list.iterator(); while (it.hasNext()) - file.add(((BEValue)it.next()).getString()); + file.add(it.next().getString()); - files.add(file); + m_files.add(Collections.unmodifiableList(file)); val = (BEValue)desc.get("path.utf-8"); if (val != null) { @@ -202,11 +215,14 @@ public class MetaInfo file = new ArrayList(path_length); it = path_list.iterator(); while (it.hasNext()) - file.add(((BEValue)it.next()).getString()); - files_utf8.add(file); + file.add(it.next().getString()); + m_files_utf8.add(Collections.unmodifiableList(file)); } } } + files = Collections.unmodifiableList(m_files); + files_utf8 = Collections.unmodifiableList(m_files_utf8); + lengths = Collections.unmodifiableList(m_lengths); length = l; } @@ -215,6 +231,7 @@ public class MetaInfo /** * Returns the string representing the URL of the tracker for this torrent. + * @return may be null! */ public String getAnnounce() { @@ -253,9 +270,8 @@ public class MetaInfo * a single name. It has the same size as the list returned by * getLengths(). */ - public List getFiles() + public List<List<String>> getFiles() { - // XXX - Immutable? return files; } @@ -264,9 +280,8 @@ public class MetaInfo * files, or null if it is a single file. It has the same size as * the list returned by getFiles(). */ - public List getLengths() + public List<Long> getLengths() { - // XXX - Immutable? return lengths; } @@ -388,33 +403,42 @@ public class MetaInfo piece_hashes, length); } - public byte[] getTorrentData() + /** + * Called by servlet to save a new torrent file generated from local data + */ + public synchronized byte[] getTorrentData() { - if (torrentdata == null) - { Map m = new HashMap(); - m.put("announce", announce); + if (announce != null) + m.put("announce", announce); Map info = createInfoMap(); m.put("info", info); - torrentdata = BEncoder.bencode(m); - } - return torrentdata; + // don't save this locally, we should only do this once + return BEncoder.bencode(m); } - private Map createInfoMap() + /** @since 0.8.4 */ + public synchronized byte[] getInfoBytes() { + if (infoMap == null) + createInfoMap(); + return BEncoder.bencode(infoMap); + } + + /** @return an unmodifiable view of the Map */ + private Map<String, BEValue> createInfoMap() { + // if we loaded this metainfo from a file, we have the map + if (infoMap != null) + return Collections.unmodifiableMap(infoMap); + // otherwise we must create it Map info = new HashMap(); - if (infoMap != null) { - info.putAll(infoMap); - return info; - } info.put("name", name); if (name_utf8 != null) info.put("name.utf-8", name_utf8); info.put("piece length", Integer.valueOf(piece_length)); info.put("pieces", piece_hashes); if (files == null) - info.put("length", new Long(length)); + info.put("length", Long.valueOf(length)); else { List l = new ArrayList(); @@ -429,7 +453,8 @@ public class MetaInfo } info.put("files", l); } - return info; + infoMap = info; + return Collections.unmodifiableMap(infoMap); } private byte[] calculateInfoHash() diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java index cd46bbf8b6a4b26da32515e6335700bf6fdaeef7..5489148121228bab903e2c4ac79ef7c1c03b979b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java @@ -20,7 +20,6 @@ package org.klomp.snark; -import java.io.BufferedInputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; @@ -28,28 +27,44 @@ import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.List; +import java.util.Map; +import net.i2p.I2PAppContext; import net.i2p.client.streaming.I2PSocket; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; import net.i2p.util.Log; +import org.klomp.snark.bencode.BEValue; + public class Peer implements Comparable { - private Log _log = new Log(Peer.class); + private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(Peer.class); // Identifying property, the peer id of the other side. private final PeerID peerID; private final byte[] my_id; - final MetaInfo metainfo; + private final byte[] infohash; + /** will start out null in magnet mode */ + private MetaInfo metainfo; + private Map<String, BEValue> handshakeMap; // The data in/output streams set during the handshake and used by // the actual connections. private DataInputStream din; private DataOutputStream dout; + /** running counters */ + private long downloaded; + private long uploaded; + // Keeps state for in/out connections. Non-null when the handshake // was successful, the connection setup and runs PeerState state; + /** shared across all peers on this torrent */ + MagnetState magnetState; + private I2PSocket sock; private boolean deregister = true; @@ -64,18 +79,22 @@ public class Peer implements Comparable static final long OPTION_EXTENSION = 0x0000000000100000l; static final long OPTION_FAST = 0x0000000000000004l; static final long OPTION_DHT = 0x0000000000000001l; + /** we use a different bit since the compact format is different */ + static final long OPTION_I2P_DHT = 0x0000000040000000l; + static final long OPTION_AZMP = 0x1000000000000000l; private long options; /** * Outgoing connection. * Creates a disconnected peer given a PeerID, your own id and the * relevant MetaInfo. + * @param metainfo null if in magnet mode */ - public Peer(PeerID peerID, byte[] my_id, MetaInfo metainfo) - throws IOException + public Peer(PeerID peerID, byte[] my_id, byte[] infohash, MetaInfo metainfo) { this.peerID = peerID; this.my_id = my_id; + this.infohash = infohash; this.metainfo = metainfo; _id = ++__id; //_log.debug("Creating a new peer with " + peerID.toString(), new Exception("creating")); @@ -89,12 +108,14 @@ public class Peer implements Comparable * get the remote peer id. To completely start the connection call * the connect() method. * + * @param metainfo null if in magnet mode * @exception IOException when an error occurred during the handshake. */ - public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, MetaInfo metainfo) + public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, byte[] infohash, MetaInfo metainfo) throws IOException { this.my_id = my_id; + this.infohash = infohash; this.metainfo = metainfo; this.sock = sock; @@ -102,7 +123,7 @@ public class Peer implements Comparable this.peerID = new PeerID(id, sock.getPeerDestination()); _id = ++__id; if (_log.shouldLog(Log.DEBUG)) - _log.debug("Creating a new peer with " + peerID.toString(), new Exception("creating " + _id)); + _log.debug("Creating a new peer " + peerID.toString(), new Exception("creating " + _id)); } /** @@ -192,7 +213,7 @@ public class Peer implements Comparable * If the given BitField is non-null it is send to the peer as first * message. */ - public void runConnection(I2PSnarkUtil util, PeerListener listener, BitField bitfield) + public void runConnection(I2PSnarkUtil util, PeerListener listener, BitField bitfield, MagnetState mState) { if (state != null) throw new IllegalStateException("Peer already started"); @@ -212,19 +233,8 @@ public class Peer implements Comparable throw new IOException("Unable to reach " + peerID); } InputStream in = sock.getInputStream(); - OutputStream out = sock.getOutputStream(); //new BufferedOutputStream(sock.getOutputStream()); - if (true) { - // buffered output streams are internally synchronized, so we can't get through to the underlying - // I2PSocket's MessageOutputStream to close() it if we are blocking on a write(...). Oh, and the - // buffer is unnecessary anyway, as unbuffered access lets the streaming lib do the 'right thing'. - //out = new BufferedOutputStream(out); - in = new BufferedInputStream(sock.getInputStream()); - } - //BufferedInputStream bis - // = new BufferedInputStream(sock.getInputStream()); - //BufferedOutputStream bos - // = new BufferedOutputStream(sock.getOutputStream()); - byte [] id = handshake(in, out); //handshake(bis, bos); + OutputStream out = sock.getOutputStream(); + byte [] id = handshake(in, out); byte [] expected_id = peerID.getID(); if (expected_id == null) { peerID.setID(id); @@ -243,14 +253,29 @@ public class Peer implements Comparable _log.debug("Already have din [" + sock + "] with " + toString()); } + // bad idea? + if (metainfo == null && (options & OPTION_EXTENSION) == 0) { + if (_log.shouldLog(Log.INFO)) + _log.info("Peer does not support extensions and we need metainfo, dropping"); + throw new IOException("Peer does not support extensions and we need metainfo, dropping"); + } + PeerConnectionIn in = new PeerConnectionIn(this, din); PeerConnectionOut out = new PeerConnectionOut(this, dout); PeerState s = new PeerState(this, listener, metainfo, in, out); if ((options & OPTION_EXTENSION) != 0) { if (_log.shouldLog(Log.DEBUG)) - _log.debug("Peer supports extensions, sending test message"); - out.sendExtension(0, ExtensionHandshake.getPayload()); + _log.debug("Peer supports extensions, sending reply message"); + int metasize = metainfo != null ? metainfo.getInfoBytes().length : -1; + out.sendExtension(0, ExtensionHandler.getHandshake(metasize)); + } + + if ((options & OPTION_I2P_DHT) != 0 && util.getDHT() != null) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Peer supports DHT, sending PORT message"); + int port = util.getDHT().getPort(); + out.sendPort(port); } // Send our bitmap @@ -259,6 +284,7 @@ public class Peer implements Comparable // We are up and running! state = s; + magnetState = mState; listener.connected(this); if (_log.shouldLog(Log.DEBUG)) @@ -293,7 +319,7 @@ public class Peer implements Comparable * Sets DataIn/OutputStreams, does the handshake and returns the id * reported by the other side. */ - private byte[] handshake(InputStream in, OutputStream out) //BufferedInputStream bis, BufferedOutputStream bos) + private byte[] handshake(InputStream in, OutputStream out) throws IOException { din = new DataInputStream(in); @@ -303,10 +329,13 @@ public class Peer implements Comparable dout.write(19); dout.write("BitTorrent protocol".getBytes("UTF-8")); // Handshake write - options - dout.writeLong(OPTION_EXTENSION); + long myOptions = OPTION_EXTENSION; + // FIXME get util here somehow + //if (util.getDHT() != null) + // myOptions |= OPTION_I2P_DHT; + dout.writeLong(myOptions); // Handshake write - metainfo hash - byte[] shared_hash = metainfo.getInfoHash(); - dout.write(shared_hash); + dout.write(infohash); // Handshake write - peer id dout.write(my_id); dout.flush(); @@ -334,7 +363,7 @@ public class Peer implements Comparable // Handshake read - metainfo hash bs = new byte[20]; din.readFully(bs); - if (!Arrays.equals(shared_hash, bs)) + if (!Arrays.equals(infohash, bs)) throw new IOException("Unexpected MetaInfo hash"); // Handshake read - peer id @@ -342,8 +371,11 @@ public class Peer implements Comparable if (_log.shouldLog(Log.DEBUG)) _log.debug("Read the remote side's hash and peerID fully from " + toString()); + if (DataHelper.eq(my_id, bs)) + throw new IOException("Connected to myself"); + if (options != 0) { - // send them something + // send them something in runConnection() above if (_log.shouldLog(Log.DEBUG)) _log.debug("Peer supports options 0x" + Long.toString(options, 16) + ": " + toString()); } @@ -351,6 +383,55 @@ public class Peer implements Comparable return bs; } + /** @since 0.8.4 */ + public long getOptions() { + return options; + } + + /** @since 0.8.4 */ + public Destination getDestination() { + if (sock == null) + return null; + return sock.getPeerDestination(); + } + + /** + * Shared state across all peers, callers must sync on returned object + * @return non-null + * @since 0.8.4 + */ + public MagnetState getMagnetState() { + return magnetState; + } + + /** @return could be null @since 0.8.4 */ + public Map<String, BEValue> getHandshakeMap() { + return handshakeMap; + } + + /** @since 0.8.4 */ + public void setHandshakeMap(Map<String, BEValue> map) { + handshakeMap = map; + } + + /** @since 0.8.4 */ + public void sendExtension(int type, byte[] payload) { + PeerState s = state; + if (s != null) + s.out.sendExtension(type, payload); + } + + /** + * Switch from magnet mode to normal mode + * @since 0.8.4 + */ + public void setMetaInfo(MetaInfo meta) { + metainfo = meta; + PeerState s = state; + if (s != null) + s.setMetaInfo(meta); + } + public boolean isConnected() { return state != null; @@ -513,14 +594,29 @@ public class Peer implements Comparable return (s == null) || s.choked; } + /** + * Increment the counter. + * @since 0.8.4 + */ + public void downloaded(int size) { + downloaded += size; + } + + /** + * Increment the counter. + * @since 0.8.4 + */ + public void uploaded(int size) { + uploaded += size; + } + /** * Returns the number of bytes that have been downloaded. * Can be reset to zero with <code>resetCounters()</code>/ */ public long getDownloaded() { - PeerState s = state; - return (s != null) ? s.downloaded : 0; + return downloaded; } /** @@ -529,8 +625,7 @@ public class Peer implements Comparable */ public long getUploaded() { - PeerState s = state; - return (s != null) ? s.uploaded : 0; + return uploaded; } /** @@ -538,12 +633,8 @@ public class Peer implements Comparable */ public void resetCounters() { - PeerState s = state; - if (s != null) - { - s.downloaded = 0; - s.uploaded = 0; - } + downloaded = 0; + uploaded = 0; } public long getInactiveTime() { diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java index 58ef3ae2db01c25582f858fcc26a74a095a46e41..0adb13d9b78f41a7735de614f8333fa0374f14c1 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java @@ -88,12 +88,11 @@ public class PeerAcceptor } if (coordinator != null) { // single torrent capability - MetaInfo meta = coordinator.getMetaInfo(); - if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) { + if (DataHelper.eq(coordinator.getInfoHash(), peerInfoHash)) { if (coordinator.needPeers()) { Peer peer = new Peer(socket, in, out, coordinator.getID(), - coordinator.getMetaInfo()); + coordinator.getInfoHash(), coordinator.getMetaInfo()); coordinator.addPeer(peer); } else @@ -101,26 +100,25 @@ public class PeerAcceptor } else { // its for another infohash, but we are only single torrent capable. b0rk. throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash) - + ") while we only support (" + Base64.encode(meta.getInfoHash()) + ")"); + + ") while we only support (" + Base64.encode(coordinator.getInfoHash()) + ")"); } } else { // multitorrent capable, so lets see what we can handle for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) { PeerCoordinator cur = (PeerCoordinator)iter.next(); - MetaInfo meta = cur.getMetaInfo(); - - if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) { + + if (DataHelper.eq(cur.getInfoHash(), peerInfoHash)) { if (cur.needPeers()) { Peer peer = new Peer(socket, in, out, cur.getID(), - cur.getMetaInfo()); + cur.getInfoHash(), cur.getMetaInfo()); cur.addPeer(peer); return; } else { if (_log.shouldLog(Log.DEBUG)) - _log.debug("Rejecting new peer for " + cur.snark.torrent); + _log.debug("Rejecting new peer for " + cur.getName()); socket.close(); return; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java index aa9cf2187db0d855dd306199125d939f710ef64d..5d7b6d66db947808fde63b67e859c85dbec3191d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCheckerTask.java @@ -37,7 +37,8 @@ class PeerCheckerTask extends TimerTask private static final long KILOPERSECOND = 1024*(PeerCoordinator.CHECK_PERIOD/1000); private final PeerCoordinator coordinator; - public I2PSnarkUtil _util; + private final I2PSnarkUtil _util; + private int _runCount; PeerCheckerTask(I2PSnarkUtil util, PeerCoordinator coordinator) { @@ -49,12 +50,10 @@ class PeerCheckerTask extends TimerTask public void run() { + _runCount++; List<Peer> peerList = coordinator.peerList(); if (peerList.isEmpty() || coordinator.halted()) { - coordinator.peerCount = 0; - coordinator.interestedAndChoking = 0; coordinator.setRateHistory(0, 0); - coordinator.uploaders = 0; if (coordinator.halted()) cancel(); return; @@ -206,7 +205,14 @@ class PeerCheckerTask extends TimerTask } } peer.retransmitRequests(); + // send PEX + if ((_runCount % 17) == 0 && !peer.isCompleted()) + coordinator.sendPeers(peer); peer.keepAlive(); + // announce them to local tracker (TrackerClient does this too) + if (_util.getDHT() != null && (_runCount % 5) == 0) { + _util.getDHT().announce(coordinator.getInfoHash(), peer.getPeerID().getDestHash()); + } } // Resync actual uploaders value @@ -247,8 +253,14 @@ class PeerCheckerTask extends TimerTask coordinator.setRateHistory(uploaded, downloaded); // close out unused files, but we don't need to do it every time - if (random.nextInt(4) == 0) - coordinator.getStorage().cleanRAFs(); + Storage storage = coordinator.getStorage(); + if (storage != null && (_runCount % 4) == 0) { + storage.cleanRAFs(); + } + // announce ourselves to local tracker (TrackerClient does this too) + if (_util.getDHT() != null && (_runCount % 16) == 0) { + _util.getDHT().announce(coordinator.getInfoHash()); + } } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java index 43bb9a7122ea121615b0646a2c096e1a20931e43..33da75263e9f20e69e3c9f2d3f932e9b9354b6c1 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionIn.java @@ -32,6 +32,13 @@ class PeerConnectionIn implements Runnable private final Peer peer; private final DataInputStream din; + // The max length of a complete message in bytes. + // The biggest is the piece message, for which the length is the + // request size (32K) plus 9. (we could also check if Storage.MAX_PIECES / 8 + // in the bitfield message is bigger but it's currently 5000/8 = 625 so don't bother) + private static final int MAX_MSG_SIZE = Math.max(PeerState.PARTSIZE + 9, + MagnetState.CHUNK_SIZE + 100); // 100 for the ext msg dictionary + private Thread thread; private volatile boolean quit; @@ -77,20 +84,16 @@ class PeerConnectionIn implements Runnable int len; // Wait till we hear something... - // The length of a complete message in bytes. - // The biggest is the piece message, for which the length is the - // request size (32K) plus 9. (we could also check if Storage.MAX_PIECES / 8 - // in the bitfield message is bigger but it's currently 5000/8 = 625 so don't bother) int i = din.readInt(); lastRcvd = System.currentTimeMillis(); - if (i < 0 || i > PeerState.PARTSIZE + 9) + if (i < 0 || i > MAX_MSG_SIZE) throw new IOException("Unexpected length prefix: " + i); if (i == 0) { ps.keepAliveMessage(); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received keepalive from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received keepalive from " + peer); continue; } @@ -102,35 +105,35 @@ class PeerConnectionIn implements Runnable case 0: ps.chokeMessage(true); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received choke from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received choke from " + peer); break; case 1: ps.chokeMessage(false); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received unchoke from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received unchoke from " + peer); break; case 2: ps.interestedMessage(true); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received interested from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received interested from " + peer); break; case 3: ps.interestedMessage(false); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received not interested from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received not interested from " + peer); break; case 4: piece = din.readInt(); ps.haveMessage(piece); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received havePiece(" + piece + ") from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received havePiece(" + piece + ") from " + peer); break; case 5: byte[] bitmap = new byte[i-1]; din.readFully(bitmap); ps.bitfieldMessage(bitmap); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received bitmap from " + peer + " on " + peer.metainfo.getName() + ": size=" + (i-1) /* + ": " + ps.bitfield */ ); + _log.debug("Received bitmap from " + peer + ": size=" + (i-1) /* + ": " + ps.bitfield */ ); break; case 6: piece = din.readInt(); @@ -138,7 +141,7 @@ class PeerConnectionIn implements Runnable len = din.readInt(); ps.requestMessage(piece, begin, len); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received request(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received request(" + piece + "," + begin + ") from " + peer); break; case 7: piece = din.readInt(); @@ -152,7 +155,7 @@ class PeerConnectionIn implements Runnable din.readFully(piece_bytes, begin, len); ps.pieceMessage(req); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received data(" + piece + "," + begin + ") from " + peer); } else { @@ -160,7 +163,7 @@ class PeerConnectionIn implements Runnable piece_bytes = new byte[len]; din.readFully(piece_bytes); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received UNWANTED data(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received UNWANTED data(" + piece + "," + begin + ") from " + peer); } break; case 8: @@ -169,22 +172,28 @@ class PeerConnectionIn implements Runnable len = din.readInt(); ps.cancelMessage(piece, begin, len); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received cancel(" + piece + "," + begin + ") from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received cancel(" + piece + "," + begin + ") from " + peer); + break; + case 9: // PORT message + int port = din.readUnsignedShort(); + ps.portMessage(port); + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Received port message from " + peer); break; case 20: // Extension message int id = din.readUnsignedByte(); byte[] payload = new byte[i-2]; din.readFully(payload); - ps.extensionMessage(id, payload); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received extension message from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received extension message from " + peer); + ps.extensionMessage(id, payload); break; default: byte[] bs = new byte[i-1]; din.readFully(bs); ps.unknownMessage(b, bs); if (_log.shouldLog(Log.DEBUG)) - _log.debug("Received unknown message from " + peer + " on " + peer.metainfo.getName()); + _log.debug("Received unknown message from " + peer); } } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java index 38cb29e3b445dcae98e8589a59690af494b02c88..225edd4923a3c73fcd83633edfc9e3bda06cfe8d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerConnectionOut.java @@ -29,8 +29,8 @@ import java.util.List; import net.i2p.I2PAppContext; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; -import net.i2p.util.SimpleScheduler; -import net.i2p.util.SimpleTimer; +//import net.i2p.util.SimpleScheduler; +//import net.i2p.util.SimpleTimer; class PeerConnectionOut implements Runnable { @@ -124,34 +124,34 @@ class PeerConnectionOut implements Runnable { if (state.choking) { it.remove(); - SimpleTimer.getInstance().removeEvent(nm.expireEvent); + //SimpleTimer.getInstance().removeEvent(nm.expireEvent); } nm = null; } else if (nm.type == Message.REQUEST && state.choked) { it.remove(); - SimpleTimer.getInstance().removeEvent(nm.expireEvent); + //SimpleTimer.getInstance().removeEvent(nm.expireEvent); nm = null; } if (m == null && nm != null) { m = nm; - SimpleTimer.getInstance().removeEvent(nm.expireEvent); + //SimpleTimer.getInstance().removeEvent(nm.expireEvent); it.remove(); } } if (m == null && !sendQueue.isEmpty()) { m = (Message)sendQueue.remove(0); - SimpleTimer.getInstance().removeEvent(m.expireEvent); + //SimpleTimer.getInstance().removeEvent(m.expireEvent); } } } if (m != null) { if (_log.shouldLog(Log.DEBUG)) - _log.debug("Send " + peer + ": " + m + " on " + peer.metainfo.getName()); + _log.debug("Send " + peer + ": " + m); // This can block for quite a while. // To help get slow peers going, and track the bandwidth better, @@ -241,6 +241,8 @@ class PeerConnectionOut implements Runnable /** remove messages not sent in 3m */ private static final int SEND_TIMEOUT = 3*60*1000; + +/***** private class RemoveTooSlow implements SimpleTimer.TimedEvent { private Message _m; public RemoveTooSlow(Message m) { @@ -258,6 +260,7 @@ class PeerConnectionOut implements Runnable _log.info("Took too long to send " + _m + " to " + peer); } } +*****/ /** * Removes a particular message type from the queue. @@ -474,7 +477,8 @@ class PeerConnectionOut implements Runnable m.off = 0; m.len = length; // since we have the data already loaded, queue a timeout to remove it - SimpleScheduler.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT); + // no longer prefetched + //SimpleScheduler.getInstance().addEvent(new RemoveTooSlow(m), SEND_TIMEOUT); addMessage(m); } @@ -547,4 +551,12 @@ class PeerConnectionOut implements Runnable m.len = bytes.length; addMessage(m); } + + /** @since 0.8.4 */ + void sendPort(int port) { + Message m = new Message(); + m.type = Message.PORT; + m.piece = port; + addMessage(m); + } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java index 8026f34f6826b51c7e703465642e847f54655fe8..cdf1e58b5e70cd3e4cd8adbb947ac76e66fa43ce 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java @@ -27,33 +27,59 @@ import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Queue; import java.util.Random; +import java.util.Set; import java.util.concurrent.LinkedBlockingQueue; import net.i2p.I2PAppContext; +import net.i2p.data.DataHelper; +import net.i2p.data.Destination; +import net.i2p.util.ConcurrentHashSet; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.SimpleTimer2; +import org.klomp.snark.bencode.BEValue; +import org.klomp.snark.bencode.InvalidBEncodingException; +import org.klomp.snark.dht.DHT; + /** * Coordinates what peer does what. */ public class PeerCoordinator implements PeerListener { private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerCoordinator.class); - final MetaInfo metainfo; - final Storage storage; - final Snark snark; + + /** + * External use by PeerMonitorTask only. + * Will be null when in magnet mode. + */ + MetaInfo metainfo; + + /** + * External use by PeerMonitorTask only. + * Will be null when in magnet mode. + */ + Storage storage; + private final Snark snark; // package local for access by CheckDownLoadersTask final static long CHECK_PERIOD = 40*1000; // 40 seconds final static int MAX_UPLOADERS = 6; - // Approximation of the number of current uploaders. - // Resynced by PeerChecker once in a while. - int uploaders = 0; - int interestedAndChoking = 0; + /** + * Approximation of the number of current uploaders. + * Resynced by PeerChecker once in a while. + * External use by PeerCheckerTask only. + */ + int uploaders; + + /** + * External use by PeerCheckerTask only. + */ + int interestedAndChoking; // final static int MAX_DOWNLOADERS = MAX_CONNECTIONS; // int downloaders = 0; @@ -61,19 +87,29 @@ public class PeerCoordinator implements PeerListener private long uploaded; private long downloaded; final static int RATE_DEPTH = 3; // make following arrays RATE_DEPTH long - private long uploaded_old[] = {-1,-1,-1}; - private long downloaded_old[] = {-1,-1,-1}; + private final long uploaded_old[] = {-1,-1,-1}; + private final long downloaded_old[] = {-1,-1,-1}; - // synchronize on this when changing peers or downloaders - // This is a Queue, not a Set, because PeerCheckerTask keeps things in order for choking/unchoking + /** + * synchronize on this when changing peers or downloaders. + * This is a Queue, not a Set, because PeerCheckerTask keeps things in order for choking/unchoking. + * External use by PeerMonitorTask only. + */ final Queue<Peer> peers; + + /** + * Peers we heard about via PEX + */ + private final Set<PeerID> pexPeers; + /** estimate of the peers, without requiring any synchronization */ - volatile int peerCount; + private volatile int peerCount; /** Timer to handle all periodical tasks. */ private final CheckEvent timer; private final byte[] id; + private final byte[] infohash; /** The wanted pieces. We could use a TreeSet but we'd have to clear and re-add everything * when priorities change. @@ -85,18 +121,21 @@ public class PeerCoordinator implements PeerListener private boolean halted = false; + private final MagnetState magnetState; private final CoordinatorListener listener; - public I2PSnarkUtil _util; + private final I2PSnarkUtil _util; private static final Random _random = I2PAppContext.getGlobalContext().random(); - public String trackerProblems = null; - public int trackerSeenPeers = 0; - - public PeerCoordinator(I2PSnarkUtil util, byte[] id, MetaInfo metainfo, Storage storage, + /** + * @param metainfo null if in magnet mode + * @param storage null if in magnet mode + */ + public PeerCoordinator(I2PSnarkUtil util, byte[] id, byte[] infohash, MetaInfo metainfo, Storage storage, CoordinatorListener listener, Snark torrent) { _util = util; this.id = id; + this.infohash = infohash; this.metainfo = metainfo; this.storage = storage; this.listener = listener; @@ -106,6 +145,8 @@ public class PeerCoordinator implements PeerListener setWantedPieces(); partialPieces = new ArrayList(getMaxConnections() + 1); peers = new LinkedBlockingQueue(); + magnetState = new MagnetState(infohash, metainfo); + pexPeers = new ConcurrentHashSet(); // Install a timer to check the uploaders. // Randomize the first start time so multiple tasks are spread out, @@ -133,6 +174,8 @@ public class PeerCoordinator implements PeerListener // only called externally from Storage after the double-check fails public void setWantedPieces() { + if (metainfo == null || storage == null) + return; // Make a list of pieces synchronized(wantedPieces) { wantedPieces.clear(); @@ -153,7 +196,6 @@ public class PeerCoordinator implements PeerListener } public Storage getStorage() { return storage; } - public CoordinatorListener getListener() { return listener; } // for web page detailed stats public List<Peer> peerList() @@ -166,8 +208,16 @@ public class PeerCoordinator implements PeerListener return id; } + public String getName() + { + return snark.getName(); + } + public boolean completed() { + // FIXME return metainfo complete status + if (storage == null) + return false; return storage.complete(); } @@ -184,9 +234,12 @@ public class PeerCoordinator implements PeerListener /** * Returns how many bytes are still needed to get the complete file. + * @return -1 if in magnet mode */ public long getLeft() { + if (metainfo == null | storage == null) + return -1; // XXX - Only an approximation. return ((long) storage.needed()) * metainfo.getPieceLength(0); } @@ -271,6 +324,12 @@ public class PeerCoordinator implements PeerListener return metainfo; } + /** @since 0.8.4 */ + public byte[] getInfoHash() + { + return infohash; + } + public boolean needPeers() { return !halted && peers.size() < getMaxConnections(); @@ -281,6 +340,8 @@ public class PeerCoordinator implements PeerListener * @return 512K: 16; 1M: 11; 2M: 6 */ private int getMaxConnections() { + if (metainfo == null) + return 6; int size = metainfo.getPieceLength(0); int max = _util.getMaxConnections(); if (size <= 512*1024 || completed()) @@ -355,8 +416,15 @@ public class PeerCoordinator implements PeerListener } else { - if (_log.shouldLog(Log.INFO)) - _log.info("New connection to peer: " + peer + " for " + metainfo.getName()); + if (_log.shouldLog(Log.INFO)) { + // just for logging + String name; + if (metainfo == null) + name = "Magnet"; + else + name = metainfo.getName(); + _log.info("New connection to peer: " + peer + " for " + name); + } // Add it to the beginning of the list. // And try to optimistically make it a uploader. @@ -415,17 +483,27 @@ public class PeerCoordinator implements PeerListener if (need_more) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + metainfo.getName(), new Exception("add/run")); - + if (_log.shouldLog(Log.DEBUG)) { + // just for logging + String name; + if (metainfo == null) + name = "Magnet"; + else + name = metainfo.getName(); + _log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + name, new Exception("add/run")); + } // Run the peer with us as listener and the current bitfield. final PeerListener listener = this; - final BitField bitfield = storage.getBitField(); + final BitField bitfield; + if (storage != null) + bitfield = storage.getBitField(); + else + bitfield = null; Runnable r = new Runnable() { public void run() { - peer.runConnection(_util, listener, bitfield); + peer.runConnection(_util, listener, bitfield, magnetState); } }; String threadName = "Snark peer " + peer.toString(); @@ -486,11 +564,6 @@ public class PeerCoordinator implements PeerListener interestedAndChoking = count; } - public byte[] getBitMap() - { - return storage.getBitField().getFieldBytes(); - } - /** * @return true if we still want the given piece */ @@ -647,6 +720,8 @@ public class PeerCoordinator implements PeerListener * @since 0.8.1 */ public void updatePiecePriorities() { + if (storage == null) + return; int[] pri = storage.getPiecePriorities(); if (pri == null) { _log.debug("Updated piece priorities called but no priorities to set?"); @@ -713,6 +788,8 @@ public class PeerCoordinator implements PeerListener { if (halted) return null; + if (metainfo == null || storage == null) + return null; try { @@ -755,6 +832,8 @@ public class PeerCoordinator implements PeerListener */ public boolean gotPiece(Peer peer, int piece, byte[] bs) { + if (metainfo == null || storage == null) + return true; if (halted) { _log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName()); return true; // We don't actually care anymore. @@ -951,6 +1030,8 @@ public class PeerCoordinator implements PeerListener * @since 0.8.2 */ public PartialPiece getPartialPiece(Peer peer, BitField havePieces) { + if (metainfo == null) + return null; synchronized(wantedPieces) { // sorts by remaining bytes, least first Collections.sort(partialPieces); @@ -1057,6 +1138,107 @@ public class PeerCoordinator implements PeerListener } } + /** + * PeerListener callback + * @since 0.8.4 + */ + public void gotExtension(Peer peer, int id, byte[] bs) { + if (_log.shouldLog(Log.DEBUG)) + _log.debug("Got extension message " + id + " from " + peer); + // basic handling done in PeerState... here we just check if we are done + if (metainfo == null && id == ExtensionHandler.ID_METADATA) { + synchronized (magnetState) { + if (magnetState.isComplete()) { + if (_log.shouldLog(Log.WARN)) + _log.warn("Got completed metainfo via extension"); + metainfo = magnetState.getMetaInfo(); + listener.gotMetaInfo(this, metainfo); + } + } + } else if (id == ExtensionHandler.ID_HANDSHAKE) { + sendPeers(peer); + } + } + + /** + * Send a PEX message to the peer, if he supports PEX. + * This just sends everybody we are connected to, we don't + * track new vs. old peers yet. + * @since 0.8.4 + */ + void sendPeers(Peer peer) { + Map<String, BEValue> handshake = peer.getHandshakeMap(); + if (handshake == null) + return; + BEValue bev = handshake.get("m"); + if (bev == null) + return; + try { + if (bev.getMap().get(ExtensionHandler.TYPE_PEX) != null) { + List<Peer> pList = peerList(); + pList.remove(peer); + if (!pList.isEmpty()) + ExtensionHandler.sendPEX(peer, pList); + } + } catch (InvalidBEncodingException ibee) {} + } + + /** + * Sets the storage after transition out of magnet mode + * Snark calls this after we call gotMetaInfo() + * @since 0.8.4 + */ + public void setStorage(Storage stg) { + storage = stg; + setWantedPieces(); + // ok we should be in business + for (Peer p : peers) { + p.setMetaInfo(metainfo); + } + } + + /** + * PeerListener callback + * Tell the DHT to ping it, this will get back the node info + * @since 0.8.4 + */ + public void gotPort(Peer peer, int port) { + DHT dht = _util.getDHT(); + if (dht != null) + dht.ping(peer.getDestination(), port); + } + + /** + * PeerListener callback + * @since 0.8.4 + */ + public void gotPeers(Peer peer, List<PeerID> peers) { + if (completed() || !needPeers()) + return; + Destination myDest = _util.getMyDestination(); + if (myDest == null) + return; + byte[] myHash = myDest.calculateHash().getData(); + List<Peer> pList = peerList(); + for (PeerID id : peers) { + if (peerIDInList(id, pList) != null) + continue; + if (DataHelper.eq(myHash, id.getDestHash())) + continue; + pexPeers.add(id); + } + // TrackerClient will poll for pexPeers and do the add in its thread, + // rather than running another thread here. + } + + /** + * Called by TrackerClient + * @since 0.8.4 + */ + Set<PeerID> getPEXPeers() { + return pexPeers; + } + /** Return number of allowed uploaders for this torrent. ** Check with Snark to see if we are over the total upload limit. */ @@ -1072,6 +1254,14 @@ public class PeerCoordinator implements PeerListener return MAX_UPLOADERS; } + /** + * @return current + * @since 0.8.4 + */ + public int getUploaders() { + return uploaders; + } + public boolean overUpBWLimit() { if (listener != null) diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java index 975c12c1062c7096c51d5da5e0c2b4290838a74e..c7650a55249d4cf485598a97ccc968080fede24a 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerListener.java @@ -179,4 +179,32 @@ interface PeerListener * @since 0.8.2 */ PartialPiece getPartialPiece(Peer peer, BitField havePieces); + + /** + * Called when an extension message is received. + * + * @param peer the Peer that got the message. + * @param id the message ID + * @param bs the message payload + * @since 0.8.4 + */ + void gotExtension(Peer peer, int id, byte[] bs); + + /** + * Called when a port message is received. + * + * @param peer the Peer that got the message. + * @param port the port + * @since 0.8.4 + */ + void gotPort(Peer peer, int port); + + /** + * Called when peers are received via PEX + * + * @param peer the Peer that got the message. + * @param pIDList the peer IDs (dest hashes) + * @since 0.8.4 + */ + void gotPeers(Peer peer, List<PeerID> pIDList); } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java index 5551bc90eb3284ccbf3c84c629d391bb15860d7c..aadc5cba25b35829c275a55b6a7ffb948462b942 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java @@ -32,15 +32,13 @@ import java.util.Set; import net.i2p.I2PAppContext; import net.i2p.util.Log; -import org.klomp.snark.bencode.BDecoder; -import org.klomp.snark.bencode.BEValue; - class PeerState implements DataLoader { private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerState.class); private final Peer peer; + /** Fixme, used by Peer.disconnect() to get to the coordinator */ final PeerListener listener; - private final MetaInfo metainfo; + private MetaInfo metainfo; // Interesting and choking describes whether we are interested in or // are choking the other side. @@ -52,10 +50,6 @@ class PeerState implements DataLoader boolean interested = false; boolean choked = true; - // Package local for use by Peer. - long downloaded; - long uploaded; - /** the pieces the peer has */ BitField bitfield; @@ -74,6 +68,9 @@ class PeerState implements DataLoader public final static int PARTSIZE = 16*1024; // outbound request private final static int MAX_PARTSIZE = 64*1024; // Don't let anybody request more than this + /** + * @param metainfo null if in magnet mode + */ PeerState(Peer peer, PeerListener listener, MetaInfo metainfo, PeerConnectionIn in, PeerConnectionOut out) { @@ -135,6 +132,9 @@ class PeerState implements DataLoader { if (_log.shouldLog(Log.DEBUG)) _log.debug(peer + " rcv have(" + piece + ")"); + // FIXME we will lose these until we get the metainfo + if (metainfo == null) + return; // Sanity check if (piece < 0 || piece >= metainfo.getPieces()) { @@ -172,8 +172,15 @@ class PeerState implements DataLoader } // XXX - Check for weird bitfield and disconnect? - bitfield = new BitField(bitmap, metainfo.getPieces()); + // FIXME will have to regenerate the bitfield after we know exactly + // how many pieces there are, as we don't know how many spare bits there are. + if (metainfo == null) + bitfield = new BitField(bitmap, bitmap.length * 8); + else + bitfield = new BitField(bitmap, metainfo.getPieces()); } + if (metainfo == null) + return; boolean interest = listener.gotBitField(peer, bitfield); setInteresting(interest); if (bitfield.complete() && !interest) { @@ -191,6 +198,8 @@ class PeerState implements DataLoader if (_log.shouldLog(Log.DEBUG)) _log.debug(peer + " rcv request(" + piece + ", " + begin + ", " + length + ") "); + if (metainfo == null) + return; if (choking) { if (_log.shouldLog(Log.INFO)) @@ -273,7 +282,7 @@ class PeerState implements DataLoader */ void uploaded(int size) { - uploaded += size; + peer.uploaded(size); listener.uploaded(peer, size); } @@ -293,7 +302,7 @@ class PeerState implements DataLoader void pieceMessage(Request req) { int size = req.len; - downloaded += size; + peer.downloaded(size); listener.downloaded(peer, size); if (_log.shouldLog(Log.DEBUG)) @@ -314,9 +323,6 @@ class PeerState implements DataLoader { if (_log.shouldLog(Log.WARN)) _log.warn("Got BAD " + req.piece + " from " + peer); - // XXX ARGH What now !?! - // FIXME Why would we set downloaded to 0? - downloaded = 0; } } @@ -360,7 +366,6 @@ class PeerState implements DataLoader _log.info("Unrequested 'piece: " + piece + ", " + begin + ", " + length + "' received from " + peer); - downloaded = 0; // XXX - punishment? return null; } @@ -385,7 +390,6 @@ class PeerState implements DataLoader + begin + ", " + length + "' received from " + peer); - downloaded = 0; // XXX - punishment? return null; } @@ -485,22 +489,36 @@ class PeerState implements DataLoader /** @since 0.8.2 */ void extensionMessage(int id, byte[] bs) { - if (id == 0) { - InputStream is = new ByteArrayInputStream(bs); - try { - BDecoder dec = new BDecoder(is); - BEValue bev = dec.bdecodeMap(); - Map map = bev.getMap(); - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Got extension handshake message " + bev.toString()); - } catch (Exception e) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Failed extension decode", e); - } + ExtensionHandler.handleMessage(peer, listener, id, bs); + // Peer coord will get metadata from MagnetState, + // verify, and then call gotMetaInfo() + listener.gotExtension(peer, id, bs); + } + + /** + * Switch from magnet mode to normal mode + * @since 0.8.4 + */ + public void setMetaInfo(MetaInfo meta) { + BitField oldBF = bitfield; + if (oldBF != null) { + if (oldBF.size() != meta.getPieces()) + // fix bitfield, it was too big by 1-7 bits + bitfield = new BitField(oldBF.getFieldBytes(), meta.getPieces()); + // else no extra } else { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Got extended message type: " + id + " length: " + bs.length); + // it will be initialized later + //bitfield = new BitField(meta.getPieces()); } + metainfo = meta; + if (bitfield.count() > 0) + setInteresting(true); + } + + /** @since 0.8.4 */ + void portMessage(int port) + { + listener.gotPort(peer, port); } void unknownMessage(int type, byte[] bs) @@ -619,6 +637,8 @@ class PeerState implements DataLoader // no bitfield yet? nothing to request then. if (bitfield == null) return; + if (metainfo == null) + return; boolean more_pieces = true; while (more_pieces) { diff --git a/apps/i2psnark/java/src/org/klomp/snark/Piece.java b/apps/i2psnark/java/src/org/klomp/snark/Piece.java index 6855d36a0ed293069ec7d3cdd1d5b4141e45a20c..dd48508a9a3ba761e22fb122c5c8d0d4197e23f9 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Piece.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Piece.java @@ -35,8 +35,8 @@ class Piece implements Comparable { @Override public boolean equals(Object o) { + if (o == null) return false; if (o instanceof Piece) { - if (o == null) return false; return this.id == ((Piece)o).id; } return false; diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 31105a2693a6906d84114b8bc8a6af5bd2126115..98bcd4eed470b086e45401ed183558c7eeb3a68e 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -26,6 +26,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Properties; @@ -107,11 +108,13 @@ public class Snark } catch (Throwable t) { System.out.println("OOM in the OOM"); } - System.exit(0); + //System.exit(0); } } +/******** No, not maintaining a command-line client + public static void main(String[] args) { System.out.println(copyright); @@ -235,19 +238,27 @@ public class Snark } } +***********/ + public static final String PROP_MAX_CONNECTIONS = "i2psnark.maxConnections"; - public String torrent; - public MetaInfo meta; - public Storage storage; - public PeerCoordinator coordinator; - public ConnectionAcceptor acceptor; - public TrackerClient trackerclient; - public String rootDataDir = "."; - public CompleteListener completeListener; - public boolean stopped; - byte[] id; - public I2PSnarkUtil _util; - private PeerCoordinatorSet _peerCoordinatorSet; + + /** most of these used to be public, use accessors below instead */ + private String torrent; + private MetaInfo meta; + private Storage storage; + private PeerCoordinator coordinator; + private ConnectionAcceptor acceptor; + private TrackerClient trackerclient; + private String rootDataDir = "."; + private final CompleteListener completeListener; + private boolean stopped; + private byte[] id; + private byte[] infoHash; + private final I2PSnarkUtil _util; + private final PeerCoordinatorSet _peerCoordinatorSet; + private String trackerProblems; + private int trackerSeenPeers; + /** from main() via parseArguments() single torrent */ Snark(I2PSnarkUtil util, String torrent, String ip, int user_port, @@ -306,31 +317,7 @@ public class Snark stopped = true; activity = "Network setup"; - // "Taking Three as the subject to reason about-- - // A convenient number to state-- - // We add Seven, and Ten, and then multiply out - // By One Thousand diminished by Eight. - // - // "The result we proceed to divide, as you see, - // By Nine Hundred and Ninety Two: - // Then subtract Seventeen, and the answer must be - // Exactly and perfectly true. - - // Create a new ID and fill it with something random. First nine - // zeros bytes, then three bytes filled with snark and then - // sixteen random bytes. - byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17; - id = new byte[20]; - Random random = I2PAppContext.getGlobalContext().random(); - int i; - for (i = 0; i < 9; i++) - id[i] = 0; - id[i++] = snark; - id[i++] = snark; - id[i++] = snark; - while (i < 20) - id[i++] = (byte)random.nextInt(256); - + id = generateID(); debug("My peer id: " + PeerID.idencode(id), Snark.INFO); int port; @@ -373,6 +360,7 @@ public class Snark } } meta = new MetaInfo(new BDecoder(in)); + infoHash = meta.getInfoHash(); } catch(IOException ioe) { @@ -406,6 +394,8 @@ public class Snark */ else fatal("Cannot open '" + torrent + "'", ioe); + } catch (OutOfMemoryError oom) { + fatal("ERROR - Out of memory, cannot create torrent " + torrent + ": " + oom.getMessage()); } finally { if (in != null) try { in.close(); } catch (IOException ioe) {} @@ -457,6 +447,64 @@ public class Snark if (start) startTorrent(); } + + /** + * multitorrent, magnet + * + * @param torrent a fake name for now (not a file name) + * @param ih 20-byte info hash + * @since 0.8.4 + */ + public Snark(I2PSnarkUtil util, String torrent, byte[] ih, + CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet, + ConnectionAcceptor connectionAcceptor, boolean start, String rootDir) + { + completeListener = complistener; + _util = util; + _peerCoordinatorSet = peerCoordinatorSet; + acceptor = connectionAcceptor; + this.torrent = torrent; + this.infoHash = ih; + this.rootDataDir = rootDir; + stopped = true; + id = generateID(); + + // All we have is an infoHash + // meta remains null + // storage remains null + + if (start) + startTorrent(); + } + + private static byte[] generateID() { + // "Taking Three as the subject to reason about-- + // A convenient number to state-- + // We add Seven, and Ten, and then multiply out + // By One Thousand diminished by Eight. + // + // "The result we proceed to divide, as you see, + // By Nine Hundred and Ninety Two: + // Then subtract Seventeen, and the answer must be + // Exactly and perfectly true. + + // Create a new ID and fill it with something random. First nine + // zeros bytes, then three bytes filled with snark and then + // sixteen random bytes. + byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17; + byte[] rv = new byte[20]; + Random random = I2PAppContext.getGlobalContext().random(); + int i; + for (i = 0; i < 9; i++) + rv[i] = 0; + rv[i++] = snark; + rv[i++] = snark; + rv[i++] = snark; + while (i < 20) + rv[i++] = (byte)random.nextInt(256); + return rv; + } + /** * Start up contacting peers and querying the tracker */ @@ -473,7 +521,7 @@ public class Snark } debug("Starting PeerCoordinator, ConnectionAcceptor, and TrackerClient", NOTICE); activity = "Collecting pieces"; - coordinator = new PeerCoordinator(_util, id, meta, storage, this, this); + coordinator = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this); if (_peerCoordinatorSet != null) { // multitorrent _peerCoordinatorSet.add(coordinator); @@ -486,7 +534,8 @@ public class Snark // single torrent acceptor = new ConnectionAcceptor(_util, serversocket, new PeerAcceptor(coordinator)); } - trackerclient = new TrackerClient(_util, meta, coordinator); + // TODO pass saved closest DHT nodes to the tracker? or direct to the coordinator? + trackerclient = new TrackerClient(_util, meta, coordinator, this); } stopped = false; @@ -496,8 +545,7 @@ public class Snark // restart safely, so lets build a new one to replace the old if (_peerCoordinatorSet != null) _peerCoordinatorSet.remove(coordinator); - PeerCoordinator newCoord = new PeerCoordinator(_util, coordinator.getID(), coordinator.getMetaInfo(), - coordinator.getStorage(), coordinator.getListener(), this); + PeerCoordinator newCoord = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this); if (_peerCoordinatorSet != null) _peerCoordinatorSet.add(newCoord); coordinator = newCoord; @@ -506,18 +554,17 @@ public class Snark if (!trackerclient.started() && !coordinatorChanged) { trackerclient.start(); } else if (trackerclient.halted() || coordinatorChanged) { - try - { - storage.reopen(rootDataDir); - } - catch (IOException ioe) - { - try { storage.close(); } catch (IOException ioee) { - ioee.printStackTrace(); - } - fatal("Could not reopen storage", ioe); - } - TrackerClient newClient = new TrackerClient(_util, coordinator.getMetaInfo(), coordinator); + if (storage != null) { + try { + storage.reopen(rootDataDir); + } catch (IOException ioe) { + try { storage.close(); } catch (IOException ioee) { + ioee.printStackTrace(); + } + fatal("Could not reopen storage", ioe); + } + } + TrackerClient newClient = new TrackerClient(_util, meta, coordinator, this); if (!trackerclient.halted()) trackerclient.halt(); trackerclient = newClient; @@ -553,18 +600,238 @@ public class Snark _util.disconnect(); } - static Snark parseArguments(String[] args) + private static Snark parseArguments(String[] args) { return parseArguments(args, null, null); } + // Accessors + + /** + * @return file name of .torrent file (should be full absolute path), or a fake name if in magnet mode. + * @since 0.8.4 + */ + public String getName() { + return torrent; + } + + /** + * @return base name of torrent [filtered version of getMetaInfo.getName()], or a fake name if in magnet mode + * @since 0.8.4 + */ + public String getBaseName() { + if (storage != null) + return storage.getBaseName(); + return torrent; + } + + /** + * @return always will be valid even in magnet mode + * @since 0.8.4 + */ + public byte[] getID() { + return id; + } + + /** + * @return always will be valid even in magnet mode + * @since 0.8.4 + */ + public byte[] getInfoHash() { + // should always be the same + if (meta != null) + return meta.getInfoHash(); + return infoHash; + } + + /** + * @return may be null if in magnet mode + * @since 0.8.4 + */ + public MetaInfo getMetaInfo() { + return meta; + } + + /** + * @return may be null if in magnet mode + * @since 0.8.4 + */ + public Storage getStorage() { + return storage; + } + + /** + * @since 0.8.4 + */ + public boolean isStopped() { + return stopped; + } + + /** + * @since 0.8.4 + */ + public long getDownloadRate() { + PeerCoordinator coord = coordinator; + if (coord != null) + return coord.getDownloadRate(); + return 0; + } + + /** + * @since 0.8.4 + */ + public long getUploadRate() { + PeerCoordinator coord = coordinator; + if (coord != null) + return coord.getUploadRate(); + return 0; + } + + /** + * @since 0.8.4 + */ + public long getDownloaded() { + PeerCoordinator coord = coordinator; + if (coord != null) + return coord.getDownloaded(); + return 0; + } + + /** + * @since 0.8.4 + */ + public long getUploaded() { + PeerCoordinator coord = coordinator; + if (coord != null) + return coord.getUploaded(); + return 0; + } + + /** + * @since 0.8.4 + */ + public int getPeerCount() { + PeerCoordinator coord = coordinator; + if (coord != null) + return coord.getPeerCount(); + return 0; + } + + /** + * @since 0.8.4 + */ + public List<Peer> getPeerList() { + PeerCoordinator coord = coordinator; + if (coord != null) + return coord.peerList(); + return Collections.EMPTY_LIST; + } + + /** + * @return String returned from tracker, or null if no error + * @since 0.8.4 + */ + public String getTrackerProblems() { + return trackerProblems; + } + + /** + * @param p tracker error string or null + * @since 0.8.4 + */ + public void setTrackerProblems(String p) { + trackerProblems = p; + } + + /** + * @return count returned from tracker + * @since 0.8.4 + */ + public int getTrackerSeenPeers() { + return trackerSeenPeers; + } + + /** + * @since 0.8.4 + */ + public void setTrackerSeenPeers(int p) { + trackerSeenPeers = p; + } + + /** + * @since 0.8.4 + */ + public void updatePiecePriorities() { + PeerCoordinator coord = coordinator; + if (coord != null) + coord.updatePiecePriorities(); + } + + /** + * @return total of all torrent files, or total of metainfo file if fetching magnet, or -1 + * @since 0.8.4 + */ + public long getTotalLength() { + if (meta != null) + return meta.getTotalLength(); + // FIXME else return metainfo length if available + return -1; + } + + /** + * @return number of pieces still needed (magnet mode or not), or -1 if unknown + * @since 0.8.4 + */ + public long getNeeded() { + if (storage != null) + return storage.needed(); + if (meta != null) + // FIXME subtract chunks we have + return meta.getTotalLength(); + // FIXME fake + return -1; + } + + /** + * @param p the piece number + * @return metainfo piece length or 16K if fetching magnet + * @since 0.8.4 + */ + public int getPieceLength(int p) { + if (meta != null) + return meta.getPieceLength(p); + return 16*1024; + } + + /** + * @return number of pieces + * @since 0.8.4 + */ + public int getPieces() { + if (meta != null) + return meta.getPieces(); + // FIXME else return metainfo pieces if available + return -1; + } + + /** + * @return true if restarted + * @since 0.8.4 + */ + public boolean restartAcceptor() { + if (acceptor == null) + return false; + acceptor.restart(); + return true; + } + /** * Sets debug, ip and torrent variables then creates a Snark * instance. Calls usage(), which terminates the program, if * non-valid argument list. The given listeners will be * passed to all components that take one. */ - static Snark parseArguments(String[] args, + private static Snark parseArguments(String[] args, StorageListener slistener, CoordinatorListener clistener) { @@ -713,13 +980,12 @@ public class Snark (" <file> \tEither a local .torrent metainfo file to download"); System.out.println (" \tor (with --share) a file to share."); - System.exit(-1); } /** * Aborts program abnormally. */ - public void fatal(String s) + private void fatal(String s) { fatal(s, null); } @@ -727,7 +993,7 @@ public class Snark /** * Aborts program abnormally. */ - public void fatal(String s, Throwable t) + private void fatal(String s, Throwable t) { _util.debug(s, ERROR, t); //System.err.println("snark: " + s + ((t == null) ? "" : (": " + t))); @@ -751,7 +1017,36 @@ public class Snark // System.out.println(peer.toString()); } - boolean allocating = false; + /** + * Called when the PeerCoordinator got the MetaInfo via magnet. + * CoordinatorListener. + * Create the storage, tell SnarkManager, and give the storage + * back to the coordinator. + * + * @throws RuntimeException via fatal() + * @since 0.8.4 + */ + public void gotMetaInfo(PeerCoordinator coordinator, MetaInfo metainfo) { + meta = metainfo; + try { + storage = new Storage(_util, meta, this); + storage.check(rootDataDir); + if (completeListener != null) { + String newName = completeListener.gotMetaInfo(this); + if (newName != null) + torrent = newName; + // else some horrible problem + } + coordinator.setStorage(storage); + } catch (IOException ioe) { + if (storage != null) { + try { storage.close(); } catch (IOException ioee) {} + } + fatal("Could not check or create storage", ioe); + } + } + + private boolean allocating = false; public void storageCreateFile(Storage storage, String name, long length) { //if (allocating) @@ -774,9 +1069,9 @@ public class Snark // System.out.println(); // We have all the disk space we need. } - boolean allChecked = false; - boolean checking = false; - boolean prechecking = true; + private boolean allChecked = false; + private boolean checking = false; + private boolean prechecking = true; public void storageChecked(Storage storage, int num, boolean checked) { allocating = false; @@ -821,16 +1116,28 @@ public class Snark coordinator.setWantedPieces(); } + /** SnarkSnutdown callback unused */ public void shutdown() { // Should not be necessary since all non-deamon threads should // have died. But in reality this does not always happen. - System.exit(0); + //System.exit(0); } public interface CompleteListener { public void torrentComplete(Snark snark); public void updateStatus(Snark snark); + + /** + * We transitioned from magnet mode, we have now initialized our + * metainfo and storage. The listener should now call getMetaInfo() + * and save the data to disk. + * + * @return the new name for the torrent or null on error + * @since 0.8.4 + */ + public String gotMetaInfo(Snark snark); + // not really listeners but the easiest way to get back to an optional SnarkManager public long getSavedTorrentTime(Snark snark); public BitField getSavedTorrentBitField(Snark snark); diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 82d1d80f4f53789bb8a06c21e709fd57ad261a44..3f9f90edfbbc8f0fcd7045337b29317cb6389386 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -5,6 +5,7 @@ import java.io.FileFilter; import java.io.FileInputStream; import java.io.FilenameFilter; import java.io.IOException; +import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; @@ -16,14 +17,18 @@ import java.util.Set; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.Collection; +import java.util.concurrent.ConcurrentHashMap; import net.i2p.I2PAppContext; import net.i2p.data.Base64; import net.i2p.data.DataHelper; +import net.i2p.util.ConcurrentHashSet; +import net.i2p.util.FileUtil; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.OrderedProperties; import net.i2p.util.SecureDirectory; +import net.i2p.util.SecureFileOutputStream; /** * Manage multiple snarks @@ -32,8 +37,14 @@ 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) */ + /** + * Map of (canonical) filename of the .torrent file to Snark instance. + * This is a CHM so listTorrentFiles() need not be synced, but + * all adds, deletes, and the DirMonitor should sync on it. + */ private final Map<String, Snark> _snarks; + /** used to prevent DirMonitor from deleting torrents that don't have a torrent file yet */ + private final Set<String> _magnets; private final Object _addSnarkLock; private /* FIXME final FIXME */ File _configFile; private Properties _config; @@ -57,6 +68,7 @@ public class SnarkManager implements Snark.CompleteListener { public static final String PROP_META_PREFIX = "i2psnark.zmeta."; public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield"; public static final String PROP_META_PRIORITY_SUFFIX = ".priority"; + public static final String PROP_META_MAGNET_PREFIX = "i2psnark.magnet."; private static final String CONFIG_FILE = "i2psnark.config"; public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops @@ -71,7 +83,8 @@ public class SnarkManager implements Snark.CompleteListener { public static final int DEFAULT_MAX_UP_BW = 10; public static final int DEFAULT_STARTUP_DELAY = 3; private SnarkManager() { - _snarks = new HashMap(); + _snarks = new ConcurrentHashMap(); + _magnets = new ConcurrentHashSet(); _addSnarkLock = new Object(); _context = I2PAppContext.getGlobalContext(); _log = _context.logManager().getLog(SnarkManager.class); @@ -90,8 +103,6 @@ public class SnarkManager implements Snark.CompleteListener { _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()); @@ -236,11 +247,9 @@ public class SnarkManager implements Snark.CompleteListener { 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); - } + _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); @@ -252,7 +261,9 @@ public class SnarkManager implements Snark.CompleteListener { String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS); if (ot != null) _util.setOpenTrackerString(ot); - // FIXME set util use open trackers property somehow + String useOT = _config.getProperty(I2PSnarkUtil.PROP_USE_OPENTRACKERS); + boolean bOT = useOT == null || Boolean.valueOf(useOT).booleanValue(); + _util.setUseOpenTrackers(bOT); getDataDir().mkdirs(); } @@ -321,15 +332,18 @@ public class SnarkManager implements Snark.CompleteListener { _util.setStartupDelay(minutes); changed = true; _config.setProperty(PROP_STARTUP_DELAY, "" + minutes); - addMessage(_("Startup delay limit changed to {0} minutes", minutes)); + addMessage(_("Startup delay changed to {0}", DataHelper.formatDuration2(minutes * 60 * 1000))); } } + // FIXME do this even if == null if (i2cpHost != null) { int oldI2CPPort = _util.getI2CPPort(); String oldI2CPHost = _util.getI2CPHost(); int port = oldI2CPPort; - try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {} + if (i2cpPort != null) { + try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {} + } String host = oldI2CPHost; Map opts = new HashMap(); if (i2cpOpts == null) i2cpOpts = ""; @@ -359,7 +373,7 @@ public class SnarkManager implements Snark.CompleteListener { Set names = listTorrentFiles(); for (Iterator iter = names.iterator(); iter.hasNext(); ) { Snark snark = getTorrent((String)iter.next()); - if ( (snark != null) && (!snark.stopped) ) { + if ( (snark != null) && (!snark.isStopped()) ) { snarksActive = true; break; } @@ -368,6 +382,7 @@ public class SnarkManager implements Snark.CompleteListener { Properties p = new Properties(); p.putAll(opts); _util.setI2CPConfig(i2cpHost, port, p); + _util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW)); 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 @@ -381,6 +396,7 @@ public class SnarkManager implements Snark.CompleteListener { p.putAll(opts); addMessage(_("I2CP settings changed to {0}", i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")")); _util.setI2CPConfig(i2cpHost, port, p); + _util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW)); boolean ok = _util.connect(); if (!ok) { addMessage(_("Unable to connect with the new settings, reverting to the old I2CP settings")); @@ -398,9 +414,8 @@ public class SnarkManager implements Snark.CompleteListener { 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())); + if (snark != null && snark.restartAcceptor()) { + addMessage(_("I2CP listener restarted for \"{0}\"", snark.getBaseName())); } } } @@ -422,6 +437,7 @@ public class SnarkManager implements Snark.CompleteListener { addMessage(_("Enabled open trackers - torrent restart required to take effect.")); else addMessage(_("Disabled open trackers - torrent restart required to take effect.")); + _util.setUseOpenTrackers(useOpenTrackers); changed = true; } if (openTrackers != null) { @@ -461,8 +477,13 @@ public class SnarkManager implements Snark.CompleteListener { /** 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<String> listTorrentFiles() { synchronized (_snarks) { return new HashSet(_snarks.keySet()); } } + /** + * Set of canonical .torrent filenames that we are dealing with. + * An unsynchronized copy. + */ + public Set<String> listTorrentFiles() { + return new HashSet(_snarks.keySet()); + } /** * Grab the torrent given the (canonical) filename of the .torrent file @@ -478,17 +499,38 @@ public class SnarkManager implements Snark.CompleteListener { public Snark getTorrentByBaseName(String filename) { synchronized (_snarks) { for (Snark s : _snarks.values()) { - if (s.storage.getBaseName().equals(filename)) + if (s.getBaseName().equals(filename)) return s; } } return null; } - /** @throws RuntimeException via Snark.fatal() */ + /** + * Grab the torrent given the info hash + * @return Snark or null + * @since 0.8.4 + */ + public Snark getTorrentByInfoHash(byte[] infohash) { + synchronized (_snarks) { + for (Snark s : _snarks.values()) { + if (DataHelper.eq(infohash, s.getInfoHash())) + return s; + } + } + return null; + } + + /** + * Caller must verify this torrent is not already added. + * @throws RuntimeException via Snark.fatal() + */ public void addTorrent(String filename) { addTorrent(filename, false); } - /** @throws RuntimeException via Snark.fatal() */ + /** + * Caller must verify this torrent is not already added. + * @throws RuntimeException via Snark.fatal() + */ public void addTorrent(String filename, boolean dontAutoStart) { if ((!dontAutoStart) && !_util.connected()) { addMessage(_("Connecting to I2P")); @@ -538,23 +580,26 @@ public class SnarkManager implements Snark.CompleteListener { 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())); + addMessage(_("Warning - No I2P trackers in \"{0}\", will announce to I2P open trackers and DHT only.", info.getName())); + } else if (_util.getDHT() != null) { + addMessage(_("Warning - No I2P trackers in \"{0}\", and open trackers are disabled, will announce to DHT 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())); + addMessage(_("Warning - No I2P trackers in \"{0}\", and DHT and open trackers are disabled, you should enable open trackers or DHT before starting the torrent.", info.getName())); dontAutoStart = true; } } - String rejectMessage = locked_validateTorrent(info); + String rejectMessage = validateTorrent(info); if (rejectMessage != null) { sfile.delete(); addMessage(rejectMessage); return; } else { + // TODO load saved closest DHT nodes and pass to the Snark ? + // This may take a LONG time torrent = new Snark(_util, filename, null, -1, null, null, this, _peerCoordinatorSet, _connectionAcceptor, false, dataDir.getPath()); loadSavedFilePriorities(torrent); - torrent.completeListener = this; synchronized (_snarks) { _snarks.put(filename, torrent); } @@ -564,6 +609,8 @@ public class SnarkManager implements Snark.CompleteListener { if (sfile.exists()) sfile.delete(); return; + } catch (OutOfMemoryError oom) { + addMessage(_("ERROR - Out of memory, cannot create torrent from {0}", sfile.getName()) + ": " + oom.getMessage()); } finally { if (fis != null) try { fis.close(); } catch (IOException ioe) {} } @@ -572,21 +619,163 @@ public class SnarkManager implements Snark.CompleteListener { 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}\"", torrent.storage.getBaseName())); + addMessage(_("Torrent added and started: \"{0}\"", torrent.getBaseName())); } else { - addMessage(_("Torrent added: \"{0}\"", torrent.storage.getBaseName())); + addMessage(_("Torrent added: \"{0}\"", torrent.getBaseName())); } } /** - * Get the timestamp for a torrent from the config file + * Add a torrent with the info hash alone (magnet / maggot) + * + * @param name hex or b32 name from the magnet link + * @param ih 20 byte info hash + * @throws RuntimeException via Snark.fatal() + * @since 0.8.4 + */ + public void addMagnet(String name, byte[] ih, boolean updateStatus) { + Snark torrent = new Snark(_util, name, ih, this, + _peerCoordinatorSet, _connectionAcceptor, + false, getDataDir().getPath()); + + synchronized (_snarks) { + Snark snark = getTorrentByInfoHash(ih); + if (snark != null) { + addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName())); + return; + } + // Tell the dir monitor not to delete us + _magnets.add(name); + if (updateStatus) + saveMagnetStatus(ih); + _snarks.put(name, torrent); + } + if (shouldAutoStart()) { + torrent.startTorrent(); + addMessage(_("Fetching {0}", name)); + boolean haveSavedPeers = false; + if ((!util().connected()) && !haveSavedPeers) { + addMessage(_("We have no saved peers and no other torrents are running. " + + "Fetch of {0} will not succeed until you start another torrent.", name)); + } + } else { + addMessage(_("Adding {0}", name)); + } + } + + /** + * Stop and delete a torrent running in magnet mode + * + * @param snark a torrent with a fake file name ("Magnet xxxx") + * @since 0.8.4 + */ + public void deleteMagnet(Snark snark) { + synchronized (_snarks) { + _snarks.remove(snark.getName()); + } + snark.stopTorrent(); + _magnets.remove(snark.getName()); + removeMagnetStatus(snark.getInfoHash()); + } + + /** + * Add a torrent from a MetaInfo. Save the MetaInfo data to filename. + * Holds the snarks lock to prevent interference from the DirMonitor. + * This verifies that a torrent with this infohash is not already added. + * This may take a LONG time to create or check the storage. + * + * @param metainfo the metainfo for the torrent + * @param bitfield the current completion status of the torrent + * @param filename the absolute path to save the metainfo to, generally ending in ".torrent", which is also the name of the torrent + * Must be a filesystem-safe name. + * @throws RuntimeException via Snark.fatal() + * @since 0.8.4 + */ + public void addTorrent(MetaInfo metainfo, BitField bitfield, String filename, boolean dontAutoStart) throws IOException { + // prevent interference by DirMonitor + synchronized (_snarks) { + Snark snark = getTorrentByInfoHash(metainfo.getInfoHash()); + if (snark != null) { + addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName())); + return; + } + // so addTorrent won't recheck + saveTorrentStatus(metainfo, bitfield, null); // no file priorities + try { + locked_writeMetaInfo(metainfo, filename); + // hold the lock for a long time + addTorrent(filename, dontAutoStart); + } catch (IOException ioe) { + addMessage(_("Failed to copy torrent file to {0}", filename)); + _log.error("Failed to write torrent file", ioe); + } + } + } + + /** + * Add a torrent from a file not in the torrent directory. Copy the file to filename. + * Holds the snarks lock to prevent interference from the DirMonitor. + * Caller must verify this torrent is not already added. + * This may take a LONG time to create or check the storage. + * + * @param fromfile where the file is now, presumably in a temp directory somewhere + * @param filename the absolute path to save the metainfo to, generally ending in ".torrent", which is also the name of the torrent + * Must be a filesystem-safe name. + * @throws RuntimeException via Snark.fatal() + * @since 0.8.4 + */ + public void copyAndAddTorrent(File fromfile, String filename) throws IOException { + // prevent interference by DirMonitor + synchronized (_snarks) { + boolean success = FileUtil.copy(fromfile.getAbsolutePath(), filename, false); + if (!success) { + addMessage(_("Failed to copy torrent file to {0}", filename)); + _log.error("Failed to write torrent file to " + filename); + return; + } + SecureFileOutputStream.setPerms(new File(filename)); + // hold the lock for a long time + addTorrent(filename); + } + } + + /** + * Write the metainfo to the file, caller must hold the snarks lock + * to prevent interference from the DirMonitor. + * + * @param metainfo The metainfo for the torrent + * @param filename The absolute path to save the metainfo to, generally ending in ".torrent". + * Must be a filesystem-safe name. + * @since 0.8.4 + */ + private static void locked_writeMetaInfo(MetaInfo metainfo, String filename) throws IOException { + File file = new File(filename); + if (file.exists()) + throw new IOException("Cannot overwrite an existing .torrent file: " + file.getPath()); + OutputStream out = null; + try { + out = new SecureFileOutputStream(filename); + out.write(metainfo.getTorrentData()); + } catch (IOException ioe) { + // remove any partial + file.delete(); + throw ioe; + } finally { + try { + if (out == null) + out.close(); + } catch (IOException ioe) {} + } + } + + /** + * Get the timestamp for a torrent from the config file. + * A Snark.CompleteListener method. */ public long getSavedTorrentTime(Snark snark) { - MetaInfo metainfo = snark.meta; - byte[] ih = metainfo.getInfoHash(); + byte[] ih = snark.getInfoHash(); String infohash = Base64.encode(ih); infohash = infohash.replace('=', '$'); String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); @@ -603,10 +792,13 @@ public class SnarkManager implements Snark.CompleteListener { /** * Get the saved bitfield for a torrent from the config file. * Convert "." to a full bitfield. + * A Snark.CompleteListener method. */ public BitField getSavedTorrentBitField(Snark snark) { - MetaInfo metainfo = snark.meta; - byte[] ih = metainfo.getInfoHash(); + MetaInfo metainfo = snark.getMetaInfo(); + if (metainfo == null) + return null; + byte[] ih = snark.getInfoHash(); String infohash = Base64.encode(ih); infohash = infohash.replace('=', '$'); String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); @@ -636,10 +828,13 @@ public class SnarkManager implements Snark.CompleteListener { * @since 0.8.1 */ public void loadSavedFilePriorities(Snark snark) { - MetaInfo metainfo = snark.meta; + MetaInfo metainfo = snark.getMetaInfo(); + Storage storage = snark.getStorage(); + if (metainfo == null || storage == null) + return; if (metainfo.getFiles() == null) return; - byte[] ih = metainfo.getInfoHash(); + byte[] ih = snark.getInfoHash(); String infohash = Base64.encode(ih); infohash = infohash.replace('=', '$'); String pri = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX); @@ -655,7 +850,7 @@ public class SnarkManager implements Snark.CompleteListener { } catch (Throwable t) {} } } - snark.storage.setFilePriorities(rv); + storage.setFilePriorities(rv); } /** @@ -666,6 +861,8 @@ public class SnarkManager implements Snark.CompleteListener { * 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. + * + * @param bitfield non-null * @param priorities may be null */ public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) { @@ -709,6 +906,8 @@ public class SnarkManager implements Snark.CompleteListener { _config.remove(prop); } + // TODO save closest DHT nodes too + saveConfig(); } @@ -726,9 +925,33 @@ public class SnarkManager implements Snark.CompleteListener { } /** + * Just remember we have it + * @since 0.8.4 + */ + public void saveMagnetStatus(byte[] ih) { + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + _config.setProperty(PROP_META_MAGNET_PREFIX + infohash, "."); + saveConfig(); + } + + /** + * Remove the magnet marker from the config file. + * @since 0.8.4 + */ + public void removeMagnetStatus(byte[] ih) { + String infohash = Base64.encode(ih); + infohash = infohash.replace('=', '$'); + _config.remove(PROP_META_MAGNET_PREFIX + infohash); + saveConfig(); + } + + /** + * Does not really delete on failure, that's the caller's responsibility. * Warning - does not validate announce URL - use TrackerClient.isValidAnnounce() + * @return failure message or null on success */ - private String locked_validateTorrent(MetaInfo info) throws IOException { + private String validateTorrent(MetaInfo info) { 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()); @@ -777,86 +1000,186 @@ public class SnarkManager implements Snark.CompleteListener { remaining = _snarks.size(); } if (torrent != null) { - boolean wasStopped = torrent.stopped; + boolean wasStopped = torrent.isStopped(); torrent.stopTorrent(); if (remaining == 0) { // should we disconnect/reconnect here (taking care to deal with the other thread's // I2PServerSocket.accept() call properly?) ////_util. } - String name; - if (torrent.storage != null) { - name = torrent.storage.getBaseName(); - } else { - name = sfile.getName(); - } if (!wasStopped) - addMessage(_("Torrent stopped: \"{0}\"", name)); + addMessage(_("Torrent stopped: \"{0}\"", torrent.getBaseName())); } return torrent; } + + /** + * Stop the torrent, leaving it on the list of torrents unless told to remove it + * @since 0.8.4 + */ + public void stopTorrent(Snark torrent, boolean shouldRemove) { + if (shouldRemove) { + synchronized (_snarks) { + _snarks.remove(torrent.getName()); + } + } + boolean wasStopped = torrent.isStopped(); + torrent.stopTorrent(); + if (!wasStopped) + addMessage(_("Torrent stopped: \"{0}\"", torrent.getBaseName())); + } + /** * Stop the torrent and delete the torrent file itself, but leaving the data * behind. + * Holds the snarks lock to prevent interference from the DirMonitor. */ public void removeTorrent(String filename) { - Snark torrent = stopTorrent(filename, true); - if (torrent != null) { + Snark torrent; + // prevent interference by DirMonitor + synchronized (_snarks) { + torrent = stopTorrent(filename, true); + if (torrent == null) + return; File torrentFile = new File(filename); torrentFile.delete(); - String name; - if (torrent.storage != null) { - removeTorrentStatus(torrent.storage.getMetaInfo()); - name = torrent.storage.getBaseName(); - } else { - name = torrentFile.getName(); - } - addMessage(_("Torrent removed: \"{0}\"", name)); } + Storage storage = torrent.getStorage(); + if (storage != null) + removeTorrentStatus(storage.getMetaInfo()); + addMessage(_("Torrent removed: \"{0}\"", torrent.getBaseName())); } 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); + // don't bother delaying if auto start is false + long delay = 60 * 1000 * getStartupDelayMinutes(); + if (delay > 0 && shouldAutoStart()) { + _messages.add(_("Adding torrents in {0}", DataHelper.formatDuration2(delay))); + try { Thread.sleep(delay); } 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(); + boolean doMagnets = true; while (true) { File dir = getDataDir(); if (_log.shouldLog(Log.DEBUG)) _log.debug("Directory Monitor loop over " + dir.getAbsolutePath()); try { - monitorTorrents(dir); + // Don't let this interfere with .torrent files being added or deleted + synchronized (_snarks) { + monitorTorrents(dir); + } } catch (Exception e) { _log.error("Error in the DirectoryMonitor", e); } + if (doMagnets) { + addMagnets(); + doMagnets = false; + } try { Thread.sleep(60*1000); } catch (InterruptedException ie) {} } } } - /** two listeners */ + // Begin Snark.CompleteListeners + + /** + * A Snark.CompleteListener method. + */ public void torrentComplete(Snark snark) { + MetaInfo meta = snark.getMetaInfo(); + Storage storage = snark.getStorage(); + if (meta == null || storage == null) + return; StringBuilder buf = new StringBuilder(256); - buf.append("<a href=\"/i2psnark/").append(snark.storage.getBaseName()); - if (snark.meta.getFiles() != null) + buf.append("<a href=\"/i2psnark/").append(storage.getBaseName()); + if (meta.getFiles() != null) buf.append('/'); - buf.append("\">").append(snark.storage.getBaseName()).append("</a>"); - long len = snark.meta.getTotalLength(); + buf.append("\">").append(storage.getBaseName()).append("</a>"); addMessage(_("Download finished: {0}", buf.toString())); // + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')'); updateStatus(snark); } + /** + * A Snark.CompleteListener method. + */ public void updateStatus(Snark snark) { - saveTorrentStatus(snark.meta, snark.storage.getBitField(), snark.storage.getFilePriorities()); + MetaInfo meta = snark.getMetaInfo(); + Storage storage = snark.getStorage(); + if (meta != null && storage != null) + saveTorrentStatus(meta, storage.getBitField(), storage.getFilePriorities()); } + /** + * We transitioned from magnet mode, we have now initialized our + * metainfo and storage. The listener should now call getMetaInfo() + * and save the data to disk. + * A Snark.CompleteListener method. + * + * @return the new name for the torrent or null on error + * @since 0.8.4 + */ + public String gotMetaInfo(Snark snark) { + MetaInfo meta = snark.getMetaInfo(); + Storage storage = snark.getStorage(); + if (meta != null && storage != null) { + String rejectMessage = validateTorrent(meta); + if (rejectMessage != null) { + addMessage(rejectMessage); + snark.stopTorrent(); + return null; + } + saveTorrentStatus(meta, storage.getBitField(), null); // no file priorities + String name = (new File(getDataDir(), storage.getBaseName() + ".torrent")).getAbsolutePath(); + try { + synchronized (_snarks) { + locked_writeMetaInfo(meta, name); + // put it in the list under the new name + _snarks.remove(snark.getName()); + _snarks.put(name, snark); + } + _magnets.remove(snark.getName()); + removeMagnetStatus(snark.getInfoHash()); + addMessage(_("Metainfo received for {0}", snark.getName())); + addMessage(_("Starting up torrent {0}", storage.getBaseName())); + return name; + } catch (IOException ioe) { + addMessage(_("Failed to copy torrent file to {0}", name)); + _log.error("Failed to write torrent file", ioe); + } + } + return null; + } + + // End Snark.CompleteListeners + + /** + * Add all magnets from the config file + * @since 0.8.4 + */ + private void addMagnets() { + for (Object o : _config.keySet()) { + String k = (String) o; + if (k.startsWith(PROP_META_MAGNET_PREFIX)) { + String b64 = k.substring(PROP_META_MAGNET_PREFIX.length()); + b64 = b64.replace('$', '='); + byte[] ih = Base64.decode(b64); + // ignore value + if (ih != null && ih.length == 20) + addMagnet("Magnet: " + I2PSnarkUtil.toHex(ih), ih, false); + // else remove from config? + } + } + } + private void monitorTorrents(File dir) { String fileNames[] = dir.list(TorrentFilenameFilter.instance()); List<String> foundNames = new ArrayList(0); @@ -887,6 +1210,8 @@ public class SnarkManager implements Snark.CompleteListener { } } } + // Don't remove magnet torrents that don't have a torrent file yet + existingNames.removeAll(_magnets); // now lets see which ones have been removed... for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) { String name = (String)iter.next(); @@ -940,12 +1265,12 @@ public class SnarkManager implements Snark.CompleteListener { /** 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; + private static Map<String, String> trackerMap = null; /** sorted map of name to announceURL=baseURL */ - public Map getTrackers() { + public Map<String, String> getTrackers() { if (trackerMap != null) // only do this once, can't be updated while running return trackerMap; - Map rv = new TreeMap(); + Map<String, String> rv = new TreeMap(); String trackers = _config.getProperty(PROP_TRACKERS); if ( (trackers == null) || (trackers.trim().length() <= 0) ) trackers = _context.getProperty(PROP_TRACKERS); @@ -984,7 +1309,7 @@ public class SnarkManager implements Snark.CompleteListener { Set names = listTorrentFiles(); for (Iterator iter = names.iterator(); iter.hasNext(); ) { Snark snark = getTorrent((String)iter.next()); - if ( (snark != null) && (!snark.stopped) ) + if ( (snark != null) && (!snark.isStopped()) ) snark.stopTorrent(); } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java b/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java index 38b470a7c9cf61b07ac1eee30df59a4872721a76..52bef12b4719d7b3d05e1785cfb5bed00f2b5665 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/StaticSnark.java @@ -38,6 +38,7 @@ public class StaticSnark //Security.addProvider(gnu); // And finally call the normal starting point. - Snark.main(args); + //Snark.main(args); + System.err.println("unsupported"); } } diff --git a/apps/i2psnark/java/src/org/klomp/snark/Storage.java b/apps/i2psnark/java/src/org/klomp/snark/Storage.java index 17fae523525630ec0abcc345796893791028fcda..dfc229dce201818fe7b9b2e1eec2891b2b9525d8 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Storage.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Storage.java @@ -87,6 +87,9 @@ public class Storage * Creates a storage from the existing file or directory together * with an appropriate MetaInfo file as can be announced on the * given announce String location. + * + * @param announce may be null + * @param listener may be null */ public Storage(I2PSnarkUtil util, File baseFile, String announce, StorageListener listener) throws IOException @@ -97,12 +100,12 @@ public class Storage getFiles(baseFile); long total = 0; - ArrayList lengthsList = new ArrayList(); + ArrayList<Long> lengthsList = new ArrayList(); for (int i = 0; i < lengths.length; i++) { long length = lengths[i]; total += length; - lengthsList.add(new Long(length)); + lengthsList.add(Long.valueOf(length)); } piece_size = MIN_PIECE_SIZE; @@ -119,10 +122,10 @@ public class Storage bitfield = new BitField(pieces); needed = 0; - List files = new ArrayList(); + List<List<String>> files = new ArrayList(); for (int i = 0; i < names.length; i++) { - List file = new ArrayList(); + List<String> file = new ArrayList(); StringTokenizer st = new StringTokenizer(names[i], File.separator); while (st.hasMoreTokens()) { @@ -535,7 +538,7 @@ public class Storage } else { // the following sets the needed variable changed = true; - checkCreateFiles(); + checkCreateFiles(false); } if (complete()) { _util.debug("Torrent is complete", Snark.NOTICE); @@ -590,7 +593,7 @@ public class Storage * Removes 'suspicious' characters from the given file name. * http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx */ - private static String filterName(String name) + public static String filterName(String name) { if (name.equals(".") || name.equals(" ")) return "_"; @@ -646,15 +649,26 @@ public class Storage /** * This is called at the beginning, and at presumed completion, * so we have to be careful about locking. + * + * @param recheck if true, this is a check after we downloaded the + * last piece, and we don't modify the global bitfield unless + * the check fails. */ - private void checkCreateFiles() throws IOException + private void checkCreateFiles(boolean recheck) throws IOException { // Whether we are resuming or not, // if any of the files already exists we assume we are resuming. boolean resume = false; _probablyComplete = true; - needed = metainfo.getPieces(); + // use local variables during the check + int need = metainfo.getPieces(); + BitField bfield; + if (recheck) { + bfield = new BitField(need); + } else { + bfield = bitfield; + } // Make sure all files are available and of correct length for (int i = 0; i < rafs.length; i++) @@ -715,8 +729,8 @@ public class Storage } if (correctHash) { - bitfield.set(i); - needed--; + bfield.set(i); + need--; } if (listener != null) @@ -736,6 +750,15 @@ public class Storage // } //} + // do this here so we don't confuse the user during checking + needed = need; + if (recheck && need > 0) { + // whoops, recheck failed + synchronized(bitfield) { + bitfield = bfield; + } + } + if (listener != null) { listener.storageAllChecked(this); if (needed <= 0) @@ -750,7 +773,8 @@ public class Storage openRAF(nr, false); // RW // XXX - Is this the best way to make sure we have enough space for // the whole file? - listener.storageCreateFile(this, names[nr], lengths[nr]); + if (listener != null) + listener.storageCreateFile(this, names[nr], lengths[nr]); final int ZEROBLOCKSIZE = metainfo.getPieceLength(0); byte[] zeros; try { @@ -899,11 +923,7 @@ public class Storage // checkCreateFiles() which will set 'needed' and 'bitfield' // and also call listener.storageCompleted() if the double-check // was successful. - // Todo: set a listener variable so the web shows "checking" and don't - // have the user panic when completed amount goes to zero temporarily? - needed = metainfo.getPieces(); - bitfield = new BitField(needed); - checkCreateFiles(); + checkCreateFiles(true); if (needed > 0) { if (listener != null) listener.setWantedPieces(this); diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index 89815035b4e6d1a4fa15cd9f7e8098d6877c93d2..c8f1cd6c96d28fdb99734176fc889fe2d1c8fb61 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -34,9 +34,12 @@ import java.util.Random; import java.util.Set; import net.i2p.I2PAppContext; +import net.i2p.data.Hash; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; +import org.klomp.snark.dht.DHT; + /** * Informs metainfo tracker of events and gets new peers for peer * coordinator. @@ -63,6 +66,7 @@ public class TrackerClient extends I2PAppThread private I2PSnarkUtil _util; private final MetaInfo meta; private final PeerCoordinator coordinator; + private final Snark snark; private final int port; private boolean stop; @@ -70,15 +74,19 @@ public class TrackerClient extends I2PAppThread private List trackers; - public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator) + /** + * @param meta null if in magnet mode + */ + public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator, Snark snark) { super(); // Set unique name. - String id = urlencode(coordinator.getID()); + String id = urlencode(snark.getID()); setName("TrackerClient " + id.substring(id.length() - 12)); _util = util; this.meta = meta; this.coordinator = coordinator; + this.snark = snark; this.port = 6881; //(port == -1) ? 9 : port; @@ -118,11 +126,10 @@ public class TrackerClient extends I2PAppThread @Override public void run() { - String infoHash = urlencode(meta.getInfoHash()); - String peerID = urlencode(coordinator.getID()); + String infoHash = urlencode(snark.getInfoHash()); + String peerID = urlencode(snark.getID()); + - _log.debug("Announce: [" + meta.getAnnounce() + "] infoHash: " + infoHash); - // Construct the list of trackers for this torrent, // starting with the primary one listed in the metainfo, // followed by the secondary open trackers @@ -130,12 +137,18 @@ public class TrackerClient extends I2PAppThread // the primary tracker, that we don't add it twice. // todo: check for b32 matches as well trackers = new ArrayList(2); - String primary = meta.getAnnounce(); - if (isValidAnnounce(primary)) { - trackers.add(new Tracker(meta.getAnnounce(), true)); - } else { - _log.warn("Skipping invalid or non-i2p announce: " + primary); + String primary = null; + if (meta != null) { + primary = meta.getAnnounce(); + if (isValidAnnounce(primary)) { + trackers.add(new Tracker(meta.getAnnounce(), true)); + _log.debug("Announce: [" + primary + "] infoHash: " + infoHash); + } else { + _log.warn("Skipping invalid or non-i2p announce: " + primary); + } } + if (primary == null) + primary = ""; List tlist = _util.getOpenTrackers(); if (tlist != null) { for (int i = 0; i < tlist.size(); i++) { @@ -160,15 +173,17 @@ public class TrackerClient extends I2PAppThread continue; if (primary.startsWith("http://i2p/" + dest)) continue; - trackers.add(new Tracker(url, false)); + // opentrackers are primary if we don't have primary + trackers.add(new Tracker(url, primary.equals(""))); _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash); } } - if (tlist.isEmpty()) { + if (trackers.isEmpty()) { // FIXME really need to get this message to the gui stop = true; _log.error("No valid trackers for infoHash: " + infoHash); + // FIXME keep going if DHT enabled return; } @@ -188,6 +203,9 @@ public class TrackerClient extends I2PAppThread Random r = I2PAppContext.getGlobalContext().random(); while(!stop) { + // Local DHT tracker announce + if (_util.getDHT() != null) + _util.getDHT().announce(snark.getInfoHash()); try { // Sleep some minutes... @@ -200,7 +218,7 @@ public class TrackerClient extends I2PAppThread firstTime = false; } else if (completed && runStarted) delay = 3*SLEEP*60*1000 + random; - else if (coordinator.trackerProblems != null && ++consecutiveFails < MAX_CONSEC_FAILS) + else if (snark.getTrackerProblems() != null && ++consecutiveFails < MAX_CONSEC_FAILS) delay = INITIAL_SLEEP; else // sleep a while, when we wake up we will contact only the trackers whose intervals have passed @@ -221,7 +239,7 @@ public class TrackerClient extends I2PAppThread uploaded = coordinator.getUploaded(); downloaded = coordinator.getDownloaded(); - left = coordinator.getLeft(); + left = coordinator.getLeft(); // -1 in magnet mode // First time we got a complete download? String event; @@ -251,7 +269,7 @@ public class TrackerClient extends I2PAppThread uploaded, downloaded, left, event); - coordinator.trackerProblems = null; + snark.setTrackerProblems(null); tr.trackerProblems = null; tr.registerFails = 0; tr.consecutiveFails = 0; @@ -260,24 +278,30 @@ public class TrackerClient extends I2PAppThread runStarted = true; tr.started = true; - Set peers = info.getPeers(); + Set<Peer> peers = info.getPeers(); tr.seenPeers = info.getPeerCount(); - if (coordinator.trackerSeenPeers < tr.seenPeers) // update rising number quickly - coordinator.trackerSeenPeers = tr.seenPeers; - if ( (left > 0) && (!completed) ) { + if (snark.getTrackerSeenPeers() < tr.seenPeers) // update rising number quickly + snark.setTrackerSeenPeers(tr.seenPeers); + + // pass everybody over to our tracker + if (_util.getDHT() != null) { + for (Peer peer : peers) { + _util.getDHT().announce(snark.getInfoHash(), peer.getPeerID().getDestHash()); + } + } + + if ( (left != 0) && (!completed) ) { // we only want to talk to new people if we need things // from them (duh) - List ordered = new ArrayList(peers); + List<Peer> ordered = new ArrayList(peers); Collections.shuffle(ordered, r); - Iterator it = ordered.iterator(); + Iterator<Peer> it = ordered.iterator(); while ((!stop) && it.hasNext()) { - Peer cur = (Peer)it.next(); + Peer cur = it.next(); // FIXME if id == us || dest == us continue; // only delay if we actually make an attempt to add peer - if(coordinator.addPeer(cur)) { - int delay = DELAY_MUL; - delay *= r.nextInt(10); - delay += DELAY_MIN; + if(coordinator.addPeer(cur) && it.hasNext()) { + int delay = (DELAY_MUL * r.nextInt(10)) + DELAY_MIN; sleptTime += delay; try { Thread.sleep(delay); } catch (InterruptedException ie) {} } @@ -293,12 +317,12 @@ public class TrackerClient extends I2PAppThread tr.trackerProblems = ioe.getMessage(); // don't show secondary tracker problems to the user if (tr.isPrimary) - coordinator.trackerProblems = tr.trackerProblems; + snark.setTrackerProblems(tr.trackerProblems); if (tr.trackerProblems.toLowerCase().startsWith(NOT_REGISTERED)) { // Give a guy some time to register it if using opentrackers too if (trackers.size() == 1) { stop = true; - coordinator.snark.stopTorrent(); + snark.stopTorrent(); } else { // hopefully each on the opentrackers list is really open if (tr.registerFails++ > MAX_REGISTER_FAILS) tr.stop = true; @@ -315,8 +339,66 @@ public class TrackerClient extends I2PAppThread maxSeenPeers = tr.seenPeers; } // *** end of trackers loop here + // Get peers from PEX + if (left > 0 && coordinator.needPeers() && !stop) { + Set<PeerID> pids = coordinator.getPEXPeers(); + if (!pids.isEmpty()) { + _util.debug("Got " + pids.size() + " from PEX", Snark.INFO); + List<Peer> peers = new ArrayList(pids.size()); + for (PeerID pID : pids) { + peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo())); + } + Collections.shuffle(peers, r); + Iterator<Peer> it = peers.iterator(); + while ((!stop) && it.hasNext()) { + Peer cur = it.next(); + if (coordinator.addPeer(cur) && it.hasNext()) { + int delay = (DELAY_MUL * r.nextInt(10)) + DELAY_MIN; + try { Thread.sleep(delay); } catch (InterruptedException ie) {} + } + } + } + } + + // Get peers from DHT + // FIXME this needs to be in its own thread + if (_util.getDHT() != null && !stop) { + int numwant; + if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers()) + numwant = 1; + else + numwant = _util.getMaxConnections(); + List<Hash> hashes = _util.getDHT().getPeers(snark.getInfoHash(), numwant, 2*60*1000); + _util.debug("Got " + hashes + " from DHT", Snark.INFO); + // announce ourselves while the token is still good + // FIXME this needs to be in its own thread + if (!stop) { + int good = _util.getDHT().announce(snark.getInfoHash(), 8, 5*60*1000); + _util.debug("Sent " + good + " good announces to DHT", Snark.INFO); + } + + // now try these peers + if ((!stop) && !hashes.isEmpty()) { + List<Peer> peers = new ArrayList(hashes.size()); + for (Hash h : hashes) { + PeerID pID = new PeerID(h.getData()); + peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), snark.getMetaInfo())); + } + Collections.shuffle(peers, r); + Iterator<Peer> it = peers.iterator(); + while ((!stop) && it.hasNext()) { + Peer cur = it.next(); + if (coordinator.addPeer(cur) && it.hasNext()) { + int delay = (DELAY_MUL * r.nextInt(10)) + DELAY_MIN; + try { Thread.sleep(delay); } catch (InterruptedException ie) {} + } + } + } + } + + // we could try and total the unique peers but that's too hard for now - coordinator.trackerSeenPeers = maxSeenPeers; + snark.setTrackerSeenPeers(maxSeenPeers); if (!runStarted) _util.debug(" Retrying in one minute...", Snark.DEBUG); } // *** end of while loop @@ -329,6 +411,9 @@ public class TrackerClient extends I2PAppThread } finally { + // Local DHT tracker unannounce + if (_util.getDHT() != null) + _util.getDHT().unannounce(snark.getInfoHash()); try { // try to contact everybody we can @@ -351,6 +436,8 @@ public class TrackerClient extends I2PAppThread long downloaded, long left, String event) throws IOException { + // What do we send for left in magnet mode? Can we omit it? + long tleft = left >= 0 ? left : 1; String s = tr.announce + "?info_hash=" + infoHash + "&peer_id=" + peerID @@ -358,10 +445,10 @@ public class TrackerClient extends I2PAppThread + "&ip=" + _util.getOurIPString() + ".i2p" + "&uploaded=" + uploaded + "&downloaded=" + downloaded - + "&left=" + left + + "&left=" + tleft + "&compact=1" // NOTE: opentracker will return 400 for &compact alone + ((! event.equals(NO_EVENT)) ? ("&event=" + event) : ""); - if (left <= 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers()) + if (left == 0 || event.equals(STOPPED_EVENT) || !coordinator.needPeers()) s += "&numwant=0"; else s += "&numwant=" + _util.getMaxConnections(); @@ -377,8 +464,8 @@ public class TrackerClient extends I2PAppThread try { in = new FileInputStream(fetched); - TrackerInfo info = new TrackerInfo(in, coordinator.getID(), - coordinator.getMetaInfo()); + TrackerInfo info = new TrackerInfo(in, snark.getID(), + snark.getInfoHash(), snark.getMetaInfo()); _util.debug("TrackerClient response: " + info, Snark.INFO); String failure = info.getFailureReason(); diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java index 360a4f47e416c3ed728523cfcfbd4e5a09ca9b55..1b829d0eef00d674fdbbf5225381014c931beb8d 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java @@ -46,19 +46,20 @@ public class TrackerInfo private int complete; private int incomplete; - public TrackerInfo(InputStream in, byte[] my_id, MetaInfo metainfo) + /** @param metainfo may be null */ + public TrackerInfo(InputStream in, byte[] my_id, byte[] infohash, MetaInfo metainfo) throws IOException { - this(new BDecoder(in), my_id, metainfo); + this(new BDecoder(in), my_id, infohash, metainfo); } - public TrackerInfo(BDecoder be, byte[] my_id, MetaInfo metainfo) + private TrackerInfo(BDecoder be, byte[] my_id, byte[] infohash, MetaInfo metainfo) throws IOException { - this(be.bdecodeMap().getMap(), my_id, metainfo); + this(be.bdecodeMap().getMap(), my_id, infohash, metainfo); } - public TrackerInfo(Map m, byte[] my_id, MetaInfo metainfo) + private TrackerInfo(Map m, byte[] my_id, byte[] infohash, MetaInfo metainfo) throws IOException { BEValue reason = (BEValue)m.get("failure reason"); @@ -84,10 +85,10 @@ public class TrackerInfo Set<Peer> p; try { // One big string (the official compact format) - p = getPeers(bePeers.getBytes(), my_id, metainfo); + p = getPeers(bePeers.getBytes(), my_id, infohash, metainfo); } catch (InvalidBEncodingException ibe) { // List of Dictionaries or List of Strings - p = getPeers(bePeers.getList(), my_id, metainfo); + p = getPeers(bePeers.getList(), my_id, infohash, metainfo); } peers = p; } @@ -123,7 +124,7 @@ public class TrackerInfo ******/ /** List of Dictionaries or List of Strings */ - private static Set<Peer> getPeers(List<BEValue> l, byte[] my_id, MetaInfo metainfo) + private static Set<Peer> getPeers(List<BEValue> l, byte[] my_id, byte[] infohash, MetaInfo metainfo) throws IOException { Set<Peer> peers = new HashSet(l.size()); @@ -144,7 +145,7 @@ public class TrackerInfo continue; } } - peers.add(new Peer(peerID, my_id, metainfo)); + peers.add(new Peer(peerID, my_id, infohash, metainfo)); } return peers; @@ -156,7 +157,7 @@ public class TrackerInfo * One big string of concatenated 32-byte hashes * @since 0.8.1 */ - private static Set<Peer> getPeers(byte[] l, byte[] my_id, MetaInfo metainfo) + private static Set<Peer> getPeers(byte[] l, byte[] my_id, byte[] infohash, MetaInfo metainfo) throws IOException { int count = l.length / HASH_LENGTH; @@ -172,7 +173,7 @@ public class TrackerInfo // won't happen continue; } - peers.add(new Peer(peerID, my_id, metainfo)); + peers.add(new Peer(peerID, my_id, infohash, metainfo)); } 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 986e456437a7e57770e442d207f12b3377019c13..4cae2881ae6c7d70a5e809c7187348b2d6484cd8 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java +++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEValue.java @@ -24,6 +24,8 @@ import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Map; +import net.i2p.data.Base64; + /** * Holds different types that a bencoded byte array can represent. * You need to call the correct get method to get the correct java @@ -178,12 +180,37 @@ public class BEValue String valueString; if (value instanceof byte[]) { + // try to do a nice job for debugging byte[] bs = (byte[])value; - // XXX - Stupid heuristic... and not UTF-8 - if (bs.length <= 12) - valueString = new String(bs); - else - valueString = "bytes:" + bs.length; + if (bs.length == 0) + valueString = "0 bytes"; + else if (bs.length <= 32) { + StringBuilder buf = new StringBuilder(32); + boolean bin = false; + for (int i = 0; i < bs.length; i++) { + int b = bs[i] & 0xff; + // no UTF-8 + if (b < ' ' || b > 0x7e) { + bin = true; + break; + } + } + if (bin && bs.length <= 8) { + buf.append(bs.length).append(" bytes: 0x"); + for (int i = 0; i < bs.length; i++) { + int b = bs[i] & 0xff; + if (b < 16) + buf.append('0'); + buf.append(Integer.toHexString(b)); + } + } else if (bin) { + buf.append(bs.length).append(" bytes: ").append(Base64.encode(bs)); + } else { + buf.append('"').append(new String(bs)).append('"'); + } + valueString = buf.toString(); + } else + valueString = bs.length + " bytes"; } else valueString = value.toString(); diff --git a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java index b8129f47722de6595fd470b162a4ad155af726a2..9584b0d9db322e8fa1276be415cc190e46bee4a3 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java +++ b/apps/i2psnark/java/src/org/klomp/snark/bencode/BEncoder.java @@ -50,6 +50,8 @@ public class BEncoder public static void bencode(Object o, OutputStream out) throws IOException, IllegalArgumentException { + if (o == null) + throw new NullPointerException("Cannot bencode null"); if (o instanceof String) bencode((String)o, out); else if (o instanceof byte[]) @@ -59,7 +61,7 @@ public class BEncoder else if (o instanceof List) bencode((List)o, out); else if (o instanceof Map) - bencode((Map)o, out); + bencode((Map<String, Object>)o, out); else if (o instanceof BEValue) bencode(((BEValue)o).getValue(), out); else @@ -153,7 +155,7 @@ public class BEncoder out.write(bs); } - public static byte[] bencode(Map m) + public static byte[] bencode(Map<String, Object> m) { try { @@ -167,20 +169,20 @@ public class BEncoder } } - public static void bencode(Map m, OutputStream out) throws IOException + public static void bencode(Map<String, Object> m, OutputStream out) throws IOException { out.write('d'); // Keys must be sorted. XXX - But is this the correct order? - Set s = m.keySet(); - List l = new ArrayList(s); + Set<String> s = m.keySet(); + List<String> l = new ArrayList(s); Collections.sort(l); - Iterator it = l.iterator(); + Iterator<String> it = l.iterator(); while(it.hasNext()) { // Keys must be Strings. - String key = (String)it.next(); + String key = it.next(); Object value = m.get(key); bencode(key, out); bencode(value, out); diff --git a/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java b/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java new file mode 100644 index 0000000000000000000000000000000000000000..a074890b93ff555c753fdb1cc0959a8eab67d45d --- /dev/null +++ b/apps/i2psnark/java/src/org/klomp/snark/dht/DHT.java @@ -0,0 +1,82 @@ +package org.klomp.snark.dht; + +/* + * GPLv2 + */ + +import java.util.List; + +import net.i2p.data.Destination; +import net.i2p.data.Hash; + + +/** + * Stub for KRPC + */ +public interface DHT { + + + /** + * @return The UDP port that should be included in a PORT message. + */ + public int getPort(); + + /** + * Ping. We don't have a NID yet so the node is presumed + * to be absent from our DHT. + * Non-blocking, does not wait for pong. + * If and when the pong is received the node will be inserted in our DHT. + */ + public void ping(Destination dest, int port); + + /** + * Get peers for a torrent. + * Blocking! + * Caller should run in a thread. + * + * @param ih the Info Hash (torrent) + * @param max maximum number of peers to return + * @param maxWait the maximum time to wait (ms) must be > 0 + * @return list or empty list (never null) + */ + public List<Hash> getPeers(byte[] ih, int max, long maxWait); + + /** + * Announce to ourselves. + * Non-blocking. + * + * @param ih the Info Hash (torrent) + */ + public void announce(byte[] ih); + + /** + * Announce somebody else we know about. + * Non-blocking. + * + * @param ih the Info Hash (torrent) + * @param peer the peer's Hash + */ + public void announce(byte[] ih, byte[] peerHash); + + /** + * Remove reference to ourselves in the local tracker. + * Use when shutting down the torrent locally. + * Non-blocking. + * + * @param ih the Info Hash (torrent) + */ + public void unannounce(byte[] ih); + + /** + * Announce to the closest DHT peers. + * Blocking unless maxWait <= 0 + * Caller should run in a thread. + * This also automatically announces ourself to our local tracker. + * For best results do a getPeers() first so we have tokens. + * + * @param ih the Info Hash (torrent) + * @param maxWait the maximum total time to wait (ms) or 0 to do all in parallel and return immediately. + * @return the number of successful announces, not counting ourselves. + */ + public int announce(byte[] ih, int max, long maxWait); +} 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 cfe95df13b7e91b1fb457414a63eab796e31812e..9bbfec8fee5dfb299952bd9db6c677140e949d50 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -6,6 +6,7 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.PrintWriter; import java.text.Collator; +import java.text.DecimalFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -26,6 +27,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import net.i2p.I2PAppContext; +import net.i2p.data.Base32; import net.i2p.data.Base64; import net.i2p.data.DataHelper; import net.i2p.util.FileUtil; @@ -33,6 +35,7 @@ import net.i2p.util.I2PAppThread; import net.i2p.util.Log; import net.i2p.util.SecureFileOutputStream; +import org.klomp.snark.I2PSnarkUtil; import org.klomp.snark.MetaInfo; import org.klomp.snark.Peer; import org.klomp.snark.Snark; @@ -58,8 +61,13 @@ public class I2PSnarkServlet extends Default { private Resource _resourceBase; private String _themePath; private String _imgPath; + private String _lastAnnounceURL = ""; public static final String PROP_CONFIG_FILE = "i2psnark.configFile"; + /** BEP 9 */ + private static final String MAGNET = "magnet:?xt=urn:btih:"; + /** http://sponge.i2p/files/maggotspec.txt */ + private static final String MAGGOT = "maggot://"; @Override public void init(ServletConfig cfg) throws ServletException { @@ -153,7 +161,7 @@ public class I2PSnarkServlet extends Default { resp.setCharacterEncoding("UTF-8"); resp.setContentType("text/html; charset=UTF-8"); Resource resource = getResource(pathInContext); - if (resource == null || (!resource.exists()) || !resource.isDirectory()) { + if (resource == null || (!resource.exists())) { resp.sendError(HttpResponse.__404_Not_Found); } else { String base = URI.addPaths(req.getRequestURI(), "/"); @@ -376,7 +384,7 @@ public class I2PSnarkServlet extends Default { for (int i = 0; i < snarks.size(); i++) { Snark snark = (Snark)snarks.get(i); boolean showDebug = "2".equals(peerParam); - boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.meta.getInfoHash()).equals(peerParam); + boolean showPeers = showDebug || "1".equals(peerParam) || Base64.encode(snark.getInfoHash()).equals(peerParam); displaySnark(out, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug); } @@ -478,10 +486,12 @@ public class I2PSnarkServlet extends Default { if (newURL != null) { if (newURL.startsWith("http://")) { _manager.addMessage(_("Fetching {0}", urlify(newURL))); - I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add"); + I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add", true); fetch.start(); + } else if (newURL.startsWith(MAGNET) || newURL.startsWith(MAGGOT)) { + addMagnet(newURL); } else { - _manager.addMessage(_("Invalid URL - must start with http://")); + _manager.addMessage(_("Invalid URL: Must start with \"http://\", \"{0}\", or \"{1}\"", MAGNET, MAGGOT)); } } else { // no file or URL specified @@ -494,8 +504,8 @@ public class I2PSnarkServlet extends Default { for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { String name = (String)iter.next(); Snark snark = _manager.getTorrent(name); - if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { - _manager.stopTorrent(name, false); + if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) { + _manager.stopTorrent(snark, false); break; } } @@ -508,11 +518,9 @@ public class I2PSnarkServlet extends Default { if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1 for (String name : _manager.listTorrentFiles()) { Snark snark = _manager.getTorrent(name); - if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { + if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) { snark.startTorrent(); - if (snark.storage != null) - name = snark.storage.getBaseName(); - _manager.addMessage(_("Starting up torrent {0}", name)); + _manager.addMessage(_("Starting up torrent {0}", snark.getBaseName())); break; } } @@ -526,8 +534,15 @@ public class I2PSnarkServlet extends Default { for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { String name = (String)iter.next(); Snark snark = _manager.getTorrent(name); - if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { - _manager.stopTorrent(name, true); + if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) { + MetaInfo meta = snark.getMetaInfo(); + if (meta == null) { + // magnet - remove and delete are the same thing + _manager.deleteMagnet(snark); + _manager.addMessage(_("Magnet deleted: {0}", name)); + return; + } + _manager.stopTorrent(snark, true); // should we delete the torrent file? // yeah, need to, otherwise it'll get autoadded again (at the moment File f = new File(name); @@ -546,13 +561,20 @@ public class I2PSnarkServlet extends Default { for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { String name = (String)iter.next(); Snark snark = _manager.getTorrent(name); - if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { - _manager.stopTorrent(name, true); + if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) { + MetaInfo meta = snark.getMetaInfo(); + if (meta == null) { + // magnet - remove and delete are the same thing + _manager.deleteMagnet(snark); + _manager.addMessage(_("Magnet deleted: {0}", name)); + return; + } + _manager.stopTorrent(snark, true); File f = new File(name); f.delete(); _manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath())); - List files = snark.meta.getFiles(); - String dataFile = snark.meta.getName(); + List files = meta.getFiles(); + String dataFile = snark.getBaseName(); f = new File(_manager.getDataDir(), dataFile); if (files == null) { // single file torrent if (f.delete()) @@ -612,23 +634,23 @@ public class I2PSnarkServlet extends Default { if (announceURL == null || announceURL.length() <= 0) _manager.addMessage(_("Error creating torrent - you must select a tracker")); else if (baseFile.exists()) { + _lastAnnounceURL = announceURL; + if (announceURL.equals("none")) + announceURL = null; try { + // This may take a long time to check the storage, but since it already exists, + // it shouldn't be THAT bad, so keep it in this thread. Storage s = new Storage(_manager.util(), baseFile, announceURL, null); s.create(); s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over MetaInfo info = s.getMetaInfo(); - File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent"); - if (torrentFile.exists()) - throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath()); - _manager.saveTorrentStatus(info, s.getBitField(), null); // so addTorrent won't recheck - // DirMonitor could grab this first, maybe hold _snarks lock? - FileOutputStream out = new FileOutputStream(torrentFile); - out.write(info.getTorrentData()); - out.close(); + File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent"); + // FIXME is the storage going to stay around thanks to the info reference? + // now add it, but don't automatically start it + _manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), true); _manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath()); - // now fire it up, but don't automatically seed it - _manager.addTorrent(torrentFile.getCanonicalPath(), true); - _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName())); + if (announceURL != null) + _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName())); } catch (IOException ioe) { _manager.addMessage(_("Error creating a torrent for \"{0}\"", baseFile.getAbsolutePath()) + ": " + ioe.getMessage()); } @@ -643,8 +665,8 @@ public class I2PSnarkServlet extends Default { List snarks = getSortedSnarks(req); for (int i = 0; i < snarks.size(); i++) { Snark snark = (Snark)snarks.get(i); - if (!snark.stopped) - _manager.stopTorrent(snark.torrent, false); + if (!snark.isStopped()) + _manager.stopTorrent(snark, false); } if (_manager.util().connected()) { // Give the stopped announces time to get out @@ -657,7 +679,7 @@ public class I2PSnarkServlet extends Default { List snarks = getSortedSnarks(req); for (int i = 0; i < snarks.size(); i++) { Snark snark = (Snark)snarks.get(i); - if (snark.stopped) + if (snark.isStopped()) snark.startTorrent(); } } else { @@ -725,7 +747,7 @@ public class I2PSnarkServlet extends Default { private static final int MAX_DISPLAYED_ERROR_LENGTH = 43; private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers, boolean isDegraded, boolean noThinsp, boolean showDebug) throws IOException { - String filename = snark.torrent; + String filename = snark.getName(); File f = new File(filename); filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name int i = filename.lastIndexOf(".torrent"); @@ -733,31 +755,28 @@ public class I2PSnarkServlet extends Default { filename = filename.substring(0, i); String fullFilename = filename; if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH) { - fullFilename = new String(filename); - filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "…"; + String start = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH); + if (start.indexOf(" ") < 0 && start.indexOf("-") < 0) { + // browser has nowhere to break it + fullFilename = filename; + filename = start + "…"; + } } - long total = snark.meta.getTotalLength(); + long total = snark.getTotalLength(); // Early typecast, avoid possibly overflowing a temp integer - long remaining = (long) snark.storage.needed() * (long) snark.meta.getPieceLength(0); + long remaining = (long) snark.getNeeded() * (long) snark.getPieceLength(0); if (remaining > total) remaining = total; - long downBps = 0; - long upBps = 0; - if (snark.coordinator != null) { - downBps = snark.coordinator.getDownloadRate(); - upBps = snark.coordinator.getUploadRate(); - } + long downBps = snark.getDownloadRate(); + long upBps = snark.getUploadRate(); long remainingSeconds; if (downBps > 0) remainingSeconds = remaining / downBps; else remainingSeconds = -1; - boolean isRunning = !snark.stopped; - long uploaded = 0; - if (snark.coordinator != null) { - uploaded = snark.coordinator.getUploaded(); - stats[0] += snark.coordinator.getDownloaded(); - } + boolean isRunning = !snark.isStopped(); + long uploaded = snark.getUploaded(); + stats[0] += snark.getDownloaded(); stats[1] += uploaded; if (isRunning) { stats[2] += downBps; @@ -765,25 +784,22 @@ public class I2PSnarkServlet extends Default { } stats[5] += total; - boolean isValid = snark.meta != null; - boolean singleFile = snark.meta.getFiles() == null; + MetaInfo meta = snark.getMetaInfo(); + // isValid means isNotMagnet + boolean isValid = meta != null; + boolean isMultiFile = isValid && meta.getFiles() != null; - String err = null; - int curPeers = 0; - int knownPeers = 0; - if (snark.coordinator != null) { - err = snark.coordinator.trackerProblems; - curPeers = snark.coordinator.getPeerCount(); - stats[4] += curPeers; - knownPeers = Math.max(curPeers, snark.coordinator.trackerSeenPeers); - } + String err = snark.getTrackerProblems(); + int curPeers = snark.getPeerCount(); + stats[4] += curPeers; + int knownPeers = Math.max(curPeers, snark.getTrackerSeenPeers()); String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd"); String statusString; if (err != null) { if (isRunning && curPeers > 0 && !showPeers) statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") + - ": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" + + ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; else if (isRunning) @@ -796,10 +812,10 @@ public class I2PSnarkServlet extends Default { statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") + "<br>" + err; } - } else if (remaining <= 0) { + } else if (remaining == 0) { // < 0 means no meta size yet if (isRunning && curPeers > 0 && !showPeers) statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "seeding.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Seeding") + - ": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" + + ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; else if (isRunning) @@ -811,7 +827,7 @@ public class I2PSnarkServlet extends Default { } else { if (isRunning && curPeers > 0 && downBps > 0 && !showPeers) statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("OK") + - ": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" + + ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; else if (isRunning && curPeers > 0 && downBps > 0) @@ -820,7 +836,7 @@ public class I2PSnarkServlet extends Default { ngettext("1 peer", "{0} peers", knownPeers); else if (isRunning && curPeers > 0 && !showPeers) statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Stalled") + - ": <a href=\"" + uri + "?p=" + Base64.encode(snark.meta.getInfoHash()) + "\">" + + ": <a href=\"" + uri + "?p=" + Base64.encode(snark.getInfoHash()) + "\">" + curPeers + thinsp(noThinsp) + ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; else if (isRunning && curPeers > 0) @@ -841,41 +857,24 @@ public class I2PSnarkServlet extends Default { out.write(statusString + "</td>\n\t"); out.write("<td class=\"" + rowClass + "\">"); - // temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash - String announce = snark.meta.getAnnounce(); - if (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") || - announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/") || announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/")) { - Map trackers = _manager.getTrackers(); - for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) { - Map.Entry entry = (Map.Entry)iter.next(); - String name = (String)entry.getKey(); - String baseURL = (String)entry.getValue(); - if (!(baseURL.startsWith(announce) || // vvv hack for non-b64 announce in list vvv - (announce.startsWith("http://lnQ6yoBT") && baseURL.startsWith("http://tracker2.postman.i2p/")) || - (announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/") && baseURL.startsWith("http://tracker2.postman.i2p/")))) - continue; - int e = baseURL.indexOf('='); - if (e < 0) - continue; - baseURL = baseURL.substring(e + 1); - out.write("<a href=\"" + baseURL + "details.php?dllist=1&filelist=1&info_hash="); - out.write(TrackerClient.urlencode(snark.meta.getInfoHash())); - out.write("\" title=\"" + _("Details at {0} tracker", name) + "\" target=\"_blank\">"); - out.write("<img alt=\"" + _("Info") + "\" border=\"0\" src=\"" + _imgPath + "details.png\">"); - out.write("</a>"); - break; - } + if (isValid) { + StringBuilder buf = new StringBuilder(128); + buf.append("<a href=\"").append(snark.getBaseName()) + .append("/\" title=\"").append(_("Torrent details")) + .append("\"><img alt=\"").append(_("Info")).append("\" border=\"0\" src=\"") + .append(_imgPath).append("details.png\"></a>"); + out.write(buf.toString()); } out.write("</td>\n<td class=\"" + rowClass + "\">"); StringBuilder buf = null; - if (remaining == 0 || snark.meta.getFiles() != null) { + if (remaining == 0 || isMultiFile) { buf = new StringBuilder(128); - buf.append("<a href=\"").append(snark.storage.getBaseName()); - if (snark.meta.getFiles() != null) + buf.append("<a href=\"").append(snark.getBaseName()); + if (isMultiFile) buf.append('/'); buf.append("\" title=\""); - if (snark.meta.getFiles() != null) + if (isMultiFile) buf.append(_("View files")); else buf.append(_("Open file")); @@ -883,21 +882,23 @@ public class I2PSnarkServlet extends Default { out.write(buf.toString()); } String icon; - if (snark.meta.getFiles() != null) + if (isMultiFile) icon = "folder"; + else if (isValid) + icon = toIcon(meta.getName()); else - icon = toIcon(snark.meta.getName()); - if (remaining == 0 || snark.meta.getFiles() != null) { + icon = "magnet"; + if (remaining == 0 || isMultiFile) { out.write(toImg(icon, _("Open"))); out.write("</a>"); } else { out.write(toImg(icon)); } out.write("</td><td class=\"snarkTorrentName " + rowClass + "\">"); - if (remaining == 0 || snark.meta.getFiles() != null) + if (remaining == 0 || isMultiFile) out.write(buf.toString()); out.write(filename); - if (remaining == 0 || snark.meta.getFiles() != null) + if (remaining == 0 || isMultiFile) out.write("</a>"); out.write("<td align=\"right\" class=\"snarkTorrentETA " + rowClass + "\">"); @@ -907,24 +908,26 @@ public class I2PSnarkServlet extends Default { out.write("<td align=\"right\" class=\"snarkTorrentDownloaded " + rowClass + "\">"); if (remaining > 0) out.write(formatSize(total-remaining) + thinsp(noThinsp) + formatSize(total)); - else + else if (remaining == 0) out.write(formatSize(total)); // 3GB + //else + // out.write("??"); // no meta size yet out.write("</td>\n\t"); out.write("<td align=\"right\" class=\"snarkTorrentUploaded " + rowClass + "\">"); - if(isRunning) + if(isRunning && isValid) out.write(formatSize(uploaded)); out.write("</td>\n\t"); out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">"); - if(isRunning && remaining > 0) + if(isRunning && remaining != 0) out.write(formatSize(downBps) + "ps"); out.write("</td>\n\t"); out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">"); - if(isRunning) + if(isRunning && isValid) out.write(formatSize(upBps) + "ps"); out.write("</td>\n\t"); out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">"); - String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.meta.getInfoHash()); - String b64 = Base64.encode(snark.meta.getInfoHash()); + String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.getInfoHash()); + String b64 = Base64.encode(snark.getInfoHash()); if (showPeers) parameters = parameters + "&p=1"; if (isRunning) { @@ -939,7 +942,6 @@ public class I2PSnarkServlet extends Default { if (isDegraded) out.write("</a>"); } else { - if (isValid) { if (isDegraded) out.write("<a href=\"/i2psnark/?action=Start_" + b64 + "&nonce=" + _nonce + "\"><img title=\""); else @@ -950,24 +952,25 @@ public class I2PSnarkServlet extends Default { out.write("\">"); if (isDegraded) out.write("</a>"); - } - if (isDegraded) - out.write("<a href=\"/i2psnark/?action=Remove_" + b64 + "&nonce=" + _nonce + "\"><img title=\""); - else - out.write("<input type=\"image\" name=\"action_Remove_" + b64 + "\" value=\"foo\" title=\""); - out.write(_("Remove the torrent from the active list, deleting the .torrent file")); - out.write("\" onclick=\"if (!confirm('"); - // Can't figure out how to escape double quotes inside the onclick string. - // Single quotes in translate strings with parameters must be doubled. - // Then the remaining single quite must be escaped - out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename)); - out.write("')) { return false; }\""); - out.write(" src=\"" + _imgPath + "remove.png\" alt=\""); - out.write(_("Remove")); - out.write("\">"); - if (isDegraded) - out.write("</a>"); + if (isValid) { + if (isDegraded) + out.write("<a href=\"/i2psnark/?action=Remove_" + b64 + "&nonce=" + _nonce + "\"><img title=\""); + else + out.write("<input type=\"image\" name=\"action\" value=\"Remove_" + b64 + "\" title=\""); + out.write(_("Remove the torrent from the active list, deleting the .torrent file")); + out.write("\" onclick=\"if (!confirm('"); + // Can't figure out how to escape double quotes inside the onclick string. + // Single quotes in translate strings with parameters must be doubled. + // Then the remaining single quite must be escaped + out.write(_("Are you sure you want to delete the file \\''{0}.torrent\\'' (downloaded data will not be deleted) ?", fullFilename)); + out.write("')) { return false; }\""); + out.write(" src=\"" + _imgPath + "remove.png\" alt=\""); + out.write(_("Remove")); + out.write("\">"); + if (isDegraded) + out.write("</a>"); + } if (isDegraded) out.write("<a href=\"/i2psnark/?action=Delete_" + b64 + "&nonce=" + _nonce + "\"><img title=\""); @@ -989,7 +992,7 @@ public class I2PSnarkServlet extends Default { out.write("</td>\n</tr>\n"); if(showPeers && isRunning && curPeers > 0) { - List<Peer> peers = snark.coordinator.peerList(); + List<Peer> peers = snark.getPeerList(); if (!showDebug) Collections.sort(peers, new PeerComparator()); for (Peer peer : peers) { @@ -1022,14 +1025,21 @@ public class I2PSnarkServlet extends Default { out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">"); out.write("</td>\n\t"); out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">"); - float pct = (float) (100.0 * (float) peer.completed() / snark.meta.getPieces()); - if (pct == 100.0) - out.write(_("Seed")); - else { - String ps = String.valueOf(pct); - if (ps.length() > 5) - ps = ps.substring(0, 5); - out.write(ps + "%"); + float pct; + if (isValid) { + pct = (float) (100.0 * (float) peer.completed() / meta.getPieces()); + if (pct == 100.0) + out.write(_("Seed")); + else { + String ps = String.valueOf(pct); + if (ps.length() > 5) + ps = ps.substring(0, 5); + out.write(ps + "%"); + } + } else { + pct = (float) 101.0; + // until we get the metainfo we don't know how many pieces there are + //out.write("??"); } out.write("</td>\n\t"); out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">"); @@ -1048,10 +1058,16 @@ public class I2PSnarkServlet extends Default { out.write("\">"); out.write(formatSize(peer.getDownloadRate()) + "ps</a></span>"); } + } else if (!isValid) { + //if (peer supports metadata extension) { + out.write("<span class=\"unchoked\">"); + out.write(formatSize(peer.getDownloadRate()) + "ps</span>"); + //} else { + //} } out.write("</td>\n\t"); out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">"); - if (pct != 100.0) { + if (isValid && pct < 100.0) { if (peer.isInterested() && !peer.isChoking()) { out.write("<span class=\"unchoked\">"); out.write(formatSize(peer.getUploadRate()) + "ps</span>"); @@ -1094,8 +1110,40 @@ public class I2PSnarkServlet extends Default { } } + /** + * @return string or null + * @since 0.8.4 + */ + private String getTrackerLink(String announce, byte[] infohash) { + // temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash + if (announce != null && (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") || + announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/") || + announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/"))) { + Map<String, String> trackers = _manager.getTrackers(); + for (Map.Entry<String, String> entry : trackers.entrySet()) { + String baseURL = entry.getValue(); + if (!(baseURL.startsWith(announce) || // vvv hack for non-b64 announce in list vvv + (announce.startsWith("http://lnQ6yoBT") && baseURL.startsWith("http://tracker2.postman.i2p/")) || + (announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/") && baseURL.startsWith("http://tracker2.postman.i2p/")))) + continue; + int e = baseURL.indexOf('='); + if (e < 0) + continue; + baseURL = baseURL.substring(e + 1); + String name = entry.getKey(); + StringBuilder buf = new StringBuilder(128); + buf.append("<a href=\"").append(baseURL).append("details.php?dllist=1&filelist=1&info_hash=") + .append(TrackerClient.urlencode(infohash)) + .append("\" title=\"").append(_("Details at {0} tracker", name)).append("\" target=\"_blank\">" + + "<img alt=\"").append(_("Info")).append("\" border=\"0\" src=\"") + .append(_imgPath).append("details.png\"></a>"); + return buf.toString(); + } + } + return null; + } + private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException { - String uri = req.getRequestURI(); String newURL = req.getParameter("newURL"); if ( (newURL == null) || (newURL.trim().length() <= 0) ) newURL = ""; @@ -1136,7 +1184,6 @@ public class I2PSnarkServlet extends Default { } private void writeSeedForm(PrintWriter out, HttpServletRequest req) throws IOException { - String uri = req.getRequestURI(); String baseFile = req.getParameter("baseFile"); if (baseFile == null || baseFile.trim().length() <= 0) baseFile = ""; @@ -1167,6 +1214,10 @@ public class I2PSnarkServlet extends Default { out.write(":<td><select name=\"announceURL\"><option value=\"\">"); out.write(_("Select a tracker")); out.write("</option>\n"); + // todo remember this one with _lastAnnounceURL also + out.write("<option value=\"none\">"); + out.write(_("Open trackers and DHT only")); + out.write("</option>\n"); Map trackers = _manager.getTrackers(); for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) { Map.Entry entry = (Map.Entry)iter.next(); @@ -1175,6 +1226,8 @@ public class I2PSnarkServlet extends Default { int e = announceURL.indexOf('='); if (e > 0) announceURL = announceURL.substring(0, e); + if (announceURL.equals(_lastAnnounceURL)) + announceURL += "\" selected=\"selected"; out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n"); } out.write("</select>\n"); @@ -1190,7 +1243,6 @@ public class I2PSnarkServlet extends Default { } private void writeConfigForm(PrintWriter out, HttpServletRequest req) throws IOException { - String uri = req.getRequestURI(); String dataDir = _manager.getDataDir().getAbsolutePath(); boolean autoStart = _manager.shouldAutoStart(); boolean useOpenTrackers = _manager.util().shouldUseOpenTrackers(); @@ -1308,15 +1360,17 @@ public class I2PSnarkServlet extends Default { out.write(" "); out.write(renderOptions(0, 4, options.remove("outbound.length"), "outbound.length", HOP)); - out.write("<tr><td>"); - out.write(_("I2CP host")); - out.write(": <td><input type=\"text\" name=\"i2cpHost\" value=\"" - + _manager.util().getI2CPHost() + "\" size=\"15\" > "); + if (!_context.isRouterContext()) { + out.write("<tr><td>"); + out.write(_("I2CP host")); + out.write(": <td><input type=\"text\" name=\"i2cpHost\" value=\"" + + _manager.util().getI2CPHost() + "\" size=\"15\" > "); - out.write("<tr><td>"); - out.write(_("I2CP port")); - out.write(": <td><input type=\"text\" name=\"i2cpPort\" class=\"r\" value=\"" + - + _manager.util().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" > <br>\n"); + out.write("<tr><td>"); + out.write(_("I2CP port")); + out.write(": <td><input type=\"text\" name=\"i2cpPort\" class=\"r\" value=\"" + + + _manager.util().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" > <br>\n"); + } StringBuilder opts = new StringBuilder(64); for (Iterator iter = options.entrySet().iterator(); iter.hasNext(); ) { @@ -1344,6 +1398,49 @@ public class I2PSnarkServlet extends Default { out.write("</a></span></span></div>\n"); } + /** + * @param url in base32 or hex, xt must be first magnet param + * @since 0.8.4 + */ + private void addMagnet(String url) { + String ihash; + String name; + if (url.startsWith(MAGNET)) { + ihash = url.substring(MAGNET.length()).trim(); + int amp = ihash.indexOf('&'); + if (amp >= 0) + ihash = url.substring(0, amp); + name = "Magnet " + ihash; + } else if (url.startsWith(MAGGOT)) { + ihash = url.substring(MAGGOT.length()).trim(); + int col = ihash.indexOf(':'); + if (col >= 0) + ihash = url.substring(0, col); + name = "Maggot " + ihash; + } else { + return; + } + byte[] ih = null; + if (ihash.length() == 32) { + ih = Base32.decode(ihash); + } else if (ihash.length() == 40) { + // Like DataHelper.fromHexString() but ensures no loss of leading zero bytes + ih = new byte[20]; + try { + for (int i = 0; i < 20; i++) { + ih[i] = (byte) (Integer.parseInt(ihash.substring(i*2, (i*2) + 2), 16) & 0xff); + } + } catch (NumberFormatException nfe) { + ih = null; + } + } + if (ih == null || ih.length != 20) { + _manager.addMessage(_("Invalid info hash in magnet URL {0}", url)); + return; + } + _manager.addMagnet(name, ih, true); + } + /** copied from ConfigTunnelsHelper */ private static final String HOP = "hop"; private static final String TUNNEL = "tunnel"; @@ -1384,6 +1481,11 @@ public class I2PSnarkServlet extends Default { return _manager.util().getString(s, o); } + /** translate */ + private String _(String s, Object o, Object o2) { + return _manager.util().getString(s, o, o2); + } + /** translate (ngettext) @since 0.7.14 */ private String ngettext(String s, String p, int n) { return _manager.util().getString(n, s, p); @@ -1459,13 +1561,11 @@ public class I2PSnarkServlet extends Default { private String getListHTML(Resource r, String base, boolean parent, Map postParams) throws IOException { - if (!r.isDirectory()) - return null; - - String[] ls = r.list(); - if (ls==null) - return null; - Arrays.sort(ls, Collator.getInstance()); + String[] ls = null; + if (r.isDirectory()) { + ls = r.list(); + Arrays.sort(ls, Collator.getInstance()); + } // if r is not a directory, we are only showing torrent info section StringBuilder buf=new StringBuilder(4096); buf.append(DOCTYPE + "<HTML><HEAD><TITLE>"); @@ -1487,6 +1587,7 @@ public class I2PSnarkServlet extends Default { if (title.endsWith("/")) title = title.substring(0, title.length() - 1); + String directory = title; title = _("Torrent") + ": " + title; buf.append(title); buf.append("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" + @@ -1495,13 +1596,70 @@ public class I2PSnarkServlet extends Default { if (parent) // always true buf.append("<div class=\"page\"><div class=\"mainsection\">"); - boolean showPriority = snark != null && !snark.storage.complete(); + boolean showPriority = ls != null && snark != null && snark.getStorage() != null && !snark.getStorage().complete(); if (showPriority) buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n"); - buf.append("<TABLE BORDER=0 class=\"snarkTorrents\" >" + - "<thead><tr><th>") + buf.append("<TABLE BORDER=0 class=\"snarkTorrents\" ><thead>"); + if (snark != null) { + // first row - torrent info + // FIXME center + buf.append("<tr><th colspan=\"" + (showPriority ? '4' : '3') + "\"><div>") + .append(_("Torrent")).append(": ").append(snark.getBaseName()); + int pieces = snark.getPieces(); + double completion = (pieces - snark.getNeeded()) / (double) pieces; + if (completion < 1.0) + buf.append("<br>").append(_("Completion")).append(": ").append((new DecimalFormat("0.00%")).format(completion)); + else + buf.append("<br>").append(_("Complete")); + // else unknown + buf.append("<br>").append(_("Size")).append(": ").append(formatSize(snark.getTotalLength())); + MetaInfo meta = snark.getMetaInfo(); + if (meta != null) { + List files = meta.getFiles(); + int fileCount = files != null ? files.size() : 1; + buf.append("<br>").append(_("Files")).append(": ").append(fileCount); + } + buf.append("<br>").append(_("Pieces")).append(": ").append(pieces); + buf.append("<br>").append(_("Piece size")).append(": ").append(formatSize(snark.getPieceLength(0))); + + if (meta != null) { + String announce = meta.getAnnounce(); + if (announce != null) { + buf.append("<br>"); + String trackerLink = getTrackerLink(announce, snark.getInfoHash()); + if (trackerLink != null) + buf.append(trackerLink).append(' '); + buf.append(_("Tracker")).append(": "); + if (announce.startsWith("http://")) + announce = announce.substring(7); + int slsh = announce.indexOf('/'); + if (slsh > 0) + announce = announce.substring(0, slsh); + if (announce.length() > 67) + announce = announce.substring(0, 40) + "…" + announce.substring(announce.length() - 8); + buf.append(announce); + } + } + + String hex = I2PSnarkUtil.toHex(snark.getInfoHash()); + buf.append("<br>").append(toImg("magnet", _("Magnet link"))).append(" <a href=\"") + .append(MAGNET).append(hex).append("\">") + .append(MAGNET).append(hex).append("</a>"); + // We don't have the hash of the torrent file + //buf.append("<br>").append(_("Maggot link")).append(": <a href=\"").append(MAGGOT).append(hex).append(':').append(hex).append("\">") + // .append(MAGGOT).append(hex).append(':').append(hex).append("</a>"); + buf.append("</div></th></tr>"); + } + if (ls == null) { + // We are only showing the torrent info section + buf.append("</thead></table></div></div></BODY></HTML>"); + return buf.toString(); + } + + // second row - dir info + buf.append("<tr><th>") .append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "file.png\" > ") - .append(title).append("</th><th align=\"right\">") + .append(_("Directory")).append(": ").append(directory).append("</th><th align=\"right\">") .append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "size.png\" > ") .append(_("Size")); buf.append("</th><th class=\"headerstatus\">") @@ -1542,15 +1700,16 @@ public class I2PSnarkServlet extends Default { complete = true; status = toImg("tick") + ' ' + _("Directory"); } else { - if (snark == null) { + if (snark == null || snark.getStorage() == null) { // Assume complete, perhaps he removed a completed torrent but kept a bookmark complete = true; status = toImg("cancel") + ' ' + _("Torrent not found?"); } else { + Storage storage = snark.getStorage(); try { File f = item.getFile(); if (f != null) { - long remaining = snark.storage.remaining(f.getCanonicalPath()); + long remaining = storage.remaining(f.getCanonicalPath()); if (remaining < 0) { complete = true; status = toImg("cancel") + ' ' + _("File not found in torrent?"); @@ -1558,7 +1717,7 @@ public class I2PSnarkServlet extends Default { complete = true; status = toImg("tick") + ' ' + _("Complete"); } else { - int priority = snark.storage.getPriority(f.getCanonicalPath()); + int priority = storage.getPriority(f.getCanonicalPath()); if (priority < 0) status = toImg("cancel"); else if (priority == 0) @@ -1614,7 +1773,7 @@ public class I2PSnarkServlet extends Default { buf.append("<td class=\"priority\">"); File f = item.getFile(); if ((!complete) && (!item.isDirectory()) && f != null) { - int pri = snark.storage.getPriority(f.getCanonicalPath()); + int pri = snark.getStorage().getPriority(f.getCanonicalPath()); buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(f.getCanonicalPath()).append("\" "); if (pri > 0) buf.append("checked=\"true\""); @@ -1716,6 +1875,9 @@ public class I2PSnarkServlet extends Default { /** @since 0.8.1 */ private void savePriorities(Snark snark, Map postParams) { + Storage storage = snark.getStorage(); + if (storage == null) + return; Set<Map.Entry> entries = postParams.entrySet(); for (Map.Entry entry : entries) { String key = (String)entry.getKey(); @@ -1724,14 +1886,13 @@ public class I2PSnarkServlet extends Default { String file = key.substring(4); String val = ((String[])entry.getValue())[0]; // jetty arrays int pri = Integer.parseInt(val); - snark.storage.setPriority(file, pri); + storage.setPriority(file, pri); //System.err.println("Priority now " + pri + " for " + file); } catch (Throwable t) { t.printStackTrace(); } } } - if (snark.coordinator != null) - snark.coordinator.updatePiecePriorities(); - _manager.saveTorrentStatus(snark.storage.getMetaInfo(), snark.storage.getBitField(), snark.storage.getFilePriorities()); + snark.updatePiecePriorities(); + _manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities()); } @@ -1753,15 +1914,17 @@ private static class FetchAndAdd implements Runnable { FileInputStream in = null; try { in = new FileInputStream(file); + // we do not retain this MetaInfo object, hopefully it will go away quickly MetaInfo info = new MetaInfo(in); + try { in.close(); } catch (IOException ioe) {} + Snark snark = _manager.getTorrentByInfoHash(info.getInfoHash()); + if (snark != null) { + _manager.addMessage(_("Torrent with this info hash is already running: {0}", snark.getBaseName())); + return; + } + String name = info.getName(); - name = DataHelper.stripHTML(name); // XSS - name = name.replace('/', '_'); - name = name.replace('\\', '_'); - name = name.replace('&', '+'); - name = name.replace('\'', '_'); - name = name.replace('"', '_'); - name = name.replace('`', '_'); + name = Storage.filterName(name); name = name + ".torrent"; File torrentFile = new File(_manager.getDataDir(), name); @@ -1773,18 +1936,15 @@ private static class FetchAndAdd implements Runnable { else _manager.addMessage(_("Torrent already in the queue: {0}", name)); } else { - boolean success = FileUtil.copy(file.getAbsolutePath(), canonical, false); - if (success) { - SecureFileOutputStream.setPerms(torrentFile); - _manager.addTorrent(canonical); - } else { - _manager.addMessage(_("Failed to copy torrent file to {0}", canonical)); - } + // This may take a LONG time to create the storage. + _manager.copyAndAddTorrent(file, canonical); } } catch (IOException ioe) { _manager.addMessage(_("Torrent at {0} was not valid", urlify(_url)) + ": " + ioe.getMessage()); + } catch (OutOfMemoryError oom) { + _manager.addMessage(_("ERROR - Out of memory, cannot create torrent from {0}", urlify(_url)) + ": " + oom.getMessage()); } finally { - try { in.close(); } catch (IOException ioe) {} + try { if (in != null) in.close(); } catch (IOException ioe) {} } } else { _manager.addMessage(_("Torrent was not retrieved from {0}", urlify(_url))); diff --git a/core/java/src/net/i2p/data/ByteArray.java b/core/java/src/net/i2p/data/ByteArray.java index 7369a27d191c01e63c2d9ca9b4f10807b4f7f344..9f460fe4753f8eb41d4a7f399ee5c54bf166619d 100644 --- a/core/java/src/net/i2p/data/ByteArray.java +++ b/core/java/src/net/i2p/data/ByteArray.java @@ -24,6 +24,7 @@ public class ByteArray implements Serializable, Comparable { public ByteArray() { } + /** Sets valid */ public ByteArray(byte[] data) { _data = data; _valid = (data != null ? data.length : 0); @@ -38,6 +39,7 @@ public class ByteArray implements Serializable, Comparable { return _data; } + /** Warning, does not set valid */ public void setData(byte[] data) { _data = data; }