/** * Bytes not yet in storage. Does NOT account for skipped files. * * @return exact value. or -1 if no storage yet. getNeeded() * pieceLength(0) isn't accurate if * last piece is still needed. * @since 0.8.9 */ public long getRemainingLength() { if (meta != null && storage != null) { long needed = storage.needed(); long length0 = meta.getPieceLength(0); long remaining = needed * length0; // fixup if last piece is needed int last = meta.getPieces() - 1; if (last != 0 && !storage.getBitField().get(last)) remaining -= length0 - meta.getPieceLength(last); return remaining; } return -1; }
/** * Switch from magnet mode to normal mode. If we already have the metainfo, this does nothing. * * @param meta non-null * @since 0.8.4 */ public void setMetaInfo(MetaInfo meta) { if (metainfo != null) return; 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 { // it will be initialized later // bitfield = new BitField(meta.getPieces()); } metainfo = meta; if (bitfield != null && bitfield.count() > 0) setInteresting(true); }
void bitfieldMessage(byte[] bitmap) { synchronized (this) { if (_log.shouldLog(Log.DEBUG)) _log.debug(peer + " rcv bitfield"); if (bitfield != null) { // XXX - Be liberal in what you accept? if (_log.shouldLog(Log.WARN)) _log.warn("Got unexpected bitfield message from " + peer); return; } // 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()); } if (metainfo == null) return; boolean interest = listener.gotBitField(peer, bitfield); setInteresting(interest); if (bitfield.complete() && !interest) { // They are seeding and we are seeding, // why did they contact us? (robert) // Dump them quick before we send our whole bitmap if (_log.shouldLog(Log.WARN)) _log.warn("Disconnecting seed that connects to seeds: " + peer); peer.disconnect(true); } }
/** * 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) { try { String base = Storage.filterName(metainfo.getName()); File baseFile; if (_util.getFilesPublic()) baseFile = new File(rootDataDir, base); else baseFile = new SecureFile(rootDataDir, base); // The following two may throw IOE... storage = new Storage(_util, baseFile, metainfo, this, false); storage.check(); // ... so don't set meta until here meta = metainfo; 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); } }
/** * Does not account (i.e. includes) for skipped files. * * @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; }
void requestMessage(int piece, int begin, int length) { if (_log.shouldLog(Log.DEBUG)) _log.debug(peer + " rcv request(" + piece + ", " + begin + ", " + length + ") "); if (metainfo == null) return; if (choking) { if (_log.shouldLog(Log.INFO)) _log.info("Request received, but choking " + peer); return; } // Sanity check if (piece < 0 || piece >= metainfo.getPieces() || begin < 0 || begin > metainfo.getPieceLength(piece) || length <= 0 || length > MAX_PARTSIZE) { // XXX - Protocol error -> disconnect? if (_log.shouldLog(Log.WARN)) _log.warn( "Got strange 'request: " + piece + ", " + begin + ", " + length + "' message from " + peer); return; } // Limit total pipelined requests to MAX_PIPELINE bytes // to conserve memory and prevent DOS // Todo: limit number of requests also? (robert 64 x 4KB) if (out.queuedBytes() + length > MAX_PIPELINE_BYTES) { if (_log.shouldLog(Log.WARN)) _log.warn("Discarding request over pipeline limit from " + peer); return; } if (_log.shouldLog(Log.DEBUG)) _log.debug("Queueing (" + piece + ", " + begin + ", " + length + ")" + " to " + peer); // don't load the data into mem now, let PeerConnectionOut do it out.sendPiece(piece, begin, length, this); }
/** @since 0.8.5 */ public static void main(String[] args) { if (args.length <= 0) { System.err.println("Usage: MetaInfo files..."); return; } for (int i = 0; i < args.length; i++) { InputStream in = null; try { in = new FileInputStream(args[i]); MetaInfo meta = new MetaInfo(in); System.out.println(args[i] + " InfoHash: " + I2PSnarkUtil.toHex(meta.getInfoHash())); } catch (IOException ioe) { System.err.println("Error in file " + args[i] + ": " + ioe); } finally { try { if (in != null) in.close(); } catch (IOException ioe) { } } } }
void haveMessage(int piece) { 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()) { // XXX disconnect? if (_log.shouldLog(Log.WARN)) _log.warn("Got strange 'have: " + piece + "' message from " + peer); return; } synchronized (this) { // Can happen if the other side never send a bitfield message. if (bitfield == null) bitfield = new BitField(metainfo.getPieces()); bitfield.set(piece); } if (listener.gotHave(peer, piece)) setInteresting(true); }
/** @since 0.8.2 */ void extensionMessage(int id, byte[] bs) { if (metainfo != null && metainfo.isPrivate() && (id == ExtensionHandler.ID_METADATA || id == ExtensionHandler.ID_PEX)) { // shouldn't get this since we didn't advertise it but they could send it anyway if (_log.shouldLog(Log.WARN)) _log.warn("Private torrent, ignoring ext msg " + id); return; } ExtensionHandler.handleMessage(peer, listener, id, bs); // Peer coord will get metadata from MagnetState, // verify, and then call gotMetaInfo() listener.gotExtension(peer, id, bs); }
public void run() { // XXX - Support other IPs String announce = meta.getAnnounce(); String infoHash = urlencode(meta.getInfoHash()); String peerID = urlencode(coordinator.getID()); long uploaded = coordinator.getUploaded(); long downloaded = coordinator.getDownloaded(); long left = coordinator.getLeft(); boolean completed = (left == 0); try { boolean started = false; while (!started) { try { // Send start. TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded, downloaded, left, STARTED_EVENT); Iterator it = info.getPeers().iterator(); while (it.hasNext()) coordinator.addPeer((Peer) it.next()); started = true; } catch (IOException ioe) { // Probably not fatal (if it doesn't last to long...) Snark.debug( "WARNING: Could not contact tracker at '" + announce + "': " + ioe, Snark.WARNING); } if (!started && !stop) { Snark.debug(" Retrying in one minute...", Snark.DEBUG); try { // Sleep one minutes... Thread.sleep(60 * 1000); } catch (InterruptedException interrupt) { // ignore } } } while (!stop) { try { // Sleep some minutes... Thread.sleep(SLEEP * 60 * 1000); } catch (InterruptedException interrupt) { // ignore } if (stop) break; uploaded = coordinator.getUploaded(); downloaded = coordinator.getDownloaded(); left = coordinator.getLeft(); // First time we got a complete download? String event; if (!completed && left == 0) { completed = true; event = COMPLETED_EVENT; } else event = NO_EVENT; // Only do a request when necessary. if (event == COMPLETED_EVENT || coordinator.needPeers() || System.currentTimeMillis() > lastRequestTime + interval) { try { TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded, downloaded, left, event); Iterator it = info.getPeers().iterator(); while (it.hasNext()) coordinator.addPeer((Peer) it.next()); } catch (IOException ioe) { // Probably not fatal (if it doesn't last to long...) Snark.debug( "WARNING: Could not contact tracker at '" + announce + "': " + ioe, Snark.WARNING); } } } } catch (Throwable t) { Snark.debug("TrackerClient: " + t, Snark.ERROR); t.printStackTrace(); } finally { try { TrackerInfo info = doRequest(announce, infoHash, peerID, uploaded, downloaded, left, STOPPED_EVENT); } catch (IOException ioe) { /* ignored */ } } }
/** * Adds a new request to the outstanding requests list. Then send interested if we weren't. Then * send new requests if not choked. If nothing to request, send not interested if we were. * * <p>This is called from several places: * * <pre> * By getOustandingRequest() when the first part of a chunk comes in * By havePiece() when somebody got a new piece completed * By chokeMessage() when we receive an unchoke * By setInteresting() when we are now interested * By PeerCoordinator.updatePiecePriorities() * </pre> */ synchronized void addRequest() { // no bitfield yet? nothing to request then. if (bitfield == null) return; if (metainfo == null) return; boolean more_pieces = true; while (more_pieces) { more_pieces = outstandingRequests.size() < MAX_PIPELINE; // We want something and we don't have outstanding requests? if (more_pieces && lastRequest == null) { // we have nothing in the queue right now if (!interesting) { // If we need something, set interesting but delay pulling // a request from the PeerCoordinator until unchoked. if (listener.needPiece(this.peer, bitfield)) { setInteresting(true); if (_log.shouldLog(Log.DEBUG)) _log.debug( peer + " addRequest() we need something, setting interesting, delaying requestNextPiece()"); } else { if (_log.shouldLog(Log.DEBUG)) _log.debug(peer + " addRequest() needs nothing"); } return; } if (choked) { // If choked, delay pulling // a request from the PeerCoordinator until unchoked. if (_log.shouldLog(Log.DEBUG)) _log.debug(peer + " addRequest() we are choked, delaying requestNextPiece()"); return; } // huh? rv unused more_pieces = requestNextPiece(); } else if (more_pieces) // We want something { int pieceLength; boolean isLastChunk; pieceLength = metainfo.getPieceLength(lastRequest.getPiece()); isLastChunk = lastRequest.off + lastRequest.len == pieceLength; // Last part of a piece? if (isLastChunk) more_pieces = requestNextPiece(); else { PartialPiece nextPiece = lastRequest.getPartialPiece(); int nextBegin = lastRequest.off + PARTSIZE; int maxLength = pieceLength - nextBegin; int nextLength = maxLength > PARTSIZE ? PARTSIZE : maxLength; Request req = new Request(nextPiece, nextBegin, nextLength); outstandingRequests.add(req); if (!choked) out.sendRequest(req); lastRequest = req; } } } // failsafe // However this is bad as it thrashes the peer when we change our mind // Ticket 691 cause here? if (interesting && lastRequest == null && outstandingRequests.isEmpty()) setInteresting(false); if (_log.shouldLog(Log.DEBUG)) _log.debug(peer + " requests " + outstandingRequests); }
/** * @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 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; }
/** * @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 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; }
/** * Runs the connection to the other peer. This method does not return until the connection is * terminated. * * <p>When the connection is correctly started the connected() method of the given PeerListener is * called. If the connection ends or the connection could not be setup correctly the * disconnected() method is called. * * <p>If the given BitField is non-null it is send to the peer as first message. */ public void runConnection( I2PSnarkUtil util, PeerListener listener, BitField bitfield, MagnetState mState) { if (state != null) throw new IllegalStateException("Peer already started"); if (_log.shouldLog(Log.DEBUG)) _log.debug("Running connection to " + peerID.toString(), new Exception("connecting")); try { // Do we need to handshake? if (din == null) { // Outgoing connection sock = util.connect(peerID); if (_log.shouldLog(Log.DEBUG)) _log.debug("Connected to " + peerID + ": " + sock); if ((sock == null) || (sock.isClosed())) { throw new IOException("Unable to reach " + peerID); } InputStream in = sock.getInputStream(); OutputStream out = sock.getOutputStream(); byte[] id = handshake(in, out); byte[] expected_id = peerID.getID(); if (expected_id == null) { peerID.setID(id); } else if (Arrays.equals(expected_id, id)) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Handshake got matching IDs with " + toString()); } else { throw new IOException( "Unexpected peerID '" + PeerID.idencode(id) + "' expected '" + PeerID.idencode(expected_id) + "'"); } } else { // Incoming connection if (_log.shouldLog(Log.DEBUG)) _log.debug("Already have din [" + sock + "] with " + toString()); } // bad idea? if (metainfo == null && (options & OPTION_EXTENSION) == 0) { if (_log.shouldLog(Log.INFO)) _log.info("Peer does not support extensions and we need metainfo, dropping"); throw new IOException("Peer does not support extensions and we need metainfo, dropping"); } PeerConnectionIn in = new PeerConnectionIn(this, din); PeerConnectionOut out = new PeerConnectionOut(this, dout); PeerState s = new PeerState(this, listener, metainfo, in, out); if ((options & OPTION_EXTENSION) != 0) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Peer supports extensions, sending reply message"); int metasize = metainfo != null ? metainfo.getInfoBytes().length : -1; out.sendExtension(0, ExtensionHandler.getHandshake(metasize)); } if ((options & OPTION_I2P_DHT) != 0 && util.getDHT() != null) { if (_log.shouldLog(Log.DEBUG)) _log.debug("Peer supports DHT, sending PORT message"); int port = util.getDHT().getPort(); out.sendPort(port); } // Send our bitmap if (bitfield != null) s.out.sendBitfield(bitfield); // We are up and running! state = s; magnetState = mState; listener.connected(this); if (_log.shouldLog(Log.DEBUG)) _log.debug("Start running the reader with " + toString()); // Use this thread for running the incomming connection. // The outgoing connection creates its own Thread. out.startup(); Thread.currentThread().setName("Snark reader from " + peerID); s.in.run(); } catch (IOException eofe) { // Ignore, probably just the other side closing the connection. // Or refusing the connection, timing out, etc. if (_log.shouldLog(Log.DEBUG)) _log.debug(this.toString(), eofe); } catch (Throwable t) { _log.error(this + ": " + t.getMessage(), t); if (t instanceof OutOfMemoryError) throw (OutOfMemoryError) t; } finally { if (deregister) listener.disconnected(this); disconnect(); } }
/** * multitorrent * * @param baseFile if null, use rootDir/torrentName; if non-null, use it instead * @throws RuntimeException via fatal() * @since 0.9.11 */ public Snark( I2PSnarkUtil util, String torrent, String ip, int user_port, StorageListener slistener, CoordinatorListener clistener, CompleteListener complistener, PeerCoordinatorSet peerCoordinatorSet, ConnectionAcceptor connectionAcceptor, boolean start, String rootDir, File baseFile) { if (slistener == null) slistener = this; completeListener = complistener; _util = util; _log = util.getContext().logManager().getLog(Snark.class); _peerCoordinatorSet = peerCoordinatorSet; acceptor = connectionAcceptor; this.torrent = torrent; this.rootDataDir = new File(rootDir); stopped = true; activity = "Network setup"; id = generateID(); if (_log.shouldLog(Log.INFO)) _log.info("My peer id: " + PeerID.idencode(id)); /* * Don't start a tunnel if the torrent isn't going to be started. * If we are starting, * startTorrent() will force a connect. * boolean ok = util.connect(); if (!ok) fatal("Unable to connect to I2P"); I2PServerSocket serversocket = util.getServerSocket(); if (serversocket == null) fatal("Unable to listen for I2P connections"); else { Destination d = serversocket.getManager().getSession().getMyDestination(); debug("Listening on I2P destination " + d.toBase64() + " / " + d.calculateHash().toBase64(), NOTICE); } */ // Figure out what the torrent argument represents. File f = null; InputStream in = null; byte[] x_infoHash = null; try { f = new File(torrent); if (f.exists()) in = new FileInputStream(f); else { /** * ** No, we don't ever fetch a torrent file this way and we don't want to block in the * constructor activity = "Getting torrent"; File torrentFile = _util.get(torrent, 3); if * (torrentFile == null) { fatal("Unable to fetch " + torrent); if (false) return; // never * reached - fatal(..) throws } else { torrentFile.deleteOnExit(); in = new * FileInputStream(torrentFile); } *** */ throw new IOException("not found"); } meta = new MetaInfo(in); x_infoHash = meta.getInfoHash(); } catch (IOException ioe) { // OK, so it wasn't a torrent metainfo file. if (f != null && f.exists()) if (ip == null) fatal( "'" + torrent + "' exists," + " but is not a valid torrent metainfo file." + System.getProperty("line.separator"), ioe); else fatal("I2PSnark does not support creating and tracking a torrent at the moment"); /* { // Try to create a new metainfo file debug ("Trying to create metainfo torrent for '" + torrent + "'", NOTICE); try { activity = "Creating torrent"; storage = new Storage (f, "http://" + ip + ":" + port + "/announce", slistener); storage.create(); meta = storage.getMetaInfo(); } catch (IOException ioe2) { fatal("Could not create torrent for '" + torrent + "'", ioe2); } } */ else fatal("Cannot open '" + torrent + "'", ioe); } catch (OutOfMemoryError oom) { fatal("ERROR - Out of memory, cannot create torrent " + torrent + ": " + oom.getMessage()); } finally { if (in != null) try { in.close(); } catch (IOException ioe) { } } infoHash = x_infoHash; // final if (_log.shouldLog(Log.INFO)) _log.info(meta.toString()); // When the metainfo torrent was created from an existing file/dir // it already exists. if (storage == null) { try { activity = "Checking storage"; boolean shouldPreserve = completeListener != null && completeListener.getSavedPreserveNamesSetting(this); if (baseFile == null) { String base = meta.getName(); if (!shouldPreserve) base = Storage.filterName(base); if (_util.getFilesPublic()) baseFile = new File(rootDataDir, base); else baseFile = new SecureFile(rootDataDir, base); } storage = new Storage(_util, baseFile, meta, slistener, shouldPreserve); if (completeListener != null) { storage.check( completeListener.getSavedTorrentTime(this), completeListener.getSavedTorrentBitField(this)); } else { storage.check(); } // have to figure out when to reopen // if (!start) // storage.close(); } catch (IOException ioe) { try { storage.close(); } catch (IOException ioee) { ioee.printStackTrace(); } fatal("Could not check or create storage", ioe); } } /* * see comment above * activity = "Collecting pieces"; coordinator = new PeerCoordinator(id, meta, storage, clistener, this); PeerCoordinatorSet set = PeerCoordinatorSet.instance(); set.add(coordinator); ConnectionAcceptor acceptor = ConnectionAcceptor.instance(); acceptor.startAccepting(set, serversocket); trackerclient = new TrackerClient(meta, coordinator); */ savedUploaded = (completeListener != null) ? completeListener.getSavedUploaded(this) : 0; if (start) startTorrent(); }