/*
* 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.
*
* 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 java.net.MalformedURLException;
import org.texai.torrent.domainEntity.MetaInfo;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.Executors;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.codec.http.DefaultHttpRequest;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpMethod;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.util.CharsetUtil;
import org.texai.network.netty.handler.AbstractHTTPResponseHandler;
import org.texai.network.netty.pipeline.HTTPClientPipelineFactory;
import org.texai.util.NetworkUtils;
import org.texai.util.StringUtils;
import org.texai.util.TexaiException;
/** Informs metainfo tracker of events and gets new peers for peer coordinator.
*
* @author Mark Wielaard (mark@klomp.org)
*/
public final class TrackerClient extends AbstractHTTPResponseHandler implements Runnable {
/** the logger */
private static final Logger LOGGER = Logger.getLogger(TrackerClient.class);
/** the maximum number of times that we are allowed to fail to make an
* initial contact with the tracker before we bail
*/
private static final int MAX_FAILURE_COUNT = 2;
/** sleep interval */
private static final int SLEEP = 1; // Check in with tracker every minute
/** the torrent metainfo */
private final MetaInfo metaInfo;
/** the peer coordinator */
private final PeerCoordinator peerCoordinator;
/** our port that accepts connections from peers */
private final int ourConnectionPort;
/** the indicator whether to stop the tracker client */
private boolean isQuit = false;
/** the tracker request interval milliseconds */
private long trackerRequestIntervalMillis;
/** the last tracker request time */
private long lastTrackerRequestTime;
/** the tracker host */
private final String trackerHost;
/** the tracker port */
private final int trackerPort;
/** the tracker info */
private TrackerInfo trackerInfo;
/** the tracker info lock */
private final Object trackerInfo_lock = new Object();
/** Creates a new TrackerClient instance.
*
* @param metaInfo the torrent metainfo
* @param peerCoordinator the peer coordinator
* @param ourConnectionPort our port that accepts connections from peers
*/
public TrackerClient(
final MetaInfo metaInfo,
final PeerCoordinator peerCoordinator,
final int ourConnectionPort) {
//Preconditions
assert metaInfo != null : "metaInfo must not be null";
assert peerCoordinator != null : "peerCoordinator must not be null";
assert ourConnectionPort > 0 : "ourConnectionPort must be positive";
this.metaInfo = metaInfo;
this.peerCoordinator = peerCoordinator;
final URL announceURL;
try {
announceURL = new URL(metaInfo.getAnnounceURLString());
} catch (MalformedURLException ex) {
throw new TexaiException(ex);
}
trackerHost = announceURL.getHost() == null ? "localhost" : announceURL.getHost();
trackerPort = announceURL.getPort();
Thread.currentThread().setName("TrackerClient-" + peerCoordinator.getURLEncodedID());
this.ourConnectionPort = ourConnectionPort;
}
/** Interrupts this thread to stop it. */
public void quit() {
isQuit = true;
Thread.currentThread().interrupt();
}
/** Executes this thread. */
@Override
@SuppressWarnings("SleepWhileInLoop")
public void run() {
LOGGER.info("running " + this);
final String announceURLString = metaInfo.getAnnounceURLString();
final String urlEncodedInfoHash = metaInfo.getURLEncodedInfoHash();
LOGGER.info("url encoded info hash " + urlEncodedInfoHash);
final String urlEncodedPeerID = peerCoordinator.getURLEncodedID();
boolean isCompletedYetToBeSent = !peerCoordinator.isCompleted();
boolean isTrackerContacted = false;
long uploaded = peerCoordinator.getUploaded();
long downloaded = peerCoordinator.getDownloaded();
long left = peerCoordinator.getApproximateNbrBytesRemaining();
String peerIPAddress = null;
try {
final InetAddress localHostAddress = NetworkUtils.getLocalHostAddress();
if (localHostAddress.isLoopbackAddress()) {
LOGGER.info("local host address is the loopback address: " + localHostAddress);
} else {
peerIPAddress = localHostAddress.getHostAddress();
LOGGER.info("local host address: " + peerIPAddress);
}
int failures = 0;
while (!isTrackerContacted && failures < MAX_FAILURE_COUNT) {
// send start to tracker
request(
announceURLString,
urlEncodedInfoHash,
urlEncodedPeerID,
uploaded,
downloaded,
left,
peerIPAddress,
Tracker.STARTED_EVENT);
synchronized (trackerInfo_lock) {
trackerInfo_lock.wait(30000);
}
if (trackerInfo == null) {
LOGGER.warn("Could not contact tracker at " + announceURLString);
failures++;
continue;
}
// process response
final TrackedPeerInfo ourTrackedPeerInfo = peerCoordinator.getOurTrackedPeerInfo();
final Set<TrackedPeerInfo> trackedPeerInfos = trackerInfo.getTrackedPeerInfos();
if (trackedPeerInfos != null) {
final Iterator<TrackedPeerInfo> trackedPeerInfos_iter = trackedPeerInfos.iterator();
while (trackedPeerInfos_iter.hasNext()) {
final TrackedPeerInfo trackedPeerInfo = trackedPeerInfos_iter.next();
if (!trackedPeerInfo.equals(ourTrackedPeerInfo)) {
peerCoordinator.addPeerThatWeContact(trackedPeerInfo);
}
}
}
isTrackerContacted = true;
if (!isTrackerContacted) {
failures++;
LOGGER.info(" Retrying in 2s...");
try {
// Sleep two seconds...
Thread.sleep(2 * 1000);
} catch (InterruptedException interrupt) {
LOGGER.debug("ignored " + interrupt.getMessage());
}
}
}
if (failures >= MAX_FAILURE_COUNT) {
throw new IOException("Could not establish initial connection");
}
// periodically, e.g. every 15 minutes, contact the tracker
while (!isQuit) {
uploaded = peerCoordinator.getUploaded();
downloaded = peerCoordinator.getDownloaded();
left = peerCoordinator.getApproximateNbrBytesRemaining();
final String event;
if (isCompletedYetToBeSent && peerCoordinator.isCompleted()) {
isCompletedYetToBeSent = false;
event = Tracker.COMPLETED_EVENT;
} else {
event = Tracker.NO_EVENT;
}
if (event.equals(Tracker.COMPLETED_EVENT) ||
(peerCoordinator.areMorePeersNeeded() &&
System.currentTimeMillis() > lastTrackerRequestTime + trackerRequestIntervalMillis)) {
request(
announceURLString,
urlEncodedInfoHash,
urlEncodedPeerID,
uploaded,
downloaded,
left,
peerIPAddress,
event);
synchronized (trackerInfo_lock) {
trackerInfo_lock.wait(30000);
}
if (trackerInfo == null) {
LOGGER.warn("Could not contact tracker at " + announceURLString);
failures++;
continue;
}
final Iterator<TrackedPeerInfo> trackedPeerInfos_iter = trackerInfo.getTrackedPeerInfos().iterator();
while (trackedPeerInfos_iter.hasNext()) {
peerCoordinator.addPeerThatWeContact(trackedPeerInfos_iter.next());
}
final DownloadListener downloadListener = peerCoordinator.getDownloadListener();
if (event.equals(Tracker.COMPLETED_EVENT) && downloadListener != null) {
downloadListener.downloadCompleted(metaInfo);
}
}
try {
// Sleep some minutes...
Thread.sleep(SLEEP * 60 * 1000);
} catch (InterruptedException interrupt) {
LOGGER.debug("ignored " + interrupt.getMessage());
}
}
} catch (IOException | InterruptedException t) {
LOGGER.log(Level.ERROR, "fatal exception in TrackerClient", t);
} finally {
try {
if (isTrackerContacted) {
request(
announceURLString,
urlEncodedInfoHash,
urlEncodedPeerID,
uploaded,
downloaded,
left,
peerIPAddress,
Tracker.STOPPED_EVENT);
}
} catch (IOException ioe) { /* ignored */
LOGGER.debug("ignored " + ioe.getMessage());
}
}
}
/** Requests peer information from the tracker, and updates the tracker regarding our status.
*
* @param announceURLString the tracker announce URL in string form
* @param urlEncodedInfoHash the URL-encoded info hash
* @param peerID the peer id
* @param uploaded the total uploaded bytes
* @param downloaded the total downloaded bytes
* @param left the file bytes remaining
* @param peerIPAddress the peer IP address
* @param event the event
* @throws IOException when an input/output error occurs
*/
@SuppressWarnings({"ThrowableResultIgnored", "null"})
private void request(
final String announceURLString,
final String urlEncodedInfoHash,
final String peerID,
final long uploaded,
final long downloaded,
final long left,
final String peerIPAddress,
final String event)
throws IOException {
//Preconditions
assert announceURLString != null : "announceURLString must not be null";
assert !announceURLString.isEmpty() : "announceURLString must not be empty";
assert urlEncodedInfoHash != null : "infoHash must not be null";
assert !urlEncodedInfoHash.isEmpty() : "infoHash must not be empty";
assert peerID != null : "peerID must not be null";
assert !peerID.isEmpty() : "peerID must not be empty";
assert uploaded >= 0 : "uploaded must not be negative";
assert downloaded >= 0 : "downloaded must not be negative";
assert left >= 0 : "left must not be negative";
assert peerIPAddress != null : "peerIPAddress must not be null";
assert !peerIPAddress.isEmpty() : "peerIPAddress must not be empty";
assert "started".equals(event) || "completed".equals(event) || "stopped".equals(event) || event.isEmpty() : "invalid event: " + event;
final ClientBootstrap clientBootstrap = new ClientBootstrap(new NioClientSocketChannelFactory(
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool()));
// configure the client pipeline
final ChannelPipeline channelPipeline = HTTPClientPipelineFactory.getPipeline(
this,
peerCoordinator.getX509SecurityInfo());
clientBootstrap.setPipeline(channelPipeline);
LOGGER.info("pipeline: " + channelPipeline.toString());
// start the connection attempt
ChannelFuture channelFuture = clientBootstrap.connect(new InetSocketAddress(trackerHost, trackerPort));
// wait until the connection attempt succeeds or fails
final Channel channel = channelFuture.awaitUninterruptibly().getChannel();
if (!channelFuture.isSuccess()) {
LOGGER.warn(StringUtils.getStackTraceAsString(channelFuture.getCause()));
throw new TexaiException(channelFuture.getCause());
}
LOGGER.info("HTTP client connected");
final StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(announceURLString);
stringBuilder.append("?info_hash=");
stringBuilder.append(urlEncodedInfoHash);
stringBuilder.append("&peer_id=");
stringBuilder.append(peerID);
stringBuilder.append("&port=");
stringBuilder.append(ourConnectionPort);
stringBuilder.append("&uploaded=");
stringBuilder.append(uploaded);
stringBuilder.append("&downloaded=");
stringBuilder.append(downloaded);
stringBuilder.append("&left=");
stringBuilder.append(left);
stringBuilder.append("&compact=1&ip=");
if (peerIPAddress != null) {
stringBuilder.append(peerIPAddress);
}
if (!event.equals(Tracker.NO_EVENT)) {
stringBuilder.append("&event=");
stringBuilder.append(event);
}
LOGGER.info("event: " + event);
final URI uri;
try {
uri = new URI(stringBuilder.toString());
} catch (URISyntaxException ex) {
throw new TexaiException(ex);
}
final HttpRequest httpRequest = new DefaultHttpRequest(
HttpVersion.HTTP_1_1,
HttpMethod.GET,
uri.toASCIIString());
httpRequest.setHeader(HttpHeaders.Names.HOST, trackerHost);
httpRequest.setHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.CLOSE);
LOGGER.info("httpRequest ...\n" + httpRequest);
channel.write(httpRequest);
// wait for the request message to be sent
channelFuture.awaitUninterruptibly();
if (!channelFuture.isSuccess()) {
LOGGER.warn(StringUtils.getStackTraceAsString(channelFuture.getCause()));
throw new TexaiException(channelFuture.getCause());
}
}
/** Asynchronously receives a tracker HTTP response message.
*
* @param channelHandlerContext the channel handler context
* @param messageEvent the message event
*/
@Override
public void messageReceived(
final ChannelHandlerContext channelHandlerContext,
final MessageEvent messageEvent) {
//Preconditions
assert channelHandlerContext != null : "channelHandlerContext must not be null";
assert messageEvent != null : "messageEvent must not be null";
LOGGER.info("received messageEvent: " + messageEvent);
final HttpResponse httpResponse = (HttpResponse) messageEvent.getMessage();
LOGGER.info("STATUS: " + httpResponse.getStatus());
LOGGER.info("VERSION: " + httpResponse.getProtocolVersion());
LOGGER.info("");
if (!httpResponse.getHeaderNames().isEmpty()) {
for (final String name : httpResponse.getHeaderNames()) {
for (final String value : httpResponse.getHeaders(name)) {
LOGGER.info("HEADER: " + name + " = " + value);
}
}
LOGGER.info("");
}
final ChannelBuffer trackerResponseContent = httpResponse.getContent();
if (trackerResponseContent.readable()) {
LOGGER.info("CONTENT {");
LOGGER.info(trackerResponseContent.toString(CharsetUtil.UTF_8));
LOGGER.info("} END OF CONTENT");
}
synchronized (trackerInfo_lock) {
try {
trackerInfo = new TrackerInfo(trackerResponseContent);
trackerInfo_lock.notifyAll();
} catch (IOException ex) {
throw new TexaiException(ex);
}
}
LOGGER.info("TrackerClient response: " + trackerInfo);
lastTrackerRequestTime = System.currentTimeMillis();
final String failure = trackerInfo.getFailureReason();
if (failure != null) {
throw new TexaiException(failure);
}
trackerRequestIntervalMillis = (long) trackerInfo.getInterval() * 1000L;
LOGGER.info("will contact tracker again in " + trackerInfo.getInterval() + " seconds");
}
/** Handles a caught exception.
*
* @param channelHandlerContext the channel handler event
* @param exceptionEvent the exception event
*/
@Override
@SuppressWarnings("ThrowableResultIgnored")
public void exceptionCaught(
final ChannelHandlerContext channelHandlerContext,
final ExceptionEvent exceptionEvent) {
//Preconditions
assert channelHandlerContext != null : "channelHandlerContext must not be null";
assert exceptionEvent != null : "exceptionEvent must not be null";
LOGGER.error("exceptionEvent: " + exceptionEvent);
throw new TexaiException(exceptionEvent.getCause());
}
/** Returns a string representation of this object.
*
* @return a string representation of this object
*/
@Override
public String toString() {
return "[TrackerClient " + peerCoordinator.getOurTrackedPeerInfo() + "]";
}
}