/* * PeerCoordinator - Coordinates which peers do what (up and downloading). * Copyright (C) 2003 Mark J. Wielaard * * This file is part of Snark. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along with * this program; if not, write to the Free Software Foundation, Inc., 59 Temple * Place - Suite 330, Boston, MA 02111-1307, USA. * * Revised by Stephen L. Reed, Dec 22, 2009. * Reformatted, fixed Checkstyle, Findbugs and PMD violations, and substituted Log4J logger * for consistency with the Texai project. */ package org.texai.torrent; import org.texai.torrent.domainEntity.MetaInfo; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; import java.util.SortedMap; import java.util.Timer; import java.util.TreeMap; import org.apache.commons.codec.net.URLCodec; import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.jboss.netty.channel.Channel; import org.texai.torrent.message.BitTorrentHandshakeMessage; import org.texai.util.ByteUtils; import org.texai.util.NetworkUtils; import org.texai.util.TexaiException; import org.texai.x509.X509SecurityInfo; /** Coordinates which peers perform uploading or downloading actions. */ public final class PeerCoordinator { /** the logger */ private static final Logger LOGGER = Logger.getLogger(PeerCoordinator.class); /** delay in milliseconds before the peer checker task is to be executed */ public static final long CHECK_PERIOD = 20 * 1000; // 20 seconds /** the maximum number of connections */ public static final int MAX_CONNECTIONS = 24; /** the maximum number of uploaders */ public static final int MAX_UPLOADERS = 4; /** the torrent metainfo */ private final MetaInfo metaInfo; /** the storage */ private final Storage storage; /** an approximation of the number of current uploaders that is re-synced by PeerChecker once in a while */ private int uploaders = 0; /** the total number of uploaded bytes of all peers */ private long nbrBytesUploaded; /** the total number of downloaded bytes of all peers */ private long nbrBytesDownloaded; /** the peer dictionary, peer identification bytes hex --> peer */ private final Map<String, Peer> peerDictionary = new HashMap<>(); /** the peer checker timer */ private final Timer peerCheckerTimer = new Timer("peer checker timer", true); /** our BitTorrent client id bytes, randomly assigned */ private final byte[] ourIdBytes = new byte[20]; /** randomized wanted piece sequence numbers */ private final List<Integer> wantedPieces; /** the indicator whether this peer is halted */ private boolean isQuit = false; /** the tracker client */ private TrackerClient trackerClient; /** the download statistics dictionary, tracked peer info --> nbr downloaded bytes */ private final Map<TrackedPeerInfo, Long> downloadStatisticsDictionary = new HashMap<>(); /** the upload statistics dictionary, tracked peer info --> nbr uploaded bytes */ private final Map<TrackedPeerInfo, Long> uploadStatisticsDictionary = new HashMap<>(); /** our tracked peer information */ private final TrackedPeerInfo ourTrackedPeerInfo; /** the download listener, or null if our peer is a seed */ private final DownloadListener downloadListener; /** the X.509 security information */ private final X509SecurityInfo x509SecurityInfo; /** the SSL torrent */ private final SSLTorrent sslTorrent; /** the client id candidate symbols */ private static final byte[] CANDIDATE_ID_SYMBOLS = new byte[62]; static { for (int i = 0; i < 10; ++i) { CANDIDATE_ID_SYMBOLS[i] = (byte) ('0' + i); } for (int i = 10; i < 36; ++i) { CANDIDATE_ID_SYMBOLS[i] = (byte) ('a' + i - 10); } for (int i = 36; i < 62; ++i) { CANDIDATE_ID_SYMBOLS[i] = (byte) ('A' + i - 36); } } /** Constructs a new PeerCoordinator instance. * * @param metaInfo the torrent metainfo * @param storage the torrent file/directory storage * @param port the port for accepting peer connections * @param x509SecurityInfo the X.509 security information * @param downloadListener the download listener, or null if our peer is a seed * @param sslTorrent the SSL torrent */ public PeerCoordinator( final MetaInfo metaInfo, final Storage storage, final int port, final X509SecurityInfo x509SecurityInfo, final DownloadListener downloadListener, final SSLTorrent sslTorrent) { //Preconditions assert metaInfo != null : "metaInfo must not be null"; assert storage != null : "storage must not be null"; assert port > 0 : "port must be positive"; assert x509SecurityInfo != null : "x509SecurityInfo must not be null"; assert sslTorrent != null : "sslTorrent must not be null"; this.metaInfo = metaInfo; this.storage = storage; this.x509SecurityInfo = x509SecurityInfo; this.downloadListener = downloadListener; this.sslTorrent = sslTorrent; // create our peer id bytes int index = 0; ourIdBytes[index++] = '-'; ourIdBytes[index++] = 'S'; ourIdBytes[index++] = 'N'; ourIdBytes[index++] = '1'; ourIdBytes[index++] = '0'; ourIdBytes[index++] = '0'; ourIdBytes[index++] = '0'; ourIdBytes[index++] = '-'; final Random random = new Random(); while (index < 20) { ourIdBytes[index++] = CANDIDATE_ID_SYMBOLS[random.nextInt(CANDIDATE_ID_SYMBOLS.length)]; } ourTrackedPeerInfo = new TrackedPeerInfo( ourIdBytes, NetworkUtils.getLocalHostAddress(), port); try { LOGGER.info(this + " our peer id: " + new String(ourIdBytes, "US-ASCII")); } catch (UnsupportedEncodingException ex) { ex.printStackTrace(); } LOGGER.log(Level.DEBUG, "our peer tracked peer info hex: " + TrackedPeerInfo.hexEncode(ourIdBytes)); // Make a random list of piece numbers. wantedPieces = new ArrayList<>(); final BitField bitfield = storage.getBitField(); for (int i = 0; i < metaInfo.getNbrPieces(); i++) { if (!bitfield.get(i)) { wantedPieces.add(i); } } //Collections.shuffle(wantedPieces); LOGGER.info(this + " indices of wanted pieces " + wantedPieces); // Install a timer to check the uploaders. peerCheckerTimer.schedule( new PeerCheckerTask(this), CHECK_PERIOD, CHECK_PERIOD); sslTorrent.addPeerCoordinator(metaInfo.getInfoHash(), this); LOGGER.info("created peer coordinator " + this); } /** Adds a peer that we contact. * * @param trackedPeerInfo the given peer tracked information * @return whether the peer was added */ public boolean addPeerThatWeContact(final TrackedPeerInfo trackedPeerInfo) { //Preconditions assert trackedPeerInfo != null : "trackedPeerInfo must not be null"; if (isQuit) { return false; } if (trackedPeerInfo.equals(ourTrackedPeerInfo)) { LOGGER.info("not adding our own peer listed at the tracker"); return false; } for (final Peer peer : peerDictionary.values()) { if (trackedPeerInfo.equals(peer.getTrackedPeerInfo()) && peer.isConnected()) { LOGGER.info("already connected to " + peer); return false; } } final boolean areMorePeersNeeded; synchronized (peerDictionary) { areMorePeersNeeded = peerDictionary.size() < MAX_CONNECTIONS; } if (areMorePeersNeeded) { final Peer peer = new Peer(trackedPeerInfo, this); final String peerIdBytesHex = ByteUtils.toHex(peer.getTrackedPeerInfo().getPeerIdBytes()); synchronized (peerDictionary) { peerDictionary.put(peerIdBytesHex, peer); } LOGGER.info("========================================================================================================="); try { LOGGER.info(this + " added peer to contact: " + peer + " " + new String(peer.getTrackedPeerInfo().getPeerIdBytes(), "US-ASCII")); } catch (UnsupportedEncodingException ex) { throw new TexaiException(ex); } LOGGER.info("========================================================================================================="); peer.sendHandshake(); return true; } else { LOGGER.info("MAX_CONNECTIONS = " + MAX_CONNECTIONS + " not accepting extra peer: " + trackedPeerInfo); return false; } } /** Adds a peer that contacted us. * * @param inetAddress the peer's IP address * @param port the peer's port * @param channel the communication channel between us and the peer * @param bitTorrentHandshakeMessage the remote peer's handshake message * @return whether the peer was added */ public boolean addPeerThatContactedUs( final InetAddress inetAddress, final int port, final Channel channel, final BitTorrentHandshakeMessage bitTorrentHandshakeMessage) { //Preconditions assert inetAddress != null : "inetAddress must not be null"; assert port > 0 : "port must be positive"; assert channel != null : "channel must not be null"; assert bitTorrentHandshakeMessage != null : "bitTorrentHandshakeMessage must not be null"; if (isQuit) { return false; } final TrackedPeerInfo trackedPeerInfo = new TrackedPeerInfo( bitTorrentHandshakeMessage.getPeerIdBytes(), inetAddress, port); if (trackedPeerInfo.equals(ourTrackedPeerInfo)) { LOGGER.info("not adding our own peer listed at the tracker"); return false; } for (final Peer peer : peerDictionary.values()) { if (trackedPeerInfo.equals(peer.getTrackedPeerInfo()) && peer.isConnected()) { LOGGER.info("already connected to " + peer); return false; } } final boolean areMorePeersNeeded; synchronized (peerDictionary) { areMorePeersNeeded = peerDictionary.size() < MAX_CONNECTIONS; } if (areMorePeersNeeded) { final Peer peer = new Peer( inetAddress, port, channel, bitTorrentHandshakeMessage, this); final String peerIdBytesHex = ByteUtils.toHex(peer.getTrackedPeerInfo().getPeerIdBytes()); synchronized (peerDictionary) { peerDictionary.put(peerIdBytesHex, peer); } LOGGER.info("========================================================================================================="); try { LOGGER.info(this + " added peer that contacted us: " + peer + " " + new String(peer.getTrackedPeerInfo().getPeerIdBytes(), "US-ASCII")); } catch (UnsupportedEncodingException ex) { throw new TexaiException(ex); } LOGGER.info("========================================================================================================="); peer.receiveHandshake(bitTorrentHandshakeMessage); return true; } else { LOGGER.info("MAX_CONNECTIONS = " + MAX_CONNECTIONS + " not accepting extra peer: " + trackedPeerInfo); return false; } } /** Processes the event in which the connection to the peer was terminated or the connection * handshake failed. * * @param peer the peer that just got disconnected. */ public void peerDisconnectedEvent(final Peer peer) { //Preconditions assert peer != null : "peer must not be equal to null"; LOGGER.log(Level.DEBUG, "Disconnected " + peer); LOGGER.info("peerDisconnectedEvent started"); synchronized (peerDictionary) { // Make sure it is no longer in our lists if (peerDictionary.remove(ByteUtils.toHex(peer.getTrackedPeerInfo().getPeerIdBytes())) != null) { // Unchoke some random other peer unchokePeer(); } } LOGGER.info("peerDisconnectedEvent completed"); } /** Processes the event in which we received a interested or unintrested message from a peer. * * @param peer the peer that sent the message * @param interest true when the peer sent a interested message, false when the peer sent an uninterested message */ public void peerInterestedOrUninterestedEvent( final Peer peer, final boolean interest) { //Preconditions assert peer != null : "peer must not be equal to null"; if (interest) { LOGGER.info("peerInterestedOrUninterestedEvent started"); synchronized (peerDictionary) { if (uploaders < MAX_UPLOADERS && peer.isChoking()) { uploaders++; peer.setChoking(false); LOGGER.log(Level.DEBUG, "Unchoke: " + peer); } } LOGGER.info("peerInterestedOrUninterestedEvent completed"); } } /** Processes the event in which a complete piece is received from a peer. The piece must be * requested by Peer.request() first. If this method returns false that * means the Peer provided a corrupted piece and the connection will be * closed. * * @param peer the peer that sent the piece. * @param pieceIndex the piece index received. * @param pieceBuffer the byte array containing the piece. * * @return true when the bytes represent the piece, false otherwise. */ public boolean peerCompletePieceEvent( final Peer peer, final int pieceIndex, final byte[] pieceBuffer) { //Preconditions assert peer != null : "peer must not be equal to null"; assert pieceIndex >= 0 : "pieceIndex must not be negative"; assert pieceBuffer != null : "pieceBuffer must not be equal to null"; if (isQuit) { return true; } synchronized (wantedPieces) { if (!wantedPieces.contains(pieceIndex)) { LOGGER.info(peer + " piece " + pieceIndex + " is no longer needed"); // No need to announce have piece to peers. // Assume we got a good piece, we don't really care anymore. return true; } try { if (storage.putPiece(pieceIndex, pieceBuffer)) { synchronized (downloadStatisticsDictionary) { Long nbrBytesDownloaded1 = downloadStatisticsDictionary.get(peer.getTrackedPeerInfo()); if (nbrBytesDownloaded1 == null) { nbrBytesDownloaded1 = (long) pieceBuffer.length; downloadStatisticsDictionary.put(peer.getTrackedPeerInfo(), nbrBytesDownloaded1); } else { downloadStatisticsDictionary.put(peer.getTrackedPeerInfo(), nbrBytesDownloaded1 + (long) pieceBuffer.length); } } LOGGER.info(this + " received piece " + pieceIndex + " from " + peer); } else { nbrBytesDownloaded -= metaInfo.getPieceLength(pieceIndex); LOGGER.info("got BAD piece " + pieceIndex + " from " + peer); return false; // No need to announce BAD piece to peers. } } catch (final IOException ioe) { throw new TexaiException(ioe); } wantedPieces.remove(Integer.valueOf(pieceIndex)); } // announce that we have it to our peers final List<Peer> myPeers = new ArrayList<>(); LOGGER.debug("peerPieceEvent critical section started"); synchronized (peerDictionary) { myPeers.addAll(peerDictionary.values()); } LOGGER.debug("peerPieceEvent critical section completed"); final Iterator<Peer> peers_iter = myPeers.iterator(); while (!isQuit && peers_iter.hasNext()) { final Peer peer1 = peers_iter.next(); if (peer1.isConnected()) { peer1.havePiece(pieceIndex); } } if (!isQuit && isCompleted()) { trackerClient.quit(); } return true; } /** Gets the peer having the the given peer identification bytes. * * @param peerIdBytes the given peer identification bytes * @return the peer, or null if not found */ public Peer getPeer(final byte[] peerIdBytes) { //Preconditions assert peerIdBytes != null : "peerIdBytes must not be null"; return peerDictionary.get(ByteUtils.toHex(peerIdBytes)); } /** Sets the tracker client. * * @param trackerClient the tracker client */ public void setTrackerClient(final TrackerClient trackerClient) { //Preconditions assert trackerClient != null : "trackerClient must not be null"; this.trackerClient = trackerClient; } /** Gets our peer identification bytes * * @return our peer identification bytes */ public byte[] getPeerIdBytes() { return ourIdBytes; } /** Gets our URL encoded id bytes * * @return our URL encoded id bytes */ public String getURLEncodedID() { return new String((new URLCodec()).encode(ourIdBytes)); } /** Returns whether this storage contains all pieces in the MetaInfo. * * @return whether this storage contains all pieces in the MetaInfo */ public boolean isCompleted() { return storage.isComplete(); } /** Returns the number of peers. * * @return the number of peers */ public int getNbrPeers() { synchronized (peerDictionary) { return peerDictionary.size(); } } /** Returns how many pieces are still missing. * * @return how many pieces are still missing */ public int getNbrNeededPieces() { return storage.getNbrNeededPieces(); } /** Returns approximately how many bytes are still needed to get the complete file. * * @return approximately how many bytes are still needed to get the complete file */ public long getApproximateNbrBytesRemaining() { return storage.getNbrNeededPieces() * metaInfo.getPieceLength(0); } /** Returns the total number of uploaded bytes of all peers. * * @return the total number of uploaded bytes of all peers */ public long getUploaded() { return nbrBytesUploaded; } /** Returns the total number of downloaded bytes of all peers. * * @return the total number of downloaded bytes of all peers */ public long getDownloaded() { return nbrBytesDownloaded; } /** Returns whether more peers are needed. * * @return whether more peers are needed */ public boolean areMorePeersNeeded() { synchronized (peerDictionary) { return !isQuit && peerDictionary.size() < MAX_CONNECTIONS; } } /** Quits the tracker client and peers. */ public void quit() { isQuit = true; LOGGER.info("halting tracker client"); trackerClient.quit(); LOGGER.info("disconnecting the peers ********"); Thread.currentThread().setPriority(Thread.MAX_PRIORITY); synchronized (peerDictionary) { LOGGER.info("obtained lock on the peers"); // Stop peer checker task. peerCheckerTimer.cancel(); // Stop peers. final Iterator<Peer> peers_iter = peerDictionary.values().iterator(); while (peers_iter.hasNext()) { final Peer peer = peers_iter.next(); LOGGER.info("disconnecting " + peer); peer.disconnect(); peers_iter.remove(); } LOGGER.info("peers disconnected ********"); } } /** Get the peers. * * @return the peers */ public Collection<Peer> getPeers() { return peerDictionary.values(); } /** Gets an approximation of the number of current uploaders that is re-synced by PeerChecker once in a while. * * @return an approximation of the number of current uploaders */ public int getUploaders() { return uploaders; } /** Sets the uploaders. * * @param uploaders the uploaders to set */ public void setUploaders(final int uploaders) { //Preconditions assert uploaders >= 0 : "uploaders must not be negative"; this.uploaders = uploaders; } /** Reports the download statistics. */ public void reportDownloadStatistics() { final SortedMap<Long, TrackedPeerInfo> sortedDownloadStatisticsDictionary = new TreeMap<>(); synchronized (downloadStatisticsDictionary) { for (final Entry<TrackedPeerInfo, Long> entry : downloadStatisticsDictionary.entrySet()) { sortedDownloadStatisticsDictionary.put(entry.getValue(), entry.getKey()); } } LOGGER.info("number bytes downloaded from peers ..."); if (sortedDownloadStatisticsDictionary.isEmpty()) { LOGGER.info(" none"); } else { for (final Entry<Long, TrackedPeerInfo> entry : sortedDownloadStatisticsDictionary.entrySet()) { LOGGER.info(" " + entry.getValue() + " " + entry.getKey()); } } } /** Reports the upload statistics. */ public void reportUploadStatistics() { final SortedMap<Long, TrackedPeerInfo> sortedUploadStatisticsDictionary = new TreeMap<>(); synchronized (uploadStatisticsDictionary) { for (final Entry<TrackedPeerInfo, Long> entry : uploadStatisticsDictionary.entrySet()) { sortedUploadStatisticsDictionary.put(entry.getValue(), entry.getKey()); } } LOGGER.info("number bytes uploaded to peers ..."); if (sortedUploadStatisticsDictionary.isEmpty()) { LOGGER.info(" none"); } else { for (final Entry<Long, TrackedPeerInfo> entry : sortedUploadStatisticsDictionary.entrySet()) { LOGGER.info(" " + entry.getValue() + " " + entry.getKey()); } } } /** Optimistically unchokes the peers. */ public synchronized void unchokePeer() { // linked list will contain all interested peers that we choke. // At the start are the peers that have us unchoked and at the end the // other peers that are interested, but are choking us. final List<Peer> interested = new LinkedList<>(); final Iterator<Peer> peers_iter = peerDictionary.values().iterator(); while (peers_iter.hasNext()) { final Peer peer = peers_iter.next(); if (uploaders < MAX_UPLOADERS && peer.isChoking() && peer.isInterested()) { if (peer.isChoked()) { interested.add(peer); } else { interested.add(0, peer); } } } while (uploaders < MAX_UPLOADERS && !interested.isEmpty()) { final Peer peer = interested.remove(0); peer.setChoking(false); uploaders++; } } /** Gets the bit map. * * @return the bit map */ public byte[] getBitMap() { return storage.getBitField().getFieldBytes(); } /** Processes the event in which a peer sent a have piece message. If this method returns true * and the peer has not yet received a interested message or we indicated * earlier to be not interested, then an interested message will be sent. * * @param peer the peer that sent the message * @param piece the piece number that the peer just got * * @return true when it is a piece that we want, false if the piece is already here. */ public boolean peerHavePieceEvent( final Peer peer, final int piece) { //Preconditions assert peer != null : "peer must not be equal to null"; assert piece >= 0 : "piece must not be negative"; synchronized (wantedPieces) { return wantedPieces.contains(piece); } } /** Processes the event in which we received a bitfield message from a peer. If this method returns true an * interested message will be send back to the peer. * * @param peer the peer that sent the message * @param bitField a BitField containing the pieces that the other side has * * @return true when the BitField contains pieces we want, false if the piece is already known */ public boolean peerBitFieldEvent( final Peer peer, final BitField bitField) { //Preconditions assert peer != null : "peer must not be equal to null"; assert bitField != null : "bitField must not be equal to null"; synchronized (wantedPieces) { final Iterator<Integer> wantedPieces_iter = wantedPieces.iterator(); while (wantedPieces_iter.hasNext()) { final int wantedPiece = wantedPieces_iter.next(); if (bitField.get(wantedPiece)) { LOGGER.info(this + " we want piece: " + wantedPiece + " from " + peer); return true; } } } LOGGER.info(this + " no wanted pieces from " + peer); return false; } /** Processes the event in which we are downloading from the peer and need to ask for a new * piece. Might be called multiple times before <code>gotPiece()</code> is * called. * * @param peer the Peer that will be asked to provide the piece * @param bitfield a BitField containing the pieces that the other side has * * @return one of the pieces from the bitfield that we want or -1 if we are no longer interested in the peer */ public int getWantedPiece( final Peer peer, final BitField bitfield) { //Preconditions assert peer != null : "peer must not be equal to null"; assert bitfield != null : "bitfield must not be equal to null"; if (isQuit) { return -1; } synchronized (wantedPieces) { Integer piece = null; final Iterator<Integer> wantedPieces_iter = wantedPieces.iterator(); while (piece == null && wantedPieces_iter.hasNext()) { final Integer wantedPiece = wantedPieces_iter.next(); if (bitfield.get(wantedPiece)) { wantedPieces_iter.remove(); piece = wantedPiece; } } if (piece == null) { return -1; } // We add it back at the back of the list. It will be removed // if gotPiece is called later. This means that the last // couple of pieces might very well be asked from multiple // peers but that is OK. wantedPieces.add(piece); return piece; } } /** Processes the event in which the peer wants (part of) a piece from us. Only called when * the peer is not choked by us (<code>peer.choke(false)</code> was * called). * * @param peer the peer that wants the piece. * @param piece the piece number requested. * @return a byte array containing the piece or null when the piece is not available (which is a protocol error). * @throws IOException when an input/output error occurs */ public byte[] getPieceBytes( final Peer peer, final int piece) throws IOException { //Preconditions assert peer != null : "peer must not be equal to null"; assert piece >= 0 : "piece must not be negative"; if (isQuit) { return null; } try { return storage.getPiece(piece); } catch (final IOException ex) { throw new TexaiException(ex); } } /** Processes the event in which a (partial) piece has been uploaded to the peer. * * @param peer the peer to which size bytes were uploaded * @param size the number of bytes that were uploaded */ public void uploaded( final Peer peer, final int size) { //Preconditions assert peer != null : "peer must not be equal to null"; assert size >= 0 : "size must not be negative"; nbrBytesUploaded += size; synchronized (uploadStatisticsDictionary) { Long nbrBytesUploaded1 = uploadStatisticsDictionary.get(peer.getTrackedPeerInfo()); if (nbrBytesUploaded1 == null) { nbrBytesUploaded1 = (long) size; uploadStatisticsDictionary.put(peer.getTrackedPeerInfo(), nbrBytesUploaded1); } else { uploadStatisticsDictionary.put(peer.getTrackedPeerInfo(), nbrBytesUploaded1 + (long) size); } } } /** Processes the event in which a (partial) piece has been downloaded from the peer. * * @param peer the Peer from which size bytes were downloaded. * @param size the number of bytes that were downloaded. */ public void downloaded( final Peer peer, final int size) { //Preconditions assert peer != null : "peer must not be equal to null"; assert size >= 0 : "size must not be negative"; nbrBytesDownloaded += size; } /** Gets the torrent metainfo. * * @return the torrent metainfo */ public MetaInfo getMetaInfo() { return metaInfo; } /** Gets the storage. * * @return the storage */ public Storage getStorage() { return storage; } /** Gets our tracked peer info. * * @return our tracked peer info */ public TrackedPeerInfo getOurTrackedPeerInfo() { return ourTrackedPeerInfo; } /** Gets the download listener. * * @return the download listener */ public DownloadListener getDownloadListener() { return downloadListener; } /** Gets the X.509 security information. * * @return the X.509 security information */ protected X509SecurityInfo getX509SecurityInfo() { return x509SecurityInfo; } /** Gets the SSL torrent. * * @return the sslTorrent */ public SSLTorrent getSSLTorrent() { return sslTorrent; } /** Returns a string representation of this object. * * @return a string representation of this object */ @Override public String toString() { return "[PeerCoordinator " + ourTrackedPeerInfo.toString() + "]"; } }