/*
* TrackerClient - Class that informs a tracker and gets new peers. 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.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Informs metainfo tracker of events and gets new peers for peer coordinator.
*
* @author Mark Wielaard (mark@klomp.org)
*/
public class TrackerClient extends Thread
{
private static final String NO_EVENT = "";
private static final String STARTED_EVENT = "started";
private static final String COMPLETED_EVENT = "completed";
private static final String STOPPED_EVENT = "stopped";
private final static int SLEEP = 1; // Check in with tracker every minute
private final MetaInfo meta;
private final PeerCoordinator coordinator;
private final int port;
private boolean stop;
private long interval;
private long lastRequestTime;
private String currentAnnounce; // Tracker in use (if announce fails, try announce-list)
public TrackerClient (MetaInfo meta, PeerCoordinator coordinator, int port)
{
// Set unique name.
super("TrackerClient-" + urlencode(coordinator.getID()));
this.meta = meta;
this.coordinator = coordinator;
// XXX - No way to actaully give the tracker feedback that we
// don't run a peer acceptor on any port so use discard 9/tcp sink null
this.port = (port == -1) ? 9 : port;
stop = false;
}
/**
* Interrupts this Thread to stop it.
*/
public void halt ()
{
stop = true;
this.interrupt();
}
@Override
public void run ()
{
// XXX - Support other IPs
String announce = meta.getAnnounce();
this.currentAnnounce = announce;
String infoHash = urlencode(meta.getInfoHash());
String peerID = urlencode(coordinator.getID());
long uploaded = coordinator.getUploaded();
long downloaded = coordinator.getDownloaded();
long left = coordinator.getLeft();
boolean completed = coordinator.completed();
boolean started = false;
try {
int failures = 0;
int announceIndex = -1;
while (!started) {
if(failures >= MAX_FAILURE_COUNT) {
List<String> announceList = meta.getAnnounceList();
announceIndex++;
if(announceList == null || announceIndex == announceList.size())
break;
else {
failures = 0;
announce = announceList.get(announceIndex);
this.currentAnnounce = announce;
}
}
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) {
//ioe.printStackTrace();
// Probably not fatal (if it doesn't last to long...)
log.log(Level.WARNING, "Could not contact tracker at '"
+ announce, ioe);
}
if (!started) {
failures++;
log.log(Level.FINER, " Retrying in 5s...");
try {
// Sleep five seconds...
Thread.sleep(5 * 1000);
} catch (InterruptedException interrupt) {
// ignore
}
}
}
if (failures >= MAX_FAILURE_COUNT) {
throw new IOException("Could not establish initial connection");
}
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 && coordinator.completed()) {
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...)
log.log(Level.WARNING, "Could not contact tracker at '"
+ announce, ioe);
}
}
}
} catch (Throwable t) {
log.log(Level.SEVERE, "Fatal exception in TrackerClient", t);
} finally {
try {
if (started) {
doRequest(announce, infoHash, peerID, uploaded, downloaded,
left, STOPPED_EVENT);
}
} catch (IOException ioe) { /* ignored */
}
}
}
/**
* Request updated to send 'compact=1' to tell the tracker that
* the client accepts binary encoded peer lists [LIMA].
*/
private TrackerInfo doRequest (String announce, String infoHash,
String peerID, long uploaded, long downloaded, long left, String event)
throws IOException
{
String s = announce + "?info_hash=" + infoHash + "&peer_id=" + peerID
+ "&port=" + port + "&uploaded=" + uploaded + "&downloaded="
+ downloaded + "&left=" + left
+ ((event != NO_EVENT) ? ("&event=" + event) : "")
+ "&compact=1";
URL u = new URL(s);
log.log(Level.FINE, "Sending TrackerClient request: " + u);
URLConnection c = u.openConnection();
c.connect();
InputStream in = c.getInputStream();
if (c instanceof HttpURLConnection) {
// Check whether the page exists
int code = ((HttpURLConnection)c).getResponseCode();
if (code == HttpURLConnection.HTTP_FORBIDDEN) {
throw new IOException("Tracker doesn't handle given info_hash");
} else if (code / 100 != 2) {
throw new IOException("Loading '" + s + "' gave error code "
+ code + ", it probably doesn't exist");
}
}
TrackerInfo info = new TrackerInfo(in, coordinator.getID(),
coordinator.getMetaInfo());
log.log(Level.FINE, "TrackerClient response: " + info);
lastRequestTime = System.currentTimeMillis();
String failure = info.getFailureReason();
if (failure != null) {
throw new IOException(failure);
}
interval = info.getInterval() * 1000;
return info;
}
/**
* Very lazy byte[] to URL encoder. Just encodes everything, even "normal"
* chars.
*/
static String urlencode (byte[] bs)
{
StringBuffer sb = new StringBuffer(bs.length * 3);
for (byte element : bs) {
int c = element & 0xFF;
sb.append('%');
if (c < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(c));
}
return sb.toString();
}
/** The Java logger used to process our log events. */
protected static final Logger log = Logger.getLogger("org.klomp.snark.TrackerClient");
/**
* The maximum number of times that we are allowed to fail to make an
* initial contact with the tracker before we bail
*/
protected static final int MAX_FAILURE_COUNT = 2;
}