import java.io.*; import java.net.ServerSocket; import java.nio.ByteBuffer; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.*; import GivenTools.TorrentInfo; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Logger; /** * The Class Manager is resonsible for the main operation of the torrent client * and handles setting up connection with peers, handling incoming messages from * peers and handling writing/reading from the file on disk. * * @author Deepak, Mike, Josh */ public class Manager extends Thread { /** The Constant PEER_ID_WE_NEED. */ public static final byte[] PEER_ID_WE_NEED = new byte[] {'R', 'U', 'B', 'T', '1', '1'}; /** The Constant log. */ private static final Logger log = Log2.getLogger(Manager.class); /** The tracker. */ Tracker tracker; /** The torrent info. */ TorrentInfo torrentInfo; /** The output file. */ File outputFile; /** The peers. */ ArrayList<Peer> peers; /** The server socket. */ ServerSocket serverSocket = null; /** The listen port. */ int listenPort = -1; /** The message queue. */ LinkedBlockingQueue<PeerMessage> messageQueue = null; /** The peer id. */ public static byte[] peerId = Utils.genPeerId(); /** The opt unchoking obj. */ OptimisticUnchoking optUnchokingObj; /** The opt unchoke. */ public Timer optUnchoke; /** The max unchoked. */ public int maxUnchoked = 4; /** The cur unchoked. */ public int curUnchoked = 0; /** The is running. */ boolean isRunning = false; /** The downloading status. */ boolean downloadingStatus = true; /** The our bitfield. */ public boolean[] ourBitfield; /** The cur requested bitfield. */ boolean[] curRequestedBitfield; /** The piece prevalence. */ int[] piecePrevalence; long rateCalulatorTotalDownload =0L; long rateCalculatorTotalUpload = 0L; Timer rateCalculatorTimer; rateCalculator rtCalc; public int numHashFails = 0; public static boolean haveFullFile = false; /** * Update piece prevalence. */ public void updatePiecePrevalence(){ Arrays.fill(this.piecePrevalence, 0); for(Peer peer : this.peers){ for(int i = 0; i < this.piecePrevalence.length; i++){ if(peer.bitfield[i] == true){ this.piecePrevalence[i] += 1; if(this.curRequestedBitfield[i]) this.piecePrevalence[i] += 10; } } } } /** * Gets the rarest piece. * * @param peer the peer * @return the rarest piece */ public int getRarestPiece(Peer peer){ updatePiecePrevalence(); int min = Integer.MAX_VALUE; for(int i = 0; i < this.piecePrevalence.length; i++){ if(this.ourBitfield[i] == false && peer.bitfield[i] == true){ if(min > this.piecePrevalence[i]){ min = piecePrevalence[i]; } } } if(min == Integer.MAX_VALUE){ return -1; } ArrayList<Integer> indices = new ArrayList<Integer>(); for(int i = 0; i < this.piecePrevalence.length; i++){ if(this.ourBitfield[i] == false && peer.bitfield[i] == true){ if(min == this.piecePrevalence[i]){ indices.add(i); } } } Random random = new Random(); int n = random.nextInt(indices.size()); return indices.get(n); } /** * Instantiates a new manager. * * @param torrentInfo the torrent info * @param file the file */ public Manager(TorrentInfo torrentInfo, File file) { super(); this.outputFile = file; this.torrentInfo = torrentInfo; this.peers = new ArrayList<Peer>(); } /** * Pick the port to connect on * * @return the int */ public int pickPort() { for (int i = 6881; i <= 6889; i++) { try { this.serverSocket = new ServerSocket(i); return this.listenPort = i; } catch (IOException e) { log.severe("Unable to create sSocket at port " + i); } } log.severe("Unable to create sSocket at all. Giving up...."); return -1; } /** * Close down manager * * @throws IOException Signals that an I/O exception has occurred. */ public void close() throws IOException { this.isRunning = false; if (this.peers != null) { for (Peer p : this.peers) { try { p.disconnect(); } catch (Exception e) { log.severe("Exception while shutting-down peer " + p + " : " + e); continue; } } } } /** * Initializes the manager * @throws IOException */ public void init() throws IOException { this.tracker = new Tracker(this.torrentInfo, Manager.peerId, this.listenPort, this); this.curRequestedBitfield = new boolean[this.torrentInfo.piece_hashes.length]; this.piecePrevalence = new int[this.torrentInfo.piece_hashes.length]; Arrays.fill(this.curRequestedBitfield, false); Arrays.fill(this.piecePrevalence, 0); ArrayList<Peer> allPeers = this.tracker.update("started", this); this.tracker.timer = new Timer(); this.tracker.trackerUpdate = new TrackerUpdate(this.tracker, this); this.tracker.timer.schedule(this.tracker.trackerUpdate, this.tracker.interval * 1000, this.tracker.interval * 1000); // Check if we have the full file on disk and turn off downloading if true this.messageQueue = new LinkedBlockingQueue<PeerMessage>(); //Try to setup a new thread that will accept peer connections on a server socket //PeerConnect pConnect = new PeerConnect(this); //pConnect.start(); if (allPeers != null) { int i = 1; for (Peer p : allPeers) { log.fine(p.toString()); if (!p.init()) { log.severe("Wrong Peer IP or unable to contact peer" + p); } else { this.peers.add(p); RUBTClient.addPeer(i, p.ip, p, p.downloadRate, p.uploadRate, p.weAreChokingPeer); ++i; } } RUBTClient.setNumPeers(peers.size()); } if(haveFullFile) { this.downloadingStatus = false; this.tracker.update("completed", this); RUBTClient.log("Finished downloading. Will now seed."); Tracker.downloaded = torrentInfo.file_length; byte[] bitfield = Utils.boolToBitfieldArray(this.ourBitfield); Message.Bitfield bitMsg = new Message.Bitfield(bitfield); // Send the Peers our completed Bitfield! for (Peer p : this.peers) { try { p.sendMessage(bitMsg); } catch (Exception e) { log.severe("Exception sending have message to peer " + p + ": " + e); continue; } } } //getUpload(); this.optUnchoke = new Timer(); this.optUnchokingObj = new OptimisticUnchoking(this); this.optUnchoke.schedule(this.optUnchokingObj, 30000, 30000); rateCalculatorTimer = new Timer(); rtCalc = new rateCalculator(this); rateCalculatorTimer.schedule(rtCalc, 3000,1000); log.fine("Finished connecting to initial peers."); } /** * Resume download. */ public void resumeDownload(){ RUBTClient.log("Resuming download"); this.isRunning = true; this.curRequestedBitfield = new boolean[this.torrentInfo.piece_hashes.length]; this.piecePrevalence = new int[this.torrentInfo.piece_hashes.length]; this.peers = new ArrayList<Peer>(); Arrays.fill(this.curRequestedBitfield, false); Arrays.fill(this.piecePrevalence, 0); ArrayList<Peer> allPeers = this.tracker.update("started", this); this.tracker.timer = new Timer(); this.tracker.trackerUpdate = new TrackerUpdate(this.tracker, this); this.tracker.timer.schedule(this.tracker.trackerUpdate, this.tracker.interval * 1000, this.tracker.interval * 1000); if (allPeers != null) { for (Peer p : allPeers) { log.fine(p.toString()); if (!p.init()) { log.severe("Wrong Peer IP or unable to contact peer" + p); } else { this.peers.add(p); } } RUBTClient.setNumPeers(peers.size()); } this.start(); log.info("Resumed download"); } /** * Pause download. */ public void pauseDownload(){ RUBTClient.log("Pausing download"); this.isRunning = false; RUBTClient.setAmountDownloaded(0); RUBTClient.setAmountUploaded(0); if (this.peers != null) { for (Peer p : this.peers) { try { p.disconnect(); } catch (Exception e) { log.severe("Exception while shutting-down peer " + p + " : " + e); continue; } } } this.peers = null; this.tracker.update("stopped", this); this.tracker.timer.cancel(); log.info("Paused download"); } /** * Recieve message. * * Adds message to a queue to be handled * * @param message the message */ public synchronized void recieveMessage(PeerMessage message) { if (message == null) { log.warning("Null messages should not be handed to the Manager."); return; } this.messageQueue.add(message); log.info("Added Message: " + message); } /** * Checks if is file complete. * * @return true, if is file complete */ public boolean isFileComplete() { for(int i = 0; i < this.ourBitfield.length; i++){ if(this.ourBitfield[i] == false){ return false; } } return true; } /** * Decode. * * Handles decoding messages as they are recieved from the * peers * * @throws Exception the exception */ public void decode() throws Exception { PeerMessage peerMsg; if ((peerMsg = this.messageQueue.take()) != null) { log.info("Decoding message: " + peerMsg.message); switch (peerMsg.message.id) { case Message.chokeID: peerMsg.peer.peerChokingUs = true; break; case Message.unchokeID: peerMsg.peer.peerChokingUs = false; if(peerMsg.peer.weAreInterestedInPeer == true){ peerMsg.peer.sendMessage(peerMsg.peer.getNextRequest()); } break; case Message.interestedID: peerMsg.peer.peerInterestedInUs = true; peerMsg.peer.sendMessage(new Message(1, Message.unchokeID)); break; case Message.uninterestedID: peerMsg.peer.peerInterestedInUs = false; break; case Message.haveID: if (peerMsg.peer.bitfield != null) peerMsg.peer.bitfield[((Message.Have) peerMsg.message).index] = true; if(peerMsg.peer.bitfield!= null) { for(int j = 0; j < peerMsg.peer.bitfield.length; j++){ if(peerMsg.peer.bitfield[j] == true && this.ourBitfield[j] == false){ peerMsg.peer.sendMessage(Message.INTERESTED); peerMsg.peer.weAreInterestedInPeer = true; break; } } } break; case Message.bitfieldID: boolean[] bitfield = Utils.bitfieldToBoolArray(((Message.Bitfield)peerMsg.message).bitfield, this.torrentInfo.piece_hashes.length); boolean isSeed = true; for (int i = 0; i < bitfield.length; ++i) { peerMsg.peer.bitfield[i] = bitfield[i]; if(!bitfield[i]) isSeed = false; } for(int j = 0; j < peerMsg.peer.bitfield.length; j++){ if(peerMsg.peer.bitfield[j] == true && this.ourBitfield[j] == false){ peerMsg.peer.sendMessage(Message.INTERESTED); peerMsg.peer.weAreInterestedInPeer = true; break; } } if(isSeed) { RUBTClient.addSeed(); } break; case Message.pieceID: Message.Have haveMsg = new Message.Have(((Message.Piece)peerMsg.message).index); //peerMsg.peer.downloadRate += ((Message.Piece)peerMsg.message).block.length; if (!this.ourBitfield[((Message.Piece)peerMsg.message).index]) { if (peerMsg.peer.appendToPieceAndVerifyIfComplete((Message.Piece)peerMsg.message, this.torrentInfo.piece_hashes, this) == true) { this.ourBitfield[((Message.Piece) (peerMsg.message)).index] = true; RUBTClient.addProgressBar(1); for (Peer p : this.peers) { try { p.sendMessage(haveMsg); } catch (Exception e) { log.severe("Exception sending have message to peer " + p + ": " + e); continue; } } } } if (this.isFileComplete()) { //log.fine("Completed download: shutting down manager"); this.downloadingStatus = false; this.tracker.update("completed", this); RUBTClient.log("Finished downloading. Will now seed."); RUBTClient.toggleProgressBarLoading(); return; } // totalDownload += (((Message.Piece)peerMsg.message).block.length / (System.currentTimeMillis() - currentDownloadTime)/10.0); // downloadCount++; //RUBTClient.setDownloadRate((totalDownload/downloadCount)); //currentDownloadTime = System.currentTimeMillis(); //RUBTClient.updatePeerDownRate(peerMsg.peer, peerMsg.peer.downloadRate/10.0); rateCalulatorTotalDownload += ((Message.Piece)peerMsg.message).length; //Tracker.downloaded+=((Message.Piece)peerMsg.message).length; if(!peerMsg.peer.peerChokingUs) peerMsg.peer.sendMessage(peerMsg.peer.getNextRequest()); break; case Message.cancelID: log.info("Not responsible for cancel...."); break; default: break; } } else return; } /** * Update file. * * Updates the file with a piece if the piece verifies sha1 check and updates tracker progress/downloaded * * @param piece the piece * @param SHA1Hash the sH a1 hash * @param data the data * @return true, if successful * @throws Exception the exception */ public boolean UpdateFile(Message.Piece piece, ByteBuffer SHA1Hash, byte[] data) throws Exception { if (verifySHA1(data, SHA1Hash, piece.index)) { RandomAccessFile raf = new RandomAccessFile(this.outputFile, "rws"); raf.seek((this.torrentInfo.piece_length * piece.index)); raf.write(data); raf.close(); Tracker.downloaded += data.length; RUBTClient.addProgress(data.length); RUBTClient.addAmountDownloaded(data.length); RUBTClient.log("Saved piece " + piece.index); log.info("Wrote to output file piece: " + piece.index); return true; } else{ numHashFails++; RUBTClient.increaseNumHashFails(numHashFails); log.info("Failed to verify piece: " + piece.index); return false; } } /** * Read file. * * Reads a piece from file on the disk in instances * where we are uploading a piece or if we are * checking the pieces of the file we already have * * @param index the index * @param offset the offset * @param length the length * @return the byte[] * @throws IOException Signals that an I/O exception has occurred. */ public byte[] readFile(int index, int offset, int length) throws IOException { RandomAccessFile raf = new RandomAccessFile(this.outputFile, "r"); byte[] data = new byte[length]; raf.seek(this.torrentInfo.piece_length * index + offset); raf.readFully(data); raf.close(); return data; } /* (non-Javadoc) * @see java.lang.Thread#run() */ @Override public void run() { log.info("##### Manager has started. #####"); while (this.isRunning == true) { try { decode(); } catch (Exception e) { log.severe("Error decoding message " + e); } } log.info("##### Manager has finished. #####"); } /** * Verify SHA1. * * Verifies the SHA1 of a piece of a file against the info hash * values * * @param piece the piece * @param SHA1Hash the sH a1 hash * @return true, if successful */ public static boolean verifySHA1(byte[] piece, ByteBuffer SHA1Hash, int index) { MessageDigest SHA1; try { SHA1 = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException e) { log.severe("Unable to find SHA1 Algorithm"); RUBTClient.log("Unable to find SHA1 Algorithm"); return false; } SHA1.update(piece); byte[] pieceHash = SHA1.digest(); if (Arrays.equals(pieceHash, SHA1Hash.array())) { log.info("Verified - " + SHA1.digest() + " - " + SHA1Hash.array() + " for index " + index); return true; } else { return false; } } /** * Set the download upload. * * Writes the downloaded uploaded stats to a file when * we stop the program * * @param download the download * @param upload the upload */ public void setDownloadUpload(int download, int upload) { String down = Integer.toString(download); String up = Integer.toString(upload); try { File tFile = new File(outputFile.getName() + ".stats"); BufferedWriter out = new BufferedWriter(new FileWriter(tFile)); out.write(up); out.close(); } catch (IOException e) { log.severe("Error: Unable to write tracker stats to a file."); } } /** * Check pieces. * * Check the pieces of the file we have on disk so that we can increment * downloaded stats and send appropriate bitfield * * @return the boolean[] * @throws IOException Signals that an I/O exception has occurred. */ public boolean[] checkPieces() throws IOException { int numPieces = this.torrentInfo.piece_hashes.length; int pieceLength = this.torrentInfo.piece_length; int fileLength = this.torrentInfo.file_length; ByteBuffer[] pieceHashes = this.torrentInfo.piece_hashes; int lastPieceLength = fileLength % pieceLength == 0 ? pieceLength : fileLength % pieceLength; byte[] piece = null; boolean[] verifiedPieces = new boolean[numPieces]; for (int i = 0; i < numPieces; i++) { if (i != numPieces - 1) { piece = new byte[pieceLength]; piece = readFile(i, 0, pieceLength); } else { piece = new byte[lastPieceLength]; piece = readFile(i, 0, lastPieceLength); } if (verifySHA1(piece, pieceHashes[i], i)) { verifiedPieces[i] = true; RUBTClient.log("Verified piece " + i); } } for(int i = 0; i < verifiedPieces.length; i++){ if(verifiedPieces[i] != false){ RUBTClient.addProgress(torrentInfo.piece_length); } if(i == verifiedPieces.length - 1){ this.downloadingStatus = false; } } return verifiedPieces; } /** * Gets the download upload. * * Grabs the download/upload stats from the file * * @return the download upload * @throws IOException Signals that an I/O exception has occurred. */ public void getUpload() throws IOException { String input; File tFile = new File(outputFile.getName() + ".stats"); BufferedReader in = null; if (tFile.exists()) { in = new BufferedReader(new FileReader(tFile)); input = in.readLine(); Tracker.uploaded = Integer.parseInt(input); log.info("Increased upload amount"); RUBTClient.addAmountUploaded(Tracker.uploaded); } else { Tracker.downloaded = 0; Tracker.uploaded = 0; } if(in != null) { in.close(); } } }