package com.limegroup.gnutella.messages.vendor;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.limegroup.gnutella.ByteOrder;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.FileDesc;
import com.limegroup.gnutella.FileManager;
import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.IncompleteFileDesc;
import com.limegroup.gnutella.PushEndpoint;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.UploadManager;
import com.limegroup.gnutella.altlocs.AlternateLocation;
import com.limegroup.gnutella.altlocs.AlternateLocationCollection;
import com.limegroup.gnutella.altlocs.DirectAltLoc;
import com.limegroup.gnutella.altlocs.PushAltLoc;
import com.limegroup.gnutella.downloader.DownloadWorker;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.settings.UploadSettings;
import com.limegroup.gnutella.util.CountingOutputStream;
import com.limegroup.gnutella.util.IntervalSet;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.util.MultiRRIterator;
import com.limegroup.gnutella.util.NetworkUtils;
/**
* a response to an HeadPing. It is a trimmed down version of the standard HEAD response,
* since we are trying to keep the sizes of the udp packets small.
*
* This message can also be used for punching firewalls if the ping requests so.
* Feature like this can be used to allow firewalled nodes to participate more
* in download meshes.
*
* Since headpings will be sent by clients who have started to download a file whose download
* mesh contains this host, it needs to contain information that will help those clients whether
* this host is a good bet to start an http download from. Therefore, the following information should
* be included in the response:
*
* - available ranges of the file
* - queue status
* - some altlocs (if space permits)
*
* the queue status can be an integer representing how many people are waiting in the queue. If
* nobody is waiting in the queue and we have slots available, the integer can be negative. So if
* we have 3 people on the queue we'd send the integer 3. If we have nobody on the queue and
* two upload slots available we would send -2. A value of 0 means all upload slots are taken but
* the queue is empty. This information can be used by the downloaders to better judge chances of
* successful start of the download.
*
* Format:
*
* 1 byte - features byte
* 2 byte - response code
* 4 bytes - vendor id
* 1 byte - queue status
* n*8 bytes - n intervals (if requested && file partial && fits in packet)
* the rest - altlocs (if requested)
*/
public class HeadPong extends VendorMessage {
private static final Log LOG = LogFactory.getLog(HeadPong.class);
/**
* cache references to the upload manager and file manager for
* easier stubbing and testing.
*/
private static UploadManager _uploadManager
= RouterService.getUploadManager();
private static FileManager _fileManager
= RouterService.getFileManager();
/**
* try to make packets less than this size
*/
private static final int PACKET_SIZE = 580;
/**
* instead of using the HTTP codes, use bit values. The first three
* possible values are mutually exclusive though. DOWNLOADING is
* possible only if PARTIAL_FILE is set as well.
*/
private static final byte FILE_NOT_FOUND= (byte)0x0;
private static final byte COMPLETE_FILE= (byte)0x1;
private static final byte PARTIAL_FILE = (byte)0x2;
private static final byte FIREWALLED = (byte)0x4;
private static final byte DOWNLOADING = (byte)0x8;
private static final byte CODES_MASK=(byte)0xF;
/**
* all our slots are full..
*/
private static final byte BUSY=(byte)0x7F;
public static final int VERSION = 1;
/**
* the features contained in this pong. Same as those of the originating ping
*/
private byte _features;
/**
* available ranges
*/
private IntervalSet _ranges;
/**
* the altlocs that were sent, if any
*/
private Set _altLocs;
/**
* the firewalled altlocs that were sent, if any
*/
private Set _pushLocs;
/**
* the queue status, can be negative
*/
private int _queueStatus;
/**
* whether the other host has the file at all
*/
private boolean _fileFound,_completeFile;
/**
* the remote host
*/
private byte [] _vendorId;
/**
* whether the other host can receive tcp
*/
private boolean _isFirewalled;
/**
* whether the other host is currently downloading the file
*/
private boolean _isDownloading;
/**
* creates a message object with data from the network.
*/
protected HeadPong(byte[] guid, byte ttl, byte hops,
int version, byte[] payload)
throws BadPacketException {
super(guid, ttl, hops, F_LIME_VENDOR_ID, F_UDP_HEAD_PONG, version, payload);
//we should have some payload
if (payload==null || payload.length<2)
throw new BadPacketException("bad payload");
//if we are version 1, the first byte has to be FILE_NOT_FOUND, PARTIAL_FILE,
//COMPLETE_FILE, FIREWALLED or DOWNLOADING
if (version == VERSION &&
payload[1]>CODES_MASK)
throw new BadPacketException("invalid payload for version "+version);
try {
DataInputStream dais = new DataInputStream(new ByteArrayInputStream(payload));
//read and mask the features
_features = (byte) (dais.readByte() & HeadPing.FEATURE_MASK);
//read the response code
byte code = dais.readByte();
//if the other host doesn't have the file, stop parsing
if (code == FILE_NOT_FOUND)
return;
else
_fileFound=true;
//is the other host firewalled?
if ((code & FIREWALLED) == FIREWALLED)
_isFirewalled = true;
//read the vendor id
_vendorId = new byte[4];
dais.readFully(_vendorId);
//read the queue status
_queueStatus = dais.readByte();
//if we have a partial file and the pong carries ranges, parse their list
if ((code & COMPLETE_FILE) == COMPLETE_FILE)
_completeFile=true;
else {
//also check if the host is downloading the file
if ((code & DOWNLOADING) == DOWNLOADING)
_isDownloading=true;
if ((_features & HeadPing.INTERVALS) == HeadPing.INTERVALS)
_ranges = readRanges(dais);
}
//parse any included firewalled altlocs
if ((_features & HeadPing.PUSH_ALTLOCS) == HeadPing.PUSH_ALTLOCS)
_pushLocs=readPushLocs(dais);
//parse any included altlocs
if ((_features & HeadPing.ALT_LOCS) == HeadPing.ALT_LOCS)
_altLocs=readLocs(dais);
} catch(IOException oops) {
throw new BadPacketException(oops.getMessage());
}
}
/**
* creates a message object as a response to a udp head request.
*/
public HeadPong(HeadPing ping) {
super(F_LIME_VENDOR_ID, F_UDP_HEAD_PONG, VERSION,
derivePayload(ping));
setGUID(new GUID(ping.getGUID()));
}
/**
* packs information about the shared file, queue status and altlocs into the body
* of the message.
* @param ping the original UDP head ping to respond to
*/
private static byte [] derivePayload(HeadPing ping) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
CountingOutputStream caos = new CountingOutputStream(baos);
DataOutputStream daos = new DataOutputStream(caos);
byte retCode=0;
byte queueStatus;
URN urn = ping.getUrn();
FileDesc desc = _fileManager.getFileDescForUrn(urn);
boolean didNotSendAltLocs=false;
boolean didNotSendPushAltLocs = false;
boolean didNotSendRanges = false;
try {
byte features = ping.getFeatures();
features &= ~HeadPing.GGEP_PING;
daos.write(features);
if (LOG.isDebugEnabled())
LOG.debug("writing features "+features);
//if we don't have the file..
if (desc == null) {
LOG.debug("we do not have the file");
daos.write(FILE_NOT_FOUND);
return baos.toByteArray();
}
//if we can't receive unsolicited tcp...
if (!RouterService.acceptedIncomingConnection())
retCode = FIREWALLED;
//we have the file... is it complete or not?
if (desc instanceof IncompleteFileDesc) {
retCode = (byte) (retCode | PARTIAL_FILE);
//also check if the file is currently being downloaded
//or is waiting for sources. This does not care for queued downloads.
IncompleteFileDesc idesc = (IncompleteFileDesc)desc;
if (idesc.isActivelyDownloading())
retCode = (byte) (retCode | DOWNLOADING);
}
else
retCode = (byte) (retCode | COMPLETE_FILE);
daos.write(retCode);
if(LOG.isDebugEnabled())
LOG.debug("our return code is "+retCode);
//write the vendor id
daos.write(F_LIME_VENDOR_ID);
//get our queue status.
int queueSize = _uploadManager.getNumQueuedUploads();
if (queueSize == UploadSettings.UPLOAD_QUEUE_SIZE.getValue())
queueStatus = BUSY;
else if (queueSize > 0)
queueStatus = (byte) queueSize;
else
//optimistic value
queueStatus = (byte)
(_uploadManager.uploadsInProgress() -
UploadSettings.HARD_MAX_UPLOADS.getValue() );
//write out the return code and the queue status
daos.writeByte(queueStatus);
if (LOG.isDebugEnabled())
LOG.debug("our queue status is "+queueStatus);
//if we sent partial file and the remote asked for ranges, send them
if (retCode == PARTIAL_FILE && ping.requestsRanges())
didNotSendRanges=!writeRanges(caos,desc);
//if we have any firewalled altlocs and enough room in the packet, add them.
if (ping.requestsPushLocs()){
boolean FWTOnly = (features & HeadPing.FWT_PUSH_ALTLOCS) ==
HeadPing.FWT_PUSH_ALTLOCS;
if (FWTOnly) {
AlternateLocationCollection push = RouterService.getAltlocManager().getPush(urn,true);
synchronized(push) {
didNotSendPushAltLocs = !writePushLocs(caos,push.iterator());
}
} else {
AlternateLocationCollection push = RouterService.getAltlocManager().getPush(urn,true);
AlternateLocationCollection fwt = RouterService.getAltlocManager().getPush(urn,false);
synchronized(push) {
synchronized(fwt) {
didNotSendPushAltLocs =
!writePushLocs(caos,
new MultiRRIterator(new Iterator[]{push.iterator(),fwt.iterator()}));
}
}
}
}
//now add any non-firewalled altlocs in case they were requested.
if (ping.requestsAltlocs()) {
AlternateLocationCollection col = RouterService.getAltlocManager().getDirect(urn);
synchronized(col) {
didNotSendAltLocs=!writeLocs(caos, col.iterator());
}
}
} catch(IOException impossible) {
ErrorService.error(impossible);
}
//done!
byte []ret = baos.toByteArray();
//if we did not add ranges or altlocs due to constraints,
//update the flags now.
if (didNotSendRanges){
LOG.debug("not sending ranges");
ret[0] = (byte) (ret[0] & ~HeadPing.INTERVALS);
}
if (didNotSendAltLocs){
LOG.debug("not sending altlocs");
ret[0] = (byte) (ret[0] & ~HeadPing.ALT_LOCS);
}
if (didNotSendPushAltLocs){
LOG.debug("not sending push altlocs");
ret[0] = (byte) (ret[0] & ~HeadPing.PUSH_ALTLOCS);
}
return ret;
}
/**
*
* @return whether the alternate location still has the file
*/
public boolean hasFile() {
return _fileFound;
}
/**
*
* @return whether the alternate location has the complete file
*/
public boolean hasCompleteFile() {
return hasFile() && _completeFile;
}
/**
*
* @return the available ranges the alternate location has
*/
public IntervalSet getRanges() {
return _ranges;
}
/**
*
* @return set of <tt>Endpoint</tt>
* containing any alternate locations this alternate location returned.
*/
public Set getAltLocs() {
return _altLocs;
}
/**
*
* @return set of <tt>PushEndpoint</tt>
* containing any firewalled locations this alternate location returned.
*/
public Set getPushLocs() {
return _pushLocs;
}
/**
* @return all altlocs carried in the pong as
* set of <tt>RemoteFileDesc</tt>
*/
public Set getAllLocsRFD(RemoteFileDesc original){
Set ret = new HashSet();
if (_altLocs!=null)
for(Iterator iter = _altLocs.iterator();iter.hasNext();) {
IpPort current = (IpPort)iter.next();
ret.add(new RemoteFileDesc(original,current));
}
if (_pushLocs!=null)
for(Iterator iter = _pushLocs.iterator();iter.hasNext();) {
PushEndpoint current = (PushEndpoint)iter.next();
ret.add(new RemoteFileDesc(original,current));
}
return ret;
}
/**
* updates the rfd with information in this pong
*/
public void updateRFD(RemoteFileDesc rfd) {
// if the rfd claims its busy, ping it again in a minute
// (we're obviously using HeadPings, so its cheap to ping it sooner
// rather than later)
if (isBusy())
rfd.setRetryAfter(DownloadWorker.RETRY_AFTER_NONE_ACTIVE);
rfd.setQueueStatus(getQueueStatus());
rfd.setAvailableRanges(getRanges());
rfd.setSerializeProxies();
}
/**
*
* @return the remote vendor as string
*/
public String getVendor() {
return new String(_vendorId);
}
/**
*
* @return whether the remote is firewalled and will need a push
*/
public boolean isFirewalled() {
return _isFirewalled;
}
public int getQueueStatus() {
return _queueStatus;
}
public boolean isBusy() {
return _queueStatus >= BUSY;
}
public boolean isDownloading() {
return _isDownloading;
}
/**
* @return whether the host that returned this pong supports ggep
*/
public boolean isGGEPPong() {
return (_features & HeadPing.GGEP_PING) != 0;
}
public String toString() {
return "HeadPong: isGGEP "+ isGGEPPong()+
" hasFile "+hasFile()+
" hasCompleteFile "+hasCompleteFile()+
" isDownloading "+isDownloading()+
" isFirewalled "+isFirewalled()+
" queue rank "+getQueueStatus()+
" \nranges "+getRanges()+
" \nalts "+getAltLocs()+
" \npushalts "+getPushLocs();
}
//*************************************
//utility methods
//**************************************
/**
* reads available ranges from an inputstream
*/
private final IntervalSet readRanges(DataInputStream dais)
throws IOException{
int rangeLength=dais.readUnsignedShort();
byte [] ranges = new byte [rangeLength];
dais.readFully(ranges);
return IntervalSet.parseBytes(ranges);
}
/**
* reads firewalled alternate locations from an input stream
*/
private final Set readPushLocs(DataInputStream dais)
throws IOException, BadPacketException {
int size = dais.readUnsignedShort();
byte [] altlocs = new byte[size];
dais.readFully(altlocs);
Set ret = new HashSet();
ret.addAll(NetworkUtils.unpackPushEPs(new ByteArrayInputStream(altlocs)));
return ret;
}
/**
* reads non-firewalled alternate locations from an input stream
*/
private final Set readLocs(DataInputStream dais)
throws IOException, BadPacketException {
int size = dais.readUnsignedShort();
byte [] altlocs = new byte[size];
dais.readFully(altlocs);
Set ret = new HashSet();
ret.addAll(NetworkUtils.unpackIps(altlocs));
return ret;
}
/**
* @param daos the output stream to write the ranges to
* @return if they were written or not.
*/
private static final boolean writeRanges(CountingOutputStream caos,
FileDesc desc) throws IOException{
DataOutputStream daos = new DataOutputStream(caos);
LOG.debug("adding ranges to pong");
IncompleteFileDesc ifd = (IncompleteFileDesc) desc;
byte [] ranges =ifd.getRangesAsByte();
//write the ranges only if they will fit in the packet
if (caos.getAmountWritten()+2 + ranges.length <= PACKET_SIZE) {
LOG.debug("added ranges");
daos.writeShort((short)ranges.length);
caos.write(ranges);
return true;
}
else { //the ranges will not fit - say we didn't send them.
LOG.debug("ranges will not fit :(");
return false;
}
}
private static final boolean writePushLocs(CountingOutputStream caos, Iterator pushlocs)
throws IOException {
if (!pushlocs.hasNext())
return false;
//push altlocs are bigger than normal altlocs, however we
//don't know by how much. The size can be between
//23 and 47 bytes. We assume its 47.
int available = (PACKET_SIZE - (caos.getAmountWritten()+2)) / 47;
// if we don't have any space left, we can't send any pushlocs
if (available == 0)
return false;
if (LOG.isDebugEnabled())
LOG.debug("trying to add up to "+available+ " push locs to pong");
long now = System.currentTimeMillis();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
while (pushlocs.hasNext() && available > 0) {
PushAltLoc loc = (PushAltLoc) pushlocs.next();
if (loc.getPushAddress().getProxies().isEmpty()) {
pushlocs.remove();
continue;
}
if (loc.canBeSent(AlternateLocation.MESH_PING)) {
baos.write(loc.getPushAddress().toBytes());
available --;
loc.send(now,AlternateLocation.MESH_PING);
} else if (!loc.canBeSentAny())
pushlocs.remove();
}
if (baos.size() == 0) {
//altlocs will not fit or none available - say we didn't send them
LOG.debug("did not send any push locs");
return false;
} else {
LOG.debug("adding push altlocs");
ByteOrder.short2beb((short)baos.size(),caos);
baos.writeTo(caos);
return true;
}
}
private static final boolean writeLocs(CountingOutputStream caos, Iterator altlocs)
throws IOException {
//do we have any altlocs?
if (!altlocs.hasNext())
return false;
//how many can we fit in the packet?
int toSend = (PACKET_SIZE - (caos.getAmountWritten()+2) ) /6;
if (toSend == 0)
return false;
if (LOG.isDebugEnabled())
LOG.debug("trying to add up to "+ toSend +" locs to pong");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int sent = 0;
long now = System.currentTimeMillis();
while(altlocs.hasNext() && sent < toSend) {
DirectAltLoc loc = (DirectAltLoc) altlocs.next();
if (loc.canBeSent(AlternateLocation.MESH_PING)) {
loc.send(now,AlternateLocation.MESH_PING);
baos.write(loc.getHost().getInetAddress().getAddress());
ByteOrder.short2leb((short)loc.getHost().getPort(),baos);
sent++;
} else if (!loc.canBeSentAny())
altlocs.remove();
}
LOG.debug("adding altlocs");
ByteOrder.short2beb((short)baos.size(),caos);
baos.writeTo(caos);
return true;
}
}