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 com.util.LOG; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.FileDesc; 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.altlocs.AlternateLocationCollection; import com.limegroup.gnutella.altlocs.PushAltLoc; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.util.CountingOutputStream; import com.limegroup.gnutella.util.IntervalSet; import com.limegroup.gnutella.util.IpPort; 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 { /** * 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()); } } /** * * @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; } /** * * @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; } //************************************* //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(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; } }