/**
* Created : May 23, 2012
*
* @author pquiring
*/
import java.io.*;
import java.net.*;
import java.util.*;
import java.security.*;
import javaforce.*;
public class TorrentClient extends Thread {
public static final String clientVersion = "0002"; //must be 4 digits
private static final int FRAGSIZE = 16 * 1024; //16k
private static final int MAXPEERS = 30; //max # active peers
private static final int NUMWANT = 80; //# peers requested from tracker (advisory)
public boolean log = false; //lots of debugging info
public String torrent, dest;
public boolean done = false;
public boolean active = true;
public boolean paused = false;
public int downSpeed = 0;
public int upSpeed = 0;
public long downAmount = 1; //must not be zero - causes / by zero
public long upAmount = 0;
public long downAmountCnt = 0;
public long upAmountCnt = 0;
public int seeders;
public float available = 0.0f;
//metainfo data
private byte metaData[];
private MetaFile metaFile;
public String status;
private boolean have[];
private int haveCnt = 0;
private String announce;
private ArrayList<String> announceList = new ArrayList<String>();
private byte info_hash[];
public long totalLength = 1; //must not be zero - causes / by zero
private long pieceLength;
private long lastPieceLength; //need to calc
public String name;
private int noFiles;
private ArrayList<TFile> files = new ArrayList<TFile>();
private byte pieces[][]; //SHA1
private int numFragsPerPiece;
private final int FRAGSTACK = 3;
//client info
private String peer_id;
//tracker data
private int complete;
private int incomplete;
private int interval, intervalCounter = 0;
private int pruneCounter = 120;
private byte[] peers;
private ArrayList<Peer> peerList = new ArrayList<Peer>();
private int peerIdx = 0;
private int peerActiveCount = 0;
private final Object peerListLock = new Object();
private Timer timer = new Timer();
private boolean registered = false;
private long now;
private final int CHOKE = 0;
private final int UNCHOKE = 1;
private final int INTERESTED = 2;
private final int NOTINTERESTED = 3;
private final int HAVE = 4;
private final int BITFIELD = 5;
private final int REQUEST = 6; //request fragment
private final int FRAGMENT = 7; //deliver fragment - piece of a piece (I call them fragments)
private final int CANCEL = 8;
private final int DHT_PORT = 9;
/** File in this torrent */
private static class TFile {
public String name;
public long length;
public RandomAccessFile file;
public final Object lock = new Object();
public void mkdirs(String dest) {
String filename = dest + "/" + name;
int idx = filename.lastIndexOf("/");
if (idx == -1) return;
new File(filename.substring(0, idx)).mkdirs();
}
}
/** Peer for this torrent */
private static class Peer {
public boolean inuse = false;
public boolean seeder = false;
public int available = 0;
public String ip, id;
public int port;
public Socket s;
public InputStream is;
public OutputStream os;
public final Object lock = new Object();
public boolean am_choking = true, am_interested = false;
public boolean peer_choking = true, peer_interested = false;
public boolean haveHandshake = false;
public PeerListener listener;
public PeerDownloader downloader;
public long lastMsg;
public boolean have[];
public byte piece[];
public boolean haveFrags[];
public int gotFragCnt, numFrags;
public int downloadingPieceIdx = -1;
public final Object chokeLock = new Object();
public final Object fragLock = new Object();
public int lastFragLength;
public int pendingFrags;
}
public static void main(String args[]) {
if (args.length != 2) {
System.out.println("Usage : TorrentClient torrent destination");
System.exit(1);
}
JFLog.init(JF.getUserPath() + "/.jftorrent.log", true);
TorrentClient client = new TorrentClient(args[0], args[1], true, false);
client.start();
}
public byte[] getHash() {
return info_hash;
}
public int getNumPeers() {
return peerList.size();
}
public TorrentClient(String torrent, String dest, boolean active, boolean paused) {
this.active = active;
this.paused = paused;
this.torrent = torrent;
this.dest = dest;
}
public void run() {
try {
status = "Reading torrent file";
//generate peer id (random)
peer_id = "-JT" + clientVersion + "-";
Random r = new Random();
while (peer_id.length() != 20) {
peer_id += (char)(r.nextBoolean() ? 'a' : 'A' + r.nextInt(26));
}
//read torrent
if (log) JFLog.log("Reading torrent:" + torrent);
readMeta();
if (log) JFLog.log("Getting info...");
Object info = metaFile.getTag(new String[] {"d", "s:info"});
info_hash = SHA1sum(Arrays.copyOfRange(metaData, metaFile.tagBegin, metaFile.tagEnd+1));
if (log) JFLog.log("info_hash = " + escape(info_hash));
announce = new String((byte[])metaFile.getTag(new String[] {"d", "s:announce"}), "UTF-8");
if (log) JFLog.log("announce=" + announce);
ArrayList<MetaTag> aList = (ArrayList<MetaTag>)metaFile.getTag(new String[] {"d", "s:announce-list"});
if (aList != null) {
for(int a=0;a<aList.size();a++) {
String str = concatList((ArrayList<MetaTag>)aList.get(a).obj, false);
announceList.add(str);
// if (log) JFLog.log("list=" + str);
}
}
Long len = (Long)metaFile.getTag(new String[] {"d", "d:info", "i:length"});
if (len != null) {
totalLength = len;
} else {
totalLength = -1;
}
if (log) JFLog.log("length=" + totalLength);
pieceLength = (Long)metaFile.getTag(new String[] {"d", "d:info", "i:piece length"});
if (log) JFLog.log("piece_length=" + pieceLength);
numFragsPerPiece = (int)(pieceLength / FRAGSIZE);
if (numFragsPerPiece * FRAGSIZE != pieceLength) throw new Exception("bad piece_length (not multiple of FRAGSIZE)");
name = new String((byte[])metaFile.getTag(new String[] {"d", "d:info", "s:name"}), "UTF-8");
if (log) JFLog.log("name=" + name);
byte[] piecesArray = (byte[])metaFile.getTag(new String[] {"d", "d:info", "s:pieces"});
int noPieces = piecesArray.length / 20;
if (log) JFLog.log("# pieces=" + noPieces);
have = new boolean[noPieces];
pieces = new byte[noPieces][];
for(int a=0;a<noPieces;a++) {
pieces[a] = new byte[20];
System.arraycopy(piecesArray, a * 20, pieces[a], 0, 20);
}
ArrayList<MetaTag> filesList = (ArrayList<MetaTag>)metaFile.getTag(new String[] {"d", "d:info", "l:files"});
if (filesList != null) {
if (log) JFLog.log("filesList");
noFiles = filesList.size();
long filesLength = 0;
for(int a=0;a<noFiles;a++) {
ArrayList<MetaTag> fileDict = (ArrayList<MetaTag>)filesList.get(a).obj;
ArrayList<MetaTag> fileName = (ArrayList<MetaTag>)metaFile.getTag(new String[]{"s:path"}, fileDict);
TFile tfile = new TFile();
tfile.name = concatList(fileName, true);
tfile.length = (Long)metaFile.getTag(new String[] {"i:length"}, fileDict);
filesLength += tfile.length;
files.add(tfile);
if (log) JFLog.log("file[]=" + tfile.name + ":length=" + tfile.length);
}
if (totalLength == -1) {
totalLength = filesLength;
}
} else {
if (log) JFLog.log("filesList=null");
noFiles = 1;
TFile tfile = new TFile();
tfile.name = name;
tfile.length = totalLength;
files.add(tfile);
}
if (log) JFLog.log("noFiles=" + noFiles);
lastPieceLength = totalLength % pieceLength;
if (lastPieceLength == 0) lastPieceLength = pieceLength;
if (log) JFLog.log("lastPieceLength=" + lastPieceLength);
//what are we doing?
checkFiles();
if (log) JFLog.log("haveCnt=" + haveCnt);
//create a timer to get info from announce[-list]
status = "Contacting tracker";
timer.schedule(new TimerTask() {
public void run() {
now = System.currentTimeMillis();
try {
if (intervalCounter <= 0) {
contactTracker(peerList.size() == 0 ? "started" : "");
intervalCounter = interval;
} else {
intervalCounter--;
}
if (!active) return;
if (paused) return;
pruneCounter--;
if (pruneCounter == 0) {
pruneCounter = 120;
prunePeers();
}
if (done) return;
addPeer();
} catch (Exception e) {
if (log) JFLog.log(e);
}
}
}, 1000, 1000);
} catch (Exception e) {
status = "Error";
if (log) JFLog.log(e);
}
}
private void readMeta() throws Exception {
FileInputStream fis = new FileInputStream(torrent);
metaData = JF.readAll(fis);
fis.close();
metaFile = new MetaFile();
metaFile.read(metaData);
}
private String concatList(ArrayList<MetaTag> list, boolean isFilePath) throws Exception {
int size = list.size();
StringBuilder str = new StringBuilder();
for(int a=0;a<size;a++) {
if ((a > 0) || (isFilePath)) str.append("/");
str.append(new String((byte[])list.get(a).obj, "UTF-8"));
}
return str.toString();
}
private void checkFiles() {
byte piece[] = new byte[(int)pieceLength];
long poff = 0;
int pidx = 0;
boolean bad = false; //part of piece is missing
try {
for(int a=0;a<noFiles;a++) {
TFile tfile = files.get(a);
String filename = dest + "/" + tfile.name;
File file = new File(filename);
if (file.exists()) {
tfile.file = new RandomAccessFile(filename, "rw");
long torrentLength = tfile.length; //file length according to torrent info
long realLength = tfile.file.length(); //current file's real length
int toRead = (int)(pieceLength - poff); //length to read rest of current piece
if (pidx == pieces.length-1) {
toRead = (int)(lastPieceLength - poff);
}
if ((torrentLength < toRead) || (realLength < toRead)) {
if (torrentLength == realLength) {
JF.readAll(tfile.file, piece, (int)poff, (int)torrentLength);
poff += torrentLength;
} else {
JFLog.log("Warning : piece bad : " + pidx + " : it will be redownloaded.");
bad = true;
poff += tfile.length;
while (poff >= pieceLength) {
bad = false;
poff -= pieceLength;
pidx++;
}
}
} else {
boolean part = false;
while ((torrentLength > 0) && (realLength > 0)) {
toRead = (int)(pieceLength - poff);
if (pidx == pieces.length-1) {
toRead = (int)(lastPieceLength - poff);
}
if (toRead > torrentLength) {toRead = (int)torrentLength; part = true;}
if (toRead > realLength) {toRead = (int)realLength; part = true;}
JF.readAll(tfile.file, piece, (int)poff, toRead);
if (part) {
poff += toRead;
break;
}
if (bad) {
bad = false;
} else {
int thisPieceLength = (int)pieceLength;
if (pidx == pieces.length-1) {
thisPieceLength = (int)lastPieceLength;
}
byte hash[] = SHA1sum(Arrays.copyOfRange(piece,0,thisPieceLength));
if (Arrays.equals(hash, pieces[pidx])) {
have[pidx] = true;
haveCnt++;
if (pidx == pieces.length-1) {
downAmount += lastPieceLength;
} else {
downAmount += pieceLength;
}
}
}
poff = 0;
pidx++;
torrentLength -= toRead;
realLength -= toRead;
}
if (torrentLength > 0) {
poff += torrentLength;
while (poff >= pieceLength) {
poff -= pieceLength;
pidx++;
}
bad = poff != 0;
}
}
} else {
poff += tfile.length;
while (poff >= pieceLength) {
poff -= pieceLength;
pidx++;
}
bad = poff != 0;
}
}
} catch (Exception e) {
JFLog.log(e);
}
done = haveCnt == pieces.length;
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
MainPanel.This.updateList();
}
});
if (done) status = "Seeding";
}
public static byte[] SHA1sum(byte[] data) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance("SHA-1");
return md.digest(data);
}
//URLEncoder only works with String's
private static String escape(byte[] hash) {
StringBuilder str = new StringBuilder();
for(int a=0;a<hash.length;a++) {
if ((hash[a] >= '0') && (hash[a] <= '9')) {
str.append((char)hash[a]);
continue;
}
if ((hash[a] >= 'a') && (hash[a] <= 'z')) {
str.append((char)hash[a]);
continue;
}
if ((hash[a] >= 'A') && (hash[a] <= 'Z')) {
str.append((char)hash[a]);
continue;
}
switch (hash[a]) {
case '.':
case '-':
case '_':
case '~':
str.append((char)hash[a]);
continue;
}
str.append("%" + String.format("%02x", (((int)hash[a]) & 0xff)));
}
return str.toString();
}
private String getip(byte data[], int pos) {
return "" + ((int)data[pos++] & 0xff) + "." + ((int)data[pos++] & 0xff) + "."
+ ((int)data[pos++] & 0xff) + "." + ((int)data[pos++] & 0xff);
}
private int getport(byte data[], int pos) {
int ret = ((int)data[pos]) & 0xff;
ret <<= 8;
ret += ((int)data[pos+1]) & 0xff;
return ret;
}
private void contactTracker(String event) throws Exception {
if (announce.startsWith("http://")) {
URL url = new URL(announce + "?info_hash=" + escape(info_hash) + "&peer_id=" + peer_id + "&port=" + TorrentServer.port +
"&uploaded=" + upAmount + "&downloaded=" + downAmount + "&left=" + (totalLength - downAmount) + "&compact=1&numwant=" + NUMWANT + "&event=" + event);
if (log) JFLog.log("url=" + url.toExternalForm());
HttpURLConnection uc = (HttpURLConnection)url.openConnection();
uc.setReadTimeout(30 * 1000);
uc.connect();
InputStream is = uc.getInputStream();
int length = uc.getContentLength();
metaData = JF.readAll(is, length);
uc.disconnect();
if (log) JFLog.log("response=" + new String(metaData));
metaFile.read(metaData);
byte failure[] = (byte[])metaFile.getTag(new String[] {"d", "s:failure reason"});
if (failure != null) {
throw new Exception(new String(failure, "UTF-8"));
}
if (event.equals("stopped") || event.equals("completed")) return;
complete = ((Long)metaFile.getTag(new String[] {"d", "i:complete"})).intValue();
incomplete = ((Long)metaFile.getTag(new String[] {"d", "i:incomplete"})).intValue();
interval = ((Long)metaFile.getTag(new String[] {"d", "i:interval"})).intValue();
peers = (byte[])metaFile.getTag(new String[] {"d", "s:peers"});
if (peers == null) throw new Exception("no peers");
int noPeers = peers.length/6;
for(int a=0;a<noPeers;a++) {
Peer p = new Peer();
p.ip = getip(peers, a*6);
p.port = getport(peers, a*6 + 4);
boolean found = false;
int size = peerList.size();
for(int b=0;b<size;b++) {
Peer pp = peerList.get(b);
if ((pp.ip.equals(p.ip)) && (pp.port == p.port)) {
found = true;
break;
}
}
if (!found) {
peerList.add(p);
if (log) JFLog.log("peer[]=" + p.ip + ":" + p.port);
}
}
if (!registered) {
TorrentServer.register(this);
registered = true;
}
if (done) {
//may be 1st client seeding this torrent
status = "Seeding";
return;
}
status = "Downloading";
if (peerList.isEmpty()) throw new Exception("no peers");
} else if (announce.startsWith("udp://")) {
throw new Exception("UDP protocol not supported yet");
} else throw new Exception("bad announce URL");
}
private void addPeer() throws Exception {
if (peerActiveCount >= MAXPEERS) return;
Peer peer;
synchronized(peerListLock) {
peer = peerList.get(peerIdx++);
if (peerIdx >= peerList.size()) peerIdx = 0;
if (peer.inuse) return;
if (log) JFLog.log("Connecting to peer:" + peer.ip + ":" + peer.port);
peer.lastMsg = now;
peer.inuse = true;
peerActiveCount++;
}
peer.haveHandshake = false;
peer.listener = new PeerListener(peer);
peer.listener.start();
}
public void addPeer(Socket s, String id) throws Exception {
//inbound peer from TorrentServer
if (peerActiveCount >= MAXPEERS) {
s.close();
return;
}
Peer peer = null;
String ip = s.getInetAddress().toString().substring(1);
int port = s.getPort();
synchronized(peerListLock) {
for(int a=0;a<peerList.size();a++) {
Peer pp = peerList.get(a);
if (pp.ip.equals(ip) && pp.port == port) {
peer = peerList.get(a);
break;
}
}
if ((peer == null) || (peer.inuse)) {
peer = new Peer();
peerList.add(peer);
}
peer.lastMsg = now;
peer.inuse = true;
}
peer.haveHandshake = true;
peer.ip = ip;
peer.s = s;
peer.port = port;
peer.id = id;
peer.is = s.getInputStream();
peer.os = s.getOutputStream();
peer.listener = new PeerListener(peer);
peer.listener.start();
}
private void prunePeers() {
long now_2mins = System.currentTimeMillis() - 120 * 1000;
synchronized(peerListLock) {
int size = peerList.size();
for(int a=0;a<size;a++) {
Peer peer = peerList.get(a);
if (!peer.inuse) continue;
if (peer.listener == null) continue;
if (peer.lastMsg < now_2mins) {
peer.listener.close();
}
}
}
}
private class PeerListener extends Thread {
private Peer peer;
public volatile boolean listenerActive = true;
public PeerListener(Peer peer) {
this.peer = peer;
}
public void run() {
try {
if (log) JFLog.log("Starting PeerListener:" + peer.ip);
if (peer.s == null) {
peer.s = new Socket(peer.ip, peer.port);
peer.is = peer.s.getInputStream();
peer.os = peer.s.getOutputStream();
// if (log) JFLog.log("send handshake:" + peer.ip);
}
//send handshake
byte handshake[] = new byte[68];
handshake[0] = 19;
System.arraycopy("BitTorrent protocol".getBytes(),0,handshake,1,19);
//8 reserved bytes
System.arraycopy(info_hash,0,handshake,28,20);
System.arraycopy(peer_id.getBytes(),0,handshake,48,20);
peer.os.write(handshake);
peer.os.flush();
if (!peer.haveHandshake) {
if (!getHandshake()) {
throw new Exception("handshake failed");
}
}
sendBitField();
// if (log) JFLog.log("Pooling PeerListener:" + peer.ip);
while (peer.s.isConnected()) {
byte msg[] = getMessage();
if (msg == null) break;
processMessage(msg);
}
} catch (ConnectException e1) {
if (log) JFLog.log("Lost connection:" + peer.ip);
} catch (Exception e2) {
if (log) JFLog.log(e2);
}
try { if (peer.s.isConnected()) peer.s.close(); } catch (Exception e1) { }
// if (log) JFLog.log("Stopping peer:" + peer.ip);
if (peer.downloader != null) {
peer.downloader.downloaderActive = false;
try { peer.downloader.join(); } catch (Exception e2) {}
peer.downloader = null;
}
peer.s = null;
peer.inuse = false;
synchronized (peerListLock) {
peerActiveCount--;
}
}
public void close() {
listenerActive = false;
try { peer.s.close(); } catch (Exception e) {}
}
private boolean getHandshake() {
byte buf[] = new byte[1024];
byte handshake[] = new byte[68];
byte peer_info_hash[] = new byte[20];
int toRead = 68;
int pos = 0;
// if (log) JFLog.log("Waiting for handshake:" + peer.ip);
try {
while (toRead > 0) {
int read = peer.is.read(buf, 0, toRead);
if (read <= 0) throw new Exception("read error:" + peer.ip);
System.arraycopy(buf, 0, handshake, pos, read);
pos += read;
toRead -= read;
}
if (handshake[0] != 19) throw new Exception("bad handshake (len!=19):" + peer.ip);
if (!new String(handshake, 1, 19).equals("BitTorrent protocol")) throw new Exception("bad handshake (unknown protocol):" + peer.ip);
System.arraycopy(handshake, 28, peer_info_hash, 0, 20);
if (!Arrays.equals(peer_info_hash, info_hash)) throw new Exception("not my torrent:" + peer.ip);
peer.id = new String(handshake, 48, 20);
//TODO : validate handshake more???
peer.haveHandshake = true;
return true;
} catch (Exception e) {
// if (log) JFLog.log("getHandshake Exception:" + peer.ip);
// if (log) JFLog.log(e);
}
return false;
}
private void sendBitField() throws Exception {
int bytes = have.length / 8;
if (bytes * 8 != have.length) bytes++;
byte bits[] = new byte[bytes + 1]; //+1 for header
bits[0] = BITFIELD;
int Bo = 1; //byte offset
int bo = 128; //bit offset
int pidx = 0;
int value = 0;
while (pidx < have.length) {
if (have[pidx++]) value |= bo;
bo >>= 1;
if (bo == 0) {bits[Bo++] = (byte)value; bo = 128; value = 0;}
}
if (bo != 128) {
bits[Bo] = (byte)value;
}
writePacket(peer.os, bits);
}
private static final int BUFSIZ = 2048;
private byte[] getMessage() {
byte buf[] = new byte[BUFSIZ];
byte len[] = new byte[4];
byte msg[];
int toRead = 4;
int pos = 0;
//read message length
try {
while (toRead > 0) {
int read = peer.is.read(buf, 0, toRead);
if (read == 0) continue;
if (read == -1) throw new Exception("read error:" + peer.ip);
System.arraycopy(buf, 0, len, pos, read);
pos += read;
toRead -= read;
}
int msglen = BE.getuint32(len, 0);
if ((msglen > FRAGSIZE + 1 + 4 + 4) && (msglen > (have.length / 8) + 1 + 1)) throw new Exception("msg too large:" + peer.ip);
msg = new byte[msglen];
toRead = msglen;
pos = 0;
while (toRead > 0) {
int read = peer.is.read(buf, 0, toRead > BUFSIZ ? BUFSIZ : toRead);
if (read == 0) continue;
if (read == -1) throw new Exception("read error:" + peer.ip);
System.arraycopy(buf, 0, msg, pos, read);
pos += read;
toRead -= read;
}
return msg;
} catch (Exception e) {
// if (log) JFLog.log("getMessage Exception:" + peer.ip);
// if (log) JFLog.log(e);
}
return null;
}
private void processMessage(byte msg[]) throws Exception {
peer.lastMsg = now;
if (msg.length == 0) {
//keep alive
return;
}
switch (msg[0]) {
case CHOKE:
peer.peer_choking = true;
break;
case UNCHOKE:
peer.peer_choking = false;
synchronized(peer.chokeLock) {peer.chokeLock.notify();}
break;
case INTERESTED:
peer.peer_interested = true;
peer.am_choking = false;
writePacket(peer.os, new byte[] {UNCHOKE});
break;
case NOTINTERESTED:
peer.peer_interested = false;
peer.am_choking = true;
writePacket(peer.os, new byte[] {CHOKE});
break;
case HAVE:
have(BE.getuint32(msg, 1));
break;
case BITFIELD: bitfield(msg); break;
case REQUEST: request(msg); break;
case FRAGMENT: fragment(msg); break;
case CANCEL: cancel(msg); break;
case DHT_PORT: dht_port(msg); break;
}
}
private void have(int pidx) {
if (peer.have[pidx]) return;
peer.have[pidx] = true;
if (!have[pidx]) synchronized(peer.chokeLock) {peer.chokeLock.notify();}
peer.available++;
if (peer.available == have.length) {
peer.seeder = true;
}
}
private void bitfield(byte msg[]) {
if (peer.downloader != null) return; //already got bitfield
peer.have = new boolean[pieces.length];
int Bo = 1;
int bo = 128;
int idx = 0;
int available = 0;
while (idx < peer.have.length) {
peer.have[idx] = (msg[Bo] & bo) == bo;
if (peer.have[idx]) available++;
idx++;
bo >>= 1;
if (bo == 0) {Bo++; bo = 128;}
}
peer.seeder = available == have.length;
peer.available = available;
if (done) return;
peer.downloader = new PeerDownloader(peer);
peer.downloader.start();
}
private void request(byte msg[]) throws Exception {
if (peer.am_choking) return;
int pidx = BE.getuint32(msg, 1);
int begin = BE.getuint32(msg, 5);
int length = BE.getuint32(msg, 9);
if (length > 65536) return;
upAmount += length;
upAmountCnt += length;
sendFragment(peer.os, pidx, begin, length);
}
private void fragment(byte msg[]) throws Exception {
// FRAGMENT PIDX(4) BEGIN(4)
int pidx = BE.getuint32(msg, 1);
if (pidx != peer.downloadingPieceIdx) {if (log) JFLog.log("frag:bad pidx:"+pidx);return;}
int begin = BE.getuint32(msg, 5);
int fidx = begin / FRAGSIZE;
if (fidx >= peer.numFrags) {if (log) JFLog.log("frag:bad fidx:"+fidx);return;}
int length = msg.length - 9;
if (peer.piece == null) {if (log) JFLog.log("frag:not ready(1)");return;}
if (begin + length > peer.piece.length) {if (log) JFLog.log("frag:bad length:"+fidx);return;}
synchronized(peer.fragLock) {
if (peer.haveFrags[fidx]) {if (log) JFLog.log("frag:already have fidx:"+fidx);return;}
System.arraycopy(msg, 9, peer.piece, begin, length);
if (log) JFLog.log("gotFrag:" + pidx + "," + fidx);
peer.haveFrags[fidx] = true;
peer.gotFragCnt++;
peer.pendingFrags--;
peer.fragLock.notify();
}
}
private void cancel(byte msg[]) {
}
private void dht_port(byte msg[]) {
}
}
private class PeerDownloader extends Thread {
private Peer peer;
public volatile boolean downloaderActive = true;
public PeerDownloader(Peer peer) {
this.peer = peer;
}
public void run() {
try {
if (log) JFLog.log("Starting PeerDownloader:" + peer.ip);
synchronized(peer.chokeLock) {
while (true) {
if (done) return;
if (!downloaderActive) return;
if (!peer.inuse) return;
//is there a piece we can get from them
int startIdx = Math.abs(new Random().nextInt(have.length)); //pure random baby
int pidx = startIdx;
boolean ok = false;
do {
if (!have[pidx] && peer.have[pidx]) {
ok = true;
break;
}
pidx++;
if (pidx == have.length) pidx = 0;
} while (pidx != startIdx);
if (!ok) {
if (peer.am_interested) {
writePacket(peer.os, new byte[] {NOTINTERESTED});
peer.am_interested = false;
}
peer.chokeLock.wait();
continue;
}
if (log) JFLog.log("want " + pidx + " from " + peer.ip);
if (!peer.am_interested) {
writePacket(peer.os, new byte[] {INTERESTED});
peer.am_interested = true;
}
while (peer.peer_choking) {
peer.chokeLock.wait();
}
//calc fragments
peer.downloadingPieceIdx = pidx;
peer.pendingFrags = 0;
if (pidx == have.length - 1) {
//last piece
peer.numFrags = (int)lastPieceLength / FRAGSIZE;
if (peer.numFrags * FRAGSIZE != pieceLength) {
peer.lastFragLength = (int)(lastPieceLength - (FRAGSIZE * peer.numFrags));
peer.numFrags++;
} else {
peer.lastFragLength = FRAGSIZE;
}
peer.piece = new byte[(int)lastPieceLength];
} else {
peer.numFrags = numFragsPerPiece;
peer.piece = new byte[(int)pieceLength];
peer.lastFragLength = FRAGSIZE;
}
peer.haveFrags = new boolean[peer.numFrags];
peer.gotFragCnt = 0;
int nextFrag = 0;
//now send frag requests (stack them up to FRAGSTACK)
if (log) JFLog.log("downloading fragments:#frags=" + peer.numFrags + ":from=" + peer.ip);
while (peer.gotFragCnt != peer.numFrags) {
synchronized (peer.fragLock) {
if ((peer.pendingFrags < FRAGSTACK) && (nextFrag < peer.numFrags)) {
requestFragment(nextFrag++);
peer.pendingFrags++;
}
if ((peer.pendingFrags >= FRAGSTACK) || (peer.gotFragCnt + FRAGSTACK >= peer.numFrags)) {
peer.fragLock.wait();
}
}
}
byte sha[] = SHA1sum(peer.piece);
if (log) {
JFLog.log("sha1.downloaded=" + escape(sha));
JFLog.log("sha1.peice =" + escape(pieces[peer.downloadingPieceIdx]));
}
if (Arrays.equals(sha, pieces[peer.downloadingPieceIdx])) {
savePiece(peer.downloadingPieceIdx, peer.piece);
if (done) return;
broadcastHave();
} else {
JFLog.log("bad piece downloaded from peer:" + peer.ip);
}
peer.downloadingPieceIdx = -1;
peer.piece = null;
}
}
} catch (Exception e) {
}
}
private void requestFragment(int fidx) throws Exception {
byte msg[] = new byte[1 + 4 + 4 + 4];
msg[0] = REQUEST;
BE.setuint32(msg, 1, peer.downloadingPieceIdx);
BE.setuint32(msg, 5, fidx * FRAGSIZE);
if (fidx == peer.haveFrags.length-1)
BE.setuint32(msg, 9, peer.lastFragLength);
else
BE.setuint32(msg, 9, FRAGSIZE);
writePacket(peer.os, msg);
if (log) JFLog.log("requestFrag:" + peer.downloadingPieceIdx + "," + fidx);
}
private void broadcastHave() {
byte msg[] = new byte[5];
msg[0] = HAVE;
BE.setuint32(msg, 1, peer.downloadingPieceIdx);
synchronized(peerListLock) {
for(int a=0;a<peerList.size();a++) {
Peer p = peerList.get(a);
if (!p.inuse) continue;
try {writePacket(p.os, msg);} catch (Exception e) {}
}
}
}
}
private void sendFragment(OutputStream os, int pidx, int fbegin, int flength) throws Exception {
//TODO : throttle bandwidth
byte frag[] = new byte[flength];
long begin = pidx * pieceLength + fbegin;
int fragOff = 0;
long pos = 0;
int toRead;
for(int a=0;a<files.size();a++) {
TFile tfile = files.get(a);
if (pos + tfile.length <= begin) {
pos += tfile.length;
continue;
}
long filePos = begin + fragOff - pos;
if (tfile.length - filePos < frag.length - fragOff) {
//read to end of file
synchronized(tfile.lock) {
if (tfile.file == null) {
tfile.file = new RandomAccessFile(dest + "/" + tfile.name, "rw");
}
tfile.file.seek(filePos);
toRead = (int)(tfile.length - filePos);
JF.readAll(tfile.file, frag, fragOff, toRead);
}
pos += tfile.length;
fragOff += toRead;
continue;
}
//read rest of fragment
synchronized(tfile.lock) {
if (tfile.file == null) {
tfile.file = new RandomAccessFile(dest + "/" + tfile.name, "rw");
}
tfile.file.seek(filePos);
toRead = (int)(frag.length - fragOff);
JF.readAll(tfile.file, frag, fragOff, toRead);
}
// pos += tfile.length;
// fragOff += toRead;
break;
}
byte msg[] = new byte[1 + 4 + 4 + frag.length];
msg[0] = FRAGMENT;
msg[1] = (byte)((pidx & 0xff000000) >>> 24);
msg[2] = (byte)((pidx & 0xff0000) >> 16);
msg[3] = (byte)((pidx & 0xff00) >> 8);
msg[4] = (byte)(pidx & 0xff);
msg[5] = (byte)((fbegin & 0xff000000) >>> 24);
msg[6] = (byte)((fbegin & 0xff0000) >> 16);
msg[7] = (byte)((fbegin & 0xff00) >> 8);
msg[8] = (byte)(fbegin & 0xff);
System.arraycopy(frag, 0, msg, 9, frag.length);
writePacket(os, msg);
}
private synchronized void savePiece(int pidx, byte piece[]) throws Exception {
//piece already validated
//piece may span files
if (have[pidx]) return; //already have this piece
if (log) JFLog.log(" --- savePiece --- idx=" + pidx);
long begin = pidx * pieceLength;
int pieceOff = 0;
long pos = 0;
int write;
for(int a=0;a<files.size();a++) {
TFile tfile = files.get(a);
if (pos + tfile.length < begin) {
pos += tfile.length;
continue;
}
long filePos = begin + pieceOff - pos;
if (tfile.length - filePos < piece.length - pieceOff) {
//write to end of file
synchronized(tfile.lock) {
if (tfile.file == null) {
tfile.mkdirs(dest);
tfile.file = new RandomAccessFile(dest + "/" + tfile.name, "rw");
}
tfile.file.seek(filePos);
write = (int)(tfile.length - filePos);
tfile.file.write(piece, pieceOff, write);
}
pos += tfile.length;
pieceOff += write;
continue;
}
//write rest of piece
synchronized(tfile.lock) {
if (tfile.file == null) {
tfile.mkdirs(dest);
tfile.file = new RandomAccessFile(dest + "/" + tfile.name, "rw");
}
tfile.file.seek(filePos);
write = (int)(piece.length - pieceOff);
tfile.file.write(piece, pieceOff, write);
}
break;
}
have[pidx] = true;
haveCnt++;
downAmount += piece.length;
downAmountCnt += piece.length;
if (haveCnt == pieces.length) {
status = "Seeding";
done = true;
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
MainPanel.This.updateList();
}
});
contactTracker("completed");
} //all downloaders will stop now
}
public void reset() {
downAmountCnt = 0;
upAmountCnt = 0;
}
public void update() {
seeders = 0;
available = 0.0f;
int size = peerList.size();
for(int a=0;a<size;a++) {
Peer peer = peerList.get(a);
if (peer.seeder) seeders++;
available += (float)peer.available / (float)have.length;
}
available = available * 100 / (float)size; //average availablity ???
//BUG : this available is not right - should not be an average ???
//what if all peers are missing the same piece - then available would be < 1.0 ???
downSpeed = (int)(downAmountCnt / 5);
downAmountCnt = 0;
upSpeed = (int)(upAmountCnt / 5);
upAmountCnt = 0;
}
private void writePacket(OutputStream os, byte msg[]) throws Exception {
byte packet[] = new byte[msg.length + 4];
packet[0] = (byte)((msg.length & 0xff000000) >>> 24);
packet[1] = (byte)((msg.length & 0xff0000) >> 16);
packet[2] = (byte)((msg.length & 0xff00) >> 8);
packet[3] = (byte)(msg.length & 0xff);
System.arraycopy(msg, 0, packet, 4, msg.length);
os.write(packet); //BUG : can you write large packets???
os.flush();
}
/** Closes the torrent, can not be re-started. */
public void close() {
if (timer == null) return;
timer.cancel();
timer = null;
if (registered) {
TorrentServer.unregister(this);
registered = false;
}
new Thread() {
public void run() {
try {contactTracker("stopped");} catch (Exception e) {}
}
}.start();
active = false;
}
public void pause() {
status = "Paused";
paused = true;
}
public void unpause() {
status = done ? "Seeding" : "Downloading";
paused = false;
}
/** Stops the torrent, but can be started again. */
public void _stop() {
if (!active) return;
active = false;
status = "Stopped";
//close all Peers
synchronized(peerListLock) {
for(int a=0;a<peerList.size();a++) {
Peer peer = peerList.get(a);
if (peer.listener != null) peer.listener.close();
}
}
}
public void _start() {
if (timer == null) return; //torrent was close()d
if (paused) {unpause(); return;}
status = done ? "Seeding" : "Downloading";
if (active) return;
active = true;
}
public void recontactTracker() {
//do this if TorrentServer.port changes
intervalCounter = 0;
}
}