diff --git a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java index 001928d47..e8ff0881f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java +++ b/apps/i2psnark/java/src/org/klomp/snark/I2PSnarkUtil.java @@ -32,6 +32,8 @@ import net.i2p.util.SimpleScheduler; import net.i2p.util.SimpleTimer; import net.i2p.util.Translate; +import org.klomp.snark.dht.KRPC; + /** * I2P specific helpers for I2PSnark * We use this class as a sort of context for i2psnark @@ -56,6 +58,7 @@ public class I2PSnarkUtil { private int _maxConnections; private File _tmpDir; private int _startupDelay; + private KRPC _krpc; public static final int DEFAULT_STARTUP_DELAY = 3; public static final String PROP_USE_OPENTRACKERS = "i2psnark.useOpentrackers"; @@ -64,6 +67,8 @@ public class I2PSnarkUtil { public static final String DEFAULT_OPENTRACKERS = "http://tracker.welterde.i2p/a"; public static final int DEFAULT_MAX_UP_BW = 8; //KBps public static final int MAX_CONNECTIONS = 16; // per torrent + private static final boolean ENABLE_DHT = true; + public I2PSnarkUtil(I2PAppContext ctx) { _context = ctx; _log = _context.logManager().getLog(Snark.class); @@ -185,10 +190,20 @@ public class I2PSnarkUtil { // opts.setProperty("i2p.streaming.readTimeout", "120000"); _manager = I2PSocketManagerFactory.createManager(_i2cpHost, _i2cpPort, opts); } + // FIXME this only instantiates krpc once, left stuck with old manager + if (ENABLE_DHT && _manager != null && _krpc == null) + _krpc = new KRPC(_context, _manager.getSession()); return (_manager != null); } + /** + * @return null if disabled or not started + * @since 0.8.4 + */ + public KRPC getDHT() { return _krpc; } + public boolean connected() { return _manager != null; } + /** * Destroy the destination itself */ diff --git a/apps/i2psnark/java/src/org/klomp/snark/Peer.java b/apps/i2psnark/java/src/org/klomp/snark/Peer.java index f42adddef..a65e45887 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Peer.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Peer.java @@ -39,6 +39,7 @@ public class Peer implements Comparable private final PeerID peerID; private final byte[] my_id; + private final byte[] infohash; final MetaInfo metainfo; // The data in/output streams set during the handshake and used by @@ -70,11 +71,13 @@ public class Peer implements Comparable * Outgoing connection. * Creates a disconnected peer given a PeerID, your own id and the * relevant MetaInfo. + * @param metainfo null if in magnet mode */ - public Peer(PeerID peerID, byte[] my_id, MetaInfo metainfo) + public Peer(PeerID peerID, byte[] my_id, byte[] infohash, MetaInfo metainfo) { this.peerID = peerID; this.my_id = my_id; + this.infohash = infohash; this.metainfo = metainfo; _id = ++__id; //_log.debug("Creating a new peer with " + peerID.getAddress().calculateHash().toBase64(), new Exception("creating")); @@ -88,12 +91,14 @@ public class Peer implements Comparable * get the remote peer id. To completely start the connection call * the connect() method. * + * @param metainfo null if in magnet mode * @exception IOException when an error occurred during the handshake. */ - public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, MetaInfo metainfo) + public Peer(final I2PSocket sock, InputStream in, OutputStream out, byte[] my_id, byte[] infohash, MetaInfo metainfo) throws IOException { this.my_id = my_id; + this.infohash = infohash; this.metainfo = metainfo; this.sock = sock; @@ -312,8 +317,7 @@ public class Peer implements Comparable // FIXME not if DHT disabled dout.writeLong(OPTION_EXTENSION | OPTION_DHT); // Handshake write - metainfo hash - byte[] shared_hash = metainfo.getInfoHash(); - dout.write(shared_hash); + dout.write(infohash); // Handshake write - peer id dout.write(my_id); dout.flush(); @@ -341,7 +345,7 @@ public class Peer implements Comparable // Handshake read - metainfo hash bs = new byte[20]; din.readFully(bs); - if (!Arrays.equals(shared_hash, bs)) + if (!Arrays.equals(infohash, bs)) throw new IOException("Unexpected MetaInfo hash"); // Handshake read - peer id diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java index 050884e5c..0adb13d9b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerAcceptor.java @@ -88,12 +88,11 @@ public class PeerAcceptor } if (coordinator != null) { // single torrent capability - MetaInfo meta = coordinator.getMetaInfo(); - if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) { + if (DataHelper.eq(coordinator.getInfoHash(), peerInfoHash)) { if (coordinator.needPeers()) { Peer peer = new Peer(socket, in, out, coordinator.getID(), - coordinator.getMetaInfo()); + coordinator.getInfoHash(), coordinator.getMetaInfo()); coordinator.addPeer(peer); } else @@ -101,19 +100,18 @@ public class PeerAcceptor } else { // its for another infohash, but we are only single torrent capable. b0rk. throw new IOException("Peer wants another torrent (" + Base64.encode(peerInfoHash) - + ") while we only support (" + Base64.encode(meta.getInfoHash()) + ")"); + + ") while we only support (" + Base64.encode(coordinator.getInfoHash()) + ")"); } } else { // multitorrent capable, so lets see what we can handle for (Iterator iter = coordinators.iterator(); iter.hasNext(); ) { PeerCoordinator cur = (PeerCoordinator)iter.next(); - MetaInfo meta = cur.getMetaInfo(); - - if (DataHelper.eq(meta.getInfoHash(), peerInfoHash)) { + + if (DataHelper.eq(cur.getInfoHash(), peerInfoHash)) { if (cur.needPeers()) { Peer peer = new Peer(socket, in, out, cur.getID(), - cur.getMetaInfo()); + cur.getInfoHash(), cur.getMetaInfo()); cur.addPeer(peer); return; } diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java index ba9727d8a..f7a3a42fc 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerCoordinator.java @@ -93,6 +93,7 @@ public class PeerCoordinator implements PeerListener private final CheckEvent timer; private final byte[] id; + private final byte[] infohash; /** The wanted pieces. We could use a TreeSet but we'd have to clear and re-add everything * when priorities change. @@ -108,11 +109,16 @@ public class PeerCoordinator implements PeerListener private final I2PSnarkUtil _util; private static final Random _random = I2PAppContext.getGlobalContext().random(); - public PeerCoordinator(I2PSnarkUtil util, byte[] id, MetaInfo metainfo, Storage storage, + /** + * @param metainfo null if in magnet mode + * @param storage null if in magnet mode + */ + public PeerCoordinator(I2PSnarkUtil util, byte[] id, byte[] infohash, MetaInfo metainfo, Storage storage, CoordinatorListener listener, Snark torrent) { _util = util; this.id = id; + this.infohash = infohash; this.metainfo = metainfo; this.storage = storage; this.listener = listener; @@ -149,6 +155,8 @@ public class PeerCoordinator implements PeerListener // only called externally from Storage after the double-check fails public void setWantedPieces() { + if (metainfo == null || storage == null) + return; // Make a list of pieces synchronized(wantedPieces) { wantedPieces.clear(); @@ -188,6 +196,9 @@ public class PeerCoordinator implements PeerListener public boolean completed() { + // FIXME return metainfo complete status + if (storage == null) + return false; return storage.complete(); } @@ -204,9 +215,12 @@ public class PeerCoordinator implements PeerListener /** * Returns how many bytes are still needed to get the complete file. + * @return -1 if in magnet mode */ public long getLeft() { + if (metainfo == null | storage == null) + return -1; // XXX - Only an approximation. return ((long) storage.needed()) * metainfo.getPieceLength(0); } @@ -291,6 +305,12 @@ public class PeerCoordinator implements PeerListener return metainfo; } + /** @since 0.8.4 */ + public byte[] getInfoHash() + { + return infohash; + } + public boolean needPeers() { return !halted && peers.size() < getMaxConnections(); @@ -301,6 +321,8 @@ public class PeerCoordinator implements PeerListener * @return 512K: 16; 1M: 11; 2M: 6 */ private int getMaxConnections() { + if (metainfo == null) + return 6; int size = metainfo.getPieceLength(0); int max = _util.getMaxConnections(); if (size <= 512*1024 || completed()) @@ -375,8 +397,15 @@ public class PeerCoordinator implements PeerListener } else { - if (_log.shouldLog(Log.INFO)) - _log.info("New connection to peer: " + peer + " for " + metainfo.getName()); + if (_log.shouldLog(Log.INFO)) { + // just for logging + String name; + if (metainfo == null) + name = "Magnet"; + else + name = metainfo.getName(); + _log.info("New connection to peer: " + peer + " for " + metainfo.getName()); + } // Add it to the beginning of the list. // And try to optimistically make it a uploader. @@ -435,12 +464,22 @@ public class PeerCoordinator implements PeerListener if (need_more) { - if (_log.shouldLog(Log.DEBUG)) - _log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + metainfo.getName(), new Exception("add/run")); - + if (_log.shouldLog(Log.DEBUG)) { + // just for logging + String name; + if (metainfo == null) + name = "Magnet"; + else + name = metainfo.getName(); + _log.debug("Adding a peer " + peer.getPeerID().toString() + " for " + name, new Exception("add/run")); + } // Run the peer with us as listener and the current bitfield. final PeerListener listener = this; - final BitField bitfield = storage.getBitField(); + final BitField bitfield; + if (storage != null) + bitfield = storage.getBitField(); + else + bitfield = null; Runnable r = new Runnable() { public void run() @@ -506,11 +545,6 @@ public class PeerCoordinator implements PeerListener interestedAndChoking = count; } - public byte[] getBitMap() - { - return storage.getBitField().getFieldBytes(); - } - /** * @return true if we still want the given piece */ @@ -679,6 +713,8 @@ public class PeerCoordinator implements PeerListener * @since 0.8.1 */ public void updatePiecePriorities() { + if (storage == null) + return; int[] pri = storage.getPiecePriorities(); if (pri == null) { _log.debug("Updated piece priorities called but no priorities to set?"); @@ -745,6 +781,8 @@ public class PeerCoordinator implements PeerListener { if (halted) return null; + if (metainfo == null || storage == null) + return null; try { @@ -787,6 +825,8 @@ public class PeerCoordinator implements PeerListener */ public boolean gotPiece(Peer peer, int piece, byte[] bs) { + if (metainfo == null || storage == null) + return true; if (halted) { _log.info("Got while-halted piece " + piece + "/" + metainfo.getPieces() +" from " + peer + " for " + metainfo.getName()); return true; // We don't actually care anymore. @@ -983,6 +1023,8 @@ public class PeerCoordinator implements PeerListener * @since 0.8.2 */ public PartialPiece getPartialPiece(Peer peer, BitField havePieces) { + if (metainfo == null) + return null; synchronized(wantedPieces) { // sorts by remaining bytes, least first Collections.sort(partialPieces); diff --git a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java index ad70df9dc..3ec3d219f 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/PeerState.java +++ b/apps/i2psnark/java/src/org/klomp/snark/PeerState.java @@ -71,6 +71,9 @@ class PeerState implements DataLoader public final static int PARTSIZE = 16*1024; // outbound request private final static int MAX_PARTSIZE = 64*1024; // Don't let anybody request more than this + /** + * @param metainfo null if in magnet mode + */ PeerState(Peer peer, PeerListener listener, MetaInfo metainfo, PeerConnectionIn in, PeerConnectionOut out) { @@ -132,6 +135,9 @@ class PeerState implements DataLoader { if (_log.shouldLog(Log.DEBUG)) _log.debug(peer + " rcv have(" + piece + ")"); + // FIXME we will lose these until we get the metainfo + if (metainfo == null) + return; // Sanity check if (piece < 0 || piece >= metainfo.getPieces()) { @@ -169,8 +175,15 @@ class PeerState implements DataLoader } // XXX - Check for weird bitfield and disconnect? - bitfield = new BitField(bitmap, metainfo.getPieces()); + // FIXME will have to regenerate the bitfield after we know exactly + // how many pieces there are, as we don't know how many spare bits there are. + if (metainfo == null) + bitfield = new BitField(bitmap, bitmap.length * 8); + else + bitfield = new BitField(bitmap, metainfo.getPieces()); } + if (metainfo == null) + return; boolean interest = listener.gotBitField(peer, bitfield); setInteresting(interest); if (bitfield.complete() && !interest) { @@ -188,6 +201,8 @@ class PeerState implements DataLoader if (_log.shouldLog(Log.DEBUG)) _log.debug(peer + " rcv request(" + piece + ", " + begin + ", " + length + ") "); + if (metainfo == null) + return; if (choking) { if (_log.shouldLog(Log.INFO)) @@ -606,6 +621,8 @@ class PeerState implements DataLoader // no bitfield yet? nothing to request then. if (bitfield == null) return; + if (metainfo == null) + return; boolean more_pieces = true; while (more_pieces) { diff --git a/apps/i2psnark/java/src/org/klomp/snark/Snark.java b/apps/i2psnark/java/src/org/klomp/snark/Snark.java index 79807d68f..2460e9f53 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/Snark.java +++ b/apps/i2psnark/java/src/org/klomp/snark/Snark.java @@ -317,31 +317,7 @@ public class Snark stopped = true; activity = "Network setup"; - // "Taking Three as the subject to reason about-- - // A convenient number to state-- - // We add Seven, and Ten, and then multiply out - // By One Thousand diminished by Eight. - // - // "The result we proceed to divide, as you see, - // By Nine Hundred and Ninety Two: - // Then subtract Seventeen, and the answer must be - // Exactly and perfectly true. - - // Create a new ID and fill it with something random. First nine - // zeros bytes, then three bytes filled with snark and then - // sixteen random bytes. - byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17; - id = new byte[20]; - Random random = I2PAppContext.getGlobalContext().random(); - int i; - for (i = 0; i < 9; i++) - id[i] = 0; - id[i++] = snark; - id[i++] = snark; - id[i++] = snark; - while (i < 20) - id[i++] = (byte)random.nextInt(256); - + id = generateID(); debug("My peer id: " + PeerID.idencode(id), Snark.INFO); int port; @@ -468,6 +444,64 @@ public class Snark if (start) startTorrent(); } + + /** + * multitorrent, magnet + * + * @param torrent a fake name for now (not a file name) + * @param ih 20-byte info hash + * @since 0.8.4 + */ + public Snark(I2PSnarkUtil util, String torrent, byte[] ih, + CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet, + ConnectionAcceptor connectionAcceptor, boolean start, String rootDir) + { + completeListener = complistener; + _util = util; + _peerCoordinatorSet = peerCoordinatorSet; + acceptor = connectionAcceptor; + this.torrent = torrent; + this.infoHash = ih; + this.rootDataDir = rootDir; + stopped = true; + id = generateID(); + + // All we have is an infoHash + // meta remains null + // storage remains null + + if (start) + startTorrent(); + } + + private static byte[] generateID() { + // "Taking Three as the subject to reason about-- + // A convenient number to state-- + // We add Seven, and Ten, and then multiply out + // By One Thousand diminished by Eight. + // + // "The result we proceed to divide, as you see, + // By Nine Hundred and Ninety Two: + // Then subtract Seventeen, and the answer must be + // Exactly and perfectly true. + + // Create a new ID and fill it with something random. First nine + // zeros bytes, then three bytes filled with snark and then + // sixteen random bytes. + byte snark = (((3 + 7 + 10) * (1000 - 8)) / 992) - 17; + byte[] rv = new byte[20]; + Random random = I2PAppContext.getGlobalContext().random(); + int i; + for (i = 0; i < 9; i++) + rv[i] = 0; + rv[i++] = snark; + rv[i++] = snark; + rv[i++] = snark; + while (i < 20) + rv[i++] = (byte)random.nextInt(256); + return rv; + } + /** * Start up contacting peers and querying the tracker */ @@ -484,7 +518,7 @@ public class Snark } debug("Starting PeerCoordinator, ConnectionAcceptor, and TrackerClient", NOTICE); activity = "Collecting pieces"; - coordinator = new PeerCoordinator(_util, id, meta, storage, this, this); + coordinator = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this); if (_peerCoordinatorSet != null) { // multitorrent _peerCoordinatorSet.add(coordinator); @@ -507,7 +541,7 @@ public class Snark // restart safely, so lets build a new one to replace the old if (_peerCoordinatorSet != null) _peerCoordinatorSet.remove(coordinator); - PeerCoordinator newCoord = new PeerCoordinator(_util, id, meta, storage, this, this); + PeerCoordinator newCoord = new PeerCoordinator(_util, id, infoHash, meta, storage, this, this); if (_peerCoordinatorSet != null) _peerCoordinatorSet.add(newCoord); coordinator = newCoord; @@ -516,18 +550,17 @@ public class Snark if (!trackerclient.started() && !coordinatorChanged) { trackerclient.start(); } else if (trackerclient.halted() || coordinatorChanged) { - try - { - storage.reopen(rootDataDir); - } - catch (IOException ioe) - { - try { storage.close(); } catch (IOException ioee) { - ioee.printStackTrace(); - } - fatal("Could not reopen storage", ioe); - } - TrackerClient newClient = new TrackerClient(_util, coordinator.getMetaInfo(), coordinator, this); + if (storage != null) { + try { + storage.reopen(rootDataDir); + } catch (IOException ioe) { + try { storage.close(); } catch (IOException ioee) { + ioee.printStackTrace(); + } + fatal("Could not reopen storage", ioe); + } + } + TrackerClient newClient = new TrackerClient(_util, meta, coordinator, this); if (!trackerclient.halted()) trackerclient.halt(); trackerclient = newClient; @@ -601,6 +634,7 @@ public class Snark * @since 0.8.4 */ public byte[] getInfoHash() { + // should always be the same if (meta != null) return meta.getInfoHash(); return infoHash; diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 6a4eb4c7c..270937688 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -578,6 +578,44 @@ public class SnarkManager implements Snark.CompleteListener { } } + /** + * 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 + * @since 0.8.4 + */ + public void addMagnet(String name, byte[] ih) { + Snark torrent = new Snark(_util, name, ih, this, + _peerCoordinatorSet, _connectionAcceptor, + false, getDataDir().getPath()); + + // TODO tell the dir monitor not to delete us + synchronized (_snarks) { + _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)); + } + } + + /** + * Delete a torrent with the info hash alone (magnet / maggot) + * + * @param ih 20 byte info hash + * @since 0.8.4 + */ + public void deleteMagnet(byte[] ih) { + } + /** * Get the timestamp for a torrent from the config file */ diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java index 6bded2100..d14abaa0c 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerClient.java @@ -38,6 +38,7 @@ import net.i2p.data.Hash; import net.i2p.util.I2PAppThread; import net.i2p.util.Log; +import org.klomp.snark.dht.KRPC; /** * Informs metainfo tracker of events and gets new peers for peer @@ -73,6 +74,9 @@ public class TrackerClient extends I2PAppThread private List trackers; + /** + * @param meta null if in magnet mode + */ public TrackerClient(I2PSnarkUtil util, MetaInfo meta, PeerCoordinator coordinator, Snark snark) { super(); @@ -173,6 +177,7 @@ public class TrackerClient extends I2PAppThread // FIXME really need to get this message to the gui stop = true; _log.error("No valid trackers for infoHash: " + infoHash); + // FIXME keep going if DHT enabled return; } @@ -192,6 +197,9 @@ public class TrackerClient extends I2PAppThread Random r = I2PAppContext.getGlobalContext().random(); while(!stop) { + // Local DHT tracker announce + if (_util.getDHT() != null) + _util.getDHT().announce(snark.getInfoHash()); try { // Sleep some minutes... @@ -319,6 +327,45 @@ public class TrackerClient extends I2PAppThread maxSeenPeers = tr.seenPeers; } // *** 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 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 peers = new ArrayList(hashes.size()); + for (Hash h : hashes) { + PeerID pID = new PeerID(h.getData()); + peers.add(new Peer(pID, snark.getID(), snark.getInfoHash(), meta)); + } + Collections.shuffle(peers, r); + Iterator it = peers.iterator(); + while ((!stop) && it.hasNext()) { + Peer cur = it.next(); + if (coordinator.addPeer(cur)) { + int delay = DELAY_MUL; + delay *= ((int)cur.getPeerID().getAddress().calculateHash().toBase64().charAt(0)) % 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 snark.setTrackerSeenPeers(maxSeenPeers); if (!runStarted) @@ -333,6 +380,9 @@ public class TrackerClient extends I2PAppThread } finally { + // Local DHT tracker unannounce + if (_util.getDHT() != null) + _util.getDHT().unannounce(snark.getInfoHash()); try { // try to contact everybody we can diff --git a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java index 360a4f47e..ef67fc58b 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java +++ b/apps/i2psnark/java/src/org/klomp/snark/TrackerInfo.java @@ -144,7 +144,7 @@ public class TrackerInfo continue; } } - peers.add(new Peer(peerID, my_id, metainfo)); + peers.add(new Peer(peerID, my_id, metainfo.getInfoHash(), metainfo)); } return peers; @@ -172,7 +172,7 @@ public class TrackerInfo // won't happen continue; } - peers.add(new Peer(peerID, my_id, metainfo)); + peers.add(new Peer(peerID, my_id, metainfo.getInfoHash(), metainfo)); } return peers; diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index 57e8bd446..917652bc0 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -61,6 +61,10 @@ public class I2PSnarkServlet extends Default { private String _imgPath; public static final String PROP_CONFIG_FILE = "i2psnark.configFile"; + /** BEP 9 */ + private static final String MAGNET = "magnet:?xt=urn:btih:"; + /** http://sponge.i2p/files/maggotspec.txt */ + private static final String MAGGOT = "maggot://"; @Override public void init(ServletConfig cfg) throws ServletException { @@ -455,8 +459,10 @@ public class I2PSnarkServlet extends Default { _manager.addMessage(_("Fetching {0}", urlify(newURL))); I2PAppThread fetch = new I2PAppThread(new FetchAndAdd(_manager, newURL), "Fetch and add", true); fetch.start(); + } else if (newURL.startsWith(MAGNET) || newURL.startsWith(MAGGOT)) { + addMagnet(newURL); } else { - _manager.addMessage(_("Invalid URL - must start with http://")); + _manager.addMessage(_("Invalid URL - must start with http://, {0} or {1}", MAGNET, MAGGOT)); } } else { // no file or URL specified @@ -503,6 +509,8 @@ public class I2PSnarkServlet extends Default { _manager.stopTorrent(name, true); MetaInfo meta = snark.getMetaInfo(); if (meta == null) { + // magnet + _manager.deleteMagnet(snark.getInfoHash()); return; } // should we delete the torrent file? @@ -527,6 +535,8 @@ public class I2PSnarkServlet extends Default { _manager.stopTorrent(name, true); MetaInfo meta = snark.getMetaInfo(); if (meta == null) { + // magnet + _manager.deleteMagnet(snark.getInfoHash()); return; } File f = new File(name); @@ -1314,6 +1324,48 @@ public class I2PSnarkServlet extends Default { out.write("\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) { + 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(ihash, ih); + } + /** copied from ConfigTunnelsHelper */ private static final String HOP = "hop"; private static final String TUNNEL = "tunnel";