package com.limegroup.gnutella.messages.vendor;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.limewire.collection.BitNumbers;
import org.limewire.collection.IntervalSet;
import org.limewire.io.BadGGEPBlockException;
import org.limewire.io.BadGGEPPropertyException;
import org.limewire.io.ConnectableImpl;
import org.limewire.io.GGEP;
import org.limewire.io.GUID;
import org.limewire.io.InvalidDataException;
import org.limewire.io.IpPort;
import org.limewire.io.NetworkUtils;
import org.limewire.util.Decorator;
import org.limewire.util.StringUtils;
import com.limegroup.gnutella.PushEndpoint;
import com.limegroup.gnutella.PushEndpointFactory;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.downloader.RemoteFileDescFactory;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.util.DataUtils;
/**
* 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.
*
* NEW GGEP FORMAT:
* A GGEP block containing:
* F: features (optional)
* supported features:
* 0x1 = TLS_CAPABLE
* C: response code (required)
* V: vendor id (required if not 404)
* Q: queue status (required if not 404)
* R: ranges (optional, shouldn't be if complete file)
* P: push locations (optional)
* A: direct locations (optional)
* T: indexes of which direct locations support TLS
*
* OLD BINARY 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 HeadPongImpl extends AbstractVendorMessage implements HeadPong {
/** available ranges */
private IntervalSet _ranges;
/** the altlocs that were sent, if any */
private Set<IpPort> _altLocs = Collections.emptySet();
/** the firewalled altlocs that were sent, if any */
private Set<PushEndpoint> _pushLocs = Collections.emptySet();
/** 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;
/** Whether the remote host supports TLS. */
private boolean _tlsCapable;
/** True if this came from a routed ping. */
private boolean _routingBroken;
private final PushEndpointFactory pushEndpointFactory;
/**
* Creates a message object with data from the network.
*
* This will correctly set the fields of this HeadPong, as opposed
* to the other constructor.
*/
HeadPongImpl(byte[] guid, byte ttl, byte hops, int version, byte[] payload, Network network, PushEndpointFactory pushEndpointFactory) throws BadPacketException {
super(guid, ttl, hops, F_LIME_VENDOR_ID, F_UDP_HEAD_PONG, version, payload, network);
// This isn't really used later on -- it's just used deep within the setFieldsFromXXX methods.
// Maybe this constructor should go away and should be made with all fields passed in.
this.pushEndpointFactory = pushEndpointFactory;
//we should have some payload
if (payload==null || payload.length<2)
throw new BadPacketException("bad payload");
if(version == BINARY_VERSION) {
setFieldsFromBinary(payload);
} else if(version >= GGEP_VERSION){
setFieldsFromGGEP(payload);
} else {
throw new BadPacketException("invalid version!");
}
}
/**
* Constructs a message to send in response to the Ping.
* If the Ping is version 1, this will construct a BINARY FORMAT pong.
* Otherwise, this will construct a GGEP FORMAT pong.
*
* NOTE: This will NOT set the fields of this class correctly.
* This constructor is intended ONLY for sending the reply
* through the network. To access a HeadPong with the
* fields set correctly, you can write this to a ByteArrayOutputStream
* and reparse the resulting bytes through MessageFactory,
* which will construct a HeadPong with the network constructor,
* where the fields are correctly set.
*/
protected HeadPongImpl(GUID guid, int version, byte[] payload) {
super(F_LIME_VENDOR_ID, F_UDP_HEAD_PONG, version, payload);
setGUID(guid);
pushEndpointFactory = null;
}
@Override
public Class<HeadPong> getHandlerClass() {
return HeadPong.class;
}
/**
* Sets all local fields based off the original version of the HeadPong,
* from which the format was not very extensible.
*
* @param payload
* @throws BadPacketException
*/
private void setFieldsFromBinary(byte[] payload) throws BadPacketException {
//the first byte has to be FILE_NOT_FOUND, PARTIAL_FILE,
//COMPLETE_FILE, FIREWALLED or DOWNLOADING
if (payload[1]>CODES_MASK)
throw new BadPacketException("invalid payload!");
try {
DataInputStream dais = new DataInputStream(new ByteArrayInputStream(payload));
//read and mask the features
byte features = (byte) (dais.readByte() & HeadPing.FEATURE_MASK);
// older clients echoed the feature mask a ping sent them,
// which can sometimes include the GGEP_PING feature.
// these older clients also didn't correctly route pings to
// their leaves. newer clients fixed this, and use this fact
// to recognize when an older push proxy sends them a bogus
// response.
_routingBroken = (features & HeadPing.GGEP_PING) == HeadPing.GGEP_PING;
//read the response code
byte code = dais.readByte();
if(!setFieldsFromCode(code))
return;
//read the vendor id
_vendorId = new byte[4];
dais.readFully(_vendorId);
//read the queue status
_queueStatus = dais.readByte();
if(!_completeFile && (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);
}
}
/**
* Sets all fields in the pong based on the GGEP format.
*
* @param payload
* @throws BadPacketException
*/
private void setFieldsFromGGEP(byte[] payload) throws BadPacketException {
GGEP ggep;
try {
ggep = new GGEP(payload, 0);
} catch (BadGGEPBlockException e) {
throw new BadPacketException(e);
}
byte[] code = getRequiredGGEPField(ggep, CODE);
if(!setFieldsFromCode(code[0]))
return;
// No pongs that support GGEP have routing broken.
_routingBroken = false;
// Otherwise, there's more required.
_vendorId = getRequiredGGEPField(ggep, VENDOR);
_queueStatus = getRequiredGGEPField(ggep, QUEUE)[0];
byte[] features = getOptionalGGEPField(ggep, FEATURES);
if(features.length > 0) {
_tlsCapable = (features[0] & TLS_CAPABLE) == TLS_CAPABLE;
}
try {
byte[] ranges = getOptionalGGEPField(ggep, RANGES);
byte [] ranges5 = getOptionalGGEPField(ggep, RANGES5);
if(ranges.length > 0 || ranges5.length > 0)
_ranges = parseRanges(ranges, ranges5);
byte[] pushLocs = getOptionalGGEPField(ggep, PUSH_LOCS);
if(pushLocs.length > 0)
_pushLocs = parsePushLocs(pushLocs);
byte[] altTLS = getOptionalGGEPField(ggep, TLS_LOCS);
BitNumbers tls = null;
if(altTLS.length > 0)
tls = new BitNumbers(altTLS);
byte[] altLocs = getOptionalGGEPField(ggep, LOCS);
if(altLocs.length > 0)
_altLocs = parseAltLocs(altLocs, tls);
} catch(IOException iox) {
throw new BadPacketException(iox);
}
}
/**
* Returns false if code is FILE_NOT_FOUND.
* Otherwise, returns true and sets _fileFound, and optionally sets
* _isFirewalled, _completeFile, and _isDownloading depending on
* what is set within coe.
*/
private boolean setFieldsFromCode(byte code) {
if (code == FILE_NOT_FOUND)
return false;
_fileFound=true;
//is the other host firewalled?
if ((code & FIREWALLED) == FIREWALLED)
_isFirewalled = true;
//if we have a partial file and the pong carries ranges, parse their list
if ((code & COMPLETE_FILE) == COMPLETE_FILE)
_completeFile=true;
//also check if the host is downloading the file
else if ((code & DOWNLOADING) == DOWNLOADING)
_isDownloading=true;
return true;
}
/** Returns a required field, throwing a BadPacketException if it doesn't exist. */
private byte[] getRequiredGGEPField(GGEP ggep, String header) throws BadPacketException {
try {
byte[] bytes = ggep.getBytes(header);
if(bytes.length == 0)
throw new BadPacketException("no data for header: " + header + "!");
return bytes;
} catch(BadGGEPPropertyException bgpe) {
throw new BadPacketException(bgpe);
}
}
/** Returns the bytes of the field in the GGEP if it exists, otherwise an empty array. */
private byte[] getOptionalGGEPField(GGEP ggep, String header) {
if(ggep.hasValueFor(header)) {
try {
return ggep.getBytes(header);
} catch(BadGGEPPropertyException ignored) {}
}
return DataUtils.EMPTY_BYTE_ARRAY;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#hasFile()
*/
public boolean hasFile() {
return _fileFound;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#hasCompleteFile()
*/
public boolean hasCompleteFile() {
return hasFile() && _completeFile;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#getRanges()
*/
public IntervalSet getRanges() {
return _ranges;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#getAltLocs()
*/
public Set<IpPort> getAltLocs() {
return _altLocs;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#getPushLocs()
*/
public Set<PushEndpoint> getPushLocs() {
return _pushLocs;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#isTLSCapable()
*/
public boolean isTLSCapable() {
return _tlsCapable;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#getAllLocsRFD(com.limegroup.gnutella.RemoteFileDesc)
*/
public Set<RemoteFileDesc> getAllLocsRFD(RemoteFileDesc original, RemoteFileDescFactory remoteFileDescFactory){
Set<RemoteFileDesc> ret = new HashSet<RemoteFileDesc>();
for(IpPort current : _altLocs)
ret.add(remoteFileDescFactory.createRemoteFileDesc(original, current));
for(PushEndpoint current : _pushLocs)
ret.add(remoteFileDescFactory.createRemoteFileDesc(original, current));
return ret;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#getVendor()
*/
public String getVendor() {
if(_vendorId != null)
return StringUtils.getASCIIString(_vendorId);
else
return null;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#isFirewalled()
*/
public boolean isFirewalled() {
return _isFirewalled;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#getQueueStatus()
*/
public int getQueueStatus() {
return _queueStatus;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#isBusy()
*/
public boolean isBusy() {
return _queueStatus >= BUSY;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#isDownloading()
*/
public boolean isDownloading() {
return _isDownloading;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#isRoutingBroken()
*/
public boolean isRoutingBroken() {
return _routingBroken;
}
@Override
public String toString() {
return "HeadPong: " +
" isRoutingBroken: "+ isRoutingBroken()+
", 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 parseRanges(ranges, DataUtils.EMPTY_BYTE_ARRAY);
}
/** Parses available ranges. */
private IntervalSet parseRanges(byte[] ranges, byte [] ranges5) throws IOException {
return IntervalSet.parseBytes(ranges, ranges5);
}
/**
* reads firewalled alternate locations from an input stream
*/
private final Set<PushEndpoint> readPushLocs(DataInputStream dais)
throws IOException, BadPacketException {
int size = dais.readUnsignedShort();
byte [] altlocs = new byte[size];
dais.readFully(altlocs);
return parsePushLocs(altlocs);
}
/** Parses push alternate locations from a byte[]. */
private Set<PushEndpoint> parsePushLocs(byte[] altlocs) throws IOException, BadPacketException {
Set<PushEndpoint> ret = new HashSet<PushEndpoint>();
ret.addAll(unpackPushEPs(new ByteArrayInputStream(altlocs)));
return ret;
}
/** Unpacks a stream of Push Endpoints. */
private List<PushEndpoint> unpackPushEPs(InputStream is)
throws BadPacketException, IOException {
List<PushEndpoint> ret = new LinkedList<PushEndpoint>();
DataInputStream dais = new DataInputStream(is);
while (dais.available() > 0)
ret.add(pushEndpointFactory.createFromBytes(dais));
return Collections.unmodifiableList(ret);
}
/**
* reads non-firewalled alternate locations from an input stream
*/
private final Set<IpPort> readLocs(DataInputStream dais)
throws IOException, BadPacketException {
int size = dais.readUnsignedShort();
byte [] altlocs = new byte[size];
dais.readFully(altlocs);
return parseAltLocs(altlocs, null);
}
/** Parses alternate locations from a byte[]. */
private Set<IpPort> parseAltLocs(byte[] altlocs, final BitNumbers tlsIdx) throws IOException, BadPacketException {
Set<IpPort> ret = new HashSet<IpPort>();
try {
if(tlsIdx == null) {
ret.addAll(NetworkUtils.unpackIps(altlocs));
} else {
// Decorate the unpacking of the IPs in order to make
// some of them TLS-capable.
ret.addAll(NetworkUtils.unpackIps(altlocs, new Decorator<IpPort, IpPort>() {
int i = 0;
public IpPort decorate(IpPort input) {
if(tlsIdx.isSet(i))
input = new ConnectableImpl(input, true);
i++;
return input;
}
}));
}
} catch(InvalidDataException ide) {
throw new BadPacketException(ide);
}
return ret;
}
// TODO: hack to let PingRankerTest compile
/* (non-Javadoc)
* @see com.limegroup.gnutella.messages.vendor.HeadPongI#getPayload()
*/
@Override
public byte[] getPayload() {
return super.getPayload();
}
}