package com.limegroup.gnutella.messages; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.UnknownHostException; import java.security.Signature; import java.security.SignatureException; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Set; import com.limegroup.gnutella.Assert; import com.limegroup.gnutella.ByteOrder; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.GUID; import com.limegroup.gnutella.Response; import com.limegroup.gnutella.RouterService; import com.limegroup.gnutella.UDPService; import com.limegroup.gnutella.search.HostData; import com.limegroup.gnutella.statistics.DroppedSentMessageStatHandler; import com.limegroup.gnutella.statistics.ReceivedErrorStat; import com.limegroup.gnutella.statistics.SentMessageStatHandler; import com.limegroup.gnutella.udpconnect.UDPConnection; import com.limegroup.gnutella.util.DataUtils; import com.limegroup.gnutella.util.IpPort; import com.limegroup.gnutella.util.IpPortSet; import com.limegroup.gnutella.util.NetworkUtils; /** * A query reply. Contains information about the responding host in addition to * an array of responses. These responses are not parsed until the getResponses * method is called. For efficiency reasons, bad query reply packets may not be * discovered until the getResponses methods are called.<p> * * This class has partial support for BearShare-style query reply trailers. You * can extract the vendor code, push flag, and busy flag. These methods may * throw BadPacketException if the metadata cannot be extracted. Note that * BadPacketException does not mean that other data (namely responses) cannot be * read; MissingDataException might have been a better name. * * This class also encapsulates xml metadata. See the description of the QHD * below for more details. */ public class QueryReply extends Message implements SecureMessage { //WARNING: see note in Message about IP addresses. // some parameters about xml, namely the max size of a xml collection string. public static final int XML_MAX_SIZE = 32768; /** 2 bytes for public area, 2 bytes for xml length. */ public static final int COMMON_PAYLOAD_LEN = 4; /** The mask for extracting the push flag from the QHD common area. */ private static final byte PUSH_MASK=(byte)0x01; /** The mask for extracting the busy flag from the QHD common area. */ private static final byte BUSY_MASK=(byte)0x04; /** The mask for extracting the busy flag from the QHD common area. */ private static final byte UPLOADED_MASK=(byte)0x08; /** The mask for extracting the busy flag from the QHD common area. */ private static final byte SPEED_MASK=(byte)0x10; /** The mask for extracting the GGEP flag from the QHD common area. */ private static final byte GGEP_MASK=(byte)0x20; /** The mask for extracting the chat flag from the QHD private area. */ private static final byte CHAT_MASK=(byte)0x01; static final int TRUE=1; static final int FALSE=0; static final int UNDEFINED=-1; /** Our static and final instance of the GGEPUtil helper class. */ private static final GGEPUtil _ggepUtil = new GGEPUtil(); /** the payload. */ private byte[] _payload; /** The raw ip address of the host returning the hit.*/ private byte[] _address = new byte[4]; /** Whether or not this message has been verified as secure. */ private int _secureStatus = SecureMessage.INSECURE; /** True if the responses and metadata have been extracted. */ private boolean _parsed = false; /** The parsed query reply data. */ private volatile QueryReplyData _data; /** The cached clientGUID. */ private byte[] clientGUID = null; /** Creates a new query reply. The number of responses is responses.length * The Browse Host GGEP extension is ON by default. * * @requires 0 < port < 2^16 (i.e., can fit in 2 unsigned bytes), * ip.length==4 and ip is in <i>BIG-endian</i> byte order, * 0 < speed < 2^32 (i.e., can fit in 4 unsigned bytes), * responses.length < 2^8 (i.e., can fit in 1 unsigned byte), * clientGUID.length==16 */ public QueryReply(byte[] guid, byte ttl, int port, byte[] ip, long speed, Response[] responses, byte[] clientGUID, boolean isMulticastReply) { this(guid, ttl, port, ip, speed, responses, clientGUID, DataUtils.EMPTY_BYTE_ARRAY, false, false, false, false, false, false, true, isMulticastReply, false, Collections.EMPTY_SET); } /** * Creates a new QueryReply with a BearShare 2.2.0-style QHD. The QHD with * the LIME vendor code and the given busy and push flags. Note that this * constructor has no support for undefined push or busy bits. * The Browse Host GGEP extension is ON by default. * * @param needsPush true iff this is firewalled and the downloader should * attempt a push without trying a normal download. * @param isBusy true iff this server is busy, i.e., has no more upload slots. * @param finishedUpload true iff this server has successfully finished an * upload * @param measuredSpeed true iff speed is measured, not as reported by the * user * @param supportsChat true iff the host currently allows chatting. */ public QueryReply(byte[] guid, byte ttl, int port, byte[] ip, long speed, Response[] responses, byte[] clientGUID, boolean needsPush, boolean isBusy, boolean finishedUpload, boolean measuredSpeed,boolean supportsChat, boolean isMulticastReply) { this(guid, ttl, port, ip, speed, responses, clientGUID, DataUtils.EMPTY_BYTE_ARRAY, true, needsPush, isBusy, finishedUpload, measuredSpeed,supportsChat, true, isMulticastReply, false, Collections.EMPTY_SET); } /** * Creates a new QueryReply with a BearShare 2.2.0-style QHD. The QHD with * the LIME vendor code and the given busy and push flags. Note that this * constructor has no support for undefined push or busy bits. * The Browse Host GGEP extension is ON by default. * * @param needsPush true iff this is firewalled and the downloader should * attempt a push without trying a normal download. * @param isBusy true iff this server is busy, i.e., has no more upload slots * @param finishedUpload true iff this server has successfully finished an * upload * @param measuredSpeed true iff speed is measured, not as reported by the * user * @param xmlBytes The (non-null) byte[] containing aggregated * and indexed information regarding file metadata. In terms of byte-size, * this should not be bigger than 65535 bytes. Anything larger will result * in an Exception being throw. This String is assumed to consist of * compressed data. * @param supportsChat true iff the host currently allows chatting. * @exception IllegalArgumentException Thrown if * xmlBytes.length > XML_MAX_SIZE */ public QueryReply(byte[] guid, byte ttl, int port, byte[] ip, long speed, Response[] responses, byte[] clientGUID, byte[] xmlBytes, boolean needsPush, boolean isBusy, boolean finishedUpload, boolean measuredSpeed,boolean supportsChat, boolean isMulticastReply) throws IllegalArgumentException { this(guid, ttl, port, ip, speed, responses, clientGUID, xmlBytes, needsPush, isBusy, finishedUpload, measuredSpeed, supportsChat, isMulticastReply, Collections.EMPTY_SET); } /** * Creates a new QueryReply with a BearShare 2.2.0-style QHD. The QHD with * the LIME vendor code and the given busy and push flags. Note that this * constructor has no support for undefined push or busy bits. * The Browse Host GGEP extension is ON by default. * * @param needsPush true iff this is firewalled and the downloader should * attempt a push without trying a normal download. * @param isBusy true iff this server is busy, i.e., has no more upload slots * @param finishedUpload true iff this server has successfully finished an * upload * @param measuredSpeed true iff speed is measured, not as reported by the * user * @param xmlBytes The (non-null) byte[] containing aggregated * and indexed information regarding file metadata. In terms of byte-size, * this should not be bigger than 65535 bytes. Anything larger will result * in an Exception being throw. This String is assumed to consist of * compressed data. * @param supportsChat true iff the host currently allows chatting. * @param proxies an array of PushProxy interfaces. will be included in * the replies GGEP extension. * @exception IllegalArgumentException Thrown if * xmlBytes.length > XML_MAX_SIZE */ public QueryReply(byte[] guid, byte ttl, int port, byte[] ip, long speed, Response[] responses, byte[] clientGUID, byte[] xmlBytes, boolean needsPush, boolean isBusy, boolean finishedUpload, boolean measuredSpeed,boolean supportsChat, boolean isMulticastReply, Set proxies) throws IllegalArgumentException { this(guid, ttl, port, ip, speed, responses, clientGUID, xmlBytes, true, needsPush, isBusy, finishedUpload, measuredSpeed,supportsChat, true, isMulticastReply, false, proxies); } /** * Creates a new QueryReply with a BearShare 2.2.0-style QHD. The QHD with * the LIME vendor code and the given busy and push flags. Note that this * constructor has no support for undefined push or busy bits. * The Browse Host GGEP extension is ON by default. * * @param needsPush true iff this is firewalled and the downloader should * attempt a push without trying a normal download. * @param isBusy true iff this server is busy, i.e., has no more upload slots * @param finishedUpload true iff this server has successfully finished an * upload * @param measuredSpeed true iff speed is measured, not as reported by the * user * @param xmlBytes The (non-null) byte[] containing aggregated * and indexed information regarding file metadata. In terms of byte-size, * this should not be bigger than 65535 bytes. Anything larger will result * in an Exception being throw. This String is assumed to consist of * compressed data. * @param supportsChat true iff the host currently allows chatting. * @param proxies an array of PushProxy interfaces. will be included in * the replies GGEP extension. * @exception IllegalArgumentException Thrown if * xmlBytes.length > XML_MAX_SIZE */ public QueryReply(byte[] guid, byte ttl, int port, byte[] ip, long speed, Response[] responses, byte[] clientGUID, byte[] xmlBytes, boolean needsPush, boolean isBusy, boolean finishedUpload, boolean measuredSpeed,boolean supportsChat, boolean isMulticastReply, boolean supportsFWTransfer, Set proxies) throws IllegalArgumentException { this(guid, ttl, port, ip, speed, responses, clientGUID, xmlBytes, true, needsPush, isBusy, finishedUpload, measuredSpeed,supportsChat, true, isMulticastReply, supportsFWTransfer, proxies); } /** Creates a new query reply with data read from the network. */ public QueryReply(byte[] guid, byte ttl, byte hops,byte[] payload) throws BadPacketException { this(guid,ttl,hops,payload,Message.N_UNKNOWN); } public QueryReply(byte[] guid, byte ttl, byte hops,byte[] payload,int network) throws BadPacketException{ super(guid, Message.F_QUERY_REPLY, ttl, hops, payload.length,network); this._payload=payload; if(!NetworkUtils.isValidPort(getPort())) { ReceivedErrorStat.REPLY_INVALID_PORT.incrementStat(); throw new BadPacketException("invalid port"); } if( (getSpeed() & 0xFFFFFFFF00000000L) != 0) { ReceivedErrorStat.REPLY_INVALID_SPEED.incrementStat(); throw new BadPacketException("invalid speed: " + getSpeed()); } setAddress(); if(!NetworkUtils.isValidAddress(getIPBytes())) { ReceivedErrorStat.REPLY_INVALID_ADDRESS.incrementStat(); throw new BadPacketException("invalid address"); } //repOk(); } /** * Copy constructor. Creates a new query reply from the passed query * Reply. The new one is same as the passed one, but with different specified * GUID.<p> * * Note: The payload is not really copied, but the reference in the newly * constructed query reply, points to the one in the passed reply. But since * the payload cannot be mutated, it shouldn't make difference if different * query replies maintain reference to same payload * * @param guid The new GUID for the reply * @param reply The query reply from where to copy the fields into the * new constructed query reply */ public QueryReply(byte[] guid, QueryReply reply){ //call the super constructor with new GUID super(guid, Message.F_QUERY_REPLY, reply.getTTL(), reply.getHops(), reply.getLength()); //set the payload field this._payload = reply._payload; setAddress(); } /** * Internal constructor. Only creates QHD if includeQHD==true. */ private QueryReply(byte[] guid, byte ttl, int port, byte[] ip, long speed, Response[] responses, byte[] clientGUID, byte[] xmlBytes, boolean includeQHD, boolean needsPush, boolean isBusy, boolean finishedUpload, boolean measuredSpeed, boolean supportsChat, boolean supportsBH, boolean isMulticastReply, boolean supportsFWTransfer, Set proxies) { super(guid, Message.F_QUERY_REPLY, ttl, (byte)0, 0, // length, update later 16); // 16-byte footer if (xmlBytes.length > XML_MAX_SIZE) throw new IllegalArgumentException("xml too large: " + new String(xmlBytes)); final int n = responses.length; if(!NetworkUtils.isValidPort(port)) { throw new IllegalArgumentException("invalid port: "+port); } else if(ip.length != 4) { throw new IllegalArgumentException("invalid ip length: "+ip.length); } else if(!NetworkUtils.isValidAddress(ip)) { throw new IllegalArgumentException("invalid address: " + NetworkUtils.ip2string(ip)); } else if((speed & 0xFFFFFFFF00000000l) != 0) { throw new IllegalArgumentException("invalid speed: "+speed); } else if(n >= 256) { throw new IllegalArgumentException("invalid num responses: "+n); } _data = new QueryReplyData(); _data.setXmlBytes(xmlBytes); _data.setProxies(proxies); _data.setSupportsFWTransfer(supportsFWTransfer); ByteArrayOutputStream baos = new ByteArrayOutputStream(); try { //Write beginning of payload. //Downcasts are ok, even if they go negative baos.write(n); ByteOrder.short2leb((short)port, baos); baos.write(ip, 0, ip.length); ByteOrder.int2leb((int)speed, baos); //Write each response for (int left=n; left>0; left--) { Response r=responses[n-left]; r.writeToStream(baos); } //Write QHD if desired if (includeQHD) { //a) vendor code. This is hardcoded here for simplicity, //efficiency, and to prevent character decoding problems. If you //change this, be sure to change CommonUtils.QHD_VENDOR_NAME as //well. baos.write(76); //'L' baos.write(73); //'I' baos.write(77); //'M' baos.write(69); //'E' //b) payload length baos.write(COMMON_PAYLOAD_LEN); // size of standard, no options, ggep block... int ggepLen= _ggepUtil.getQRGGEP(false, false, false, Collections.EMPTY_SET).length; //c) PART 1: common area flags and controls. See format in //parseResults2. boolean hasProxies = (proxies != null) && (proxies.size() > 0); byte flags= (byte)((needsPush && !isMulticastReply ? PUSH_MASK : 0) | BUSY_MASK | UPLOADED_MASK | SPEED_MASK | GGEP_MASK); byte controls= (byte)(PUSH_MASK | (isBusy && !isMulticastReply ? BUSY_MASK : 0) | (finishedUpload ? UPLOADED_MASK : 0) | (measuredSpeed || isMulticastReply ? SPEED_MASK : 0) | (supportsBH || isMulticastReply || hasProxies || supportsFWTransfer ? GGEP_MASK : (ggepLen > 0 ? GGEP_MASK : 0)) ); baos.write(flags); baos.write(controls); //d) PART 2: size of xmlBytes + 1. int xmlSize = xmlBytes.length + 1; if (xmlSize > XML_MAX_SIZE) xmlSize = XML_MAX_SIZE; // yes, truncate! ByteOrder.short2leb(((short) xmlSize), baos); //e) private area: one byte with flags //for chat support byte chatSupport=(byte)(supportsChat ? CHAT_MASK : 0); baos.write(chatSupport); //f) the GGEP block byte[] ggepBytes = _ggepUtil.getQRGGEP(supportsBH, isMulticastReply, supportsFWTransfer, proxies); baos.write(ggepBytes, 0, ggepBytes.length); writeSecureGGEP(baos, xmlBytes); //g) actual xml. baos.write(xmlBytes, 0, xmlBytes.length); // write null after xml, as specified baos.write(0); } //Write footer baos.write(clientGUID, 0, 16); // setup payload params _payload = baos.toByteArray(); updateLength(_payload.length); } catch (IOException reallyBad) { ErrorService.error(reallyBad); } setAddress(); } /** Writes the 'secureGGEP' GGEP. */ protected void writeSecureGGEP(ByteArrayOutputStream out, byte[] xml) { // writes the secure ggep portion. // don't forget to secure the null after the XML also. } /** * Sets the IP address bytes. */ private void setAddress() { _address[0] = _payload[3]; _address[1] = _payload[4]; _address[2] = _payload[5]; _address[3] = _payload[6]; } public void setOOBAddress(InetAddress addr, int port) { _address =addr.getAddress(); ByteOrder.short2leb((short)port,_payload,1); } /** * Sets the guid for this message. Is needed, when we want to cache * query replies or sfor some other reason want to change the GUID as * per the guid of query request * @param guid The guid to be set */ public void setGUID(GUID guid) { super.setGUID(guid); } // inherit doc comment public void writePayload(OutputStream out) throws IOException { out.write(_payload); SentMessageStatHandler.TCP_QUERY_REPLIES.addMessage(this); } /** * Sets this reply to be considered a 'browse host' reply. */ public void setBrowseHostReply(boolean isBH) { parseResults(); _data.setBrowseHostReply(isBH); } /** * Gets whether or not this reply is from a browse host request. */ public boolean isBrowseHostReply() { parseResults(); return _data.isBrowseHostReply(); } /** Return the associated xml metadata string if the queryreply * contained one. */ public byte[] getXMLBytes() { parseResults(); return _data.getXmlBytes(); } /** Return the number of results N in this query. */ public short getResultCount() { //The result of ubyte2int always fits in a short, so downcast is ok. return (short)ByteOrder.ubyte2int(_payload[0]); } /** * @return the number of unique results (per SHA1) carried in this message */ public short getUniqueResultCount() { parseResults(); return _data.getUniqueResultURNs(); } public int getPort() { return ByteOrder.ushort2int(ByteOrder.leb2short(_payload,1)); } /** Returns the IP address of the responding host in standard * dotted-decimal format, e.g., "192.168.0.1" */ public String getIP() { return NetworkUtils.ip2string(_address); //takes care of signs } /** * Accessor the IP address in byte array form. * * @return the IP address for this query hit as an array of bytes */ public byte[] getIPBytes() { return _address; } public long getSpeed() { return ByteOrder.uint2long(ByteOrder.leb2int(_payload,7)); } /** * Returns the Response[]. Throws BadPacketException if this * data couldn't be extracted. */ public Response[] getResultsArray() throws BadPacketException { parseResults(); Response[] responses = _data.getResponses(); if(responses == null) throw new BadPacketException(); return responses; } /** Returns an iterator that will yield the results, each as an * instance of the Response class. Throws BadPacketException if * this data couldn't be extracted. */ public Iterator getResults() throws BadPacketException { parseResults(); Response[] responses = _data.getResponses(); if (responses==null) throw new BadPacketException(); List list=Arrays.asList(responses); return list.iterator(); } /** Returns a List that will yield the results, each as an * instance of the Response class. Throws BadPacketException if * this data couldn't be extracted. */ public List getResultsAsList() throws BadPacketException { parseResults(); Response[] responses = _data.getResponses(); if (responses==null) throw new BadPacketException("results are null"); List list=Arrays.asList(responses); return list; } /** * Returns the name of this' vendor, all capitalized. Throws * BadPacketException if the data couldn't be extracted, either because it * is missing or corrupted. */ public String getVendor() throws BadPacketException { parseResults(); String vendor = _data.getVendor(); if (vendor==null) throw new BadPacketException(); return vendor; } /** * Returns true if this's push flag is set, i.e., a push download is needed. * Returns false if the flag is present but not set. Throws * BadPacketException if the flag couldn't be extracted, either because it * is missing or corrupted. */ public boolean getNeedsPush() throws BadPacketException { parseResults(); switch (_data.getPushFlag()) { case UNDEFINED: throw new BadPacketException(); case TRUE: return true; case FALSE: return false; default: Assert.that(false, "Bad value for push flag: " + _data.getPushFlag()); return false; } } /** * Returns true if this has no more download slots. Returns false if the * busy bit is present but not set. Throws BadPacketException if the flag * couldn't be extracted, either because it is missing or corrupted. */ public boolean getIsBusy() throws BadPacketException { parseResults(); switch (_data.getBusyFlag()) { case UNDEFINED: throw new BadPacketException(); case TRUE: return true; case FALSE: return false; default: Assert.that(false, "Bad value for busy flag: " + _data.getBusyFlag()); return false; } } /** * Returns true if this has successfully uploaded a complete file (bit set). * Returns false if the bit is not set. Throws BadPacketException if the * flag couldn't be extracted, either because it is missing or corrupted. */ public boolean getHadSuccessfulUpload() throws BadPacketException { parseResults(); switch (_data.getUploadedFlag()) { case UNDEFINED: throw new BadPacketException(); case TRUE: return true; case FALSE: return false; default: Assert.that(false, "Bad value for uploaded flag: " + _data.getUploadedFlag()); return false; } } /** * Returns true if the speed in this QueryReply was measured (bit set). * Returns false if it was set by the user (bit unset). Throws * BadPacketException if the flag couldn't be extracted, either because it * is missing or corrupted. */ public boolean getIsMeasuredSpeed() throws BadPacketException { parseResults(); switch (_data.getMeasuredSpeedFlag()) { case UNDEFINED: throw new BadPacketException(); case TRUE: return true; case FALSE: return false; default: Assert.that(false, "Bad value for measured speed flag: " + _data.getMeasuredSpeedFlag()); return false; } } /** Returns the bytes of the signature from the secure GGEP block. */ public byte[] getSecureSignature() { parseResults(); SecureGGEPData sg = _data.getSecureGGEP(); if(sg != null) { try { return sg.getGGEP().getBytes(GGEP.GGEP_HEADER_SIGNATURE); } catch(BadGGEPPropertyException bgpe) { return null; } } else { return null; } } /** Passes in the appropriate bytes of the payload to the signature. */ public void updateSignatureWithSecuredBytes(Signature signature) throws SignatureException { parseResults(); SecureGGEPData sg = _data.getSecureGGEP(); if(sg != null) { signature.update(_payload, 0, sg.getStartIndex()); int end = sg.getEndIndex(); int length = _payload.length - 16 - end; signature.update(_payload, end, length); } } /** Determines if the message was verified. */ public synchronized int getSecureStatus() { return _secureStatus; } /** Sets whether or not the message is verified. */ public synchronized void setSecureStatus(int secureStatus) { this._secureStatus = secureStatus; } /** * Returns true iff the client supports chat. */ public boolean getSupportsChat() { parseResults(); return _data.isSupportsChat(); } /** @return true if the remote host can firewalled transfers. */ public boolean getSupportsFWTransfer() { parseResults(); return _data.isSupportsFWTransfer(); } /** @return 1 or greater if FW Transfer is supported, else 0. */ public byte getFWTransferVersion() { parseResults(); return _data.getFwTransferVersion(); } /** * Returns true iff the client supports browse host feature. */ public boolean getSupportsBrowseHost() { parseResults(); return _data.isSupportsBrowseHost(); } /** * Returns true iff the reply was sent in response to a multicast query. * @return true, iff the reply was sent in response to a multicast query, * false otherwise * @exception Throws BadPacketException if * the flag couldn't be extracted, either because it is missing or * corrupted. Typically this exception is treated the same way as returning * false. */ public boolean isReplyToMulticastQuery() { parseResults(); return _data.isReplyToMulticast(); } /** * @return null or a non-zero lenght array of PushProxy hosts. */ public Set getPushProxies() { parseResults(); return _data.getProxies(); } /** * Returns the HostData object describing information * about this QueryReply. */ public HostData getHostData() throws BadPacketException { parseResults(); HostData hd = _data.getHostData(); if( hd == null ) throw new BadPacketException(); return hd; } /** * Determines if this result has secure data. * This does NOT determine if the result has been verified * as secure. */ public boolean hasSecureData() { parseResults(); return _data.getSecureGGEP() != null; } /** @modifies _data * @effects tries to extract responses from payload and store in responses. */ private synchronized void parseResults() { if (_parsed) return; _parsed=true; parseResults2(); } /** * Parses the individual results for the hit. If any one of the * results is invalid, none of them will be initialized, and the * accessor methods for this class will all throw * <tt>BadPacketException</tt>. This is because a single invalid * response invalidates other invariants, such as the field for * the number of results matching the size of the result array. */ private void parseResults2() { //index into payload to look for next response int i=11; _data = new QueryReplyData(); //1. Extract responses. These are not copied to this.responses until //they are verified. Note, however that the metainformation need not be //verified for these to be acceptable. Also note that exceptions are //silently caught. int left=getResultCount(); //number of records left to get Response[] responses=new Response[left]; Set urns = new HashSet(); // set for the urns carried in this reply short uniqueURNs = 0; try { InputStream bais = new ByteArrayInputStream(_payload,i,_payload.length-i); //For each record... for ( ; left > 0; left--) { Response r = Response.createFromStream(bais); responses[responses.length-left] = r; i+=r.getLength(); if (r.getUrns().isEmpty()) uniqueURNs++; else urns.addAll(r.getUrns()); } //All set. Accept parsed results. _data.setResponses(responses); } catch (ArrayIndexOutOfBoundsException e) { return; } catch (IOException e) { return; } // remember how many unique urns this reply carries uniqueURNs += (short) urns.size(); _data.setUniqueResultURNs(uniqueURNs); //2. Extract BearShare-style metainformation, if any. Any exceptions //are silently caught. The definitive reference for this format is at //http://www.clip2.com/GnutellaProtocol04.pdf. Briefly, the format is // vendor code (4 bytes, case insensitive) // common payload length (4 byte, unsigned, always>0) // common payload (length given above. See below.) // vendor payload (length until clientGUID) //The normal 16 byte clientGUID follows, of course. // //The first byte of the common payload has a one in its 0'th bit* if we //should try a push. However, if there is a second byte, and if the //0'th bit of this byte is zero, the 0'th bit of the first byte should //actually be interpreted as MAYBE. Unfortunately LimeWire 1.4 failed //to set this bit in the second byte, so it should be ignored when //parsing, though set on writing. // //The remaining bits of the first byte of the common payload area tell //whether the corresponding bits in the optional second byte is defined. //The idea behind having two bits per flag is to distinguish between //YES, NO, and MAYBE. These bits are as followed: // bit 1* undefined, for historical reasons // bit 2 1 iff server is busy // bit 3 1 iff server has successfully completed an upload // bit 4 1 iff server's reported speed was actually measured, not // simply set by the user. // // GGEP Stuff // Byte 5 and 6, if the 5th bit is set, signal that there is a GGEP // block. The GGEP block will be after the common payload and will be // headed by the GGEP magic prefix (see the GGEP class for more details. // // If there is a GGEP block, then we look to see what is supported. // //*Here, we use 0-(N-1) numbering. So "0'th bit" refers to the least //significant bit. /* ---------------------------------------------------------------- * QHD UPDATE 8/17/01 * Here is an updated QHD spec. * * Byte 0-3 : Vendor Code * Byte 4 : Public area size (COMMON_PAYLOAD_LEN) * Byte 5-6 : Public area (as described above) * Byte 7-8 : Size of XML + 1 (for a null), you need to count backward * from the client GUID. * Byte 9 : private vendor flag * Byte 10-X: GGEP area (may contain multiple GGEP blocks) * Byte X-beginning of xml : (new) private area * Byte (payload.length - 16 - xmlSize (above)) - (payload.length - 16 - 1) : XML!! * Byte (payload.length - 16 - 1) : NULL * Last 16 Bytes: client GUID. */ try { if (i >= (_payload.length-16)) { //see above throw new BadPacketException("No QHD"); } //Attempt to verify. Results are not copied to this until verified. String vendorT=null; int pushFlagT=UNDEFINED; int busyFlagT=UNDEFINED; int uploadedFlagT=UNDEFINED; int measuredSpeedFlagT=UNDEFINED; boolean supportsChatT=false; boolean supportsBrowseHostT=false; boolean replyToMulticastT=false; Set proxies=null; //a) extract vendor code try { //Must use ISO encoding since characters are more than two //bytes on other platforms. vendorT=new String(_payload, i, 4, "ISO-8859-1"); Assert.that(vendorT.length()==4, "Vendor length wrong. Wrong character encoding?"); } catch (UnsupportedEncodingException e) { Assert.that(false, "No support for ISO-8859-1 encoding"); } i+=4; //b) extract payload length int length=ByteOrder.ubyte2int(_payload[i]); if (length<=0) throw new BadPacketException("Common payload length zero."); i++; if ((i + length) > (_payload.length-16)) // 16 is trailing GUID size throw new BadPacketException("Common payload length imprecise!"); //c) extract push and busy bits from common payload // REMEMBER THAT THE PUSH BIT IS SET OPPOSITE THAN THE OTHERS. // (The 'I understand' is the second bit, the Yes/No is the first) if (length > 1) { //BearShare 2.2.0+ byte control=_payload[i]; byte flags=_payload[i+1]; if ((flags & PUSH_MASK)!=0) pushFlagT = (control&PUSH_MASK)==1 ? TRUE : FALSE; if ((control & BUSY_MASK)!=0) busyFlagT = (flags&BUSY_MASK)!=0 ? TRUE : FALSE; if ((control & UPLOADED_MASK)!=0) uploadedFlagT = (flags&UPLOADED_MASK)!=0 ? TRUE : FALSE; if ((control & SPEED_MASK)!=0) measuredSpeedFlagT = (flags&SPEED_MASK)!=0 ? TRUE : FALSE; if ((control & GGEP_MASK) != 0 && (flags & GGEP_MASK) != 0) { GGEPParser parser = new GGEPParser(); parser.scanForGGEPs(_payload, i + 2); GGEP ggep = parser.getNormalGGEP(); if (ggep != null) { try { supportsBrowseHostT = ggep.hasKey(GGEP.GGEP_HEADER_BROWSE_HOST); if (ggep.hasKey(GGEP.GGEP_HEADER_FW_TRANS)) { _data.setFwTransferVersion(ggep.getBytes(GGEP.GGEP_HEADER_FW_TRANS)[0]); _data.setSupportsFWTransfer(_data.getFwTransferVersion() > 0); } replyToMulticastT = ggep.hasKey(GGEP.GGEP_HEADER_MULTICAST_RESPONSE); proxies = _ggepUtil.getPushProxies(ggep); } catch (BadGGEPPropertyException bgpe) { } } // store the data about the secure result, if it's there. if(parser.getSecureGGEP() != null) { _data.setSecureGGEP(new SecureGGEPData(parser)); } } i+=2; // increment used bytes appropriately... } if (length > 2) { // expecting XML. //d) we need to get the xml stuff. //first we should get its size, then we have to look //backwards and get the actual xml... int a, b, temp; temp = ByteOrder.ubyte2int(_payload[i++]); a = temp; temp = ByteOrder.ubyte2int(_payload[i++]); b = temp << 8; int xmlSize = a | b; if (xmlSize > 1) { int xmlInPayloadIndex = _payload.length-16-xmlSize; byte[] xmlBytes = new byte[xmlSize-1]; System.arraycopy(_payload, xmlInPayloadIndex, xmlBytes, 0, (xmlSize-1)); _data.setXmlBytes(xmlBytes); } else _data.setXmlBytes(DataUtils.EMPTY_BYTE_ARRAY); } //Parse LimeWire's private area. Currently only a single byte //whose LSB is 0x1 if we support chat, or 0x0 if we do. //Shareaza also supports our chat, don't disclude them... int privateLength=_payload.length-i; if (privateLength>0 && (vendorT.equals("LIME") || vendorT.equals("RAZA"))) { byte privateFlags = _payload[i]; supportsChatT = (privateFlags & CHAT_MASK) != 0; } if (i>_payload.length-16) throw new BadPacketException( "Common payload length too large."); //All set. Accept parsed values. Assert.that(vendorT!=null); _data.setVendor(vendorT.toUpperCase(Locale.US)); _data.setPushFlag(pushFlagT); _data.setBusyFlag(busyFlagT); _data.setUploadedFlag(uploadedFlagT); _data.setMeasuredSpeedFlag(measuredSpeedFlagT); _data.setSupportsChat(supportsChatT); _data.setSupportsBrowseHost(supportsBrowseHostT); _data.setReplyToMulticast(replyToMulticastT); if(proxies == null) _data.setProxies(Collections.EMPTY_SET); else _data.setProxies(proxies); _data.setHostData(new HostData(this)); } catch (BadPacketException e) { return; } catch (IndexOutOfBoundsException e) { return; } } /** Returns the 16 byte client ID (i.e., the "footer") of the * responding host. */ public byte[] getClientGUID() { if(clientGUID == null) { byte[] result = new byte[16]; //Copy the last 16 bytes of payload to result. Note that there may //be metainformation before the client GUID. So it is not correct //to simply count after the last result record. int length=super.getLength(); System.arraycopy(_payload, length-16, result, 0, 16); clientGUID = result; } return clientGUID; } /** Returns this, because it's always safe to send big replies. */ public Message stripExtendedPayload() { return this; } public String toString() { return ("QueryReply::\r\n"+ getResultCount()+" hits\r\n"+ super.toString()+"\r\n"+ "ip: "+getIP()+"\r\n"); } /** * This method calculates the quality of service for a given host. The * calculation is some function of whether or not the host is busy, whether * or not the host has ever received an incoming connection, etc. * * Moved this code from SearchView to here permanently, so we avoid * duplication. It makes sense from a data point of view, but this method * isn't really essential an essential method. * * @return a int from -1 to 3, with -1 for "never work" and 3 for "always * work". Typically a return value of N means N+1 stars will be displayed * in the GUI. * @param iFirewalled switch to indicate if the client is firewalled or * not. See RouterService.acceptingIncomingConnection or Acceptor for * details. */ public int calculateQualityOfService(boolean iFirewalled) { final int YES=1; final int MAYBE=0; final int NO=-1; /* Is the remote host busy? */ int busy; try { busy=this.getIsBusy() ? YES : NO; } catch (BadPacketException e) { busy = MAYBE; } boolean isMCastReply = this.isReplyToMulticastQuery(); /* Is the remote host firewalled? */ int heFirewalled; if( isMCastReply ) { iFirewalled = false; heFirewalled = NO; } else if(NetworkUtils.isPrivateAddress(this.getIPBytes())) { heFirewalled = YES; } else { try { heFirewalled=this.getNeedsPush()? YES : NO; } catch (BadPacketException e) { heFirewalled = MAYBE; } } /* Push Proxy availability? */ boolean hasPushProxies = false; if ((this.getPushProxies() != null) && (this.getPushProxies().size() > 1)) hasPushProxies = true; if (getSupportsFWTransfer() && UDPService.instance().canDoFWT()) { iFirewalled = false; heFirewalled = NO; } /* In the old days, busy hosts were considered bad. Now they're ok (but * not great) because of alternate locations. WARNING: before changing * this method, take a look at isFirewalledQuality! */ if(Arrays.equals(_address, RouterService.getAddress())) { return 3; // same address -- display it } else if (isMCastReply) { return 4; // multicast, maybe busy (but doesn't matter) } else if (iFirewalled && heFirewalled==YES) { return -1; // both firewalled; transfer impossible } else if (busy==MAYBE || heFirewalled==MAYBE) { return 0; //* older client; can't tell } else if (busy==YES) { Assert.that(heFirewalled==NO || !iFirewalled); if (heFirewalled==YES) return 0; //* busy, push else return 1; //** busy, direct connect } else if (busy==NO) { Assert.that(heFirewalled==NO || !iFirewalled); if (heFirewalled==YES && !hasPushProxies) return 2; //*** not busy, no/not many proxies, old push else return 3; //**** not busy, has proxies or direct connect } else { Assert.that(false, "Unexpected case!"); return -1; } } /** * Utility method for determining whether or not the given "quality" * score for a <tt>QueryReply</tt> denotes that the host is firewalled * or not. * * @param quality the quality, or score, in question * @return <tt>true</tt> if the quality denotes that the host is * firewalled, otherwise <tt>false</tt> */ public static boolean isFirewalledQuality(int quality) { return quality==0 || quality==2; } // inherit doc comment public void recordDrop() { DroppedSentMessageStatHandler.TCP_QUERY_REPLIES.addMessage(this); } /** Handles all our GGEP stuff. Caches potential GGEP blocks for efficiency. */ static class GGEPUtil { /** The standard GGEP block for a LimeWire QueryReply. * Currently has no keys. */ private final byte[] _standardGGEP; /** A GGEP block that has the 'Browse Host' extension. Useful for Query * Replies. */ private final byte[] _bhGGEP; /** A GGEP block that has the 'Multicast Source' extension. * Useful for Query Replies for a Query from a multicast source. */ private final byte[] _mcGGEP; /** A GGEP block that has everything a QR could possible need. */ private final byte[] _comboGGEP; public GGEPUtil() { ByteArrayOutputStream oStream = new ByteArrayOutputStream(); // the standard GGEP has nothing. try { GGEP standard = new GGEP(false); standard.write(oStream); } catch (IOException writeError) {} _standardGGEP = oStream.toByteArray(); // a GGEP block with JUST BHOST oStream.reset(); try { GGEP bhost = new GGEP(false); bhost.put(GGEP.GGEP_HEADER_BROWSE_HOST); bhost.write(oStream); } catch (IOException writeError) {} _bhGGEP = oStream.toByteArray(); Assert.that(_bhGGEP != null); // a GGEP block with JUST MCAST oStream.reset(); try { GGEP mcast = new GGEP(false); mcast.put(GGEP.GGEP_HEADER_MULTICAST_RESPONSE); mcast.write(oStream); } catch (IOException writeError) {} _mcGGEP = oStream.toByteArray(); Assert.that(_mcGGEP != null); // a GGEP block with everything.... oStream.reset(); try { GGEP combo = new GGEP(false); combo.put(GGEP.GGEP_HEADER_MULTICAST_RESPONSE); combo.put(GGEP.GGEP_HEADER_BROWSE_HOST); combo.write(oStream); } catch (IOException writeError) {} _comboGGEP = oStream.toByteArray(); Assert.that(_comboGGEP != null); } /** @return The appropriate byte[] corresponding to the GGEP block you * desire. */ public byte[] getQRGGEP(boolean supportsBH, boolean isMulticastResponse, boolean supportsFWTransfer, Set proxies) { byte[] retGGEPBlock = _standardGGEP; if ((proxies != null) && (proxies.size() > 0)) { final int MAX_PROXIES = 4; GGEP retGGEP = new GGEP(); // write easy extensions if applicable if (supportsBH) retGGEP.put(GGEP.GGEP_HEADER_BROWSE_HOST); if (isMulticastResponse) retGGEP.put(GGEP.GGEP_HEADER_MULTICAST_RESPONSE); if (supportsFWTransfer) retGGEP.put(GGEP.GGEP_HEADER_FW_TRANS, new byte[] {UDPConnection.VERSION}); // if a PushProxyInterface is valid, write up to MAX_PROXIES ByteArrayOutputStream baos = new ByteArrayOutputStream(); int numWritten = 0; Iterator iter = proxies.iterator(); while(iter.hasNext() && (numWritten < MAX_PROXIES)) { IpPort ppi = (IpPort)iter.next(); String host = ppi.getAddress(); int port = ppi.getPort(); try { IPPortCombo combo = new IPPortCombo(host, port); baos.write(combo.toBytes()); numWritten++; } catch (UnknownHostException bad) { } catch (IOException terrible) { ErrorService.error(terrible); } } try { // add the PushProxies if (numWritten > 0) retGGEP.put(GGEP.GGEP_HEADER_PUSH_PROXY, baos.toByteArray()); // set up return value baos.reset(); retGGEP.write(baos); retGGEPBlock = baos.toByteArray(); } catch (IOException terrible) { ErrorService.error(terrible); } } // else if (supportsBH && supportsFWTransfer && // isMulticastResponse), since supportsFWTransfer is only helpful // if we have proxies else if (supportsBH && isMulticastResponse) retGGEPBlock = _comboGGEP; else if (supportsBH) retGGEPBlock = _bhGGEP; else if (isMulticastResponse) retGGEPBlock = _mcGGEP; return retGGEPBlock; } /** @return a <tt>Set</tt> of <tt>IpPortCombo</tt> instances, * which can be empty but is guaranteed not to be <tt>null</tt>, as * described by the GGEP blocks. * * @param ggeps the array of GGEP extensions that may or may not * contain push proxy data */ public Set getPushProxies(GGEP ggep) { Set proxies = null; if (ggep.hasKey(GGEP.GGEP_HEADER_PUSH_PROXY)) { try { byte[] proxyBytes = ggep.getBytes(GGEP.GGEP_HEADER_PUSH_PROXY); ByteArrayInputStream bais = new ByteArrayInputStream(proxyBytes); while (bais.available() > 0) { byte[] combo = new byte[6]; if (bais.read(combo, 0, combo.length) == combo.length) { try { if(proxies == null) proxies = new IpPortSet(); proxies.add(IPPortCombo.getCombo(combo)); } catch (BadPacketException malformedPair) {} } } } catch (BadGGEPPropertyException bad) {} } if(proxies == null) return Collections.EMPTY_SET; else return proxies; } } }