propagate from branch 'i2p.i2p.zzz.test4' (head 1b67209a056b1c17df560e857ee8b114a59254a3)

to branch 'i2p.i2p.zzz.dhtsnark' (head 463d86d9ccc9ed8c2faa79f2cf959effa8f92089)
This commit is contained in:
zzz
2011-01-13 16:32:26 +00:00
28 changed files with 2538 additions and 628 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

View File

@@ -137,6 +137,11 @@ public class ConnectionAcceptor implements Runnable
} }
} }
} else { } 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"); Thread t = new I2PAppThread(new Handler(socket), "I2PSnark incoming connection");
t.start(); t.start();
} }
@@ -174,11 +179,8 @@ public class ConnectionAcceptor implements Runnable
try { try {
InputStream in = _socket.getInputStream(); InputStream in = _socket.getInputStream();
OutputStream out = _socket.getOutputStream(); OutputStream out = _socket.getOutputStream();
// this is for the readahead in PeerAcceptor.connection()
if (true) {
in = new BufferedInputStream(in); in = new BufferedInputStream(in);
//out = new BufferedOutputStream(out);
}
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Handling socket from " + _socket.getPeerDestination().calculateHash().toBase64()); _log.debug("Handling socket from " + _socket.getPeerDestination().calculateHash().toBase64());
peeracceptor.connection(_socket, in, out); peeracceptor.connection(_socket, in, out);

View File

@@ -31,6 +31,12 @@ public interface CoordinatorListener
*/ */
void peerChange(PeerCoordinator coordinator, Peer peer); 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 overUploadLimit(int uploaders);
public boolean overUpBWLimit(); public boolean overUpBWLimit();
public boolean overUpBWLimit(long total); public boolean overUpBWLimit(long total);

View File

@@ -0,0 +1,347 @@
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;
public static final String TYPE_PEX = "ut_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()
}
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 ?
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 ?
return;
}
int metaSize = msize.getInt();
if (_log.shouldLog(Log.WARN))
_log.debug("Got the metainfo size: " + metaSize);
MagnetState state = peer.getMagnetState();
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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -34,6 +34,9 @@ import net.i2p.util.SimpleScheduler;
import net.i2p.util.SimpleTimer; import net.i2p.util.SimpleTimer;
import net.i2p.util.Translate; import net.i2p.util.Translate;
import org.klomp.snark.dht.DHT;
import org.klomp.snark.dht.KRPC;
/** /**
* I2P specific helpers for I2PSnark * I2P specific helpers for I2PSnark
* We use this class as a sort of context for i2psnark * We use this class as a sort of context for i2psnark
@@ -58,6 +61,8 @@ public class I2PSnarkUtil {
private int _maxConnections; private int _maxConnections;
private File _tmpDir; private File _tmpDir;
private int _startupDelay; private int _startupDelay;
private boolean _shouldUseOT;
private DHT _dht;
public static final int DEFAULT_STARTUP_DELAY = 3; public static final int DEFAULT_STARTUP_DELAY = 3;
public static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers"; 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 String DEFAULT_OPENTRACKERS = "http://tracker.welterde.i2p/a";
public static final int DEFAULT_MAX_UP_BW = 8; //KBps public static final int DEFAULT_MAX_UP_BW = 8; //KBps
public static final int MAX_CONNECTIONS = 16; // per torrent 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) { public I2PSnarkUtil(I2PAppContext ctx) {
_context = ctx; _context = ctx;
_log = _context.logManager().getLog(Snark.class); _log = _context.logManager().getLog(Snark.class);
@@ -78,6 +86,7 @@ public class I2PSnarkUtil {
_maxUpBW = DEFAULT_MAX_UP_BW; _maxUpBW = DEFAULT_MAX_UP_BW;
_maxConnections = MAX_CONNECTIONS; _maxConnections = MAX_CONNECTIONS;
_startupDelay = DEFAULT_STARTUP_DELAY; _startupDelay = DEFAULT_STARTUP_DELAY;
_shouldUseOT = DEFAULT_USE_OPENTRACKERS;
// This is used for both announce replies and .torrent file downloads, // This is used for both announce replies and .torrent file downloads,
// so it must be available even if not connected to I2CP. // so it must be available even if not connected to I2CP.
// so much for multiple instances // so much for multiple instances
@@ -124,9 +133,21 @@ public class I2PSnarkUtil {
_configured = true; _configured = true;
} }
/**
* @param KBps
*/
public void setMaxUpBW(int limit) { public void setMaxUpBW(int limit) {
_maxUpBW = limit; _maxUpBW = limit;
_opts.put(PROP_MAX_BW, Integer.toString(limit * (1024 * 6 / 5))); // add a little for overhead
_configured = true; _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) { public void setMaxConnections(int limit) {
@@ -146,6 +167,10 @@ public class I2PSnarkUtil {
public int getEepProxyPort() { return _proxyPort; } public int getEepProxyPort() { return _proxyPort; }
public boolean getEepProxySet() { return _shouldProxy; } public boolean getEepProxySet() { return _shouldProxy; }
public int getMaxUploaders() { return _maxUploaders; } public int getMaxUploaders() { return _maxUploaders; }
/**
* @return KBps
*/
public int getMaxUpBW() { return _maxUpBW; } public int getMaxUpBW() { return _maxUpBW; }
public int getMaxConnections() { return _maxConnections; } public int getMaxConnections() { return _maxConnections; }
public int getStartupDelay() { return _startupDelay; } public int getStartupDelay() { return _startupDelay; }
@@ -158,7 +183,7 @@ public class I2PSnarkUtil {
// try to find why reconnecting after stop // try to find why reconnecting after stop
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Connecting to I2P", new Exception("I did it")); _log.debug("Connecting to I2P", new Exception("I did it"));
Properties opts = new Properties(); Properties opts = _context.getProperties();
if (_opts != null) { if (_opts != null) {
for (Iterator iter = _opts.keySet().iterator(); iter.hasNext(); ) { for (Iterator iter = _opts.keySet().iterator(); iter.hasNext(); ) {
String key = (String)iter.next(); String key = (String)iter.next();
@@ -187,10 +212,20 @@ public class I2PSnarkUtil {
// opts.setProperty("i2p.streaming.readTimeout", "120000"); // opts.setProperty("i2p.streaming.readTimeout", "120000");
_manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts); _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 (_manager != null);
} }
/**
* @return null if disabled or not started
* @since 0.8.4
*/
public DHT getDHT() { return _dht; }
public boolean connected() { return _manager != null; } public boolean connected() { return _manager != null; }
/** /**
* Destroy the destination itself * Destroy the destination itself
*/ */
@@ -214,6 +249,8 @@ public class I2PSnarkUtil {
Destination addr = peer.getAddress(); Destination addr = peer.getAddress();
if (addr == null) if (addr == null)
throw new IOException("Null address"); throw new IOException("Null address");
if (addr.equals(getMyDestination()))
throw new IOException("Attempt to connect to myself");
Hash dest = addr.calculateHash(); Hash dest = addr.calculateHash();
if (_shitlist.contains(dest)) if (_shitlist.contains(dest))
throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are shitlisted"); throw new IOException("Not trying to contact " + dest.toBase64() + ", as they are shitlisted");
@@ -287,17 +324,25 @@ public class I2PSnarkUtil {
} }
String getOurIPString() { String getOurIPString() {
if (_manager == null) Destination dest = getMyDestination();
return "unknown";
I2PSession sess = _manager.getSession();
if (sess != null) {
Destination dest = sess.getMyDestination();
if (dest != null) if (dest != null)
return dest.toBase64(); return dest.toBase64();
}
return "unknown"; return "unknown";
} }
/**
* @return dest or null
* @since 0.8.4
*/
Destination getMyDestination() {
if (_manager == null)
return null;
I2PSession sess = _manager.getSession();
if (sess != null)
return sess.getMyDestination();
return null;
}
/** Base64 only - static (no naming service) */ /** Base64 only - static (no naming service) */
static Destination getDestinationFromBase64(String ip) { static Destination getDestinationFromBase64(String ip) {
if (ip == null) return null; if (ip == null) return null;
@@ -400,10 +445,10 @@ public class I2PSnarkUtil {
/** comma delimited list open trackers to use as backups */ /** comma delimited list open trackers to use as backups */
/** sorted map of name to announceURL=baseURL */ /** sorted map of name to announceURL=baseURL */
public List getOpenTrackers() { public List<String> getOpenTrackers() {
if (!shouldUseOpenTrackers()) if (!shouldUseOpenTrackers())
return null; return null;
List rv = new ArrayList(1); List<String> rv = new ArrayList(1);
String trackers = getOpenTrackerString(); String trackers = getOpenTrackerString();
StringTokenizer tok = new StringTokenizer(trackers, ", "); StringTokenizer tok = new StringTokenizer(trackers, ", ");
while (tok.hasMoreTokens()) while (tok.hasMoreTokens())
@@ -414,11 +459,27 @@ public class I2PSnarkUtil {
return rv; return rv;
} }
public void setUseOpenTrackers(boolean yes) {
_shouldUseOT = yes;
}
public boolean shouldUseOpenTrackers() { public boolean shouldUseOpenTrackers() {
String rv = (String) _opts.get(PROP_USE_OPENTRACKERS); return _shouldUseOT;
if (rv == null) }
return DEFAULT_USE_OPENTRACKERS;
return Boolean.valueOf(rv).booleanValue(); /**
* 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 */ /** hook between snark's logger and an i2p log */

View File

@@ -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;
}
}

View File

@@ -53,6 +53,7 @@ class Message
// Used for HAVE, REQUEST, PIECE and CANCEL messages. // Used for HAVE, REQUEST, PIECE and CANCEL messages.
// low byte used for EXTENSION message // low byte used for EXTENSION message
// low two bytes used for PORT message
int piece; int piece;
// Used for REQUEST, PIECE and CANCEL messages. // Used for REQUEST, PIECE and CANCEL messages.
@@ -67,7 +68,8 @@ class Message
// Used to do deferred fetch of data // Used to do deferred fetch of data
DataLoader dataLoader; DataLoader dataLoader;
SimpleTimer.TimedEvent expireEvent; // now unused
//SimpleTimer.TimedEvent expireEvent;
/** Utility method for sending a message through a DataStream. */ /** Utility method for sending a message through a DataStream. */
void sendMessage(DataOutputStream dos) throws IOException void sendMessage(DataOutputStream dos) throws IOException
@@ -103,10 +105,13 @@ class Message
if (type == REQUEST || type == CANCEL) if (type == REQUEST || type == CANCEL)
datalen += 4; datalen += 4;
// length is 1 byte // msg type is 1 byte
if (type == EXTENSION) if (type == EXTENSION)
datalen += 1; datalen += 1;
if (type == PORT)
datalen += 2;
// add length of data for piece or bitfield array. // add length of data for piece or bitfield array.
if (type == BITFIELD || type == PIECE || type == EXTENSION) if (type == BITFIELD || type == PIECE || type == EXTENSION)
datalen += len; datalen += len;
@@ -130,6 +135,9 @@ class Message
if (type == EXTENSION) if (type == EXTENSION)
dos.writeByte((byte) piece & 0xff); dos.writeByte((byte) piece & 0xff);
if (type == PORT)
dos.writeShort(piece & 0xffff);
// Send actual data // Send actual data
if (type == BITFIELD || type == PIECE || type == EXTENSION) if (type == BITFIELD || type == PIECE || type == EXTENSION)
dos.write(data, off, len); dos.write(data, off, len);
@@ -160,6 +168,8 @@ class Message
return "PIECE(" + piece + "," + begin + "," + length + ")"; return "PIECE(" + piece + "," + begin + "," + length + ")";
case CANCEL: case CANCEL:
return "CANCEL(" + piece + "," + begin + "," + length + ")"; return "CANCEL(" + piece + "," + begin + "," + length + ")";
case PORT:
return "PORT(" + piece + ")";
case EXTENSION: case EXTENSION:
return "EXTENSION(" + piece + ',' + data.length + ')'; return "EXTENSION(" + piece + ',' + data.length + ')';
default: default:

View File

@@ -25,6 +25,7 @@ import java.io.InputStream;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@@ -53,37 +54,43 @@ public class MetaInfo
private final byte[] info_hash; private final byte[] info_hash;
private final String name; private final String name;
private final String name_utf8; private final String name_utf8;
private final List files; private final List<List<String>> files;
private final List files_utf8; private final List<List<String>> files_utf8;
private final List lengths; private final List<Long> lengths;
private final int piece_length; private final int piece_length;
private final byte[] piece_hashes; private final byte[] piece_hashes;
private final long length; private final long length;
private final Map infoMap; private Map<String, BEValue> infoMap;
private byte[] torrentdata; /**
* Called by Storage when creating a new torrent from local data
MetaInfo(String announce, String name, String name_utf8, List files, List lengths, *
* @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) int piece_length, byte[] piece_hashes, long length)
{ {
this.announce = announce; this.announce = announce;
this.name = name; this.name = name;
this.name_utf8 = name_utf8; this.name_utf8 = name_utf8;
this.files = files; this.files = files == null ? null : Collections.unmodifiableList(files);
this.files_utf8 = null; this.files_utf8 = null;
this.lengths = lengths; this.lengths = lengths == null ? null : Collections.unmodifiableList(lengths);
this.piece_length = piece_length; this.piece_length = piece_length;
this.piece_hashes = piece_hashes; this.piece_hashes = piece_hashes;
this.length = length; this.length = length;
this.info_hash = calculateInfoHash(); this.info_hash = calculateInfoHash();
infoMap = null; //infoMap = null;
} }
/** /**
* Creates a new MetaInfo from the given InputStream. The * Creates a new MetaInfo from the given InputStream. The
* InputStream must start with a correctly bencoded dictonary * InputStream must start with a correctly bencoded dictonary
* describing the torrent. * describing the torrent.
* Caller must close the stream.
*/ */
public MetaInfo(InputStream in) throws IOException 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 * Creates a new MetaInfo from a Map of BEValues and the SHA1 over
* the original bencoded info dictonary (this is a hack, we could * the original bencoded info dictonary (this is a hack, we could
* reconstruct the bencoded stream and recalculate the hash). Will * reconstruct the bencoded stream and recalculate the hash). Will
* throw a InvalidBEncodingException if the given map does not * NOT throw a InvalidBEncodingException if the given map does not
* contain a valid announce string or info dictonary. * 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 public MetaInfo(Map m) throws InvalidBEncodingException
{ {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Creating a metaInfo: " + m, new Exception("source")); _log.debug("Creating a metaInfo: " + m, new Exception("source"));
BEValue val = (BEValue)m.get("announce"); BEValue val = (BEValue)m.get("announce");
if (val == null) // Disabled check, we can get info from a magnet now
throw new InvalidBEncodingException("Missing announce string"); if (val == null) {
//throw new InvalidBEncodingException("Missing announce string");
this.announce = null;
} else {
this.announce = val.getString(); this.announce = val.getString();
}
val = (BEValue)m.get("info"); val = (BEValue)m.get("info");
if (val == null) if (val == null)
throw new InvalidBEncodingException("Missing info map"); throw new InvalidBEncodingException("Missing info map");
Map info = val.getMap(); Map info = val.getMap();
infoMap = info; infoMap = Collections.unmodifiableMap(info);
val = (BEValue)info.get("name"); val = (BEValue)info.get("name");
if (val == null) if (val == null)
@@ -160,39 +173,39 @@ public class MetaInfo
throw new InvalidBEncodingException throw new InvalidBEncodingException
("Missing length number and/or files list"); ("Missing length number and/or files list");
List list = val.getList(); List<BEValue> list = val.getList();
int size = list.size(); int size = list.size();
if (size == 0) if (size == 0)
throw new InvalidBEncodingException("zero size files list"); throw new InvalidBEncodingException("zero size files list");
files = new ArrayList(size); List<List<String>> m_files = new ArrayList(size);
files_utf8 = new ArrayList(size); List<List<String>> m_files_utf8 = new ArrayList(size);
lengths = new ArrayList(size); List<Long> m_lengths = new ArrayList(size);
long l = 0; long l = 0;
for (int i = 0; i < list.size(); i++) for (int i = 0; i < list.size(); i++)
{ {
Map desc = ((BEValue)list.get(i)).getMap(); Map<String, BEValue> desc = list.get(i).getMap();
val = (BEValue)desc.get("length"); val = desc.get("length");
if (val == null) if (val == null)
throw new InvalidBEncodingException("Missing length number"); throw new InvalidBEncodingException("Missing length number");
long len = val.getLong(); long len = val.getLong();
lengths.add(new Long(len)); m_lengths.add(Long.valueOf(len));
l += len; l += len;
val = (BEValue)desc.get("path"); val = (BEValue)desc.get("path");
if (val == null) if (val == null)
throw new InvalidBEncodingException("Missing path list"); throw new InvalidBEncodingException("Missing path list");
List path_list = val.getList(); List<BEValue> path_list = val.getList();
int path_length = path_list.size(); int path_length = path_list.size();
if (path_length == 0) if (path_length == 0)
throw new InvalidBEncodingException("zero size file path list"); throw new InvalidBEncodingException("zero size file path list");
List file = new ArrayList(path_length); List<String> file = new ArrayList(path_length);
Iterator it = path_list.iterator(); Iterator<BEValue> it = path_list.iterator();
while (it.hasNext()) 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"); val = (BEValue)desc.get("path.utf-8");
if (val != null) { if (val != null) {
@@ -202,11 +215,14 @@ public class MetaInfo
file = new ArrayList(path_length); file = new ArrayList(path_length);
it = path_list.iterator(); it = path_list.iterator();
while (it.hasNext()) while (it.hasNext())
file.add(((BEValue)it.next()).getString()); file.add(it.next().getString());
files_utf8.add(file); 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; length = l;
} }
@@ -215,6 +231,7 @@ public class MetaInfo
/** /**
* Returns the string representing the URL of the tracker for this torrent. * Returns the string representing the URL of the tracker for this torrent.
* @return may be null!
*/ */
public String getAnnounce() public String getAnnounce()
{ {
@@ -253,9 +270,8 @@ public class MetaInfo
* a single name. It has the same size as the list returned by * a single name. It has the same size as the list returned by
* getLengths(). * getLengths().
*/ */
public List getFiles() public List<List<String>> getFiles()
{ {
// XXX - Immutable?
return files; return files;
} }
@@ -264,9 +280,8 @@ public class MetaInfo
* files, or null if it is a single file. It has the same size as * files, or null if it is a single file. It has the same size as
* the list returned by getFiles(). * the list returned by getFiles().
*/ */
public List getLengths() public List<Long> getLengths()
{ {
// XXX - Immutable?
return lengths; return lengths;
} }
@@ -388,33 +403,42 @@ public class MetaInfo
piece_hashes, length); piece_hashes, length);
} }
public byte[] getTorrentData() /**
{ * Called by servlet to save a new torrent file generated from local data
if (torrentdata == null) */
public synchronized byte[] getTorrentData()
{ {
Map m = new HashMap(); Map m = new HashMap();
if (announce != null)
m.put("announce", announce); m.put("announce", announce);
Map info = createInfoMap(); Map info = createInfoMap();
m.put("info", info); m.put("info", info);
torrentdata = BEncoder.bencode(m); // don't save this locally, we should only do this once
} return BEncoder.bencode(m);
return torrentdata;
} }
private Map createInfoMap() /** @since 0.8.4 */
{ public synchronized byte[] getInfoBytes() {
Map info = new HashMap(); if (infoMap == null)
if (infoMap != null) { createInfoMap();
info.putAll(infoMap); return BEncoder.bencode(infoMap);
return info;
} }
/** @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();
info.put("name", name); info.put("name", name);
if (name_utf8 != null) if (name_utf8 != null)
info.put("name.utf-8", name_utf8); info.put("name.utf-8", name_utf8);
info.put("piece length", Integer.valueOf(piece_length)); info.put("piece length", Integer.valueOf(piece_length));
info.put("pieces", piece_hashes); info.put("pieces", piece_hashes);
if (files == null) if (files == null)
info.put("length", new Long(length)); info.put("length", Long.valueOf(length));
else else
{ {
List l = new ArrayList(); List l = new ArrayList();
@@ -429,7 +453,8 @@ public class MetaInfo
} }
info.put("files", l); info.put("files", l);
} }
return info; infoMap = info;
return Collections.unmodifiableMap(infoMap);
} }
private byte[] calculateInfoHash() private byte[] calculateInfoHash()

View File

@@ -20,7 +20,6 @@
package org.klomp.snark; package org.klomp.snark;
import java.io.BufferedInputStream;
import java.io.DataInputStream; import java.io.DataInputStream;
import java.io.DataOutputStream; import java.io.DataOutputStream;
import java.io.IOException; import java.io.IOException;
@@ -28,10 +27,15 @@ import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import net.i2p.client.streaming.I2PSocket; import net.i2p.client.streaming.I2PSocket;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.util.Log; import net.i2p.util.Log;
import org.klomp.snark.bencode.BEValue;
public class Peer implements Comparable public class Peer implements Comparable
{ {
private Log _log = new Log(Peer.class); private Log _log = new Log(Peer.class);
@@ -39,17 +43,27 @@ public class Peer implements Comparable
private final PeerID peerID; private final PeerID peerID;
private final byte[] my_id; 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 data in/output streams set during the handshake and used by
// the actual connections. // the actual connections.
private DataInputStream din; private DataInputStream din;
private DataOutputStream dout; private DataOutputStream dout;
/** running counters */
private long downloaded;
private long uploaded;
// Keeps state for in/out connections. Non-null when the handshake // Keeps state for in/out connections. Non-null when the handshake
// was successful, the connection setup and runs // was successful, the connection setup and runs
PeerState state; PeerState state;
/** shared across all peers on this torrent */
MagnetState magnetState;
private I2PSocket sock; private I2PSocket sock;
private boolean deregister = true; private boolean deregister = true;
@@ -64,18 +78,20 @@ public class Peer implements Comparable
static final long OPTION_EXTENSION = 0x0000000000100000l; static final long OPTION_EXTENSION = 0x0000000000100000l;
static final long OPTION_FAST = 0x0000000000000004l; static final long OPTION_FAST = 0x0000000000000004l;
static final long OPTION_DHT = 0x0000000000000001l; static final long OPTION_DHT = 0x0000000000000001l;
static final long OPTION_AZMP = 0x1000000000000000l;
private long options; private long options;
/** /**
* Outgoing connection. * Outgoing connection.
* Creates a disconnected peer given a PeerID, your own id and the * Creates a disconnected peer given a PeerID, your own id and the
* relevant MetaInfo. * relevant MetaInfo.
* @param metainfo null if in magnet mode
*/ */
public Peer(PeerID peerID, byte[] my_id, MetaInfo metainfo) public Peer(PeerID peerID, byte[] my_id, byte[] infohash, MetaInfo metainfo)
throws IOException
{ {
this.peerID = peerID; this.peerID = peerID;
this.my_id = my_id; this.my_id = my_id;
this.infohash = infohash;
this.metainfo = metainfo; this.metainfo = metainfo;
_id = ++__id; _id = ++__id;
//_log.debug("Creating a new peer with " + peerID.toString(), new Exception("creating")); //_log.debug("Creating a new peer with " + peerID.toString(), new Exception("creating"));
@@ -89,12 +105,14 @@ public class Peer implements Comparable
* get the remote peer id. To completely start the connection call * get the remote peer id. To completely start the connection call
* the connect() method. * the connect() method.
* *
* @param metainfo null if in magnet mode
* @exception IOException when an error occurred during the handshake. * @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 throws IOException
{ {
this.my_id = my_id; this.my_id = my_id;
this.infohash = infohash;
this.metainfo = metainfo; this.metainfo = metainfo;
this.sock = sock; this.sock = sock;
@@ -102,7 +120,7 @@ public class Peer implements Comparable
this.peerID = new PeerID(id, sock.getPeerDestination()); this.peerID = new PeerID(id, sock.getPeerDestination());
_id = ++__id; _id = ++__id;
if (_log.shouldLog(Log.DEBUG)) 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 +210,7 @@ public class Peer implements Comparable
* If the given BitField is non-null it is send to the peer as first * If the given BitField is non-null it is send to the peer as first
* message. * message.
*/ */
public void runConnection(I2PSnarkUtil util, PeerListener listener, BitField bitfield) public void runConnection(I2PSnarkUtil util, PeerListener listener, BitField bitfield, MagnetState mState)
{ {
if (state != null) if (state != null)
throw new IllegalStateException("Peer already started"); throw new IllegalStateException("Peer already started");
@@ -212,19 +230,8 @@ public class Peer implements Comparable
throw new IOException("Unable to reach " + peerID); throw new IOException("Unable to reach " + peerID);
} }
InputStream in = sock.getInputStream(); InputStream in = sock.getInputStream();
OutputStream out = sock.getOutputStream(); //new BufferedOutputStream(sock.getOutputStream()); OutputStream out = sock.getOutputStream();
if (true) { byte [] id = handshake(in, out);
// 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);
byte [] expected_id = peerID.getID(); byte [] expected_id = peerID.getID();
if (expected_id == null) { if (expected_id == null) {
peerID.setID(id); peerID.setID(id);
@@ -243,14 +250,29 @@ public class Peer implements Comparable
_log.debug("Already have din [" + sock + "] with " + toString()); _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); PeerConnectionIn in = new PeerConnectionIn(this, din);
PeerConnectionOut out = new PeerConnectionOut(this, dout); PeerConnectionOut out = new PeerConnectionOut(this, dout);
PeerState s = new PeerState(this, listener, metainfo, in, out); PeerState s = new PeerState(this, listener, metainfo, in, out);
if ((options & OPTION_EXTENSION) != 0) { if ((options & OPTION_EXTENSION) != 0) {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Peer supports extensions, sending test message"); _log.debug("Peer supports extensions, sending reply message");
out.sendExtension(0, ExtensionHandshake.getPayload()); int metasize = metainfo != null ? metainfo.getInfoBytes().length : -1;
out.sendExtension(0, ExtensionHandler.getHandshake(metasize));
}
if ((options & OPTION_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 // Send our bitmap
@@ -259,6 +281,7 @@ public class Peer implements Comparable
// We are up and running! // We are up and running!
state = s; state = s;
magnetState = mState;
listener.connected(this); listener.connected(this);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
@@ -293,7 +316,7 @@ public class Peer implements Comparable
* Sets DataIn/OutputStreams, does the handshake and returns the id * Sets DataIn/OutputStreams, does the handshake and returns the id
* reported by the other side. * 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 throws IOException
{ {
din = new DataInputStream(in); din = new DataInputStream(in);
@@ -303,10 +326,10 @@ public class Peer implements Comparable
dout.write(19); dout.write(19);
dout.write("BitTorrent protocol".getBytes("UTF-8")); dout.write("BitTorrent protocol".getBytes("UTF-8"));
// Handshake write - options // Handshake write - options
dout.writeLong(OPTION_EXTENSION); // FIXME not if DHT disabled
dout.writeLong(OPTION_EXTENSION | OPTION_DHT);
// Handshake write - metainfo hash // Handshake write - metainfo hash
byte[] shared_hash = metainfo.getInfoHash(); dout.write(infohash);
dout.write(shared_hash);
// Handshake write - peer id // Handshake write - peer id
dout.write(my_id); dout.write(my_id);
dout.flush(); dout.flush();
@@ -334,7 +357,7 @@ public class Peer implements Comparable
// Handshake read - metainfo hash // Handshake read - metainfo hash
bs = new byte[20]; bs = new byte[20];
din.readFully(bs); din.readFully(bs);
if (!Arrays.equals(shared_hash, bs)) if (!Arrays.equals(infohash, bs))
throw new IOException("Unexpected MetaInfo hash"); throw new IOException("Unexpected MetaInfo hash");
// Handshake read - peer id // Handshake read - peer id
@@ -342,8 +365,11 @@ public class Peer implements Comparable
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Read the remote side's hash and peerID fully from " + toString()); _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) { if (options != 0) {
// send them something // send them something in runConnection() above
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Peer supports options 0x" + Long.toString(options, 16) + ": " + toString()); _log.debug("Peer supports options 0x" + Long.toString(options, 16) + ": " + toString());
} }
@@ -351,6 +377,55 @@ public class Peer implements Comparable
return bs; 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() public boolean isConnected()
{ {
return state != null; return state != null;
@@ -513,14 +588,29 @@ public class Peer implements Comparable
return (s == null) || s.choked; 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. * Returns the number of bytes that have been downloaded.
* Can be reset to zero with <code>resetCounters()</code>/ * Can be reset to zero with <code>resetCounters()</code>/
*/ */
public long getDownloaded() public long getDownloaded()
{ {
PeerState s = state; return downloaded;
return (s != null) ? s.downloaded : 0;
} }
/** /**
@@ -529,8 +619,7 @@ public class Peer implements Comparable
*/ */
public long getUploaded() public long getUploaded()
{ {
PeerState s = state; return uploaded;
return (s != null) ? s.uploaded : 0;
} }
/** /**
@@ -538,12 +627,8 @@ public class Peer implements Comparable
*/ */
public void resetCounters() public void resetCounters()
{ {
PeerState s = state; downloaded = 0;
if (s != null) uploaded = 0;
{
s.downloaded = 0;
s.uploaded = 0;
}
} }
public long getInactiveTime() { public long getInactiveTime() {

View File

@@ -88,12 +88,11 @@ public class PeerAcceptor
} }
if (coordinator != null) { if (coordinator != null) {
// single torrent capability // single torrent capability
MetaInfo meta = coordinator.getMetaInfo(); if (DataHelper.eq(coordinator.getInfoHash(), peerInfoHash)) {
if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) {
if (coordinator.needPeers()) if (coordinator.needPeers())
{ {
Peer peer = new Peer(socket, in, out, coordinator.getID(), Peer peer = new Peer(socket, in, out, coordinator.getID(),
coordinator.getMetaInfo()); coordinator.getInfoHash(), coordinator.getMetaInfo());
coordinator.addPeer(peer); coordinator.addPeer(peer);
} }
else else
@@ -101,26 +100,25 @@ public class PeerAcceptor
} else { } else {
// its for another infohash, but we are only single torrent capable. b0rk. // its for another infohash, but we are only single torrent capable. b0rk.
throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash) 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 { } else {
// multitorrent capable, so lets see what we can handle // multitorrent capable, so lets see what we can handle
for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) { for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) {
PeerCoordinator cur = (PeerCoordinator)iter.next(); PeerCoordinator cur = (PeerCoordinator)iter.next();
MetaInfo meta = cur.getMetaInfo();
if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) { if (DataHelper.eq(cur.getInfoHash(), peerInfoHash)) {
if (cur.needPeers()) if (cur.needPeers())
{ {
Peer peer = new Peer(socket, in, out, cur.getID(), Peer peer = new Peer(socket, in, out, cur.getID(),
cur.getMetaInfo()); cur.getInfoHash(), cur.getMetaInfo());
cur.addPeer(peer); cur.addPeer(peer);
return; return;
} }
else else
{ {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Rejecting new peer for " + cur.snark.torrent); _log.debug("Rejecting new peer for " + cur.getName());
socket.close(); socket.close();
return; return;
} }

View File

@@ -37,7 +37,8 @@ class PeerCheckerTask extends TimerTask
private static final long KILOPERSECOND = 1024*(PeerCoordinator.CHECK_PERIOD/1000); private static final long KILOPERSECOND = 1024*(PeerCoordinator.CHECK_PERIOD/1000);
private final PeerCoordinator coordinator; private final PeerCoordinator coordinator;
public I2PSnarkUtil _util; private final I2PSnarkUtil _util;
private int _runCount;
PeerCheckerTask(I2PSnarkUtil util, PeerCoordinator coordinator) PeerCheckerTask(I2PSnarkUtil util, PeerCoordinator coordinator)
{ {
@@ -49,12 +50,10 @@ class PeerCheckerTask extends TimerTask
public void run() public void run()
{ {
_runCount++;
List<Peer> peerList = coordinator.peerList(); List<Peer> peerList = coordinator.peerList();
if (peerList.isEmpty() || coordinator.halted()) { if (peerList.isEmpty() || coordinator.halted()) {
coordinator.peerCount = 0;
coordinator.interestedAndChoking = 0;
coordinator.setRateHistory(0, 0); coordinator.setRateHistory(0, 0);
coordinator.uploaders = 0;
if (coordinator.halted()) if (coordinator.halted())
cancel(); cancel();
return; return;
@@ -207,6 +206,11 @@ class PeerCheckerTask extends TimerTask
} }
peer.retransmitRequests(); peer.retransmitRequests();
peer.keepAlive(); 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());
}
// send PEX
} }
// Resync actual uploaders value // Resync actual uploaders value
@@ -247,8 +251,14 @@ class PeerCheckerTask extends TimerTask
coordinator.setRateHistory(uploaded, downloaded); coordinator.setRateHistory(uploaded, downloaded);
// close out unused files, but we don't need to do it every time // close out unused files, but we don't need to do it every time
if (random.nextInt(4) == 0) Storage storage = coordinator.getStorage();
coordinator.getStorage().cleanRAFs(); 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());
}
} }
} }

View File

@@ -32,6 +32,13 @@ class PeerConnectionIn implements Runnable
private final Peer peer; private final Peer peer;
private final DataInputStream din; 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 Thread thread;
private volatile boolean quit; private volatile boolean quit;
@@ -77,20 +84,16 @@ class PeerConnectionIn implements Runnable
int len; int len;
// Wait till we hear something... // 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(); int i = din.readInt();
lastRcvd = System.currentTimeMillis(); lastRcvd = System.currentTimeMillis();
if (i < 0 || i > PeerState.PARTSIZE + 9) if (i < 0 || i > MAX_MSG_SIZE)
throw new IOException("Unexpected length prefix: " + i); throw new IOException("Unexpected length prefix: " + i);
if (i == 0) if (i == 0)
{ {
ps.keepAliveMessage(); ps.keepAliveMessage();
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Received keepalive from " + peer + " on " + peer.metainfo.getName()); _log.debug("Received keepalive from " + peer);
continue; continue;
} }
@@ -102,35 +105,35 @@ class PeerConnectionIn implements Runnable
case 0: case 0:
ps.chokeMessage(true); ps.chokeMessage(true);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Received choke from " + peer + " on " + peer.metainfo.getName()); _log.debug("Received choke from " + peer);
break; break;
case 1: case 1:
ps.chokeMessage(false); ps.chokeMessage(false);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Received unchoke from " + peer + " on " + peer.metainfo.getName()); _log.debug("Received unchoke from " + peer);
break; break;
case 2: case 2:
ps.interestedMessage(true); ps.interestedMessage(true);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Received interested from " + peer + " on " + peer.metainfo.getName()); _log.debug("Received interested from " + peer);
break; break;
case 3: case 3:
ps.interestedMessage(false); ps.interestedMessage(false);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Received not interested from " + peer + " on " + peer.metainfo.getName()); _log.debug("Received not interested from " + peer);
break; break;
case 4: case 4:
piece = din.readInt(); piece = din.readInt();
ps.haveMessage(piece); ps.haveMessage(piece);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Received havePiece(" + piece + ") from " + peer + " on " + peer.metainfo.getName()); _log.debug("Received havePiece(" + piece + ") from " + peer);
break; break;
case 5: case 5:
byte[] bitmap = new byte[i-1]; byte[] bitmap = new byte[i-1];
din.readFully(bitmap); din.readFully(bitmap);
ps.bitfieldMessage(bitmap); ps.bitfieldMessage(bitmap);
if (_log.shouldLog(Log.DEBUG)) 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; break;
case 6: case 6:
piece = din.readInt(); piece = din.readInt();
@@ -138,7 +141,7 @@ class PeerConnectionIn implements Runnable
len = din.readInt(); len = din.readInt();
ps.requestMessage(piece, begin, len); ps.requestMessage(piece, begin, len);
if (_log.shouldLog(Log.DEBUG)) 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; break;
case 7: case 7:
piece = din.readInt(); piece = din.readInt();
@@ -152,7 +155,7 @@ class PeerConnectionIn implements Runnable
din.readFully(piece_bytes, begin, len); din.readFully(piece_bytes, begin, len);
ps.pieceMessage(req); ps.pieceMessage(req);
if (_log.shouldLog(Log.DEBUG)) 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 else
{ {
@@ -160,7 +163,7 @@ class PeerConnectionIn implements Runnable
piece_bytes = new byte[len]; piece_bytes = new byte[len];
din.readFully(piece_bytes); din.readFully(piece_bytes);
if (_log.shouldLog(Log.DEBUG)) 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; break;
case 8: case 8:
@@ -169,22 +172,28 @@ class PeerConnectionIn implements Runnable
len = din.readInt(); len = din.readInt();
ps.cancelMessage(piece, begin, len); ps.cancelMessage(piece, begin, len);
if (_log.shouldLog(Log.DEBUG)) 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; break;
case 20: // Extension message case 20: // Extension message
int id = din.readUnsignedByte(); int id = din.readUnsignedByte();
byte[] payload = new byte[i-2]; byte[] payload = new byte[i-2];
din.readFully(payload); din.readFully(payload);
ps.extensionMessage(id, payload);
if (_log.shouldLog(Log.DEBUG)) 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; break;
default: default:
byte[] bs = new byte[i-1]; byte[] bs = new byte[i-1];
din.readFully(bs); din.readFully(bs);
ps.unknownMessage(b, bs); ps.unknownMessage(b, bs);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Received unknown message from " + peer + " on " + peer.metainfo.getName()); _log.debug("Received unknown message from " + peer);
} }
} }
} }

View File

@@ -29,8 +29,8 @@ import java.util.List;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.util.I2PAppThread; import net.i2p.util.I2PAppThread;
import net.i2p.util.Log; import net.i2p.util.Log;
import net.i2p.util.SimpleScheduler; //import net.i2p.util.SimpleScheduler;
import net.i2p.util.SimpleTimer; //import net.i2p.util.SimpleTimer;
class PeerConnectionOut implements Runnable class PeerConnectionOut implements Runnable
{ {
@@ -124,34 +124,34 @@ class PeerConnectionOut implements Runnable
{ {
if (state.choking) { if (state.choking) {
it.remove(); it.remove();
SimpleTimer.getInstance().removeEvent(nm.expireEvent); //SimpleTimer.getInstance().removeEvent(nm.expireEvent);
} }
nm = null; nm = null;
} }
else if (nm.type == Message.REQUEST && state.choked) else if (nm.type == Message.REQUEST && state.choked)
{ {
it.remove(); it.remove();
SimpleTimer.getInstance().removeEvent(nm.expireEvent); //SimpleTimer.getInstance().removeEvent(nm.expireEvent);
nm = null; nm = null;
} }
if (m == null && nm != null) if (m == null && nm != null)
{ {
m = nm; m = nm;
SimpleTimer.getInstance().removeEvent(nm.expireEvent); //SimpleTimer.getInstance().removeEvent(nm.expireEvent);
it.remove(); it.remove();
} }
} }
if (m == null && !sendQueue.isEmpty()) { if (m == null && !sendQueue.isEmpty()) {
m = (Message)sendQueue.remove(0); m = (Message)sendQueue.remove(0);
SimpleTimer.getInstance().removeEvent(m.expireEvent); //SimpleTimer.getInstance().removeEvent(m.expireEvent);
} }
} }
} }
if (m != null) if (m != null)
{ {
if (_log.shouldLog(Log.DEBUG)) 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. // This can block for quite a while.
// To help get slow peers going, and track the bandwidth better, // 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 */ /** remove messages not sent in 3m */
private static final int SEND_TIMEOUT = 3*60*1000; private static final int SEND_TIMEOUT = 3*60*1000;
/*****
private class RemoveTooSlow implements SimpleTimer.TimedEvent { private class RemoveTooSlow implements SimpleTimer.TimedEvent {
private Message _m; private Message _m;
public RemoveTooSlow(Message m) { public RemoveTooSlow(Message m) {
@@ -258,6 +260,7 @@ class PeerConnectionOut implements Runnable
_log.info("Took too long to send " + _m + " to " + peer); _log.info("Took too long to send " + _m + " to " + peer);
} }
} }
*****/
/** /**
* Removes a particular message type from the queue. * Removes a particular message type from the queue.
@@ -474,7 +477,8 @@ class PeerConnectionOut implements Runnable
m.off = 0; m.off = 0;
m.len = length; m.len = length;
// since we have the data already loaded, queue a timeout to remove it // 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); addMessage(m);
} }
@@ -547,4 +551,12 @@ class PeerConnectionOut implements Runnable
m.len = bytes.length; m.len = bytes.length;
addMessage(m); addMessage(m);
} }
/** @since 0.8.4 */
void sendPort(int port) {
Message m = new Message();
m.type = Message.PORT;
m.piece = port;
addMessage(m);
}
} }

View File

@@ -36,24 +36,43 @@ import net.i2p.util.I2PAppThread;
import net.i2p.util.Log; import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2; import net.i2p.util.SimpleTimer2;
import org.klomp.snark.dht.DHT;
/** /**
* Coordinates what peer does what. * Coordinates what peer does what.
*/ */
public class PeerCoordinator implements PeerListener public class PeerCoordinator implements PeerListener
{ {
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerCoordinator.class); 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 // package local for access by CheckDownLoadersTask
final static long CHECK_PERIOD = 40*1000; // 40 seconds final static long CHECK_PERIOD = 40*1000; // 40 seconds
final static int MAX_UPLOADERS = 6; final static int MAX_UPLOADERS = 6;
// Approximation of the number of current uploaders. /**
// Resynced by PeerChecker once in a while. * Approximation of the number of current uploaders.
int uploaders = 0; * Resynced by PeerChecker once in a while.
int interestedAndChoking = 0; * External use by PeerCheckerTask only.
*/
int uploaders;
/**
* External use by PeerCheckerTask only.
*/
int interestedAndChoking;
// final static int MAX_DOWNLOADERS = MAX_CONNECTIONS; // final static int MAX_DOWNLOADERS = MAX_CONNECTIONS;
// int downloaders = 0; // int downloaders = 0;
@@ -61,19 +80,24 @@ public class PeerCoordinator implements PeerListener
private long uploaded; private long uploaded;
private long downloaded; private long downloaded;
final static int RATE_DEPTH = 3; // make following arrays RATE_DEPTH long final static int RATE_DEPTH = 3; // make following arrays RATE_DEPTH long
private long uploaded_old[] = {-1,-1,-1}; private final long uploaded_old[] = {-1,-1,-1};
private long downloaded_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; final Queue<Peer> peers;
/** estimate of the peers, without requiring any synchronization */ /** estimate of the peers, without requiring any synchronization */
volatile int peerCount; private volatile int peerCount;
/** Timer to handle all periodical tasks. */ /** Timer to handle all periodical tasks. */
private final CheckEvent timer; private final CheckEvent timer;
private final byte[] id; 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 /** The wanted pieces. We could use a TreeSet but we'd have to clear and re-add everything
* when priorities change. * when priorities change.
@@ -85,18 +109,21 @@ public class PeerCoordinator implements PeerListener
private boolean halted = false; private boolean halted = false;
private final MagnetState magnetState;
private final CoordinatorListener listener; private final CoordinatorListener listener;
public I2PSnarkUtil _util; private final I2PSnarkUtil _util;
private static final Random _random = I2PAppContext.getGlobalContext().random(); private static final Random _random = I2PAppContext.getGlobalContext().random();
public String trackerProblems = null; /**
public int trackerSeenPeers = 0; * @param metainfo null if in magnet mode
* @param storage null if in magnet mode
public PeerCoordinator(I2PSnarkUtil util, byte[] id, MetaInfo metainfo, Storage storage, */
public PeerCoordinator(I2PSnarkUtil util, byte[] id, byte[] infohash, MetaInfo metainfo, Storage storage,
CoordinatorListener listener, Snark torrent) CoordinatorListener listener, Snark torrent)
{ {
_util = util; _util = util;
this.id = id; this.id = id;
this.infohash = infohash;
this.metainfo = metainfo; this.metainfo = metainfo;
this.storage = storage; this.storage = storage;
this.listener = listener; this.listener = listener;
@@ -106,6 +133,7 @@ public class PeerCoordinator implements PeerListener
setWantedPieces(); setWantedPieces();
partialPieces = new ArrayList(getMaxConnections() + 1); partialPieces = new ArrayList(getMaxConnections() + 1);
peers = new LinkedBlockingQueue(); peers = new LinkedBlockingQueue();
magnetState = new MagnetState(infohash, metainfo);
// Install a timer to check the uploaders. // Install a timer to check the uploaders.
// Randomize the first start time so multiple tasks are spread out, // Randomize the first start time so multiple tasks are spread out,
@@ -133,6 +161,8 @@ public class PeerCoordinator implements PeerListener
// only called externally from Storage after the double-check fails // only called externally from Storage after the double-check fails
public void setWantedPieces() public void setWantedPieces()
{ {
if (metainfo == null || storage == null)
return;
// Make a list of pieces // Make a list of pieces
synchronized(wantedPieces) { synchronized(wantedPieces) {
wantedPieces.clear(); wantedPieces.clear();
@@ -153,7 +183,6 @@ public class PeerCoordinator implements PeerListener
} }
public Storage getStorage() { return storage; } public Storage getStorage() { return storage; }
public CoordinatorListener getListener() { return listener; }
// for web page detailed stats // for web page detailed stats
public List<Peer> peerList() public List<Peer> peerList()
@@ -166,8 +195,16 @@ public class PeerCoordinator implements PeerListener
return id; return id;
} }
public String getName()
{
return snark.getName();
}
public boolean completed() public boolean completed()
{ {
// FIXME return metainfo complete status
if (storage == null)
return false;
return storage.complete(); return storage.complete();
} }
@@ -184,9 +221,12 @@ public class PeerCoordinator implements PeerListener
/** /**
* Returns how many bytes are still needed to get the complete file. * Returns how many bytes are still needed to get the complete file.
* @return -1 if in magnet mode
*/ */
public long getLeft() public long getLeft()
{ {
if (metainfo == null | storage == null)
return -1;
// XXX - Only an approximation. // XXX - Only an approximation.
return ((long) storage.needed()) * metainfo.getPieceLength(0); return ((long) storage.needed()) * metainfo.getPieceLength(0);
} }
@@ -271,6 +311,12 @@ public class PeerCoordinator implements PeerListener
return metainfo; return metainfo;
} }
/** @since 0.8.4 */
public byte[] getInfoHash()
{
return infohash;
}
public boolean needPeers() public boolean needPeers()
{ {
return !halted && peers.size() < getMaxConnections(); return !halted && peers.size() < getMaxConnections();
@@ -281,6 +327,8 @@ public class PeerCoordinator implements PeerListener
* @return 512K: 16; 1M: 11; 2M: 6 * @return 512K: 16; 1M: 11; 2M: 6
*/ */
private int getMaxConnections() { private int getMaxConnections() {
if (metainfo == null)
return 6;
int size = metainfo.getPieceLength(0); int size = metainfo.getPieceLength(0);
int max = _util.getMaxConnections(); int max = _util.getMaxConnections();
if (size <= 512*1024 || completed()) if (size <= 512*1024 || completed())
@@ -355,8 +403,15 @@ public class PeerCoordinator implements PeerListener
} }
else else
{ {
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO)) {
_log.info("New connection to peer: " + peer + " for " + metainfo.getName()); // 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. // Add it to the beginning of the list.
// And try to optimistically make it a uploader. // And try to optimistically make it a uploader.
@@ -415,17 +470,27 @@ public class PeerCoordinator implements PeerListener
if (need_more) if (need_more)
{ {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG)) {
_log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + metainfo.getName(), new Exception("add/run")); // 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. // Run the peer with us as listener and the current bitfield.
final PeerListener listener = this; 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() Runnable r = new Runnable()
{ {
public void run() public void run()
{ {
peer.runConnection(_util, listener, bitfield); peer.runConnection(_util, listener, bitfield, magnetState);
} }
}; };
String threadName = "Snark peer " + peer.toString(); String threadName = "Snark peer " + peer.toString();
@@ -486,11 +551,6 @@ public class PeerCoordinator implements PeerListener
interestedAndChoking = count; interestedAndChoking = count;
} }
public byte[] getBitMap()
{
return storage.getBitField().getFieldBytes();
}
/** /**
* @return true if we still want the given piece * @return true if we still want the given piece
*/ */
@@ -647,6 +707,8 @@ public class PeerCoordinator implements PeerListener
* @since 0.8.1 * @since 0.8.1
*/ */
public void updatePiecePriorities() { public void updatePiecePriorities() {
if (storage == null)
return;
int[] pri = storage.getPiecePriorities(); int[] pri = storage.getPiecePriorities();
if (pri == null) { if (pri == null) {
_log.debug("Updated piece priorities called but no priorities to set?"); _log.debug("Updated piece priorities called but no priorities to set?");
@@ -713,6 +775,8 @@ public class PeerCoordinator implements PeerListener
{ {
if (halted) if (halted)
return null; return null;
if (metainfo == null || storage == null)
return null;
try try
{ {
@@ -755,6 +819,8 @@ public class PeerCoordinator implements PeerListener
*/ */
public boolean gotPiece(Peer peer, int piece, byte[] bs) public boolean gotPiece(Peer peer, int piece, byte[] bs)
{ {
if (metainfo == null || storage == null)
return true;
if (halted) { if (halted) {
_log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName()); _log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName());
return true; // We don't actually care anymore. return true; // We don't actually care anymore.
@@ -951,6 +1017,8 @@ public class PeerCoordinator implements PeerListener
* @since 0.8.2 * @since 0.8.2
*/ */
public PartialPiece getPartialPiece(Peer peer, BitField havePieces) { public PartialPiece getPartialPiece(Peer peer, BitField havePieces) {
if (metainfo == null)
return null;
synchronized(wantedPieces) { synchronized(wantedPieces) {
// sorts by remaining bytes, least first // sorts by remaining bytes, least first
Collections.sort(partialPieces); Collections.sort(partialPieces);
@@ -1057,6 +1125,69 @@ 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) {
try {
if (peer.getHandshakeMap().get("m").getMap().get(ExtensionHandler.TYPE_PEX) != null) {
List<Peer> pList = peerList();
pList.remove(peer);
ExtensionHandler.sendPEX(peer, pList);
}
} catch (Exception e) {
// NPE, no map
}
}
}
/**
* 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) {
// spin off thread or timer task to do a new Peer() and an addPeer() for each one
}
/** Return number of allowed uploaders for this torrent. /** Return number of allowed uploaders for this torrent.
** Check with Snark to see if we are over the total upload limit. ** Check with Snark to see if we are over the total upload limit.
*/ */
@@ -1072,6 +1203,14 @@ public class PeerCoordinator implements PeerListener
return MAX_UPLOADERS; return MAX_UPLOADERS;
} }
/**
* @return current
* @since 0.8.4
*/
public int getUploaders() {
return uploaders;
}
public boolean overUpBWLimit() public boolean overUpBWLimit()
{ {
if (listener != null) if (listener != null)

View File

@@ -179,4 +179,32 @@ interface PeerListener
* @since 0.8.2 * @since 0.8.2
*/ */
PartialPiece getPartialPiece(Peer peer, BitField havePieces); 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);
} }

View File

@@ -32,15 +32,13 @@ import java.util.Set;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.util.Log; import net.i2p.util.Log;
import org.klomp.snark.bencode.BDecoder;
import org.klomp.snark.bencode.BEValue;
class PeerState implements DataLoader class PeerState implements DataLoader
{ {
private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerState.class); private final Log _log = I2PAppContext.getGlobalContext().logManager().getLog(PeerState.class);
private final Peer peer; private final Peer peer;
/** Fixme, used by Peer.disconnect() to get to the coordinator */
final PeerListener listener; final PeerListener listener;
private final MetaInfo metainfo; private MetaInfo metainfo;
// Interesting and choking describes whether we are interested in or // Interesting and choking describes whether we are interested in or
// are choking the other side. // are choking the other side.
@@ -52,10 +50,6 @@ class PeerState implements DataLoader
boolean interested = false; boolean interested = false;
boolean choked = true; boolean choked = true;
// Package local for use by Peer.
long downloaded;
long uploaded;
/** the pieces the peer has */ /** the pieces the peer has */
BitField bitfield; BitField bitfield;
@@ -74,6 +68,9 @@ class PeerState implements DataLoader
public final static int PARTSIZE = 16*1024; // outbound request 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 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, PeerState(Peer peer, PeerListener listener, MetaInfo metainfo,
PeerConnectionIn in, PeerConnectionOut out) PeerConnectionIn in, PeerConnectionOut out)
{ {
@@ -135,6 +132,9 @@ class PeerState implements DataLoader
{ {
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " rcv have(" + piece + ")"); _log.debug(peer + " rcv have(" + piece + ")");
// FIXME we will lose these until we get the metainfo
if (metainfo == null)
return;
// Sanity check // Sanity check
if (piece < 0 || piece >= metainfo.getPieces()) if (piece < 0 || piece >= metainfo.getPieces())
{ {
@@ -172,8 +172,15 @@ class PeerState implements DataLoader
} }
// XXX - Check for weird bitfield and disconnect? // XXX - Check for weird bitfield and disconnect?
// 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()); bitfield = new BitField(bitmap, metainfo.getPieces());
} }
if (metainfo == null)
return;
boolean interest = listener.gotBitField(peer, bitfield); boolean interest = listener.gotBitField(peer, bitfield);
setInteresting(interest); setInteresting(interest);
if (bitfield.complete() && !interest) { if (bitfield.complete() && !interest) {
@@ -191,6 +198,8 @@ class PeerState implements DataLoader
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug(peer + " rcv request(" _log.debug(peer + " rcv request("
+ piece + ", " + begin + ", " + length + ") "); + piece + ", " + begin + ", " + length + ") ");
if (metainfo == null)
return;
if (choking) if (choking)
{ {
if (_log.shouldLog(Log.INFO)) if (_log.shouldLog(Log.INFO))
@@ -273,7 +282,7 @@ class PeerState implements DataLoader
*/ */
void uploaded(int size) void uploaded(int size)
{ {
uploaded += size; peer.uploaded(size);
listener.uploaded(peer, size); listener.uploaded(peer, size);
} }
@@ -293,7 +302,7 @@ class PeerState implements DataLoader
void pieceMessage(Request req) void pieceMessage(Request req)
{ {
int size = req.len; int size = req.len;
downloaded += size; peer.downloaded(size);
listener.downloaded(peer, size); listener.downloaded(peer, size);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
@@ -314,9 +323,6 @@ class PeerState implements DataLoader
{ {
if (_log.shouldLog(Log.WARN)) if (_log.shouldLog(Log.WARN))
_log.warn("Got BAD " + req.piece + " from " + peer); _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 + ", " _log.info("Unrequested 'piece: " + piece + ", "
+ begin + ", " + length + "' received from " + begin + ", " + length + "' received from "
+ peer); + peer);
downloaded = 0; // XXX - punishment?
return null; return null;
} }
@@ -385,7 +390,6 @@ class PeerState implements DataLoader
+ begin + ", " + begin + ", "
+ length + "' received from " + length + "' received from "
+ peer); + peer);
downloaded = 0; // XXX - punishment?
return null; return null;
} }
@@ -485,22 +489,36 @@ class PeerState implements DataLoader
/** @since 0.8.2 */ /** @since 0.8.2 */
void extensionMessage(int id, byte[] bs) void extensionMessage(int id, byte[] bs)
{ {
if (id == 0) { ExtensionHandler.handleMessage(peer, listener, id, bs);
InputStream is = new ByteArrayInputStream(bs); // Peer coord will get metadata from MagnetState,
try { // verify, and then call gotMetaInfo()
BDecoder dec = new BDecoder(is); listener.gotExtension(peer, id, bs);
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);
} }
/**
* 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 { } else {
if (_log.shouldLog(Log.DEBUG)) // it will be initialized later
_log.debug("Got extended message type: " + id + " length: " + bs.length); //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) void unknownMessage(int type, byte[] bs)
@@ -619,6 +637,8 @@ class PeerState implements DataLoader
// no bitfield yet? nothing to request then. // no bitfield yet? nothing to request then.
if (bitfield == null) if (bitfield == null)
return; return;
if (metainfo == null)
return;
boolean more_pieces = true; boolean more_pieces = true;
while (more_pieces) while (more_pieces)
{ {

View File

@@ -35,8 +35,8 @@ class Piece implements Comparable {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (o instanceof Piece) {
if (o == null) return false; if (o == null) return false;
if (o instanceof Piece) {
return this.id == ((Piece)o).id; return this.id == ((Piece)o).id;
} }
return false; return false;

View File

@@ -26,6 +26,7 @@ import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
@@ -107,11 +108,13 @@ public class Snark
} catch (Throwable t) { } catch (Throwable t) {
System.out.println("OOM in the OOM"); 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) public static void main(String[] args)
{ {
System.out.println(copyright); System.out.println(copyright);
@@ -235,19 +238,27 @@ public class Snark
} }
} }
***********/
public static final String PROP_MAX_CONNECTIONS = "i2psnark.maxConnections"; public static final String PROP_MAX_CONNECTIONS = "i2psnark.maxConnections";
public String torrent;
public MetaInfo meta; /** most of these used to be public, use accessors below instead */
public Storage storage; private String torrent;
public PeerCoordinator coordinator; private MetaInfo meta;
public ConnectionAcceptor acceptor; private Storage storage;
public TrackerClient trackerclient; private PeerCoordinator coordinator;
public String rootDataDir = "."; private ConnectionAcceptor acceptor;
public CompleteListener completeListener; private TrackerClient trackerclient;
public boolean stopped; private String rootDataDir = ".";
byte[] id; private final CompleteListener completeListener;
public I2PSnarkUtil _util; private boolean stopped;
private PeerCoordinatorSet _peerCoordinatorSet; 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 */ /** from main() via parseArguments() single torrent */
Snark(I2PSnarkUtil util, String torrent, String ip, int user_port, Snark(I2PSnarkUtil util, String torrent, String ip, int user_port,
@@ -306,31 +317,7 @@ public class Snark
stopped = true; stopped = true;
activity = "Network setup"; activity = "Network setup";
// "Taking Three as the subject to reason about-- id = generateID();
// 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);
debug("My peer id: " + PeerID.idencode(id), Snark.INFO); debug("My peer id: " + PeerID.idencode(id), Snark.INFO);
int port; int port;
@@ -373,6 +360,7 @@ public class Snark
} }
} }
meta = new MetaInfo(new BDecoder(in)); meta = new MetaInfo(new BDecoder(in));
infoHash = meta.getInfoHash();
} }
catch(IOException ioe) catch(IOException ioe)
{ {
@@ -406,6 +394,8 @@ public class Snark
*/ */
else else
fatal("Cannot open '" + torrent + "'", ioe); fatal("Cannot open '" + torrent + "'", ioe);
} catch (OutOfMemoryError oom) {
fatal("ERROR - Out of memory, cannot create torrent " + torrent + ": " + oom.getMessage());
} finally { } finally {
if (in != null) if (in != null)
try { in.close(); } catch (IOException ioe) {} try { in.close(); } catch (IOException ioe) {}
@@ -457,6 +447,64 @@ public class Snark
if (start) if (start)
startTorrent(); 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 * Start up contacting peers and querying the tracker
*/ */
@@ -473,7 +521,7 @@ public class Snark
} }
debug("Starting PeerCoordinator, ConnectionAcceptor, and TrackerClient", NOTICE); debug("Starting PeerCoordinator, ConnectionAcceptor, and TrackerClient", NOTICE);
activity = "Collecting pieces"; 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) { if (_peerCoordinatorSet != null) {
// multitorrent // multitorrent
_peerCoordinatorSet.add(coordinator); _peerCoordinatorSet.add(coordinator);
@@ -486,7 +534,8 @@ public class Snark
// single torrent // single torrent
acceptor = new ConnectionAcceptor(_util, serversocket, new PeerAcceptor(coordinator)); 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; stopped = false;
@@ -496,8 +545,7 @@ public class Snark
// restart safely, so lets build a new one to replace the old // restart safely, so lets build a new one to replace the old
if (_peerCoordinatorSet != null) if (_peerCoordinatorSet != null)
_peerCoordinatorSet.remove(coordinator); _peerCoordinatorSet.remove(coordinator);
PeerCoordinator newCoord = new PeerCoordinator(_util, coordinator.getID(), coordinator.getMetaInfo(), PeerCoordinator newCoord = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this);
coordinator.getStorage(), coordinator.getListener(), this);
if (_peerCoordinatorSet != null) if (_peerCoordinatorSet != null)
_peerCoordinatorSet.add(newCoord); _peerCoordinatorSet.add(newCoord);
coordinator = newCoord; coordinator = newCoord;
@@ -506,18 +554,17 @@ public class Snark
if (!trackerclient.started() && !coordinatorChanged) { if (!trackerclient.started() && !coordinatorChanged) {
trackerclient.start(); trackerclient.start();
} else if (trackerclient.halted() || coordinatorChanged) { } else if (trackerclient.halted() || coordinatorChanged) {
try if (storage != null) {
{ try {
storage.reopen(rootDataDir); storage.reopen(rootDataDir);
} } catch (IOException ioe) {
catch (IOException ioe)
{
try { storage.close(); } catch (IOException ioee) { try { storage.close(); } catch (IOException ioee) {
ioee.printStackTrace(); ioee.printStackTrace();
} }
fatal("Could not reopen storage", ioe); fatal("Could not reopen storage", ioe);
} }
TrackerClient newClient = new TrackerClient(_util, coordinator.getMetaInfo(), coordinator); }
TrackerClient newClient = new TrackerClient(_util, meta, coordinator, this);
if (!trackerclient.halted()) if (!trackerclient.halted())
trackerclient.halt(); trackerclient.halt();
trackerclient = newClient; trackerclient = newClient;
@@ -553,18 +600,238 @@ public class Snark
_util.disconnect(); _util.disconnect();
} }
static Snark parseArguments(String[] args) private static Snark parseArguments(String[] args)
{ {
return parseArguments(args, null, null); 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 * Sets debug, ip and torrent variables then creates a Snark
* instance. Calls usage(), which terminates the program, if * instance. Calls usage(), which terminates the program, if
* non-valid argument list. The given listeners will be * non-valid argument list. The given listeners will be
* passed to all components that take one. * passed to all components that take one.
*/ */
static Snark parseArguments(String[] args, private static Snark parseArguments(String[] args,
StorageListener slistener, StorageListener slistener,
CoordinatorListener clistener) CoordinatorListener clistener)
{ {
@@ -713,13 +980,12 @@ public class Snark
(" <file> \tEither a local .torrent metainfo file to download"); (" <file> \tEither a local .torrent metainfo file to download");
System.out.println System.out.println
(" \tor (with --share) a file to share."); (" \tor (with --share) a file to share.");
System.exit(-1);
} }
/** /**
* Aborts program abnormally. * Aborts program abnormally.
*/ */
public void fatal(String s) private void fatal(String s)
{ {
fatal(s, null); fatal(s, null);
} }
@@ -727,7 +993,7 @@ public class Snark
/** /**
* Aborts program abnormally. * Aborts program abnormally.
*/ */
public void fatal(String s, Throwable t) private void fatal(String s, Throwable t)
{ {
_util.debug(s, ERROR, t); _util.debug(s, ERROR, t);
//System.err.println("snark: " + s + ((t == null) ? "" : (": " + t))); //System.err.println("snark: " + s + ((t == null) ? "" : (": " + t)));
@@ -751,7 +1017,36 @@ public class Snark
// System.out.println(peer.toString()); // 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) public void storageCreateFile(Storage storage, String name, long length)
{ {
//if (allocating) //if (allocating)
@@ -774,9 +1069,9 @@ public class Snark
// System.out.println(); // We have all the disk space we need. // System.out.println(); // We have all the disk space we need.
} }
boolean allChecked = false; private boolean allChecked = false;
boolean checking = false; private boolean checking = false;
boolean prechecking = true; private boolean prechecking = true;
public void storageChecked(Storage storage, int num, boolean checked) public void storageChecked(Storage storage, int num, boolean checked)
{ {
allocating = false; allocating = false;
@@ -821,16 +1116,28 @@ public class Snark
coordinator.setWantedPieces(); coordinator.setWantedPieces();
} }
/** SnarkSnutdown callback unused */
public void shutdown() public void shutdown()
{ {
// Should not be necessary since all non-deamon threads should // Should not be necessary since all non-deamon threads should
// have died. But in reality this does not always happen. // have died. But in reality this does not always happen.
System.exit(0); //System.exit(0);
} }
public interface CompleteListener { public interface CompleteListener {
public void torrentComplete(Snark snark); public void torrentComplete(Snark snark);
public void updateStatus(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 // not really listeners but the easiest way to get back to an optional SnarkManager
public long getSavedTorrentTime(Snark snark); public long getSavedTorrentTime(Snark snark);
public BitField getSavedTorrentBitField(Snark snark); public BitField getSavedTorrentBitField(Snark snark);

View File

@@ -5,6 +5,7 @@ import java.io.FileFilter;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@@ -16,14 +17,18 @@ import java.util.Set;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.Collection; import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.data.Base64; import net.i2p.data.Base64;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
import net.i2p.util.ConcurrentHashSet;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread; import net.i2p.util.I2PAppThread;
import net.i2p.util.Log; import net.i2p.util.Log;
import net.i2p.util.OrderedProperties; import net.i2p.util.OrderedProperties;
import net.i2p.util.SecureDirectory; import net.i2p.util.SecureDirectory;
import net.i2p.util.SecureFileOutputStream;
/** /**
* Manage multiple snarks * Manage multiple snarks
@@ -32,8 +37,14 @@ public class SnarkManager implements Snark.CompleteListener {
private static SnarkManager _instance = new SnarkManager(); private static SnarkManager _instance = new SnarkManager();
public static SnarkManager instance() { return _instance; } 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; 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 final Object _addSnarkLock;
private /* FIXME final FIXME */ File _configFile; private /* FIXME final FIXME */ File _configFile;
private Properties _config; 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_PREFIX = "i2psnark.zmeta.";
public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield"; public static final String PROP_META_BITFIELD_SUFFIX = ".bitfield";
public static final String PROP_META_PRIORITY_SUFFIX = ".priority"; 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"; private static final String CONFIG_FILE = "i2psnark.config";
public static final String PROP_AUTO_START = "i2snark.autoStart"; // oops 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_MAX_UP_BW = 10;
public static final int DEFAULT_STARTUP_DELAY = 3; public static final int DEFAULT_STARTUP_DELAY = 3;
private SnarkManager() { private SnarkManager() {
_snarks = new HashMap(); _snarks = new ConcurrentHashMap();
_magnets = new ConcurrentHashSet();
_addSnarkLock = new Object(); _addSnarkLock = new Object();
_context = I2PAppContext.getGlobalContext(); _context = I2PAppContext.getGlobalContext();
_log = _context.logManager().getLog(SnarkManager.class); _log = _context.logManager().getLog(SnarkManager.class);
@@ -90,8 +103,6 @@ public class SnarkManager implements Snark.CompleteListener {
_running = true; _running = true;
_peerCoordinatorSet = new PeerCoordinatorSet(); _peerCoordinatorSet = new PeerCoordinatorSet();
_connectionAcceptor = new ConnectionAcceptor(_util); _connectionAcceptor = new ConnectionAcceptor(_util);
int minutes = getStartupDelayMinutes();
_messages.add(_("Adding torrents in {0} minutes", minutes));
_monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true); _monitor = new I2PAppThread(new DirMonitor(), "Snark DirMonitor", true);
_monitor.start(); _monitor.start();
_context.addShutdownTask(new SnarkManagerShutdown()); _context.addShutdownTask(new SnarkManagerShutdown());
@@ -236,11 +247,9 @@ public class SnarkManager implements Snark.CompleteListener {
i2cpOpts.put(pair.substring(0, split), pair.substring(split+1)); i2cpOpts.put(pair.substring(0, split), pair.substring(split+1));
} }
} }
if (i2cpHost != null) {
_util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts); _util.setI2CPConfig(i2cpHost, i2cpPort, i2cpOpts);
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Configuring with I2CP options " + i2cpOpts); _log.debug("Configuring with I2CP options " + i2cpOpts);
}
//I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties()); //I2PSnarkUtil.instance().setI2CPConfig("66.111.51.110", 7654, new Properties());
//String eepHost = _config.getProperty(PROP_EEP_HOST); //String eepHost = _config.getProperty(PROP_EEP_HOST);
//int eepPort = getInt(PROP_EEP_PORT, 4444); //int eepPort = getInt(PROP_EEP_PORT, 4444);
@@ -252,7 +261,9 @@ public class SnarkManager implements Snark.CompleteListener {
String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS); String ot = _config.getProperty(I2PSnarkUtil.PROP_OPENTRACKERS);
if (ot != null) if (ot != null)
_util.setOpenTrackerString(ot); _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(); getDataDir().mkdirs();
} }
@@ -321,15 +332,18 @@ public class SnarkManager implements Snark.CompleteListener {
_util.setStartupDelay(minutes); _util.setStartupDelay(minutes);
changed = true; changed = true;
_config.setProperty(PROP_STARTUP_DELAY, "" + minutes); _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) { if (i2cpHost != null) {
int oldI2CPPort = _util.getI2CPPort(); int oldI2CPPort = _util.getI2CPPort();
String oldI2CPHost = _util.getI2CPHost(); String oldI2CPHost = _util.getI2CPHost();
int port = oldI2CPPort; int port = oldI2CPPort;
if (i2cpPort != null) {
try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {} try { port = Integer.parseInt(i2cpPort); } catch (NumberFormatException nfe) {}
}
String host = oldI2CPHost; String host = oldI2CPHost;
Map opts = new HashMap(); Map opts = new HashMap();
if (i2cpOpts == null) i2cpOpts = ""; if (i2cpOpts == null) i2cpOpts = "";
@@ -359,7 +373,7 @@ public class SnarkManager implements Snark.CompleteListener {
Set names = listTorrentFiles(); Set names = listTorrentFiles();
for (Iterator iter = names.iterator(); iter.hasNext(); ) { for (Iterator iter = names.iterator(); iter.hasNext(); ) {
Snark snark = getTorrent((String)iter.next()); Snark snark = getTorrent((String)iter.next());
if ( (snark != null) && (!snark.stopped) ) { if ( (snark != null) && (!snark.isStopped()) ) {
snarksActive = true; snarksActive = true;
break; break;
} }
@@ -368,6 +382,7 @@ public class SnarkManager implements Snark.CompleteListener {
Properties p = new Properties(); Properties p = new Properties();
p.putAll(opts); p.putAll(opts);
_util.setI2CPConfig(i2cpHost, port, p); _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")); addMessage(_("I2CP and tunnel changes will take effect after stopping all torrents"));
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts _log.debug("i2cp host [" + i2cpHost + "] i2cp port " + port + " opts [" + opts
@@ -381,6 +396,7 @@ public class SnarkManager implements Snark.CompleteListener {
p.putAll(opts); p.putAll(opts);
addMessage(_("I2CP settings changed to {0}", i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")")); addMessage(_("I2CP settings changed to {0}", i2cpHost + ":" + port + " (" + i2cpOpts.trim() + ")"));
_util.setI2CPConfig(i2cpHost, port, p); _util.setI2CPConfig(i2cpHost, port, p);
_util.setMaxUpBW(getInt(PROP_UPBW_MAX, DEFAULT_MAX_UP_BW));
boolean ok = _util.connect(); boolean ok = _util.connect();
if (!ok) { if (!ok) {
addMessage(_("Unable to connect with the new settings, reverting to the old I2CP settings")); 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(); ) { for (Iterator iter = names.iterator(); iter.hasNext(); ) {
String name = (String)iter.next(); String name = (String)iter.next();
Snark snark = getTorrent(name); Snark snark = getTorrent(name);
if ( (snark != null) && (snark.acceptor != null) ) { if (snark != null && snark.restartAcceptor()) {
snark.acceptor.restart(); addMessage(_("I2CP listener restarted for \"{0}\"", snark.getBaseName()));
addMessage(_("I2CP listener restarted for \"{0}\"", snark.meta.getName()));
} }
} }
} }
@@ -422,6 +437,7 @@ public class SnarkManager implements Snark.CompleteListener {
addMessage(_("Enabled open trackers - torrent restart required to take effect.")); addMessage(_("Enabled open trackers - torrent restart required to take effect."));
else else
addMessage(_("Disabled open trackers - torrent restart required to take effect.")); addMessage(_("Disabled open trackers - torrent restart required to take effect."));
_util.setUseOpenTrackers(useOpenTrackers);
changed = true; changed = true;
} }
if (openTrackers != null) { 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. */ /** hardcoded for sanity. perhaps this should be customizable, for people who increase their ulimit, etc. */
private static final int MAX_FILES_PER_TORRENT = 512; 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 * 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) { public Snark getTorrentByBaseName(String filename) {
synchronized (_snarks) { synchronized (_snarks) {
for (Snark s : _snarks.values()) { for (Snark s : _snarks.values()) {
if (s.storage.getBaseName().equals(filename)) if (s.getBaseName().equals(filename))
return s; return s;
} }
} }
return null; 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); } 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) { public void addTorrent(String filename, boolean dontAutoStart) {
if ((!dontAutoStart) && !_util.connected()) { if ((!dontAutoStart) && !_util.connected()) {
addMessage(_("Connecting to I2P")); addMessage(_("Connecting to I2P"));
@@ -538,23 +580,26 @@ public class SnarkManager implements Snark.CompleteListener {
if (!TrackerClient.isValidAnnounce(info.getAnnounce())) { if (!TrackerClient.isValidAnnounce(info.getAnnounce())) {
if (_util.shouldUseOpenTrackers() && _util.getOpenTrackers() != null) { 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 { } 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; dontAutoStart = true;
} }
} }
String rejectMessage = locked_validateTorrent(info); String rejectMessage = validateTorrent(info);
if (rejectMessage != null) { if (rejectMessage != null) {
sfile.delete(); sfile.delete();
addMessage(rejectMessage); addMessage(rejectMessage);
return; return;
} else { } 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, torrent = new Snark(_util, filename, null, -1, null, null, this,
_peerCoordinatorSet, _connectionAcceptor, _peerCoordinatorSet, _connectionAcceptor,
false, dataDir.getPath()); false, dataDir.getPath());
loadSavedFilePriorities(torrent); loadSavedFilePriorities(torrent);
torrent.completeListener = this;
synchronized (_snarks) { synchronized (_snarks) {
_snarks.put(filename, torrent); _snarks.put(filename, torrent);
} }
@@ -564,6 +609,8 @@ public class SnarkManager implements Snark.CompleteListener {
if (sfile.exists()) if (sfile.exists())
sfile.delete(); sfile.delete();
return; return;
} catch (OutOfMemoryError oom) {
addMessage(_("ERROR - Out of memory, cannot create torrent from {0}", sfile.getName()) + ": " + oom.getMessage());
} finally { } finally {
if (fis != null) try { fis.close(); } catch (IOException ioe) {} if (fis != null) try { fis.close(); } catch (IOException ioe) {}
} }
@@ -572,21 +619,164 @@ public class SnarkManager implements Snark.CompleteListener {
return; return;
} }
// ok, snark created, now lets start it up or configure it further // ok, snark created, now lets start it up or configure it further
File f = new File(filename);
if (!dontAutoStart && shouldAutoStart()) { if (!dontAutoStart && shouldAutoStart()) {
torrent.startTorrent(); torrent.startTorrent();
addMessage(_("Torrent added and started: \"{0}\"", torrent.storage.getBaseName())); addMessage(_("Torrent added and started: \"{0}\"", torrent.getBaseName()));
} else { } 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 void locked_writeMetaInfo(MetaInfo metainfo, String filename) throws IOException {
// prevent interference by DirMonitor
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) { public long getSavedTorrentTime(Snark snark) {
MetaInfo metainfo = snark.meta; byte[] ih = snark.getInfoHash();
byte[] ih = metainfo.getInfoHash();
String infohash = Base64.encode(ih); String infohash = Base64.encode(ih);
infohash = infohash.replace('=', '$'); infohash = infohash.replace('=', '$');
String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); String time = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
@@ -603,10 +793,13 @@ public class SnarkManager implements Snark.CompleteListener {
/** /**
* Get the saved bitfield for a torrent from the config file. * Get the saved bitfield for a torrent from the config file.
* Convert "." to a full bitfield. * Convert "." to a full bitfield.
* A Snark.CompleteListener method.
*/ */
public BitField getSavedTorrentBitField(Snark snark) { public BitField getSavedTorrentBitField(Snark snark) {
MetaInfo metainfo = snark.meta; MetaInfo metainfo = snark.getMetaInfo();
byte[] ih = metainfo.getInfoHash(); if (metainfo == null)
return null;
byte[] ih = snark.getInfoHash();
String infohash = Base64.encode(ih); String infohash = Base64.encode(ih);
infohash = infohash.replace('=', '$'); infohash = infohash.replace('=', '$');
String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX); String bf = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_BITFIELD_SUFFIX);
@@ -636,10 +829,13 @@ public class SnarkManager implements Snark.CompleteListener {
* @since 0.8.1 * @since 0.8.1
*/ */
public void loadSavedFilePriorities(Snark snark) { 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) if (metainfo.getFiles() == null)
return; return;
byte[] ih = metainfo.getInfoHash(); byte[] ih = snark.getInfoHash();
String infohash = Base64.encode(ih); String infohash = Base64.encode(ih);
infohash = infohash.replace('=', '$'); infohash = infohash.replace('=', '$');
String pri = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX); String pri = _config.getProperty(PROP_META_PREFIX + infohash + PROP_META_PRIORITY_SUFFIX);
@@ -655,7 +851,7 @@ public class SnarkManager implements Snark.CompleteListener {
} catch (Throwable t) {} } catch (Throwable t) {}
} }
} }
snark.storage.setFilePriorities(rv); storage.setFilePriorities(rv);
} }
/** /**
@@ -666,6 +862,8 @@ public class SnarkManager implements Snark.CompleteListener {
* The time is a standard long converted to string. * The time is a standard long converted to string.
* The status is either a bitfield converted to Base64 or "." for a completed * The status is either a bitfield converted to Base64 or "." for a completed
* torrent to save space in the config file and in memory. * torrent to save space in the config file and in memory.
*
* @param bitfield non-null
* @param priorities may be null * @param priorities may be null
*/ */
public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) { public void saveTorrentStatus(MetaInfo metainfo, BitField bitfield, int[] priorities) {
@@ -709,6 +907,8 @@ public class SnarkManager implements Snark.CompleteListener {
_config.remove(prop); _config.remove(prop);
} }
// TODO save closest DHT nodes too
saveConfig(); saveConfig();
} }
@@ -726,9 +926,33 @@ public class SnarkManager implements Snark.CompleteListener {
} }
/** /**
* Warning - does not validate announce URL - use TrackerClient.isValidAnnounce() * Just remember we have it
* @since 0.8.4
*/ */
private String locked_validateTorrent(MetaInfo info) throws IOException { 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 validateTorrent(MetaInfo info) {
List files = info.getFiles(); List files = info.getFiles();
if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) { if ( (files != null) && (files.size() > MAX_FILES_PER_TORRENT) ) {
return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size()); return _("Too many files in \"{0}\" ({1}), deleting it!", info.getName(), files.size());
@@ -777,84 +1001,184 @@ public class SnarkManager implements Snark.CompleteListener {
remaining = _snarks.size(); remaining = _snarks.size();
} }
if (torrent != null) { if (torrent != null) {
boolean wasStopped = torrent.stopped; boolean wasStopped = torrent.isStopped();
torrent.stopTorrent(); torrent.stopTorrent();
if (remaining == 0) { if (remaining == 0) {
// should we disconnect/reconnect here (taking care to deal with the other thread's // should we disconnect/reconnect here (taking care to deal with the other thread's
// I2PServerSocket.accept() call properly?) // I2PServerSocket.accept() call properly?)
////_util. ////_util.
} }
String name;
if (torrent.storage != null) {
name = torrent.storage.getBaseName();
} else {
name = sfile.getName();
}
if (!wasStopped) if (!wasStopped)
addMessage(_("Torrent stopped: \"{0}\"", name)); addMessage(_("Torrent stopped: \"{0}\"", torrent.getBaseName()));
} }
return torrent; 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 * Stop the torrent and delete the torrent file itself, but leaving the data
* behind. * behind.
* Holds the snarks lock to prevent interference from the DirMonitor.
*/ */
public void removeTorrent(String filename) { public void removeTorrent(String filename) {
Snark torrent = stopTorrent(filename, true); Snark torrent;
if (torrent != null) { // prevent interference by DirMonitor
synchronized (_snarks) {
torrent = stopTorrent(filename, true);
if (torrent == null)
return;
File torrentFile = new File(filename); File torrentFile = new File(filename);
torrentFile.delete(); 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 { private class DirMonitor implements Runnable {
public void run() { public void run() {
try { Thread.sleep(60*1000*getStartupDelayMinutes()); } catch (InterruptedException ie) {} // 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" // the first message was a "We are starting up in 1m"
synchronized (_messages) { synchronized (_messages) {
if (_messages.size() == 1) if (_messages.size() == 1)
_messages.remove(0); _messages.remove(0);
} }
}
// here because we need to delay until I2CP is up // here because we need to delay until I2CP is up
// although the user will see the default until then // although the user will see the default until then
getBWLimit(); getBWLimit();
boolean doMagnets = true;
while (true) { while (true) {
File dir = getDataDir(); File dir = getDataDir();
if (_log.shouldLog(Log.DEBUG)) if (_log.shouldLog(Log.DEBUG))
_log.debug("Directory Monitor loop over " + dir.getAbsolutePath()); _log.debug("Directory Monitor loop over " + dir.getAbsolutePath());
try { try {
// Don't let this interfere with .torrent files being added or deleted
synchronized (_snarks) {
monitorTorrents(dir); monitorTorrents(dir);
}
} catch (Exception e) { } catch (Exception e) {
_log.error("Error in the DirectoryMonitor", e); _log.error("Error in the DirectoryMonitor", e);
} }
if (doMagnets) {
addMagnets();
doMagnets = false;
}
try { Thread.sleep(60*1000); } catch (InterruptedException ie) {} try { Thread.sleep(60*1000); } catch (InterruptedException ie) {}
} }
} }
} }
/** two listeners */ // Begin Snark.CompleteListeners
/**
* A Snark.CompleteListener method.
*/
public void torrentComplete(Snark snark) { public void torrentComplete(Snark snark) {
MetaInfo meta = snark.getMetaInfo();
Storage storage = snark.getStorage();
if (meta == null || storage == null)
return;
StringBuilder buf = new StringBuilder(256); StringBuilder buf = new StringBuilder(256);
buf.append("<a href=\"/i2psnark/").append(snark.storage.getBaseName()); buf.append("<a href=\"/i2psnark/").append(storage.getBaseName());
if (snark.meta.getFiles() != null) if (meta.getFiles() != null)
buf.append('/'); buf.append('/');
buf.append("\">").append(snark.storage.getBaseName()).append("</a>"); buf.append("\">").append(storage.getBaseName()).append("</a>");
long len = snark.meta.getTotalLength();
addMessage(_("Download finished: {0}", buf.toString())); // + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')'); addMessage(_("Download finished: {0}", buf.toString())); // + " (" + _("size: {0}B", DataHelper.formatSize2(len)) + ')');
updateStatus(snark); updateStatus(snark);
} }
/**
* A Snark.CompleteListener method.
*/
public void updateStatus(Snark snark) { 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) { private void monitorTorrents(File dir) {
@@ -887,6 +1211,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... // now lets see which ones have been removed...
for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) { for (Iterator iter = existingNames.iterator(); iter.hasNext(); ) {
String name = (String)iter.next(); String name = (String)iter.next();
@@ -940,12 +1266,12 @@ public class SnarkManager implements Snark.CompleteListener {
/** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */ /** comma delimited list of name=announceURL=baseURL for the trackers to be displayed */
public static final String PROP_TRACKERS = "i2psnark.trackers"; 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 */ /** 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 if (trackerMap != null) // only do this once, can't be updated while running
return trackerMap; return trackerMap;
Map rv = new TreeMap(); Map<String, String> rv = new TreeMap();
String trackers = _config.getProperty(PROP_TRACKERS); String trackers = _config.getProperty(PROP_TRACKERS);
if ( (trackers == null) || (trackers.trim().length() <= 0) ) if ( (trackers == null) || (trackers.trim().length() <= 0) )
trackers = _context.getProperty(PROP_TRACKERS); trackers = _context.getProperty(PROP_TRACKERS);
@@ -984,9 +1310,10 @@ public class SnarkManager implements Snark.CompleteListener {
Set names = listTorrentFiles(); Set names = listTorrentFiles();
for (Iterator iter = names.iterator(); iter.hasNext(); ) { for (Iterator iter = names.iterator(); iter.hasNext(); ) {
Snark snark = getTorrent((String)iter.next()); Snark snark = getTorrent((String)iter.next());
if ( (snark != null) && (!snark.stopped) ) if ( (snark != null) && (!snark.isStopped()) )
snark.stopTorrent(); snark.stopTorrent();
} }
//save magnets
} }
} }
} }

View File

@@ -38,6 +38,7 @@ public class StaticSnark
//Security.addProvider(gnu); //Security.addProvider(gnu);
// And finally call the normal starting point. // And finally call the normal starting point.
Snark.main(args); //Snark.main(args);
System.err.println("unsupported");
} }
} }

View File

@@ -87,6 +87,9 @@ public class Storage
* Creates a storage from the existing file or directory together * Creates a storage from the existing file or directory together
* with an appropriate MetaInfo file as can be announced on the * with an appropriate MetaInfo file as can be announced on the
* given announce String location. * given announce String location.
*
* @param announce may be null
* @param listener may be null
*/ */
public Storage(I2PSnarkUtil util, File baseFile, String announce, StorageListener listener) public Storage(I2PSnarkUtil util, File baseFile, String announce, StorageListener listener)
throws IOException throws IOException
@@ -97,12 +100,12 @@ public class Storage
getFiles(baseFile); getFiles(baseFile);
long total = 0; long total = 0;
ArrayList lengthsList = new ArrayList(); ArrayList<Long> lengthsList = new ArrayList();
for (int i = 0; i < lengths.length; i++) for (int i = 0; i < lengths.length; i++)
{ {
long length = lengths[i]; long length = lengths[i];
total += length; total += length;
lengthsList.add(new Long(length)); lengthsList.add(Long.valueOf(length));
} }
piece_size = MIN_PIECE_SIZE; piece_size = MIN_PIECE_SIZE;
@@ -119,10 +122,10 @@ public class Storage
bitfield = new BitField(pieces); bitfield = new BitField(pieces);
needed = 0; needed = 0;
List files = new ArrayList(); List<List<String>> files = new ArrayList();
for (int i = 0; i < names.length; i++) 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); StringTokenizer st = new StringTokenizer(names[i], File.separator);
while (st.hasMoreTokens()) while (st.hasMoreTokens())
{ {
@@ -535,7 +538,7 @@ public class Storage
} else { } else {
// the following sets the needed variable // the following sets the needed variable
changed = true; changed = true;
checkCreateFiles(); checkCreateFiles(false);
} }
if (complete()) { if (complete()) {
_util.debug("Torrent is complete", Snark.NOTICE); _util.debug("Torrent is complete", Snark.NOTICE);
@@ -590,7 +593,7 @@ public class Storage
* Removes 'suspicious' characters from the given file name. * Removes 'suspicious' characters from the given file name.
* http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx * 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(" ")) if (name.equals(".") || name.equals(" "))
return "_"; return "_";
@@ -646,15 +649,26 @@ public class Storage
/** /**
* This is called at the beginning, and at presumed completion, * This is called at the beginning, and at presumed completion,
* so we have to be careful about locking. * 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, // Whether we are resuming or not,
// if any of the files already exists we assume we are resuming. // if any of the files already exists we assume we are resuming.
boolean resume = false; boolean resume = false;
_probablyComplete = true; _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 // Make sure all files are available and of correct length
for (int i = 0; i < rafs.length; i++) for (int i = 0; i < rafs.length; i++)
@@ -715,8 +729,8 @@ public class Storage
} }
if (correctHash) if (correctHash)
{ {
bitfield.set(i); bfield.set(i);
needed--; need--;
} }
if (listener != null) 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) { if (listener != null) {
listener.storageAllChecked(this); listener.storageAllChecked(this);
if (needed <= 0) if (needed <= 0)
@@ -750,6 +773,7 @@ public class Storage
openRAF(nr, false); // RW openRAF(nr, false); // RW
// XXX - Is this the best way to make sure we have enough space for // XXX - Is this the best way to make sure we have enough space for
// the whole file? // the whole file?
if (listener != null)
listener.storageCreateFile(this, names[nr], lengths[nr]); listener.storageCreateFile(this, names[nr], lengths[nr]);
final int ZEROBLOCKSIZE = metainfo.getPieceLength(0); final int ZEROBLOCKSIZE = metainfo.getPieceLength(0);
byte[] zeros; byte[] zeros;
@@ -899,11 +923,7 @@ public class Storage
// checkCreateFiles() which will set 'needed' and 'bitfield' // checkCreateFiles() which will set 'needed' and 'bitfield'
// and also call listener.storageCompleted() if the double-check // and also call listener.storageCompleted() if the double-check
// was successful. // was successful.
// Todo: set a listener variable so the web shows "checking" and don't checkCreateFiles(true);
// have the user panic when completed amount goes to zero temporarily?
needed = metainfo.getPieces();
bitfield = new BitField(needed);
checkCreateFiles();
if (needed > 0) { if (needed > 0) {
if (listener != null) if (listener != null)
listener.setWantedPieces(this); listener.setWantedPieces(this);

View File

@@ -34,9 +34,12 @@ import java.util.Random;
import java.util.Set; import java.util.Set;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.data.Hash;
import net.i2p.util.I2PAppThread; import net.i2p.util.I2PAppThread;
import net.i2p.util.Log; import net.i2p.util.Log;
import org.klomp.snark.dht.DHT;
/** /**
* Informs metainfo tracker of events and gets new peers for peer * Informs metainfo tracker of events and gets new peers for peer
* coordinator. * coordinator.
@@ -63,6 +66,7 @@ public class TrackerClient extends I2PAppThread
private I2PSnarkUtil _util; private I2PSnarkUtil _util;
private final MetaInfo meta; private final MetaInfo meta;
private final PeerCoordinator coordinator; private final PeerCoordinator coordinator;
private final Snark snark;
private final int port; private final int port;
private boolean stop; private boolean stop;
@@ -70,15 +74,19 @@ public class TrackerClient extends I2PAppThread
private List trackers; 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(); super();
// Set unique name. // Set unique name.
String id = urlencode(coordinator.getID()); String id = urlencode(snark.getID());
setName("TrackerClient " + id.substring(id.length() - 12)); setName("TrackerClient " + id.substring(id.length() - 12));
_util = util; _util = util;
this.meta = meta; this.meta = meta;
this.coordinator = coordinator; this.coordinator = coordinator;
this.snark = snark;
this.port = 6881; //(port == -1) ? 9 : port; this.port = 6881; //(port == -1) ? 9 : port;
@@ -118,10 +126,9 @@ public class TrackerClient extends I2PAppThread
@Override @Override
public void run() public void run()
{ {
String infoHash = urlencode(meta.getInfoHash()); String infoHash = urlencode(snark.getInfoHash());
String peerID = urlencode(coordinator.getID()); String peerID = urlencode(snark.getID());
_log.debug("Announce: [" + meta.getAnnounce() + "] infoHash: " + infoHash);
// Construct the list of trackers for this torrent, // Construct the list of trackers for this torrent,
// starting with the primary one listed in the metainfo, // starting with the primary one listed in the metainfo,
@@ -130,12 +137,18 @@ public class TrackerClient extends I2PAppThread
// the primary tracker, that we don't add it twice. // the primary tracker, that we don't add it twice.
// todo: check for b32 matches as well // todo: check for b32 matches as well
trackers = new ArrayList(2); trackers = new ArrayList(2);
String primary = meta.getAnnounce(); String primary = null;
if (meta != null) {
primary = meta.getAnnounce();
if (isValidAnnounce(primary)) { if (isValidAnnounce(primary)) {
trackers.add(new Tracker(meta.getAnnounce(), true)); trackers.add(new Tracker(meta.getAnnounce(), true));
_log.debug("Announce: [" + primary + "] infoHash: " + infoHash);
} else { } else {
_log.warn("Skipping invalid or non-i2p announce: " + primary); _log.warn("Skipping invalid or non-i2p announce: " + primary);
} }
}
if (primary == null)
primary = "";
List tlist = _util.getOpenTrackers(); List tlist = _util.getOpenTrackers();
if (tlist != null) { if (tlist != null) {
for (int i = 0; i < tlist.size(); i++) { for (int i = 0; i < tlist.size(); i++) {
@@ -160,15 +173,17 @@ public class TrackerClient extends I2PAppThread
continue; continue;
if (primary.startsWith("http://i2p/" + dest)) if (primary.startsWith("http://i2p/" + dest))
continue; 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); _log.debug("Additional announce: [" + url + "] for infoHash: " + infoHash);
} }
} }
if (tlist.isEmpty()) { if (trackers.isEmpty()) {
// FIXME really need to get this message to the gui // FIXME really need to get this message to the gui
stop = true; stop = true;
_log.error("No valid trackers for infoHash: " + infoHash); _log.error("No valid trackers for infoHash: " + infoHash);
// FIXME keep going if DHT enabled
return; return;
} }
@@ -188,6 +203,9 @@ public class TrackerClient extends I2PAppThread
Random r = I2PAppContext.getGlobalContext().random(); Random r = I2PAppContext.getGlobalContext().random();
while(!stop) while(!stop)
{ {
// Local DHT tracker announce
if (_util.getDHT() != null)
_util.getDHT().announce(snark.getInfoHash());
try try
{ {
// Sleep some minutes... // Sleep some minutes...
@@ -200,7 +218,7 @@ public class TrackerClient extends I2PAppThread
firstTime = false; firstTime = false;
} else if (completed && runStarted) } else if (completed && runStarted)
delay = 3*SLEEP*60*1000 + random; 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; delay = INITIAL_SLEEP;
else else
// sleep a while, when we wake up we will contact only the trackers whose intervals have passed // 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(); uploaded = coordinator.getUploaded();
downloaded = coordinator.getDownloaded(); downloaded = coordinator.getDownloaded();
left = coordinator.getLeft(); left = coordinator.getLeft(); // -1 in magnet mode
// First time we got a complete download? // First time we got a complete download?
String event; String event;
@@ -251,7 +269,7 @@ public class TrackerClient extends I2PAppThread
uploaded, downloaded, left, uploaded, downloaded, left,
event); event);
coordinator.trackerProblems = null; snark.setTrackerProblems(null);
tr.trackerProblems = null; tr.trackerProblems = null;
tr.registerFails = 0; tr.registerFails = 0;
tr.consecutiveFails = 0; tr.consecutiveFails = 0;
@@ -260,18 +278,26 @@ public class TrackerClient extends I2PAppThread
runStarted = true; runStarted = true;
tr.started = true; tr.started = true;
Set peers = info.getPeers(); Set<Peer> peers = info.getPeers();
tr.seenPeers = info.getPeerCount(); tr.seenPeers = info.getPeerCount();
if (coordinator.trackerSeenPeers < tr.seenPeers) // update rising number quickly if (snark.getTrackerSeenPeers() < tr.seenPeers) // update rising number quickly
coordinator.trackerSeenPeers = tr.seenPeers; snark.setTrackerSeenPeers(tr.seenPeers);
if ( (left > 0) && (!completed) ) {
// 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 // we only want to talk to new people if we need things
// from them (duh) // from them (duh)
List ordered = new ArrayList(peers); List<Peer> ordered = new ArrayList(peers);
Collections.shuffle(ordered, r); Collections.shuffle(ordered, r);
Iterator it = ordered.iterator(); Iterator<Peer> it = ordered.iterator();
while ((!stop) && it.hasNext()) { while ((!stop) && it.hasNext()) {
Peer cur = (Peer)it.next(); Peer cur = it.next();
// FIXME if id == us || dest == us continue; // FIXME if id == us || dest == us continue;
// only delay if we actually make an attempt to add peer // only delay if we actually make an attempt to add peer
if(coordinator.addPeer(cur)) { if(coordinator.addPeer(cur)) {
@@ -293,12 +319,12 @@ public class TrackerClient extends I2PAppThread
tr.trackerProblems = ioe.getMessage(); tr.trackerProblems = ioe.getMessage();
// don't show secondary tracker problems to the user // don't show secondary tracker problems to the user
if (tr.isPrimary) if (tr.isPrimary)
coordinator.trackerProblems = tr.trackerProblems; snark.setTrackerProblems(tr.trackerProblems);
if (tr.trackerProblems.toLowerCase().startsWith(NOT_REGISTERED)) { if (tr.trackerProblems.toLowerCase().startsWith(NOT_REGISTERED)) {
// Give a guy some time to register it if using opentrackers too // Give a guy some time to register it if using opentrackers too
if (trackers.size() == 1) { if (trackers.size() == 1) {
stop = true; stop = true;
coordinator.snark.stopTorrent(); snark.stopTorrent();
} else { // hopefully each on the opentrackers list is really open } else { // hopefully each on the opentrackers list is really open
if (tr.registerFails++ > MAX_REGISTER_FAILS) if (tr.registerFails++ > MAX_REGISTER_FAILS)
tr.stop = true; tr.stop = true;
@@ -315,8 +341,47 @@ public class TrackerClient extends I2PAppThread
maxSeenPeers = tr.seenPeers; maxSeenPeers = tr.seenPeers;
} // *** end of trackers loop here } // *** end of trackers loop here
// 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)) {
int delay = DELAY_MUL;
delay *= r.nextInt(10);
delay += DELAY_MIN;
try { Thread.sleep(delay); } catch (InterruptedException ie) {}
}
}
}
}
// we could try and total the unique peers but that's too hard for now // we could try and total the unique peers but that's too hard for now
coordinator.trackerSeenPeers = maxSeenPeers; snark.setTrackerSeenPeers(maxSeenPeers);
if (!runStarted) if (!runStarted)
_util.debug(" Retrying in one minute...", Snark.DEBUG); _util.debug(" Retrying in one minute...", Snark.DEBUG);
} // *** end of while loop } // *** end of while loop
@@ -329,6 +394,9 @@ public class TrackerClient extends I2PAppThread
} }
finally finally
{ {
// Local DHT tracker unannounce
if (_util.getDHT() != null)
_util.getDHT().unannounce(snark.getInfoHash());
try try
{ {
// try to contact everybody we can // try to contact everybody we can
@@ -351,6 +419,8 @@ public class TrackerClient extends I2PAppThread
long downloaded, long left, String event) long downloaded, long left, String event)
throws IOException 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 String s = tr.announce
+ "?info_hash=" + infoHash + "?info_hash=" + infoHash
+ "&peer_id=" + peerID + "&peer_id=" + peerID
@@ -358,10 +428,10 @@ public class TrackerClient extends I2PAppThread
+ "&ip=" + _util.getOurIPString() + ".i2p" + "&ip=" + _util.getOurIPString() + ".i2p"
+ "&uploaded=" + uploaded + "&uploaded=" + uploaded
+ "&downloaded=" + downloaded + "&downloaded=" + downloaded
+ "&left=" + left + "&left=" + tleft
+ "&compact=1" // NOTE: opentracker will return 400 for &compact alone + "&compact=1" // NOTE: opentracker will return 400 for &compact alone
+ ((! event.equals(NO_EVENT)) ? ("&event=" + event) : ""); + ((! 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"; s += "&numwant=0";
else else
s += "&numwant=" + _util.getMaxConnections(); s += "&numwant=" + _util.getMaxConnections();
@@ -377,8 +447,8 @@ public class TrackerClient extends I2PAppThread
try { try {
in = new FileInputStream(fetched); in = new FileInputStream(fetched);
TrackerInfo info = new TrackerInfo(in, coordinator.getID(), TrackerInfo info = new TrackerInfo(in, snark.getID(),
coordinator.getMetaInfo()); snark.getInfoHash(), snark.getMetaInfo());
_util.debug("TrackerClient response: " + info, Snark.INFO); _util.debug("TrackerClient response: " + info, Snark.INFO);
String failure = info.getFailureReason(); String failure = info.getFailureReason();

View File

@@ -46,19 +46,20 @@ public class TrackerInfo
private int complete; private int complete;
private int incomplete; 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 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 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 throws IOException
{ {
BEValue reason = (BEValue)m.get("failure reason"); BEValue reason = (BEValue)m.get("failure reason");
@@ -84,10 +85,10 @@ public class TrackerInfo
Set<Peer> p; Set<Peer> p;
try { try {
// One big string (the official compact format) // 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) { } catch (InvalidBEncodingException ibe) {
// List of Dictionaries or List of Strings // List of Dictionaries or List of Strings
p = getPeers(bePeers.getList(), my_id, metainfo); p = getPeers(bePeers.getList(), my_id, infohash, metainfo);
} }
peers = p; peers = p;
} }
@@ -123,7 +124,7 @@ public class TrackerInfo
******/ ******/
/** List of Dictionaries or List of Strings */ /** 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 throws IOException
{ {
Set<Peer> peers = new HashSet(l.size()); Set<Peer> peers = new HashSet(l.size());
@@ -144,7 +145,7 @@ public class TrackerInfo
continue; continue;
} }
} }
peers.add(new Peer(peerID, my_id, metainfo)); peers.add(new Peer(peerID, my_id, infohash, metainfo));
} }
return peers; return peers;
@@ -156,7 +157,7 @@ public class TrackerInfo
* One big string of concatenated 32-byte hashes * One big string of concatenated 32-byte hashes
* @since 0.8.1 * @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 throws IOException
{ {
int count = l.length / HASH_LENGTH; int count = l.length / HASH_LENGTH;
@@ -172,7 +173,7 @@ public class TrackerInfo
// won't happen // won't happen
continue; continue;
} }
peers.add(new Peer(peerID, my_id, metainfo)); peers.add(new Peer(peerID, my_id, infohash, metainfo));
} }
return peers; return peers;

View File

@@ -24,6 +24,8 @@ import java.io.UnsupportedEncodingException;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import net.i2p.data.Base64;
/** /**
* Holds different types that a bencoded byte array can represent. * Holds different types that a bencoded byte array can represent.
* You need to call the correct get method to get the correct java * You need to call the correct get method to get the correct java
@@ -178,12 +180,37 @@ public class BEValue
String valueString; String valueString;
if (value instanceof byte[]) if (value instanceof byte[])
{ {
// try to do a nice job for debugging
byte[] bs = (byte[])value; byte[] bs = (byte[])value;
// XXX - Stupid heuristic... and not UTF-8 if (bs.length == 0)
if (bs.length <= 12) valueString = "0 bytes";
valueString = new String(bs); else if (bs.length <= 32) {
else StringBuilder buf = new StringBuilder(32);
valueString = "bytes:" + bs.length; 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 else
valueString = value.toString(); valueString = value.toString();

View File

@@ -50,6 +50,8 @@ public class BEncoder
public static void bencode(Object o, OutputStream out) public static void bencode(Object o, OutputStream out)
throws IOException, IllegalArgumentException throws IOException, IllegalArgumentException
{ {
if (o == null)
throw new NullPointerException("Cannot bencode null");
if (o instanceof String) if (o instanceof String)
bencode((String)o, out); bencode((String)o, out);
else if (o instanceof byte[]) else if (o instanceof byte[])
@@ -59,7 +61,7 @@ public class BEncoder
else if (o instanceof List) else if (o instanceof List)
bencode((List)o, out); bencode((List)o, out);
else if (o instanceof Map) else if (o instanceof Map)
bencode((Map)o, out); bencode((Map<String, Object>)o, out);
else if (o instanceof BEValue) else if (o instanceof BEValue)
bencode(((BEValue)o).getValue(), out); bencode(((BEValue)o).getValue(), out);
else else
@@ -153,7 +155,7 @@ public class BEncoder
out.write(bs); out.write(bs);
} }
public static byte[] bencode(Map m) public static byte[] bencode(Map<String, Object> m)
{ {
try 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'); out.write('d');
// Keys must be sorted. XXX - But is this the correct order? // Keys must be sorted. XXX - But is this the correct order?
Set s = m.keySet(); Set<String> s = m.keySet();
List l = new ArrayList(s); List<String> l = new ArrayList(s);
Collections.sort(l); Collections.sort(l);
Iterator it = l.iterator(); Iterator<String> it = l.iterator();
while(it.hasNext()) while(it.hasNext())
{ {
// Keys must be Strings. // Keys must be Strings.
String key = (String)it.next(); String key = it.next();
Object value = m.get(key); Object value = m.get(key);
bencode(key, out); bencode(key, out);
bencode(value, out); bencode(value, out);

View File

@@ -0,0 +1,83 @@
package org.klomp.snark.dht;
/*
* Copyright 2010 zzz (zzz@mail.i2p)
* 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);
}

View File

@@ -6,6 +6,7 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.text.Collator; import java.text.Collator;
import java.text.DecimalFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -26,6 +27,7 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import net.i2p.I2PAppContext; import net.i2p.I2PAppContext;
import net.i2p.data.Base32;
import net.i2p.data.Base64; import net.i2p.data.Base64;
import net.i2p.data.DataHelper; import net.i2p.data.DataHelper;
import net.i2p.util.FileUtil; import net.i2p.util.FileUtil;
@@ -33,6 +35,7 @@ import net.i2p.util.I2PAppThread;
import net.i2p.util.Log; import net.i2p.util.Log;
import net.i2p.util.SecureFileOutputStream; import net.i2p.util.SecureFileOutputStream;
import org.klomp.snark.I2PSnarkUtil;
import org.klomp.snark.MetaInfo; import org.klomp.snark.MetaInfo;
import org.klomp.snark.Peer; import org.klomp.snark.Peer;
import org.klomp.snark.Snark; import org.klomp.snark.Snark;
@@ -58,8 +61,13 @@ public class I2PSnarkServlet extends Default {
private Resource _resourceBase; private Resource _resourceBase;
private String _themePath; private String _themePath;
private String _imgPath; private String _imgPath;
private String _lastAnnounceURL = "";
public static final String PROP_CONFIG_FILE = "i2psnark.configFile"; 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 @Override
public void init(ServletConfig cfg) throws ServletException { public void init(ServletConfig cfg) throws ServletException {
@@ -153,7 +161,7 @@ public class I2PSnarkServlet extends Default {
resp.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/html; charset=UTF-8"); resp.setContentType("text/html; charset=UTF-8");
Resource resource = getResource(pathInContext); Resource resource = getResource(pathInContext);
if (resource == null || (!resource.exists()) || !resource.isDirectory()) { if (resource == null || (!resource.exists())) {
resp.sendError(HttpResponse.__404_Not_Found); resp.sendError(HttpResponse.__404_Not_Found);
} else { } else {
String base = URI.addPaths(req.getRequestURI(), "/"); String base = URI.addPaths(req.getRequestURI(), "/");
@@ -376,7 +384,7 @@ public class I2PSnarkServlet extends Default {
for (int i = 0; i < snarks.size(); i++) { for (int i = 0; i < snarks.size(); i++) {
Snark snark = (Snark)snarks.get(i); Snark snark = (Snark)snarks.get(i);
boolean showDebug = "2".equals(peerParam); 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); displaySnark(out, snark, uri, i, stats, showPeers, isDegraded, noThinsp, showDebug);
} }
@@ -478,10 +486,12 @@ public class I2PSnarkServlet extends Default {
if (newURL != null) { if (newURL != null) {
if (newURL.startsWith("http://")) { if (newURL.startsWith("http://")) {
_manager.addMessage(_("Fetching {0}", urlify(newURL))); _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(); fetch.start();
} else if (newURL.startsWith(MAGNET) || newURL.startsWith(MAGGOT)) {
addMagnet(newURL);
} else { } else {
_manager.addMessage(_("Invalid URL - must start with http://")); _manager.addMessage(_("Invalid URL: Must start with \"http://\", \"{0}\", or \"{1}\"", MAGNET, MAGGOT));
} }
} else { } else {
// no file or URL specified // no file or URL specified
@@ -494,8 +504,8 @@ public class I2PSnarkServlet extends Default {
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
String name = (String)iter.next(); String name = (String)iter.next();
Snark snark = _manager.getTorrent(name); Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
_manager.stopTorrent(name, false); _manager.stopTorrent(snark, false);
break; break;
} }
} }
@@ -508,11 +518,9 @@ public class I2PSnarkServlet extends Default {
if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1 if ( (infoHash != null) && (infoHash.length == 20) ) { // valid sha1
for (String name : _manager.listTorrentFiles()) { for (String name : _manager.listTorrentFiles()) {
Snark snark = _manager.getTorrent(name); Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
snark.startTorrent(); snark.startTorrent();
if (snark.storage != null) _manager.addMessage(_("Starting up torrent {0}", snark.getBaseName()));
name = snark.storage.getBaseName();
_manager.addMessage(_("Starting up torrent {0}", name));
break; break;
} }
} }
@@ -526,8 +534,15 @@ public class I2PSnarkServlet extends Default {
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
String name = (String)iter.next(); String name = (String)iter.next();
Snark snark = _manager.getTorrent(name); Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
_manager.stopTorrent(name, true); 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? // should we delete the torrent file?
// yeah, need to, otherwise it'll get autoadded again (at the moment // yeah, need to, otherwise it'll get autoadded again (at the moment
File f = new File(name); File f = new File(name);
@@ -546,13 +561,20 @@ public class I2PSnarkServlet extends Default {
for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) { for (Iterator iter = _manager.listTorrentFiles().iterator(); iter.hasNext(); ) {
String name = (String)iter.next(); String name = (String)iter.next();
Snark snark = _manager.getTorrent(name); Snark snark = _manager.getTorrent(name);
if ( (snark != null) && (DataHelper.eq(infoHash, snark.meta.getInfoHash())) ) { if ( (snark != null) && (DataHelper.eq(infoHash, snark.getInfoHash())) ) {
_manager.stopTorrent(name, true); 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); File f = new File(name);
f.delete(); f.delete();
_manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath())); _manager.addMessage(_("Torrent file deleted: {0}", f.getAbsolutePath()));
List files = snark.meta.getFiles(); List files = meta.getFiles();
String dataFile = snark.meta.getName(); String dataFile = snark.getBaseName();
f = new File(_manager.getDataDir(), dataFile); f = new File(_manager.getDataDir(), dataFile);
if (files == null) { // single file torrent if (files == null) { // single file torrent
if (f.delete()) if (f.delete())
@@ -612,22 +634,22 @@ public class I2PSnarkServlet extends Default {
if (announceURL == null || announceURL.length() <= 0) if (announceURL == null || announceURL.length() <= 0)
_manager.addMessage(_("Error creating torrent - you must select a tracker")); _manager.addMessage(_("Error creating torrent - you must select a tracker"));
else if (baseFile.exists()) { else if (baseFile.exists()) {
_lastAnnounceURL = announceURL;
if (announceURL.equals("none"))
announceURL = null;
try { 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); Storage s = new Storage(_manager.util(), baseFile, announceURL, null);
s.create(); s.create();
s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over s.close(); // close the files... maybe need a way to pass this Storage to addTorrent rather than starting over
MetaInfo info = s.getMetaInfo(); MetaInfo info = s.getMetaInfo();
File torrentFile = new File(baseFile.getParent(), baseFile.getName() + ".torrent"); File torrentFile = new File(_manager.getDataDir(), s.getBaseName() + ".torrent");
if (torrentFile.exists()) // FIXME is the storage going to stay around thanks to the info reference?
throw new IOException("Cannot overwrite an existing .torrent file: " + torrentFile.getPath()); // now add it, but don't automatically start it
_manager.saveTorrentStatus(info, s.getBitField(), null); // so addTorrent won't recheck _manager.addTorrent(info, s.getBitField(), torrentFile.getAbsolutePath(), true);
// DirMonitor could grab this first, maybe hold _snarks lock?
FileOutputStream out = new FileOutputStream(torrentFile);
out.write(info.getTorrentData());
out.close();
_manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath()); _manager.addMessage(_("Torrent created for \"{0}\"", baseFile.getName()) + ": " + torrentFile.getAbsolutePath());
// now fire it up, but don't automatically seed it if (announceURL != null)
_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())); _manager.addMessage(_("Many I2P trackers require you to register new torrents before seeding - please do so before starting \"{0}\"", baseFile.getName()));
} catch (IOException ioe) { } catch (IOException ioe) {
_manager.addMessage(_("Error creating a torrent for \"{0}\"", baseFile.getAbsolutePath()) + ": " + ioe.getMessage()); _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); List snarks = getSortedSnarks(req);
for (int i = 0; i < snarks.size(); i++) { for (int i = 0; i < snarks.size(); i++) {
Snark snark = (Snark)snarks.get(i); Snark snark = (Snark)snarks.get(i);
if (!snark.stopped) if (!snark.isStopped())
_manager.stopTorrent(snark.torrent, false); _manager.stopTorrent(snark, false);
} }
if (_manager.util().connected()) { if (_manager.util().connected()) {
// Give the stopped announces time to get out // Give the stopped announces time to get out
@@ -657,7 +679,7 @@ public class I2PSnarkServlet extends Default {
List snarks = getSortedSnarks(req); List snarks = getSortedSnarks(req);
for (int i = 0; i < snarks.size(); i++) { for (int i = 0; i < snarks.size(); i++) {
Snark snark = (Snark)snarks.get(i); Snark snark = (Snark)snarks.get(i);
if (snark.stopped) if (snark.isStopped())
snark.startTorrent(); snark.startTorrent();
} }
} else { } else {
@@ -725,7 +747,7 @@ public class I2PSnarkServlet extends Default {
private static final int MAX_DISPLAYED_ERROR_LENGTH = 43; private static final int MAX_DISPLAYED_ERROR_LENGTH = 43;
private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers, private void displaySnark(PrintWriter out, Snark snark, String uri, int row, long stats[], boolean showPeers,
boolean isDegraded, boolean noThinsp, boolean showDebug) throws IOException { boolean isDegraded, boolean noThinsp, boolean showDebug) throws IOException {
String filename = snark.torrent; String filename = snark.getName();
File f = new File(filename); File f = new File(filename);
filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name filename = f.getName(); // the torrent may be the canonical name, so lets just grab the local name
int i = filename.lastIndexOf(".torrent"); int i = filename.lastIndexOf(".torrent");
@@ -733,31 +755,28 @@ public class I2PSnarkServlet extends Default {
filename = filename.substring(0, i); filename = filename.substring(0, i);
String fullFilename = filename; String fullFilename = filename;
if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH) { if (filename.length() > MAX_DISPLAYED_FILENAME_LENGTH) {
fullFilename = new String(filename); String start = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH);
filename = filename.substring(0, MAX_DISPLAYED_FILENAME_LENGTH) + "&hellip;"; if (start.indexOf(" ") < 0 && start.indexOf("-") < 0) {
// browser has nowhere to break it
fullFilename = filename;
filename = start + "&hellip;";
} }
long total = snark.meta.getTotalLength(); }
long total = snark.getTotalLength();
// Early typecast, avoid possibly overflowing a temp integer // 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) if (remaining > total)
remaining = total; remaining = total;
long downBps = 0; long downBps = snark.getDownloadRate();
long upBps = 0; long upBps = snark.getUploadRate();
if (snark.coordinator != null) {
downBps = snark.coordinator.getDownloadRate();
upBps = snark.coordinator.getUploadRate();
}
long remainingSeconds; long remainingSeconds;
if (downBps > 0) if (downBps > 0)
remainingSeconds = remaining / downBps; remainingSeconds = remaining / downBps;
else else
remainingSeconds = -1; remainingSeconds = -1;
boolean isRunning = !snark.stopped; boolean isRunning = !snark.isStopped();
long uploaded = 0; long uploaded = snark.getUploaded();
if (snark.coordinator != null) { stats[0] += snark.getDownloaded();
uploaded = snark.coordinator.getUploaded();
stats[0] += snark.coordinator.getDownloaded();
}
stats[1] += uploaded; stats[1] += uploaded;
if (isRunning) { if (isRunning) {
stats[2] += downBps; stats[2] += downBps;
@@ -765,25 +784,22 @@ public class I2PSnarkServlet extends Default {
} }
stats[5] += total; stats[5] += total;
boolean isValid = snark.meta != null; MetaInfo meta = snark.getMetaInfo();
boolean singleFile = snark.meta.getFiles() == null; // isValid means isNotMagnet
boolean isValid = meta != null;
boolean isMultiFile = isValid && meta.getFiles() != null;
String err = null; String err = snark.getTrackerProblems();
int curPeers = 0; int curPeers = snark.getPeerCount();
int knownPeers = 0;
if (snark.coordinator != null) {
err = snark.coordinator.trackerProblems;
curPeers = snark.coordinator.getPeerCount();
stats[4] += curPeers; stats[4] += curPeers;
knownPeers = Math.max(curPeers, snark.coordinator.trackerSeenPeers); int knownPeers = Math.max(curPeers, snark.getTrackerSeenPeers());
}
String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd"); String rowClass = (row % 2 == 0 ? "snarkTorrentEven" : "snarkTorrentOdd");
String statusString; String statusString;
if (err != null) { if (err != null) {
if (isRunning && curPeers > 0 && !showPeers) if (isRunning && curPeers > 0 && !showPeers)
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") + 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) + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
else if (isRunning) 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") + statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "trackererror.png\" title=\"" + err + "\"></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Tracker Error") +
"<br>" + err; "<br>" + err;
} }
} else if (remaining <= 0) { } else if (remaining == 0) { // < 0 means no meta size yet
if (isRunning && curPeers > 0 && !showPeers) if (isRunning && curPeers > 0 && !showPeers)
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "seeding.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Seeding") + 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) + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
else if (isRunning) else if (isRunning)
@@ -811,7 +827,7 @@ public class I2PSnarkServlet extends Default {
} else { } else {
if (isRunning && curPeers > 0 && downBps > 0 && !showPeers) if (isRunning && curPeers > 0 && downBps > 0 && !showPeers)
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "downloading.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("OK") + 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) + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
else if (isRunning && curPeers > 0 && downBps > 0) else if (isRunning && curPeers > 0 && downBps > 0)
@@ -820,7 +836,7 @@ public class I2PSnarkServlet extends Default {
ngettext("1 peer", "{0} peers", knownPeers); ngettext("1 peer", "{0} peers", knownPeers);
else if (isRunning && curPeers > 0 && !showPeers) else if (isRunning && curPeers > 0 && !showPeers)
statusString = "<img alt=\"\" border=\"0\" src=\"" + _imgPath + "stalled.png\" ></td><td class=\"snarkTorrentStatus " + rowClass + "\">" + _("Stalled") + 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) + curPeers + thinsp(noThinsp) +
ngettext("1 peer", "{0} peers", knownPeers) + "</a>"; ngettext("1 peer", "{0} peers", knownPeers) + "</a>";
else if (isRunning && curPeers > 0) else if (isRunning && curPeers > 0)
@@ -841,41 +857,24 @@ public class I2PSnarkServlet extends Default {
out.write(statusString + "</td>\n\t"); out.write(statusString + "</td>\n\t");
out.write("<td class=\"" + rowClass + "\">"); out.write("<td class=\"" + rowClass + "\">");
// temporarily hardcoded for postman* and anonymity, requires bytemonsoon patch for lookup by info_hash if (isValid) {
String announce = snark.meta.getAnnounce(); StringBuilder buf = new StringBuilder(128);
if (announce.startsWith("http://YRgrgTLG") || announce.startsWith("http://8EoJZIKr") || buf.append("<a href=\"").append(snark.getBaseName())
announce.startsWith("http://lnQ6yoBT") || announce.startsWith("http://tracker2.postman.i2p/") || announce.startsWith("http://ahsplxkbhemefwvvml7qovzl5a2b5xo5i7lyai7ntdunvcyfdtna.b32.i2p/")) { .append("/\" title=\"").append(_("Torrent details"))
Map trackers = _manager.getTrackers(); .append("\"><img alt=\"").append(_("Info")).append("\" border=\"0\" src=\"")
for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) { .append(_imgPath).append("details.png\"></a>");
Map.Entry entry = (Map.Entry)iter.next(); out.write(buf.toString());
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&amp;filelist=1&amp;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;
}
} }
out.write("</td>\n<td class=\"" + rowClass + "\">"); out.write("</td>\n<td class=\"" + rowClass + "\">");
StringBuilder buf = null; StringBuilder buf = null;
if (remaining == 0 || snark.meta.getFiles() != null) { if (remaining == 0 || isMultiFile) {
buf = new StringBuilder(128); buf = new StringBuilder(128);
buf.append("<a href=\"").append(snark.storage.getBaseName()); buf.append("<a href=\"").append(snark.getBaseName());
if (snark.meta.getFiles() != null) if (isMultiFile)
buf.append('/'); buf.append('/');
buf.append("\" title=\""); buf.append("\" title=\"");
if (snark.meta.getFiles() != null) if (isMultiFile)
buf.append(_("View files")); buf.append(_("View files"));
else else
buf.append(_("Open file")); buf.append(_("Open file"));
@@ -883,21 +882,23 @@ public class I2PSnarkServlet extends Default {
out.write(buf.toString()); out.write(buf.toString());
} }
String icon; String icon;
if (snark.meta.getFiles() != null) if (isMultiFile)
icon = "folder"; icon = "folder";
else if (isValid)
icon = toIcon(meta.getName());
else else
icon = toIcon(snark.meta.getName()); icon = "magnet";
if (remaining == 0 || snark.meta.getFiles() != null) { if (remaining == 0 || isMultiFile) {
out.write(toImg(icon, _("Open"))); out.write(toImg(icon, _("Open")));
out.write("</a>"); out.write("</a>");
} else { } else {
out.write(toImg(icon)); out.write(toImg(icon));
} }
out.write("</td><td class=\"snarkTorrentName " + rowClass + "\">"); out.write("</td><td class=\"snarkTorrentName " + rowClass + "\">");
if (remaining == 0 || snark.meta.getFiles() != null) if (remaining == 0 || isMultiFile)
out.write(buf.toString()); out.write(buf.toString());
out.write(filename); out.write(filename);
if (remaining == 0 || snark.meta.getFiles() != null) if (remaining == 0 || isMultiFile)
out.write("</a>"); out.write("</a>");
out.write("<td align=\"right\" class=\"snarkTorrentETA " + rowClass + "\">"); 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 + "\">"); out.write("<td align=\"right\" class=\"snarkTorrentDownloaded " + rowClass + "\">");
if (remaining > 0) if (remaining > 0)
out.write(formatSize(total-remaining) + thinsp(noThinsp) + formatSize(total)); out.write(formatSize(total-remaining) + thinsp(noThinsp) + formatSize(total));
else else if (remaining == 0)
out.write(formatSize(total)); // 3GB out.write(formatSize(total)); // 3GB
else
out.write("??"); // no meta size yet
out.write("</td>\n\t"); out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentUploaded " + rowClass + "\">"); out.write("<td align=\"right\" class=\"snarkTorrentUploaded " + rowClass + "\">");
if(isRunning) if(isRunning && isValid)
out.write(formatSize(uploaded)); out.write(formatSize(uploaded));
out.write("</td>\n\t"); out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">"); out.write("<td align=\"right\" class=\"snarkTorrentRateDown\">");
if(isRunning && remaining > 0) if(isRunning && remaining != 0)
out.write(formatSize(downBps) + "ps"); out.write(formatSize(downBps) + "ps");
out.write("</td>\n\t"); out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">"); out.write("<td align=\"right\" class=\"snarkTorrentRateUp\">");
if(isRunning) if(isRunning && isValid)
out.write(formatSize(upBps) + "ps"); out.write(formatSize(upBps) + "ps");
out.write("</td>\n\t"); out.write("</td>\n\t");
out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">"); out.write("<td align=\"center\" class=\"snarkTorrentAction " + rowClass + "\">");
String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.meta.getInfoHash()); String parameters = "&nonce=" + _nonce + "&torrent=" + Base64.encode(snark.getInfoHash());
String b64 = Base64.encode(snark.meta.getInfoHash()); String b64 = Base64.encode(snark.getInfoHash());
if (showPeers) if (showPeers)
parameters = parameters + "&p=1"; parameters = parameters + "&p=1";
if (isRunning) { if (isRunning) {
@@ -939,7 +942,6 @@ public class I2PSnarkServlet extends Default {
if (isDegraded) if (isDegraded)
out.write("</a>"); out.write("</a>");
} else { } else {
if (isValid) {
if (isDegraded) if (isDegraded)
out.write("<a href=\"/i2psnark/?action=Start_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\""); out.write("<a href=\"/i2psnark/?action=Start_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\"");
else else
@@ -950,12 +952,12 @@ public class I2PSnarkServlet extends Default {
out.write("\">"); out.write("\">");
if (isDegraded) if (isDegraded)
out.write("</a>"); out.write("</a>");
}
if (isValid) {
if (isDegraded) if (isDegraded)
out.write("<a href=\"/i2psnark/?action=Remove_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\""); out.write("<a href=\"/i2psnark/?action=Remove_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\"");
else else
out.write("<input type=\"image\" name=\"action_Remove_" + b64 + "\" value=\"foo\" title=\""); 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(_("Remove the torrent from the active list, deleting the .torrent file"));
out.write("\" onclick=\"if (!confirm('"); out.write("\" onclick=\"if (!confirm('");
// Can't figure out how to escape double quotes inside the onclick string. // Can't figure out how to escape double quotes inside the onclick string.
@@ -968,6 +970,7 @@ public class I2PSnarkServlet extends Default {
out.write("\">"); out.write("\">");
if (isDegraded) if (isDegraded)
out.write("</a>"); out.write("</a>");
}
if (isDegraded) if (isDegraded)
out.write("<a href=\"/i2psnark/?action=Delete_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\""); out.write("<a href=\"/i2psnark/?action=Delete_" + b64 + "&amp;nonce=" + _nonce + "\"><img title=\"");
@@ -989,7 +992,7 @@ public class I2PSnarkServlet extends Default {
out.write("</td>\n</tr>\n"); out.write("</td>\n</tr>\n");
if(showPeers && isRunning && curPeers > 0) { if(showPeers && isRunning && curPeers > 0) {
List<Peer> peers = snark.coordinator.peerList(); List<Peer> peers = snark.getPeerList();
if (!showDebug) if (!showDebug)
Collections.sort(peers, new PeerComparator()); Collections.sort(peers, new PeerComparator());
for (Peer peer : peers) { for (Peer peer : peers) {
@@ -1022,7 +1025,9 @@ public class I2PSnarkServlet extends Default {
out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">"); out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
out.write("</td>\n\t"); out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">"); out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
float pct = (float) (100.0 * (float) peer.completed() / snark.meta.getPieces()); float pct;
if (isValid) {
pct = (float) (100.0 * (float) peer.completed() / meta.getPieces());
if (pct == 100.0) if (pct == 100.0)
out.write(_("Seed")); out.write(_("Seed"));
else { else {
@@ -1031,6 +1036,11 @@ public class I2PSnarkServlet extends Default {
ps = ps.substring(0, 5); ps = ps.substring(0, 5);
out.write(ps + "%"); 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>\n\t");
out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">"); out.write("<td class=\"snarkTorrentStatus " + rowClass + "\">");
out.write("</td>\n\t"); out.write("</td>\n\t");
@@ -1051,7 +1061,7 @@ public class I2PSnarkServlet extends Default {
} }
out.write("</td>\n\t"); out.write("</td>\n\t");
out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">"); out.write("<td align=\"right\" class=\"snarkTorrentStatus " + rowClass + "\">");
if (pct != 100.0) { if (isValid && pct < 100.0) {
if (peer.isInterested() && !peer.isChoking()) { if (peer.isInterested() && !peer.isChoking()) {
out.write("<span class=\"unchoked\">"); out.write("<span class=\"unchoked\">");
out.write(formatSize(peer.getUploadRate()) + "ps</span>"); out.write(formatSize(peer.getUploadRate()) + "ps</span>");
@@ -1094,8 +1104,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&amp;filelist=1&amp;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 { private void writeAddForm(PrintWriter out, HttpServletRequest req) throws IOException {
String uri = req.getRequestURI();
String newURL = req.getParameter("newURL"); String newURL = req.getParameter("newURL");
if ( (newURL == null) || (newURL.trim().length() <= 0) ) if ( (newURL == null) || (newURL.trim().length() <= 0) )
newURL = ""; newURL = "";
@@ -1136,7 +1178,6 @@ public class I2PSnarkServlet extends Default {
} }
private void writeSeedForm(PrintWriter out, HttpServletRequest req) throws IOException { private void writeSeedForm(PrintWriter out, HttpServletRequest req) throws IOException {
String uri = req.getRequestURI();
String baseFile = req.getParameter("baseFile"); String baseFile = req.getParameter("baseFile");
if (baseFile == null || baseFile.trim().length() <= 0) if (baseFile == null || baseFile.trim().length() <= 0)
baseFile = ""; baseFile = "";
@@ -1167,6 +1208,10 @@ public class I2PSnarkServlet extends Default {
out.write(":<td><select name=\"announceURL\"><option value=\"\">"); out.write(":<td><select name=\"announceURL\"><option value=\"\">");
out.write(_("Select a tracker")); out.write(_("Select a tracker"));
out.write("</option>\n"); 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(); Map trackers = _manager.getTrackers();
for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) { for (Iterator iter = trackers.entrySet().iterator(); iter.hasNext(); ) {
Map.Entry entry = (Map.Entry)iter.next(); Map.Entry entry = (Map.Entry)iter.next();
@@ -1175,6 +1220,8 @@ public class I2PSnarkServlet extends Default {
int e = announceURL.indexOf('='); int e = announceURL.indexOf('=');
if (e > 0) if (e > 0)
announceURL = announceURL.substring(0, e); announceURL = announceURL.substring(0, e);
if (announceURL.equals(_lastAnnounceURL))
announceURL += "\" selected=\"selected";
out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n"); out.write("\t<option value=\"" + announceURL + "\">" + name + "</option>\n");
} }
out.write("</select>\n"); out.write("</select>\n");
@@ -1190,7 +1237,6 @@ public class I2PSnarkServlet extends Default {
} }
private void writeConfigForm(PrintWriter out, HttpServletRequest req) throws IOException { private void writeConfigForm(PrintWriter out, HttpServletRequest req) throws IOException {
String uri = req.getRequestURI();
String dataDir = _manager.getDataDir().getAbsolutePath(); String dataDir = _manager.getDataDir().getAbsolutePath();
boolean autoStart = _manager.shouldAutoStart(); boolean autoStart = _manager.shouldAutoStart();
boolean useOpenTrackers = _manager.util().shouldUseOpenTrackers(); boolean useOpenTrackers = _manager.util().shouldUseOpenTrackers();
@@ -1308,6 +1354,7 @@ public class I2PSnarkServlet extends Default {
out.write("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;"); out.write("&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;");
out.write(renderOptions(0, 4, options.remove("outbound.length"), "outbound.length", HOP)); out.write(renderOptions(0, 4, options.remove("outbound.length"), "outbound.length", HOP));
if (!_context.isRouterContext()) {
out.write("<tr><td>"); out.write("<tr><td>");
out.write(_("I2CP host")); out.write(_("I2CP host"));
out.write(": <td><input type=\"text\" name=\"i2cpHost\" value=\"" out.write(": <td><input type=\"text\" name=\"i2cpHost\" value=\""
@@ -1317,6 +1364,7 @@ public class I2PSnarkServlet extends Default {
out.write(_("I2CP port")); out.write(_("I2CP port"));
out.write(": <td><input type=\"text\" name=\"i2cpPort\" class=\"r\" value=\"" + out.write(": <td><input type=\"text\" name=\"i2cpPort\" class=\"r\" value=\"" +
+ _manager.util().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" > <br>\n"); + _manager.util().getI2CPPort() + "\" size=\"5\" maxlength=\"5\" > <br>\n");
}
StringBuilder opts = new StringBuilder(64); StringBuilder opts = new StringBuilder(64);
for (Iterator iter = options.entrySet().iterator(); iter.hasNext(); ) { for (Iterator iter = options.entrySet().iterator(); iter.hasNext(); ) {
@@ -1344,6 +1392,49 @@ public class I2PSnarkServlet extends Default {
out.write("</a></span></span></div>\n"); 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 */ /** copied from ConfigTunnelsHelper */
private static final String HOP = "hop"; private static final String HOP = "hop";
private static final String TUNNEL = "tunnel"; private static final String TUNNEL = "tunnel";
@@ -1384,6 +1475,11 @@ public class I2PSnarkServlet extends Default {
return _manager.util().getString(s, o); 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 */ /** translate (ngettext) @since 0.7.14 */
private String ngettext(String s, String p, int n) { private String ngettext(String s, String p, int n) {
return _manager.util().getString(n, s, p); return _manager.util().getString(n, s, p);
@@ -1459,13 +1555,11 @@ public class I2PSnarkServlet extends Default {
private String getListHTML(Resource r, String base, boolean parent, Map postParams) private String getListHTML(Resource r, String base, boolean parent, Map postParams)
throws IOException throws IOException
{ {
if (!r.isDirectory()) String[] ls = null;
return null; if (r.isDirectory()) {
ls = r.list();
String[] ls = r.list();
if (ls==null)
return null;
Arrays.sort(ls, Collator.getInstance()); Arrays.sort(ls, Collator.getInstance());
} // if r is not a directory, we are only showing torrent info section
StringBuilder buf=new StringBuilder(4096); StringBuilder buf=new StringBuilder(4096);
buf.append(DOCTYPE + "<HTML><HEAD><TITLE>"); buf.append(DOCTYPE + "<HTML><HEAD><TITLE>");
@@ -1487,6 +1581,7 @@ public class I2PSnarkServlet extends Default {
if (title.endsWith("/")) if (title.endsWith("/"))
title = title.substring(0, title.length() - 1); title = title.substring(0, title.length() - 1);
String directory = title;
title = _("Torrent") + ": " + title; title = _("Torrent") + ": " + title;
buf.append(title); buf.append(title);
buf.append("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" + buf.append("</TITLE>").append(HEADER_A).append(_themePath).append(HEADER_B).append("<link rel=\"shortcut icon\" href=\"" + _themePath + "favicon.ico\">" +
@@ -1495,13 +1590,68 @@ public class I2PSnarkServlet extends Default {
if (parent) // always true if (parent) // always true
buf.append("<div class=\"page\"><div class=\"mainsection\">"); 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) if (showPriority)
buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n"); buf.append("<form action=\"").append(base).append("\" method=\"POST\">\n");
buf.append("<TABLE BORDER=0 class=\"snarkTorrents\" >" + buf.append("<TABLE BORDER=0 class=\"snarkTorrents\" ><thead>");
"<thead><tr><th>") 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);
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\" >&nbsp;") .append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "file.png\" >&nbsp;")
.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\" >&nbsp;") .append("<img alt=\"\" border=\"0\" src=\"" + _imgPath + "size.png\" >&nbsp;")
.append(_("Size")); .append(_("Size"));
buf.append("</th><th class=\"headerstatus\">") buf.append("</th><th class=\"headerstatus\">")
@@ -1542,15 +1692,16 @@ public class I2PSnarkServlet extends Default {
complete = true; complete = true;
status = toImg("tick") + ' ' + _("Directory"); status = toImg("tick") + ' ' + _("Directory");
} else { } else {
if (snark == null) { if (snark == null || snark.getStorage() == null) {
// Assume complete, perhaps he removed a completed torrent but kept a bookmark // Assume complete, perhaps he removed a completed torrent but kept a bookmark
complete = true; complete = true;
status = toImg("cancel") + ' ' + _("Torrent not found?"); status = toImg("cancel") + ' ' + _("Torrent not found?");
} else { } else {
Storage storage = snark.getStorage();
try { try {
File f = item.getFile(); File f = item.getFile();
if (f != null) { if (f != null) {
long remaining = snark.storage.remaining(f.getCanonicalPath()); long remaining = storage.remaining(f.getCanonicalPath());
if (remaining < 0) { if (remaining < 0) {
complete = true; complete = true;
status = toImg("cancel") + ' ' + _("File not found in torrent?"); status = toImg("cancel") + ' ' + _("File not found in torrent?");
@@ -1558,7 +1709,7 @@ public class I2PSnarkServlet extends Default {
complete = true; complete = true;
status = toImg("tick") + ' ' + _("Complete"); status = toImg("tick") + ' ' + _("Complete");
} else { } else {
int priority = snark.storage.getPriority(f.getCanonicalPath()); int priority = storage.getPriority(f.getCanonicalPath());
if (priority < 0) if (priority < 0)
status = toImg("cancel"); status = toImg("cancel");
else if (priority == 0) else if (priority == 0)
@@ -1614,7 +1765,7 @@ public class I2PSnarkServlet extends Default {
buf.append("<td class=\"priority\">"); buf.append("<td class=\"priority\">");
File f = item.getFile(); File f = item.getFile();
if ((!complete) && (!item.isDirectory()) && f != null) { 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("\" "); buf.append("<input type=\"radio\" value=\"5\" name=\"pri.").append(f.getCanonicalPath()).append("\" ");
if (pri > 0) if (pri > 0)
buf.append("checked=\"true\""); buf.append("checked=\"true\"");
@@ -1716,6 +1867,9 @@ public class I2PSnarkServlet extends Default {
/** @since 0.8.1 */ /** @since 0.8.1 */
private void savePriorities(Snark snark, Map postParams) { private void savePriorities(Snark snark, Map postParams) {
Storage storage = snark.getStorage();
if (storage == null)
return;
Set<Map.Entry> entries = postParams.entrySet(); Set<Map.Entry> entries = postParams.entrySet();
for (Map.Entry entry : entries) { for (Map.Entry entry : entries) {
String key = (String)entry.getKey(); String key = (String)entry.getKey();
@@ -1724,14 +1878,13 @@ public class I2PSnarkServlet extends Default {
String file = key.substring(4); String file = key.substring(4);
String val = ((String[])entry.getValue())[0]; // jetty arrays String val = ((String[])entry.getValue())[0]; // jetty arrays
int pri = Integer.parseInt(val); int pri = Integer.parseInt(val);
snark.storage.setPriority(file, pri); storage.setPriority(file, pri);
//System.err.println("Priority now " + pri + " for " + file); //System.err.println("Priority now " + pri + " for " + file);
} catch (Throwable t) { t.printStackTrace(); } } catch (Throwable t) { t.printStackTrace(); }
} }
} }
if (snark.coordinator != null) snark.updatePiecePriorities();
snark.coordinator.updatePiecePriorities(); _manager.saveTorrentStatus(snark.getMetaInfo(), storage.getBitField(), storage.getFilePriorities());
_manager.saveTorrentStatus(snark.storage.getMetaInfo(), snark.storage.getBitField(), snark.storage.getFilePriorities());
} }
@@ -1753,15 +1906,17 @@ private static class FetchAndAdd implements Runnable {
FileInputStream in = null; FileInputStream in = null;
try { try {
in = new FileInputStream(file); in = new FileInputStream(file);
// we do not retain this MetaInfo object, hopefully it will go away quickly
MetaInfo info = new MetaInfo(in); 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(); String name = info.getName();
name = DataHelper.stripHTML(name); // XSS name = Storage.filterName(name);
name = name.replace('/', '_');
name = name.replace('\\', '_');
name = name.replace('&', '+');
name = name.replace('\'', '_');
name = name.replace('"', '_');
name = name.replace('`', '_');
name = name + ".torrent"; name = name + ".torrent";
File torrentFile = new File(_manager.getDataDir(), name); File torrentFile = new File(_manager.getDataDir(), name);
@@ -1773,18 +1928,15 @@ private static class FetchAndAdd implements Runnable {
else else
_manager.addMessage(_("Torrent already in the queue: {0}", name)); _manager.addMessage(_("Torrent already in the queue: {0}", name));
} else { } else {
boolean success = FileUtil.copy(file.getAbsolutePath(), canonical, false); // This may take a LONG time to create the storage.
if (success) { _manager.copyAndAddTorrent(file, canonical);
SecureFileOutputStream.setPerms(torrentFile);
_manager.addTorrent(canonical);
} else {
_manager.addMessage(_("Failed to copy torrent file to {0}", canonical));
}
} }
} catch (IOException ioe) { } catch (IOException ioe) {
_manager.addMessage(_("Torrent at {0} was not valid", urlify(_url)) + ": " + ioe.getMessage()); _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 { } finally {
try { in.close(); } catch (IOException ioe) {} try { if (in != null) in.close(); } catch (IOException ioe) {}
} }
} else { } else {
_manager.addMessage(_("Torrent was not retrieved from {0}", urlify(_url))); _manager.addMessage(_("Torrent was not retrieved from {0}", urlify(_url)));