package com.limegroup.bittorrent;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.ByteOrder;
import com.limegroup.gnutella.Constants;
import com.limegroup.gnutella.RouterService;
/**
* handle UDP tracker requests
*
* TODO this doesn't work yet
*/
public class UDPTrackerRequest extends TrackerRequester {
private static final Log LOG = LogFactory.getLog(TrackerRequester.class);
// 20 seconds
private static final int UDP_TRACKER_TIMEOUT = 20 * 1000;
// try tracker 3 times in a row (at most)
private static final int UDP_TRACKER_RETRIES = 3;
/*
* constant message identifiers
*/
private static final int ACTION_CONNECT = 0;
private static final int ACTION_ANNOUNCE = 1;
// we don't support scaping, it doesn't return any useful information, just
// some general statistics about the tracker. Everything that may be even
// remotely of use is already transmitted over the regular tracker request
// private static final int ACTION_SCRAPE = 2;
private static final int ACTION_ERROR = 3;
// needed to distinguish between concurrent requests to the same tracker
private final int TRANSACTION_ID;
// the tracker address
private final InetAddress ADDRESS;
// tracker port
private final int PORT;
private final BTMetaInfo INFO;
private final ManagedTorrent TORRENT;
private final int EVENT;
// lock to wait for tracker response
private final Object _messageLock = new Object();
/*
* integer telling the process() what to expect as the next packet, the
* first message we expect is the connect reply
*/
private int _status = ACTION_CONNECT;
/*
* the connection id assigned to us by the tracker initialized to
* 0x41727101980L as expected by the protocol
*/
private long _connectionId = 0x41727101980L;
/*
* the response that will be returned by connectUDP
*/
private TrackerResponse _response = null;
public UDPTrackerRequest(URL url, BTMetaInfo info, ManagedTorrent torrent, int event) throws IOException {
ADDRESS = InetAddress.getByName(url.getHost());
PORT = url.getPort();
INFO = info;
TORRENT = torrent;
EVENT = event;
TRANSACTION_ID = (int) Math.random() * Integer.MAX_VALUE;
}
public TrackerResponse connectUDP() {
// we try UDP_TRACKER_RETRIES times or until we get a response, -
// this
// response may be bad but we won't retry for a while after
// receiving it
for (int i = 0; i < UDP_TRACKER_RETRIES && _response == null; i++) {
sendConnectRequest();
// we expect to be notified once _response has been set
try {
_messageLock.wait(UDP_TRACKER_TIMEOUT);
} catch (InterruptedException ie) {
// ignore
}
}
return _response;
}
private void sendConnectRequest() {
// TODO
// UDPService doesn't handle sending raw byte arrays, plus we need to
// register some kind of message listener with UDPServie...
// UDPService.instance().send(createConnectRequest(), ADDRESS, PORT);
}
private void sendAnnounceRequest() {
// TODO
// UDPService doesn't handle sending raw byte arrays, plus we need to
// register some kind of message listener with UDPServie...
// UDPService.instance().send(createAnnounceRequest(), ADDRESS, PORT);
}
/*
* (non-Javadoc)
*
* @see com.limegroup.gnutella.UDPMessageHandler#process(java.net.DatagramPacket)
*/
public void process(DatagramPacket datagram) {
// message validation, primarily checks if the message was for us. If it
// scceeds we have reason enough to believe that this is supposed to be
// a tracker response and that it is intended for us
if (!checkMessage(datagram))
return;
// here we do the real message handling, depending on what state the
// request is in
if (_status == ACTION_CONNECT) {
try {
_connectionId = parseConnectResponse(datagram.getData());
sendAnnounceRequest();
_status = ACTION_ANNOUNCE;
} catch (BadTrackerResponseException btre) {
// zero tolerance for bad tracker responses
_response = new TrackerResponse(new ArrayList(), 0, 0, 0, btre
.getMessage());
_messageLock.notify();
}
} else if (_status == ACTION_ANNOUNCE) {
try {
_response = parseAnnounceResponse(datagram.getData());
_messageLock.notify();
} catch (BadTrackerResponseException btre) {
// zero tolerance again;
_response = new TrackerResponse(new ArrayList(), 0, 0, 0, btre
.getMessage());
_messageLock.notify();
}
}
}
private boolean checkMessage(DatagramPacket datagram) {
if (!datagram.getAddress().equals(ADDRESS)
|| datagram.getPort() != PORT)
// message is not for us...
return false;
// the transaction id of every message identifying the session are
// always bytes 4-7 of any UDP tracker response
if (datagram.getData().length < 8
|| TRANSACTION_ID != ByteOrder.beb2int(datagram.getData(), 4))
// if the transaction id does not match the message is not for us
// either
return false;
return true;
}
/**
* creates a connection message, - always send this message before sending
* anything else.
*
* @param transactionId
* the ID for this tracker session, needed for concurrent tracker
* requests
* @return byte array containing the message body
*/
private byte[] createConnectRequest() {
byte[] message = new byte[16];
ByteOrder.long2beb(_connectionId, message, 0);
ByteOrder.int2beb(ACTION_CONNECT, message, 8);
ByteOrder.int2beb(TRANSACTION_ID, message, 12);
return message;
}
/**
* creates a new announce request message
*
* @param connectionId
* the session id created by the server and sent to us via
* connect response
* @param transactionId
* the session id we generated
* @param event
* the event we should send to the tracker
* @param info
* the BTMetaInfo for this download
* @param torrent
* the ManagedTorrent for this download
* @return a byte array containing the message body
*/
private byte[] createAnnounceRequest() {
byte[] message = new byte[98];
// first the session id from the server
ByteOrder.long2beb(_connectionId, message, 0);
// the action, ACTION_ANNOUNCE
ByteOrder.int2beb(ACTION_ANNOUNCE, message, 8);
// the session id that we created, - don't ask me why the redundancy...
ByteOrder.int2beb(TRANSACTION_ID, message, 12);
// the info hash
System.arraycopy(INFO.getInfoHash(), 0, message, 16, 20);
// our peer id
System.arraycopy(RouterService.getTorrentManager().getPeerId(), 0,
message, 36, 20);
// the amound downloaded this session
ByteOrder
.long2beb(TORRENT.getDownloader().getAmountRead(), message, 56);
// the amount left to download
ByteOrder.long2beb(INFO.getTotalSize()
- INFO.getVerifyingFolder().getBlockSize(), message, 72);
// the amount uploaded this session
ByteOrder.long2beb(TORRENT.getUploader().getTotalAmountUploaded(),
message, 72);
// the event like in the regular http tracker request
ByteOrder.int2beb(EVENT, message, 80);
// originally our IP address but 0 since we want the remote host to use
// the ip from the UDP message
ByteOrder.int2beb(0, message, 84);
// 4 byte unique key for the session, - doesn't make sense at all, use
// part of the peerId. Same as 'key' parameter in HTTP tracker requests
System.arraycopy(RouterService.getTorrentManager().getPeerId(), 16,
message, 88, 4);
// our listening port
ByteOrder.int2beb(RouterService.getPort(), message, 92);
// there seem to be some disagreements about the bytes 94-95, according
// to some specs there is an extension here, - but Azureus doesn't
// support it and neither do we
return message;
}
/**
* create authentication message
*
* @param connectId
* the long returned by a previous connect response
* @return
* @throws BadTrackerResponseException
*/
private static long parseConnectResponse(byte[] message)
throws BadTrackerResponseException {
int code = ByteOrder.beb2int(message, 0, 4);
if (code == ACTION_CONNECT) {
if (message.length != 16)
throw new BadTrackerResponseException(
"bad connect message length");
return ByteOrder.beb2long(message, 8, 8);
} else if (code == ACTION_ERROR)
throw new BadTrackerResponseException(parseErrorResponse(message));
else
throw new BadTrackerResponseException("unknown tracker message "
+ code);
}
/**
* @param message
* the message to parse
* @return a new TrackerResponse
* @throws BadTrackerResponseException
*/
private static TrackerResponse parseAnnounceResponse(byte[] message) throws BadTrackerResponseException {
// parse the code, identifying the message
int code = ByteOrder.beb2int(message, 0, 4);
if (code == ACTION_ERROR) throw new BadTrackerResponseException(parseErrorResponse(message));
if (code != ACTION_ANNOUNCE) throw new BadTrackerResponseException("unknown tracker message " + code);
if (message.length < 20) throw new BadTrackerResponseException("short announce tracker message");
int interval = ByteOrder.beb2int(message, 8);
int leechers = ByteOrder.beb2int(message, 12);
int seeders = ByteOrder.beb2int(message, 16);
// get the interesting part, the peer addresses
byte[] peerBytes = new byte[message.length - 20];
System.arraycopy(message, 20, peerBytes, 0, peerBytes.length);
List peers = new ArrayList();
try {
peers = TrackerResponse.parsePeers(peerBytes);
} catch (ValueException ve) {
LOG.debug(ve);
}
return new TrackerResponse(peers, interval, leechers + seeders, seeders, null);
}
/**
* @param message
* the message to parse
* @return human readable error String from the message
* @throws BadTrackerResponseException
*/
private static String parseErrorResponse(byte[] message)
throws BadTrackerResponseException {
if (message.length < 8)
throw new BadTrackerResponseException("bad error message length");
byte[] error = new byte[message.length - 8];
System.arraycopy(message, 8, error, 0, error.length);
try {
return new String(error, Constants.ASCII_ENCODING);
} catch (UnsupportedEncodingException uee) {
return null;
}
}
}