forked from I2P_Developers/i2p.i2p
propagate from branch 'i2p.i2p' (head 27fc588723d201c76ea9c18a6c715b11efcb5b0e)
to branch 'i2p.i2p.zzz.dhtsnark' (head cae6d265415ba9ed4242b3fc888ffcf2a1c1b2f2)
This commit is contained in:
@ -28,6 +28,9 @@ abstract class ExtensionHandler {
|
||||
public static final int ID_PEX = 2;
|
||||
/** not ut_pex since the compact format is different */
|
||||
public static final String TYPE_PEX = "i2p_pex";
|
||||
public static final int ID_DHT = 3;
|
||||
/** not using the option bit since the compact format is different */
|
||||
public static final String TYPE_DHT = "i2p_dht";
|
||||
/** 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;
|
||||
@ -36,9 +39,10 @@ abstract class ExtensionHandler {
|
||||
/**
|
||||
* @param metasize -1 if unknown
|
||||
* @param pexAndMetadata advertise these capabilities
|
||||
* @param dht advertise DHT capability
|
||||
* @return bencoded outgoing handshake message
|
||||
*/
|
||||
public static byte[] getHandshake(int metasize, boolean pexAndMetadata) {
|
||||
public static byte[] getHandshake(int metasize, boolean pexAndMetadata, boolean dht) {
|
||||
Map<String, Object> handshake = new HashMap();
|
||||
Map<String, Integer> m = new HashMap();
|
||||
if (pexAndMetadata) {
|
||||
@ -47,6 +51,9 @@ abstract class ExtensionHandler {
|
||||
if (metasize >= 0)
|
||||
handshake.put("metadata_size", Integer.valueOf(metasize));
|
||||
}
|
||||
if (dht) {
|
||||
m.put(TYPE_DHT, Integer.valueOf(ID_DHT));
|
||||
}
|
||||
// include the map even if empty so the far-end doesn't NPE
|
||||
handshake.put("m", m);
|
||||
handshake.put("p", Integer.valueOf(6881));
|
||||
@ -65,6 +72,8 @@ abstract class ExtensionHandler {
|
||||
handleMetadata(peer, listener, bs, log);
|
||||
else if (id == ID_PEX)
|
||||
handlePEX(peer, listener, bs, log);
|
||||
else if (id == ID_DHT)
|
||||
handleDHT(peer, listener, bs, log);
|
||||
else if (log.shouldLog(Log.INFO))
|
||||
log.info("Unknown extension msg " + id + " from " + peer);
|
||||
}
|
||||
@ -87,6 +96,12 @@ abstract class ExtensionHandler {
|
||||
// peer state calls peer listener calls sendPEX()
|
||||
}
|
||||
|
||||
if (msgmap.get(TYPE_DHT) != null) {
|
||||
if (log.shouldLog(Log.DEBUG))
|
||||
log.debug("Peer supports DHT extension: " + peer);
|
||||
// peer state calls peer listener calls sendDHT()
|
||||
}
|
||||
|
||||
MagnetState state = peer.getMagnetState();
|
||||
|
||||
if (msgmap.get(TYPE_METADATA) == null) {
|
||||
@ -332,6 +347,28 @@ abstract class ExtensionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Receive the DHT port numbers
|
||||
* @since DHT
|
||||
*/
|
||||
private static void handleDHT(Peer peer, PeerListener listener, byte[] bs, Log log) {
|
||||
if (log.shouldLog(Log.DEBUG))
|
||||
log.debug("Got DHT 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 qport = map.get("port").getInt();
|
||||
int rport = map.get("rport").getInt();
|
||||
listener.gotPort(peer, qport, rport);
|
||||
} catch (Exception e) {
|
||||
if (log.shouldLog(Log.INFO))
|
||||
log.info("DHT msg exception from " + peer, e);
|
||||
//peer.disconnect(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* added.f and dropped unsupported
|
||||
* @param pList non-null
|
||||
@ -359,4 +396,22 @@ abstract class ExtensionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the DHT port numbers
|
||||
* @since DHT
|
||||
*/
|
||||
public static void sendDHT(Peer peer, int qport, int rport) {
|
||||
Map<String, Object> map = new HashMap();
|
||||
map.put("port", Integer.valueOf(qport));
|
||||
map.put("rport", Integer.valueOf(rport));
|
||||
byte[] payload = BEncoder.bencode(map);
|
||||
try {
|
||||
int hisMsgCode = peer.getHandshakeMap().get("m").getMap().get(TYPE_DHT).getInt();
|
||||
peer.sendExtension(hisMsgCode, payload);
|
||||
} catch (Exception e) {
|
||||
// NPE, no DHT caps
|
||||
//if (log.shouldLog(Log.INFO))
|
||||
// log.info("DHT msg exception to " + peer, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ import net.i2p.util.SimpleTimer;
|
||||
import net.i2p.util.Translate;
|
||||
|
||||
import org.klomp.snark.dht.DHT;
|
||||
//import org.klomp.snark.dht.KRPC;
|
||||
import org.klomp.snark.dht.KRPC;
|
||||
|
||||
/**
|
||||
* I2P specific helpers for I2PSnark
|
||||
@ -64,6 +64,7 @@ public class I2PSnarkUtil {
|
||||
private final File _tmpDir;
|
||||
private int _startupDelay;
|
||||
private boolean _shouldUseOT;
|
||||
private boolean _shouldUseDHT;
|
||||
private boolean _areFilesPublic;
|
||||
private List<String> _openTrackers;
|
||||
private DHT _dht;
|
||||
@ -76,7 +77,7 @@ public class I2PSnarkUtil {
|
||||
public static final int DEFAULT_MAX_UP_BW = 8; //KBps
|
||||
public static final int MAX_CONNECTIONS = 16; // per torrent
|
||||
public static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond";
|
||||
//private static final boolean ENABLE_DHT = true;
|
||||
public static final boolean DEFAULT_USE_DHT = false;
|
||||
|
||||
public I2PSnarkUtil(I2PAppContext ctx) {
|
||||
_context = ctx;
|
||||
@ -93,6 +94,7 @@ public class I2PSnarkUtil {
|
||||
_shouldUseOT = DEFAULT_USE_OPENTRACKERS;
|
||||
// FIXME split if default has more than one
|
||||
_openTrackers = Collections.singletonList(DEFAULT_OPENTRACKERS);
|
||||
_shouldUseDHT = DEFAULT_USE_DHT;
|
||||
// This is used for both announce replies and .torrent file downloads,
|
||||
// so it must be available even if not connected to I2CP.
|
||||
// so much for multiple instances
|
||||
@ -239,8 +241,8 @@ public class I2PSnarkUtil {
|
||||
_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());
|
||||
if (_shouldUseDHT && _manager != null && _dht == null)
|
||||
_dht = new KRPC(_context, _manager.getSession());
|
||||
return (_manager != null);
|
||||
}
|
||||
|
||||
@ -264,12 +266,17 @@ public class I2PSnarkUtil {
|
||||
/**
|
||||
* Destroy the destination itself
|
||||
*/
|
||||
public void disconnect() {
|
||||
public synchronized void disconnect() {
|
||||
if (_dht != null) {
|
||||
_dht.stop();
|
||||
_dht = null;
|
||||
}
|
||||
I2PSocketManager mgr = _manager;
|
||||
// FIXME this can cause race NPEs elsewhere
|
||||
_manager = null;
|
||||
_shitlist.clear();
|
||||
mgr.destroySocketManager();
|
||||
if (mgr != null)
|
||||
mgr.destroySocketManager();
|
||||
// this will delete a .torrent file d/l in progress so don't do that...
|
||||
FileUtil.rmdir(_tmpDir, false);
|
||||
// in case the user will d/l a .torrent file next...
|
||||
@ -434,7 +441,8 @@ public class I2PSnarkUtil {
|
||||
if (sess != null) {
|
||||
byte[] b = Base32.decode(ip.substring(0, BASE32_HASH_LENGTH));
|
||||
if (b != null) {
|
||||
Hash h = new Hash(b);
|
||||
//Hash h = new Hash(b);
|
||||
Hash h = Hash.create(b);
|
||||
if (_log.shouldLog(Log.INFO))
|
||||
_log.info("Using existing session for lookup of " + ip);
|
||||
try {
|
||||
@ -509,6 +517,22 @@ public class I2PSnarkUtil {
|
||||
public boolean shouldUseOpenTrackers() {
|
||||
return _shouldUseOT;
|
||||
}
|
||||
|
||||
/** @since DHT */
|
||||
public synchronized void setUseDHT(boolean yes) {
|
||||
_shouldUseDHT = yes;
|
||||
if (yes && _manager != null && _dht == null) {
|
||||
_dht = new KRPC(_context, _manager.getSession());
|
||||
} else if (!yes && _dht != null) {
|
||||
_dht.stop();
|
||||
_dht = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** @since DHT */
|
||||
public boolean shouldUseDHT() {
|
||||
return _shouldUseDHT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Like DataHelper.toHexString but ensures no loss of leading zero bytes
|
||||
|
@ -80,7 +80,9 @@ public class Peer implements Comparable
|
||||
static final long OPTION_FAST = 0x0000000000000004l;
|
||||
static final long OPTION_DHT = 0x0000000000000001l;
|
||||
/** we use a different bit since the compact format is different */
|
||||
/* no, let's use an extension message
|
||||
static final long OPTION_I2P_DHT = 0x0000000040000000l;
|
||||
*/
|
||||
static final long OPTION_AZMP = 0x1000000000000000l;
|
||||
private long options;
|
||||
|
||||
@ -269,15 +271,17 @@ public class Peer implements Comparable
|
||||
_log.debug("Peer supports extensions, sending reply message");
|
||||
int metasize = metainfo != null ? metainfo.getInfoBytes().length : -1;
|
||||
boolean pexAndMetadata = metainfo == null || !metainfo.isPrivate();
|
||||
out.sendExtension(0, ExtensionHandler.getHandshake(metasize, pexAndMetadata));
|
||||
boolean dht = util.getDHT() != null;
|
||||
out.sendExtension(0, ExtensionHandler.getHandshake(metasize, pexAndMetadata, dht));
|
||||
}
|
||||
|
||||
if ((options & OPTION_I2P_DHT) != 0 && util.getDHT() != null) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Peer supports DHT, sending PORT message");
|
||||
int port = util.getDHT().getPort();
|
||||
out.sendPort(port);
|
||||
}
|
||||
// Old DHT PORT message
|
||||
//if ((options & OPTION_I2P_DHT) != 0 && util.getDHT() != null) {
|
||||
// if (_log.shouldLog(Log.DEBUG))
|
||||
// _log.debug("Peer supports DHT, sending PORT message");
|
||||
// int port = util.getDHT().getPort();
|
||||
// out.sendPort(port);
|
||||
//}
|
||||
|
||||
// Send our bitmap
|
||||
if (bitfield != null)
|
||||
|
@ -1273,6 +1273,7 @@ class PeerCoordinator implements PeerListener
|
||||
}
|
||||
} else if (id == ExtensionHandler.ID_HANDSHAKE) {
|
||||
sendPeers(peer);
|
||||
sendDHT(peer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1301,6 +1302,26 @@ class PeerCoordinator implements PeerListener
|
||||
} catch (InvalidBEncodingException ibee) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a DHT message to the peer, if we both support DHT.
|
||||
* @since DHT
|
||||
*/
|
||||
void sendDHT(Peer peer) {
|
||||
DHT dht = _util.getDHT();
|
||||
if (dht == null)
|
||||
return;
|
||||
Map<String, BEValue> handshake = peer.getHandshakeMap();
|
||||
if (handshake == null)
|
||||
return;
|
||||
BEValue bev = handshake.get("m");
|
||||
if (bev == null)
|
||||
return;
|
||||
try {
|
||||
if (bev.getMap().get(ExtensionHandler.TYPE_DHT) != null)
|
||||
ExtensionHandler.sendDHT(peer, dht.getPort(), dht.getRPort());
|
||||
} catch (InvalidBEncodingException ibee) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the storage after transition out of magnet mode
|
||||
* Snark calls this after we call gotMetaInfo()
|
||||
@ -1318,11 +1339,13 @@ class PeerCoordinator implements PeerListener
|
||||
/**
|
||||
* PeerListener callback
|
||||
* Tell the DHT to ping it, this will get back the node info
|
||||
* @param rport must be port + 1
|
||||
* @since 0.8.4
|
||||
*/
|
||||
public void gotPort(Peer peer, int port) {
|
||||
public void gotPort(Peer peer, int port, int rport) {
|
||||
DHT dht = _util.getDHT();
|
||||
if (dht != null)
|
||||
if (dht != null &&
|
||||
port > 0 && port < 65535 && rport == port + 1)
|
||||
dht.ping(peer.getDestination(), port);
|
||||
}
|
||||
|
||||
|
@ -190,13 +190,14 @@ interface PeerListener
|
||||
void gotExtension(Peer peer, int id, byte[] bs);
|
||||
|
||||
/**
|
||||
* Called when a port message is received.
|
||||
* Called when a DHT port message is received.
|
||||
*
|
||||
* @param peer the Peer that got the message.
|
||||
* @param port the port
|
||||
* @param port the query port
|
||||
* @param port the response port
|
||||
* @since 0.8.4
|
||||
*/
|
||||
void gotPort(Peer peer, int port);
|
||||
void gotPort(Peer peer, int port, int qport);
|
||||
|
||||
/**
|
||||
* Called when peers are received via PEX
|
||||
|
@ -526,10 +526,14 @@ class PeerState implements DataLoader
|
||||
setInteresting(true);
|
||||
}
|
||||
|
||||
/** @since 0.8.4 */
|
||||
/**
|
||||
* Unused
|
||||
* @since 0.8.4
|
||||
*/
|
||||
void portMessage(int port)
|
||||
{
|
||||
listener.gotPort(peer, port);
|
||||
// for compatibility with old DHT PORT message
|
||||
listener.gotPort(peer, port, port + 1);
|
||||
}
|
||||
|
||||
void unknownMessage(int type, byte[] bs)
|
||||
|
@ -87,6 +87,7 @@ public class SnarkManager implements Snark.CompleteListener {
|
||||
private static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers";
|
||||
public static final String PROP_OPENTRACKERS = "i2psnark.opentrackers";
|
||||
public static final String PROP_PRIVATETRACKERS = "i2psnark.privatetrackers";
|
||||
private static final String PROP_USE_DHT = "i2psnark.enableDHT";
|
||||
|
||||
public static final int MIN_UP_BW = 2;
|
||||
public static final int DEFAULT_MAX_UP_BW = 10;
|
||||
@ -275,6 +276,8 @@ public class SnarkManager implements Snark.CompleteListener {
|
||||
_config.setProperty(PROP_STARTUP_DELAY, Integer.toString(DEFAULT_STARTUP_DELAY));
|
||||
if (!_config.containsKey(PROP_THEME))
|
||||
_config.setProperty(PROP_THEME, DEFAULT_THEME);
|
||||
if (!_config.containsKey(PROP_USE_DHT))
|
||||
_config.setProperty(PROP_USE_DHT, Boolean.toString(I2PSnarkUtil.DEFAULT_USE_DHT));
|
||||
updateConfig();
|
||||
}
|
||||
/**
|
||||
@ -349,6 +352,7 @@ public class SnarkManager implements Snark.CompleteListener {
|
||||
String useOT = _config.getProperty(PROP_USE_OPENTRACKERS);
|
||||
boolean bOT = useOT == null || Boolean.valueOf(useOT).booleanValue();
|
||||
_util.setUseOpenTrackers(bOT);
|
||||
_util.setUseDHT(Boolean.valueOf(_config.getProperty(PROP_USE_DHT)).booleanValue());
|
||||
getDataDir().mkdirs();
|
||||
initTrackerMap();
|
||||
}
|
||||
@ -367,7 +371,7 @@ public class SnarkManager implements Snark.CompleteListener {
|
||||
public void updateConfig(String dataDir, boolean filesPublic, boolean autoStart, String refreshDelay,
|
||||
String startDelay, String seedPct, String eepHost,
|
||||
String eepPort, String i2cpHost, String i2cpPort, String i2cpOpts,
|
||||
String upLimit, String upBW, boolean useOpenTrackers, String theme) {
|
||||
String upLimit, String upBW, boolean useOpenTrackers, boolean useDHT, String theme) {
|
||||
boolean changed = false;
|
||||
//if (eepHost != null) {
|
||||
// // unused, we use socket eepget
|
||||
@ -551,6 +555,15 @@ public class SnarkManager implements Snark.CompleteListener {
|
||||
_util.setUseOpenTrackers(useOpenTrackers);
|
||||
changed = true;
|
||||
}
|
||||
if (_util.shouldUseDHT() != useDHT) {
|
||||
_config.setProperty(PROP_USE_DHT, Boolean.toString(useDHT));
|
||||
if (useDHT)
|
||||
addMessage(_("Enabled DHT."));
|
||||
else
|
||||
addMessage(_("Disabled DHT."));
|
||||
_util.setUseDHT(useDHT);
|
||||
changed = true;
|
||||
}
|
||||
if (theme != null) {
|
||||
if(!theme.equals(_config.getProperty(PROP_THEME))) {
|
||||
_config.setProperty(PROP_THEME, theme);
|
||||
@ -1590,13 +1603,14 @@ public class SnarkManager implements Snark.CompleteListener {
|
||||
}
|
||||
}
|
||||
|
||||
public class SnarkManagerShutdown extends I2PAppThread {
|
||||
private class SnarkManagerShutdown extends I2PAppThread {
|
||||
@Override
|
||||
public void run() {
|
||||
Set names = listTorrentFiles();
|
||||
int running = 0;
|
||||
for (Iterator iter = names.iterator(); iter.hasNext(); ) {
|
||||
Snark snark = getTorrent((String)iter.next());
|
||||
if ( (snark != null) && (!snark.isStopped()) ) {
|
||||
if (snark != null && !snark.isStopped()) {
|
||||
snark.stopTorrent();
|
||||
try { Thread.sleep(50); } catch (InterruptedException ie) {}
|
||||
}
|
||||
|
@ -454,7 +454,8 @@ public class TrackerClient implements Runnable {
|
||||
// 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);
|
||||
// announce only to the 1 closest
|
||||
int good = _util.getDHT().announce(snark.getInfoHash(), 1, 5*60*1000);
|
||||
_util.debug("Sent " + good + " good announces to DHT", Snark.INFO);
|
||||
}
|
||||
|
||||
|
@ -17,10 +17,15 @@ public interface DHT {
|
||||
|
||||
|
||||
/**
|
||||
* @return The UDP port that should be included in a PORT message.
|
||||
* @return The UDP query port
|
||||
*/
|
||||
public int getPort();
|
||||
|
||||
/**
|
||||
* @return The UDP response port
|
||||
*/
|
||||
public int getRPort();
|
||||
|
||||
/**
|
||||
* Ping. We don't have a NID yet so the node is presumed
|
||||
* to be absent from our DHT.
|
||||
@ -79,4 +84,9 @@ public interface DHT {
|
||||
* @return the number of successful announces, not counting ourselves.
|
||||
*/
|
||||
public int announce(byte[] ih, int max, long maxWait);
|
||||
|
||||
/**
|
||||
* Stop everything.
|
||||
*/
|
||||
public void stop();
|
||||
}
|
||||
|
126
apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java
Normal file
126
apps/i2psnark/java/src/org/klomp/snark/dht/DHTNodes.java
Normal file
@ -0,0 +1,126 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, modded and relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.crypto.SHA1Hash;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleTimer2;
|
||||
|
||||
/**
|
||||
* All the nodes we know about, stored as a mapping from
|
||||
* node ID to a Destination and Port.
|
||||
* Also uses the keySet as a subsitute for kbuckets.
|
||||
*
|
||||
* Swap this out for a real DHT later.
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class DHTNodes extends ConcurrentHashMap<NID, NodeInfo> {
|
||||
|
||||
private final I2PAppContext _context;
|
||||
private long _expireTime;
|
||||
private final Log _log;
|
||||
private volatile boolean _isRunning;
|
||||
|
||||
/** stagger with other cleaners */
|
||||
private static final long CLEAN_TIME = 237*1000;
|
||||
private static final long MAX_EXPIRE_TIME = 60*60*1000;
|
||||
private static final long MIN_EXPIRE_TIME = 5*60*1000;
|
||||
private static final long DELTA_EXPIRE_TIME = 7*60*1000;
|
||||
private static final int MAX_PEERS = 999;
|
||||
|
||||
public DHTNodes(I2PAppContext ctx) {
|
||||
super();
|
||||
_context = ctx;
|
||||
_expireTime = MAX_EXPIRE_TIME;
|
||||
_log = _context.logManager().getLog(DHTNodes.class);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
_isRunning = true;
|
||||
new Cleaner();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
clear();
|
||||
_isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake DHT
|
||||
* @param sha1 either a InfoHash or a NID
|
||||
*/
|
||||
List<NodeInfo> findClosest(SHA1Hash h, int numWant) {
|
||||
// sort the whole thing
|
||||
Set<NID> all = new TreeSet(new SHA1Comparator(h));
|
||||
all.addAll(keySet());
|
||||
int sz = all.size();
|
||||
int max = Math.min(numWant, sz);
|
||||
|
||||
// return the first ones
|
||||
List<NodeInfo> rv = new ArrayList(max);
|
||||
int count = 0;
|
||||
for (NID nid : all) {
|
||||
if (count++ >= max)
|
||||
break;
|
||||
NodeInfo nInfo = get(nid);
|
||||
if (nInfo == null)
|
||||
continue;
|
||||
rv.add(nInfo);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
/**** used CHM methods to be replaced:
|
||||
public Collection<NodeInfo> values() {}
|
||||
public NodeInfo get(NID nID) {}
|
||||
public NodeInfo putIfAbssent(NID nID, NodeInfo nInfo) {}
|
||||
public int size() {}
|
||||
****/
|
||||
|
||||
/** */
|
||||
private class Cleaner extends SimpleTimer2.TimedEvent {
|
||||
|
||||
public Cleaner() {
|
||||
super(SimpleTimer2.getInstance(), CLEAN_TIME);
|
||||
}
|
||||
|
||||
public void timeReached() {
|
||||
if (!_isRunning)
|
||||
return;
|
||||
long now = _context.clock().now();
|
||||
int peerCount = 0;
|
||||
for (Iterator<NodeInfo> iter = DHTNodes.this.values().iterator(); iter.hasNext(); ) {
|
||||
NodeInfo peer = iter.next();
|
||||
if (peer.lastSeen() < now - _expireTime)
|
||||
iter.remove();
|
||||
else
|
||||
peerCount++;
|
||||
}
|
||||
|
||||
if (peerCount > MAX_PEERS)
|
||||
_expireTime = Math.max(_expireTime - DELTA_EXPIRE_TIME, MIN_EXPIRE_TIME);
|
||||
else
|
||||
_expireTime = Math.min(_expireTime + DELTA_EXPIRE_TIME, MAX_EXPIRE_TIME);
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("DHT storage cleaner done, now with " +
|
||||
peerCount + " peers, " +
|
||||
DataHelper.formatDuration(_expireTime) + " expiration");
|
||||
|
||||
schedule(CLEAN_TIME);
|
||||
}
|
||||
}
|
||||
}
|
143
apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java
Normal file
143
apps/i2psnark/java/src/org/klomp/snark/dht/DHTTracker.java
Normal file
@ -0,0 +1,143 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Hash;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SimpleTimer2;
|
||||
|
||||
/**
|
||||
* The tracker stores peers, i.e. Dest hashes (not nodes).
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class DHTTracker {
|
||||
|
||||
private final I2PAppContext _context;
|
||||
private final Torrents _torrents;
|
||||
private long _expireTime;
|
||||
private final Log _log;
|
||||
private volatile boolean _isRunning;
|
||||
|
||||
/** stagger with other cleaners */
|
||||
private static final long CLEAN_TIME = 199*1000;
|
||||
/** make this longer than postman's tracker */
|
||||
private static final long MAX_EXPIRE_TIME = 95*60*1000;
|
||||
private static final long MIN_EXPIRE_TIME = 5*60*1000;
|
||||
private static final long DELTA_EXPIRE_TIME = 7*60*1000;
|
||||
private static final int MAX_PEERS = 2000;
|
||||
|
||||
DHTTracker(I2PAppContext ctx) {
|
||||
_context = ctx;
|
||||
_torrents = new Torrents();
|
||||
_expireTime = MAX_EXPIRE_TIME;
|
||||
_log = _context.logManager().getLog(DHTTracker.class);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
_isRunning = true;
|
||||
new Cleaner();
|
||||
}
|
||||
|
||||
void stop() {
|
||||
_torrents.clear();
|
||||
_isRunning = false;
|
||||
}
|
||||
|
||||
void announce(InfoHash ih, Hash hash) {
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("Announce " + hash + " for " + ih);
|
||||
Peers peers = _torrents.get(ih);
|
||||
if (peers == null) {
|
||||
peers = new Peers();
|
||||
Peers peers2 = _torrents.putIfAbsent(ih, peers);
|
||||
if (peers2 != null)
|
||||
peers = peers2;
|
||||
}
|
||||
|
||||
Peer peer = new Peer(hash.getData());
|
||||
Peer peer2 = peers.putIfAbsent(peer, peer);
|
||||
if (peer2 != null)
|
||||
peer = peer2;
|
||||
peer.setLastSeen(_context.clock().now());
|
||||
}
|
||||
|
||||
void unannounce(InfoHash ih, Hash hash) {
|
||||
Peers peers = _torrents.get(ih);
|
||||
if (peers == null)
|
||||
return;
|
||||
Peer peer = new Peer(hash.getData());
|
||||
peers.remove(peer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Caller's responsibility to remove himself from the list
|
||||
* @return list or empty list (never null)
|
||||
*/
|
||||
List<Hash> getPeers(InfoHash ih, int max) {
|
||||
Peers peers = _torrents.get(ih);
|
||||
if (peers == null)
|
||||
return Collections.EMPTY_LIST;
|
||||
|
||||
int size = peers.size();
|
||||
List<Hash> rv = new ArrayList(peers.values());
|
||||
if (max < size) {
|
||||
Collections.shuffle(rv, _context.random());
|
||||
rv = rv.subList(0, max);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
private class Cleaner extends SimpleTimer2.TimedEvent {
|
||||
|
||||
public Cleaner() {
|
||||
super(SimpleTimer2.getInstance(), CLEAN_TIME);
|
||||
}
|
||||
|
||||
public void timeReached() {
|
||||
if (!_isRunning)
|
||||
return;
|
||||
long now = _context.clock().now();
|
||||
int torrentCount = 0;
|
||||
int peerCount = 0;
|
||||
for (Iterator<Peers> iter = _torrents.values().iterator(); iter.hasNext(); ) {
|
||||
Peers p = iter.next();
|
||||
int recent = 0;
|
||||
for (Iterator<Peer> iterp = p.values().iterator(); iterp.hasNext(); ) {
|
||||
Peer peer = iterp.next();
|
||||
if (peer.lastSeen() < now - _expireTime)
|
||||
iterp.remove();
|
||||
else {
|
||||
recent++;
|
||||
peerCount++;
|
||||
}
|
||||
}
|
||||
if (recent <= 0)
|
||||
iter.remove();
|
||||
else
|
||||
torrentCount++;
|
||||
}
|
||||
|
||||
if (peerCount > MAX_PEERS)
|
||||
_expireTime = Math.max(_expireTime - DELTA_EXPIRE_TIME, MIN_EXPIRE_TIME);
|
||||
else
|
||||
_expireTime = Math.min(_expireTime + DELTA_EXPIRE_TIME, MAX_EXPIRE_TIME);
|
||||
|
||||
if (_log.shouldLog(Log.DEBUG))
|
||||
_log.debug("DHT tracker cleaner done, now with " +
|
||||
torrentCount + " torrents, " +
|
||||
peerCount + " peers, " +
|
||||
DataHelper.formatDuration(_expireTime) + " expiration");
|
||||
schedule(CLEAN_TIME);
|
||||
}
|
||||
}
|
||||
}
|
19
apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java
Normal file
19
apps/i2psnark/java/src/org/klomp/snark/dht/InfoHash.java
Normal file
@ -0,0 +1,19 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, modded and relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import net.i2p.crypto.SHA1Hash;
|
||||
|
||||
/**
|
||||
* A 20-byte SHA1 info hash
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class InfoHash extends SHA1Hash {
|
||||
|
||||
public InfoHash(byte[] data) {
|
||||
super(data);
|
||||
}
|
||||
}
|
1421
apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java
Normal file
1421
apps/i2psnark/java/src/org/klomp/snark/dht/KRPC.java
Normal file
File diff suppressed because it is too large
Load Diff
32
apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java
Normal file
32
apps/i2psnark/java/src/org/klomp/snark/dht/MsgID.java
Normal file
@ -0,0 +1,32 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* GPLv2
|
||||
*/
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.ByteArray;
|
||||
|
||||
/**
|
||||
* Used for both incoming and outgoing message IDs
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class MsgID extends ByteArray {
|
||||
|
||||
private static final int MY_TOK_LEN = 8;
|
||||
|
||||
/** outgoing - generate a random ID */
|
||||
public MsgID(I2PAppContext ctx) {
|
||||
super(null);
|
||||
byte[] data = new byte[MY_TOK_LEN];
|
||||
ctx.random().nextBytes(data);
|
||||
setData(data);
|
||||
setValid(MY_TOK_LEN);
|
||||
}
|
||||
|
||||
/** incoming - save the ID (arbitrary length) */
|
||||
public MsgID(byte[] data) {
|
||||
super(data);
|
||||
}
|
||||
}
|
19
apps/i2psnark/java/src/org/klomp/snark/dht/NID.java
Normal file
19
apps/i2psnark/java/src/org/klomp/snark/dht/NID.java
Normal file
@ -0,0 +1,19 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, modded and relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import net.i2p.crypto.SHA1Hash;
|
||||
|
||||
/**
|
||||
* A 20-byte peer ID, used as a Map key in lots of places.
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class NID extends SHA1Hash {
|
||||
|
||||
public NID(byte[] data) {
|
||||
super(data);
|
||||
}
|
||||
}
|
231
apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java
Normal file
231
apps/i2psnark/java/src/org/klomp/snark/dht/NodeInfo.java
Normal file
@ -0,0 +1,231 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, modded and relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.data.DataHelper;
|
||||
import net.i2p.data.Destination;
|
||||
import net.i2p.data.Hash;
|
||||
import net.i2p.data.SimpleDataStructure;
|
||||
|
||||
/*
|
||||
* A Node ID, Hash, and port, and an optional Destination.
|
||||
* This is what DHTNodes remembers. The DHT tracker just stores Hashes.
|
||||
* getData() returns the 54 byte compact info (NID, Hash, port).
|
||||
*
|
||||
* Things are a little tricky in KRPC since we exchange Hashes and don't
|
||||
* always have the Destination.
|
||||
* The conpact info is immutable. The Destination may be added later.
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
|
||||
class NodeInfo extends SimpleDataStructure {
|
||||
|
||||
private long lastSeen;
|
||||
private NID nID;
|
||||
private Hash hash;
|
||||
private Destination dest;
|
||||
private int port;
|
||||
|
||||
public static final int LENGTH = NID.HASH_LENGTH + Hash.HASH_LENGTH + 2;
|
||||
|
||||
/**
|
||||
* Use this if we have the full destination
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public NodeInfo(NID nID, Destination dest, int port) {
|
||||
super();
|
||||
this.nID = nID;
|
||||
this.dest = dest;
|
||||
this.hash = dest.calculateHash();
|
||||
this.port = port;
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* No Destination yet available
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public NodeInfo(NID nID, Hash hash, int port) {
|
||||
super();
|
||||
this.nID = nID;
|
||||
this.hash = hash;
|
||||
this.port = port;
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* No Destination yet available
|
||||
* @param compactInfo 20 byte node ID, 32 byte destHash, 2 byte port
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public NodeInfo(byte[] compactInfo) {
|
||||
super(compactInfo);
|
||||
initialize(compactInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* No Destination yet available
|
||||
* @param compactInfo 20 byte node ID, 32 byte destHash, 2 byte port
|
||||
* @param offset starting at this offset in compactInfo
|
||||
* @throws IllegalArgumentException
|
||||
* @throws AIOOBE
|
||||
*/
|
||||
public NodeInfo(byte[] compactInfo, int offset) {
|
||||
super();
|
||||
byte[] d = new byte[LENGTH];
|
||||
System.arraycopy(compactInfo, offset, d, 0, LENGTH);
|
||||
setData(d);
|
||||
initialize(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Form persistent storage string.
|
||||
* Format: NID:Hash:Destination:port
|
||||
* First 3 in base 64; Destination may be empty string
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
public NodeInfo(String s) throws DataFormatException {
|
||||
super();
|
||||
String[] parts = s.split(":", 4);
|
||||
if (parts.length != 4)
|
||||
throw new DataFormatException("Bad format");
|
||||
byte[] nid = Base64.decode(parts[0]);
|
||||
if (nid == null)
|
||||
throw new DataFormatException("Bad NID");
|
||||
nID = new NID(nid);
|
||||
byte[] h = Base64.decode(parts[1]);
|
||||
if (h == null)
|
||||
throw new DataFormatException("Bad hash");
|
||||
//hash = new Hash(h);
|
||||
hash = Hash.create(h);
|
||||
if (parts[2].length() > 0)
|
||||
dest = new Destination(parts[2]);
|
||||
try {
|
||||
port = Integer.parseInt(parts[3]);
|
||||
} catch (NumberFormatException nfe) {
|
||||
throw new DataFormatException("Bad port", nfe);
|
||||
}
|
||||
initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates data structures from the compact info
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
private void initialize(byte[] compactInfo) {
|
||||
if (compactInfo.length != LENGTH)
|
||||
throw new IllegalArgumentException("Bad compact info length");
|
||||
byte[] ndata = new byte[NID.HASH_LENGTH];
|
||||
System.arraycopy(compactInfo, 0, ndata, 0, NID.HASH_LENGTH);
|
||||
this.nID = new NID(ndata);
|
||||
//byte[] hdata = new byte[Hash.HASH_LENGTH];
|
||||
//System.arraycopy(compactInfo, NID.HASH_LENGTH, hdata, 0, Hash.HASH_LENGTH);
|
||||
//this.hash = new Hash(hdata);
|
||||
this.hash = Hash.create(compactInfo, NID.HASH_LENGTH);
|
||||
this.port = (int) DataHelper.fromLong(compactInfo, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2);
|
||||
if (port <= 0 || port >= 65535)
|
||||
throw new IllegalArgumentException("Bad port");
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates 54-byte compact info
|
||||
* @throws IllegalArgumentException
|
||||
*/
|
||||
private void initialize() {
|
||||
if (port <= 0 || port >= 65535)
|
||||
throw new IllegalArgumentException("Bad port");
|
||||
byte[] compactInfo = new byte[LENGTH];
|
||||
System.arraycopy(nID.getData(), 0, compactInfo, 0, NID.HASH_LENGTH);
|
||||
System.arraycopy(hash.getData(), 0, compactInfo, NID.HASH_LENGTH, Hash.HASH_LENGTH);
|
||||
DataHelper.toLong(compactInfo, NID.HASH_LENGTH + Hash.HASH_LENGTH, 2, port);
|
||||
setData(compactInfo);
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return LENGTH;
|
||||
}
|
||||
|
||||
public NID getNID() {
|
||||
return this.nID;
|
||||
}
|
||||
|
||||
/** @return may be null if we don't have it */
|
||||
public Destination getDestination() {
|
||||
return this.dest;
|
||||
}
|
||||
|
||||
public Hash getHash() {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Hash calculateHash() {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* This can come in later but the hash must match.
|
||||
* @throws IllegalArgumentException if hash of dest doesn't match previous hash
|
||||
*/
|
||||
public void setDestination(Destination dest) throws IllegalArgumentException {
|
||||
if (this.dest != null)
|
||||
return;
|
||||
if (!dest.calculateHash().equals(this.hash))
|
||||
throw new IllegalArgumentException("Hash mismatch, was: " + this.hash + " new: " + dest.calculateHash());
|
||||
this.dest = dest;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return this.port;
|
||||
}
|
||||
|
||||
public long lastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public void setLastSeen(long now) {
|
||||
lastSeen = now;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode() ^ nID.hashCode() ^ port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
try {
|
||||
NodeInfo ni = (NodeInfo) o;
|
||||
// assume dest matches, ignore it
|
||||
return this.hash.equals(ni.hash) && nID.equals(ni.nID) && port == ni.port;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "NodeInfo: " + nID + ' ' + hash + " port: " + port;
|
||||
}
|
||||
|
||||
/**
|
||||
* To persistent storage string.
|
||||
* Format: NID:Hash:Destination:port
|
||||
* First 3 in base 64; Destination may be empty string
|
||||
*/
|
||||
public String toPersistentString() {
|
||||
StringBuilder buf = new StringBuilder(650);
|
||||
buf.append(nID.toBase64()).append(':');
|
||||
buf.append(hash.toBase64()).append(':');
|
||||
if (dest != null)
|
||||
buf.append(dest.toBase64());
|
||||
buf.append(':').append(port);
|
||||
return buf.toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, modded and relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import net.i2p.crypto.SHA1Hash;
|
||||
import net.i2p.data.DataHelper;
|
||||
|
||||
/**
|
||||
* Closest to a InfoHash or NID key.
|
||||
* Use for NodeInfos.
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class NodeInfoComparator implements Comparator<NodeInfo> {
|
||||
private final SHA1Hash _base;
|
||||
|
||||
public NodeInfoComparator(SHA1Hash h) {
|
||||
_base = h;
|
||||
}
|
||||
|
||||
public int compare(NodeInfo lhs, NodeInfo rhs) {
|
||||
byte lhsDelta[] = DataHelper.xor(lhs.getNID().getData(), _base.getData());
|
||||
byte rhsDelta[] = DataHelper.xor(rhs.getNID().getData(), _base.getData());
|
||||
return DataHelper.compareTo(lhsDelta, rhsDelta);
|
||||
}
|
||||
|
||||
}
|
30
apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java
Normal file
30
apps/i2psnark/java/src/org/klomp/snark/dht/Peer.java
Normal file
@ -0,0 +1,30 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, modded and relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import net.i2p.data.Hash;
|
||||
|
||||
/**
|
||||
* A single peer for a single torrent.
|
||||
* This is what the DHT tracker remembers.
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class Peer extends Hash {
|
||||
|
||||
private long lastSeen;
|
||||
|
||||
public Peer(byte[] data) {
|
||||
super(data);
|
||||
}
|
||||
|
||||
public long lastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
public void setLastSeen(long now) {
|
||||
lastSeen = now;
|
||||
}
|
||||
}
|
21
apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java
Normal file
21
apps/i2psnark/java/src/org/klomp/snark/dht/Peers.java
Normal file
@ -0,0 +1,21 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, modded and relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import net.i2p.data.Hash;
|
||||
|
||||
/**
|
||||
* All the peers for a single torrent
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class Peers extends ConcurrentHashMap<Hash, Peer> {
|
||||
|
||||
public Peers() {
|
||||
super();
|
||||
}
|
||||
}
|
77
apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java
Normal file
77
apps/i2psnark/java/src/org/klomp/snark/dht/PersistDHT.java
Normal file
@ -0,0 +1,77 @@
|
||||
package org.klomp.snark.dht;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.DataFormatException;
|
||||
import net.i2p.util.Log;
|
||||
import net.i2p.util.SecureFileOutputStream;
|
||||
|
||||
/**
|
||||
* Retrieve / Store the local DHT in a file
|
||||
*
|
||||
*/
|
||||
abstract class PersistDHT {
|
||||
|
||||
public static synchronized void loadDHT(KRPC krpc, File file) {
|
||||
Log log = I2PAppContext.getGlobalContext().logManager().getLog(PersistDHT.class);
|
||||
int count = 0;
|
||||
FileInputStream in = null;
|
||||
try {
|
||||
in = new FileInputStream(file);
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(in, "ISO-8859-1"));
|
||||
String line = null;
|
||||
while ( (line = br.readLine()) != null) {
|
||||
if (line.startsWith("#"))
|
||||
continue;
|
||||
try {
|
||||
krpc.addNode(new NodeInfo(line));
|
||||
count++;
|
||||
// TODO limit number? this will flush the router's SDS caches
|
||||
} catch (IllegalArgumentException iae) {
|
||||
if (log.shouldLog(Log.WARN))
|
||||
log.warn("Error reading DHT entry", iae);
|
||||
} catch (DataFormatException dfe) {
|
||||
if (log.shouldLog(Log.WARN))
|
||||
log.warn("Error reading DHT entry", dfe);
|
||||
}
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
if (log.shouldLog(Log.WARN) && file.exists())
|
||||
log.warn("Error reading the DHT File", ioe);
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
if (log.shouldLog(Log.INFO))
|
||||
log.info("Loaded " + count + " nodes from " + file);
|
||||
}
|
||||
|
||||
public static synchronized void saveDHT(DHTNodes nodes, File file) {
|
||||
Log log = I2PAppContext.getGlobalContext().logManager().getLog(PersistDHT.class);
|
||||
int count = 0;
|
||||
PrintWriter out = null;
|
||||
try {
|
||||
out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(file), "ISO-8859-1")));
|
||||
out.println("# DHT nodes, format is NID:Hash:Destination:port");
|
||||
for (NodeInfo ni : nodes.values()) {
|
||||
// DHTNodes shouldn't contain us, if that changes check here
|
||||
out.println(ni.toPersistentString());
|
||||
count++;
|
||||
}
|
||||
} catch (IOException ioe) {
|
||||
if (log.shouldLog(Log.WARN))
|
||||
log.warn("Error writing the DHT File", ioe);
|
||||
} finally {
|
||||
if (out != null) out.close();
|
||||
}
|
||||
if (log.shouldLog(Log.INFO))
|
||||
log.info("Stored " + count + " nodes to " + file);
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, modded and relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import net.i2p.crypto.SHA1Hash;
|
||||
import net.i2p.data.DataHelper;
|
||||
|
||||
/**
|
||||
* Closest to a InfoHash or NID key.
|
||||
* Use for InfoHashes and NIDs.
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class SHA1Comparator implements Comparator<SHA1Hash> {
|
||||
private final byte[] _base;
|
||||
|
||||
public SHA1Comparator(SHA1Hash h) {
|
||||
_base = h.getData();
|
||||
}
|
||||
|
||||
public int compare(SHA1Hash lhs, SHA1Hash rhs) {
|
||||
byte lhsDelta[] = DataHelper.xor(lhs.getData(), _base);
|
||||
byte rhsDelta[] = DataHelper.xor(rhs.getData(), _base);
|
||||
return DataHelper.compareTo(lhsDelta, rhsDelta);
|
||||
}
|
||||
|
||||
}
|
71
apps/i2psnark/java/src/org/klomp/snark/dht/Token.java
Normal file
71
apps/i2psnark/java/src/org/klomp/snark/dht/Token.java
Normal file
@ -0,0 +1,71 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* GPLv2
|
||||
*/
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.data.ByteArray;
|
||||
import net.i2p.data.DataHelper;
|
||||
|
||||
/**
|
||||
* Used for Both outgoing and incoming tokens
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class Token extends ByteArray {
|
||||
|
||||
private static final int MY_TOK_LEN = 8;
|
||||
private final long lastSeen;
|
||||
|
||||
/** outgoing - generate a random token */
|
||||
public Token(I2PAppContext ctx) {
|
||||
super(null);
|
||||
byte[] data = new byte[MY_TOK_LEN];
|
||||
ctx.random().nextBytes(data);
|
||||
setData(data);
|
||||
setValid(MY_TOK_LEN);
|
||||
lastSeen = ctx.clock().now();
|
||||
}
|
||||
|
||||
/** incoming - save the token (arbitrary length) */
|
||||
public Token(I2PAppContext ctx, byte[] data) {
|
||||
super(data);
|
||||
lastSeen = ctx.clock().now();
|
||||
}
|
||||
|
||||
/** incoming - for lookup only, not storage, lastSeen is 0 */
|
||||
public Token(byte[] data) {
|
||||
super(data);
|
||||
lastSeen = 0;
|
||||
}
|
||||
|
||||
public long lastSeen() {
|
||||
return lastSeen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder buf = new StringBuilder(64);
|
||||
buf.append("[Token: ");
|
||||
byte[] bs = getData();
|
||||
if (bs.length == 0) {
|
||||
buf.append("0 bytes");
|
||||
} else {
|
||||
buf.append(bs.length).append(" bytes: 0x");
|
||||
// backwards, but the same way BEValue does it
|
||||
for (int i = 0; i < bs.length; i++) {
|
||||
int b = bs[i] & 0xff;
|
||||
if (b < 16)
|
||||
buf.append('0');
|
||||
buf.append(Integer.toHexString(b));
|
||||
}
|
||||
}
|
||||
if (lastSeen > 0)
|
||||
buf.append(" created ").append((new Date(lastSeen)).toString());
|
||||
buf.append(']');
|
||||
return buf.toString();
|
||||
}
|
||||
}
|
20
apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java
Normal file
20
apps/i2psnark/java/src/org/klomp/snark/dht/TokenKey.java
Normal file
@ -0,0 +1,20 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* GPLv2
|
||||
*/
|
||||
|
||||
import net.i2p.crypto.SHA1Hash;
|
||||
import net.i2p.data.DataHelper;
|
||||
|
||||
/**
|
||||
* Used to index incoming Tokens
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class TokenKey extends SHA1Hash {
|
||||
|
||||
public TokenKey(NID nID, InfoHash ih) {
|
||||
super(DataHelper.xor(nID.getData(), ih.getData()));
|
||||
}
|
||||
}
|
19
apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java
Normal file
19
apps/i2psnark/java/src/org/klomp/snark/dht/Torrents.java
Normal file
@ -0,0 +1,19 @@
|
||||
package org.klomp.snark.dht;
|
||||
/*
|
||||
* From zzzot, relicensed to GPLv2
|
||||
*/
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* All the torrents
|
||||
*
|
||||
* @since 0.8.4
|
||||
* @author zzz
|
||||
*/
|
||||
class Torrents extends ConcurrentHashMap<InfoHash, Peers> {
|
||||
|
||||
public Torrents() {
|
||||
super();
|
||||
}
|
||||
}
|
@ -690,11 +690,12 @@ public class I2PSnarkServlet extends DefaultServlet {
|
||||
String refreshDel = req.getParameter("refreshDelay");
|
||||
String startupDel = req.getParameter("startupDelay");
|
||||
boolean useOpenTrackers = req.getParameter("useOpenTrackers") != null;
|
||||
boolean useDHT = req.getParameter("useDHT") != null;
|
||||
//String openTrackers = req.getParameter("openTrackers");
|
||||
String theme = req.getParameter("theme");
|
||||
_manager.updateConfig(dataDir, filesPublic, autoStart, refreshDel, startupDel,
|
||||
seedPct, eepHost, eepPort, i2cpHost, i2cpPort, i2cpOpts,
|
||||
upLimit, upBW, useOpenTrackers, theme);
|
||||
upLimit, upBW, useOpenTrackers, useDHT, theme);
|
||||
} else if ("Save2".equals(action)) {
|
||||
String taction = req.getParameter("taction");
|
||||
if (taction != null)
|
||||
@ -1469,6 +1470,7 @@ public class I2PSnarkServlet extends DefaultServlet {
|
||||
boolean autoStart = _manager.shouldAutoStart();
|
||||
boolean useOpenTrackers = _manager.util().shouldUseOpenTrackers();
|
||||
//String openTrackers = _manager.util().getOpenTrackerString();
|
||||
boolean useDHT = _manager.util().shouldUseDHT();
|
||||
//int seedPct = 0;
|
||||
|
||||
out.write("<form action=\"/i2psnark/configure\" method=\"POST\">\n" +
|
||||
@ -1582,6 +1584,14 @@ public class I2PSnarkServlet extends DefaultServlet {
|
||||
+ (useOpenTrackers ? "checked " : "")
|
||||
+ "title=\"");
|
||||
out.write(_("If checked, announce torrents to open trackers as well as the tracker listed in the torrent file"));
|
||||
out.write("\" ></td></tr>\n" +
|
||||
|
||||
"<tr><td>");
|
||||
out.write(_("Enable DHT") + " (**BETA**)");
|
||||
out.write(": <td><input type=\"checkbox\" class=\"optbox\" name=\"useDHT\" value=\"true\" "
|
||||
+ (useDHT ? "checked " : "")
|
||||
+ "title=\"");
|
||||
out.write(_("If checked, use DHT"));
|
||||
out.write("\" ></td></tr>\n");
|
||||
|
||||
// "<tr><td>");
|
||||
|
Reference in New Issue
Block a user