/* * Peer - All public information concerning a peer. 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. */ package org.klomp.snark; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; public class Peer implements Comparable<Peer> { // Identifying property, the peer id of the other side. private final PeerID peerID; private final byte[] my_id; private final MetaInfo metainfo; // The data in/output streams set during the handshake and used by // the actual connections. private DataInputStream din; private DataOutputStream dout; // Keeps state for in/out connections. Non-null when the handshake // was successful, the connection setup and runs PeerState state; private boolean deregister = true; /** * Creates a disconnected peer given a PeerID, your own id and the relevant * MetaInfo. */ public Peer (PeerID peerID, byte[] my_id, MetaInfo metainfo) throws IOException { this.peerID = peerID; this.my_id = my_id; this.metainfo = metainfo; } /** * Creates a unconnected peer from the input and output stream got from the * socket. Note that the complete handshake (which can take some time or * block indefinitely) is done in the calling Thread to get the remote peer * id. To completely start the connection call the connect() method. * * @exception IOException * when an error occurred during the handshake. */ public Peer (final Socket sock, BufferedInputStream bis, BufferedOutputStream bos, byte[] my_id, MetaInfo metainfo) throws IOException { this.my_id = my_id; this.metainfo = metainfo; byte[] id = handshake(bis, bos); this.peerID = new PeerID(id, sock.getInetAddress(), sock.getPort()); } /** * Returns the id of the peer. */ public PeerID getPeerID () { return peerID; } /** * Returns the String representation of the peerID. */ @Override public String toString () { return peerID.toString(); } /** * The hash code of a Peer is the hash code of the peerID. */ @Override public int hashCode () { return peerID.hashCode(); } /** * Two Peers are equal when they have the same PeerID. All other properties * are ignored. */ @Override public boolean equals (Object o) { if (o instanceof Peer) { Peer p = (Peer)o; return peerID.equals(p.peerID); } else { return false; } } /** * Compares the PeerIDs. */ public int compareTo (Peer p) { return peerID.compareTo(p.peerID); } /** * Runs the connection to the other peer. This method does not return until * the connection is terminated. * * 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. * * If the given BitField is non-null it is send to the peer as first * message. */ public void runConnection (PeerListener listener, BitField bitfield) { if (state != null) { throw new IllegalStateException("Peer already started"); } try { // Do we need to handshake? if (din == null) { Socket sock = new Socket(peerID.getAddress(), peerID.getPort()); BufferedInputStream bis = new BufferedInputStream( sock.getInputStream()); BufferedOutputStream bos = new BufferedOutputStream( sock.getOutputStream()); byte[] id = handshake(bis, bos); // If peer list was binary encoded, the peer id was not given, // and therefore is set to null under creation [LIMA]. if(peerID.getID() == null) peerID.setID(id); byte[] expected_id = peerID.getID(); if (!Arrays.equals(expected_id, id)) { throw new IOException("Unexpected peerID '" + PeerID.idencode(id) + "' expected '" + PeerID.idencode(expected_id) + "'"); } } PeerConnectionIn in = new PeerConnectionIn(this, din); PeerConnectionOut out = new PeerConnectionOut(this, dout); PeerState s = new PeerState(this, listener, metainfo, in, out); // Send our bitmap if (bitfield != null) { s.out.sendBitfield(bitfield); } // We are up and running! state = s; listener.connected(this); // Use this thread for running the incomming connection. // The outgoing connection has created its own Thread. s.in.run(); } catch (IOException eofe) { log.log(Level.FINE, "Peer connection to " + peerID.getAddress() + " failed ", eofe); } catch (Throwable t) { log.log(Level.SEVERE, "Peer connection failed " + toString(), t); t.printStackTrace(); } finally { if (deregister) { listener.disconnected(this); } } } /** * Sets DataIn/OutputStreams, does the handshake and returns the id reported * by the other side. */ private byte[] handshake (BufferedInputStream bis, BufferedOutputStream bos) throws IOException { din = new DataInputStream(bis); dout = new DataOutputStream(bos); // Handshake write - header dout.write(19); dout.write("BitTorrent protocol".getBytes("UTF-8")); // Handshake write - zeros byte[] zeros = new byte[8]; dout.write(zeros); // Handshake write - metainfo hash byte[] shared_hash = metainfo.getInfoHash(); dout.write(shared_hash); // Handshake write - peer id dout.write(my_id); dout.flush(); // Handshake read - header byte b = din.readByte(); if (b != 19) { throw new IOException("Handshake failure, expected 19, got " + (b & 0xff)); } byte[] bs = new byte[19]; din.readFully(bs); String bittorrentProtocol = new String(bs, "UTF-8"); if (!"BitTorrent protocol".equals(bittorrentProtocol)) { throw new IOException("Handshake failure, expected " + "'Bittorrent protocol', got '" + bittorrentProtocol + "'"); } // Handshake read - zeros din.readFully(zeros); // Handshake read - metainfo hash bs = new byte[20]; din.readFully(bs); if (!Arrays.equals(shared_hash, bs)) { throw new IOException("Unexpected MetaInfo hash"); } // Handshake read - peer id din.readFully(bs); return bs; } public boolean isConnected () { return state != null; } /** * Disconnects this peer if it was connected. If deregister is true, * PeerListener.disconnected() will be called when the connection is * completely terminated. Otherwise the connection is silently terminated. */ public void disconnect (boolean deregister) { // Both in and out connection will call this. this.deregister = deregister; disconnect(); } void disconnect () { PeerState s = state; if (s != null) { state = null; PeerConnectionIn in = s.in; if (in != null) { in.disconnect(); } PeerConnectionOut out = s.out; if (out != null) { out.disconnect(); } } } /** * Tell the peer we have another piece. */ public void have (int piece) { PeerState s = state; if (s != null) { s.havePiece(piece); } } /** * Whether or not the peer is interested in pieces we have. Returns false if * not connected. */ public boolean isInterested () { PeerState s = state; return (s != null) && s.interested; } /** * Sets whether or not we are interested in pieces from this peer. Defaults * to false. When interest is true and this peer unchokes us then we start * downloading from it. Has no effect when not connected. */ public void setInteresting (boolean interest) { PeerState s = state; if (s != null) { s.setInteresting(interest); } } /** * Whether or not the peer has pieces we want from it. Returns false if not * connected. */ public boolean isInteresting () { PeerState s = state; return (s != null) && s.interesting; } /** * Sets whether or not we are choking the peer. Defaults to true. When choke * is false and the peer requests some pieces we upload them, otherwise * requests of this peer are ignored. */ public void setChoking (boolean choke) { PeerState s = state; if (s != null) { s.setChoking(choke); } } /** * Whether or not we are choking the peer. Returns true when not connected. */ public boolean isChoking () { PeerState s = state; return (s == null) || s.choking; } /** * Whether or not the peer choked us. Returns true when not connected. */ public boolean isChoked () { PeerState s = state; return (s == null) || s.choked; } /** * Returns the number of bytes that have been downloaded. Can be reset to * zero with <code>resetCounters()</code>/ */ public long getDownloaded () { PeerState s = state; return (s != null) ? s.downloaded : 0; } /** * Returns a bitfield representing the pieces that the peer has [LIMA]. */ public BitField getHaves() { return state == null ? null : state.bitfield; } /** * Returns the number of bytes that have been uploaded. Can be reset to zero * with <code>resetCounters()</code>/ */ public long getUploaded () { PeerState s = state; return (s != null) ? s.uploaded : 0; } /** * Resets the downloaded and uploaded counters to zero. */ public void resetCounters () { PeerState s = state; if (s != null) { s.downloaded = 0; s.uploaded = 0; } } /** The Java logger used to process our log events. */ protected static final Logger log = Logger.getLogger("org.klomp.snark.peer"); }