package com.limegroup.gnutella.messages; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Set; import org.limewire.io.BadGGEPBlockException; import org.limewire.io.GGEP; import org.limewire.io.IpPort; import org.limewire.io.NetworkInstanceUtils; import org.limewire.security.SecurityToken; import org.limewire.util.ByteUtils; import com.google.inject.Inject; import com.google.inject.Singleton; import com.limegroup.gnutella.Response; import com.limegroup.gnutella.ResponseFactory; import com.limegroup.gnutella.NetworkManager; import com.limegroup.gnutella.messages.Message.Network; import com.limegroup.gnutella.util.DataUtils; @Singleton public class QueryReplyFactoryImpl implements QueryReplyFactory { private final ResponseFactory responseFactory; private final NetworkManager networkManager; private final NetworkInstanceUtils networkInstanceUtils; @Inject public QueryReplyFactoryImpl(NetworkManager networkManager, NetworkInstanceUtils networkInstanceUtils, ResponseFactory responseFactory) { this.networkManager = networkManager; this.networkInstanceUtils = networkInstanceUtils; this.responseFactory = responseFactory; } /** * Creates a new query reply. The number of responses is responses.length * The Browse Host GGEP extension is ON by default. * * @requires * <xmp> * 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 *</xmp> */ public QueryReply createQueryReply(byte[] guid, byte ttl, int port, byte[] ip, long speed, Response[] responses, byte[] clientGUID, boolean isMulticastReply) { return createInternal(guid, ttl, port, ip, speed, responses, clientGUID, DataUtils.EMPTY_BYTE_ARRAY, false, false, false, false, false, false, true, isMulticastReply, false, IpPort.EMPTY_SET, null); } /** * 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 createQueryReply(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) { return createInternal(guid, ttl, port, ip, speed, responses, clientGUID, DataUtils.EMPTY_BYTE_ARRAY, true, needsPush, isBusy, finishedUpload, measuredSpeed, supportsChat, true, isMulticastReply, false, IpPort.EMPTY_SET, null); } /** * 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 createQueryReply(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 { return createQueryReply(guid, ttl, port, ip, speed, responses, clientGUID, xmlBytes, needsPush, isBusy, finishedUpload, measuredSpeed, supportsChat, isMulticastReply, IpPort.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 xmlBytes ghe (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 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. * @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 createQueryReply(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<? extends IpPort> proxies) throws IllegalArgumentException { return createInternal(guid, ttl, port, ip, speed, responses, clientGUID, xmlBytes, true, needsPush, isBusy, finishedUpload, measuredSpeed, supportsChat, true, isMulticastReply, false, proxies, null); } /** * 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 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 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. * @param proxies an array of PushProxy interfaces. will be included in the * replies GGEP extension. * @param the security token to echo along with the query reply * @exception IllegalArgumentException Thrown if xmlBytes.length > * XML_MAX_SIZE */ public QueryReply createQueryReply(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<? extends IpPort> proxies, SecurityToken securityToken) throws IllegalArgumentException { return createInternal(guid, ttl, port, ip, speed, responses, clientGUID, xmlBytes, true, needsPush, isBusy, finishedUpload, measuredSpeed, supportsChat, true, isMulticastReply, false, proxies, securityToken); } /** * 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 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 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. * @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 createQueryReply(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<? extends IpPort> proxies) throws IllegalArgumentException { return createInternal(guid, ttl, port, ip, speed, responses, clientGUID, xmlBytes, true, needsPush, isBusy, finishedUpload, measuredSpeed, supportsChat, true, isMulticastReply, supportsFWTransfer, proxies, null); } /** * 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 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 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. * @param proxies an array of PushProxy interfaces. will be included in the * replies GGEP extension. * @param securityToken might be null * @exception IllegalArgumentException Thrown if xmlBytes.length > * XML_MAX_SIZE */ public QueryReply createQueryReply(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<? extends IpPort> proxies, SecurityToken securityToken) throws IllegalArgumentException { return createInternal(guid, ttl, port, ip, speed, responses, clientGUID, xmlBytes, true, needsPush, isBusy, finishedUpload, measuredSpeed, supportsChat, true, isMulticastReply, supportsFWTransfer, proxies, securityToken); } /** Creates a new query reply with data read from the network. */ public QueryReply createFromNetwork(byte[] guid, byte ttl, byte hops, byte[] payload) throws BadPacketException { return createFromNetwork(guid, ttl, hops, payload, Network.UNKNOWN); } /** * 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 createQueryReply(byte[] guid, QueryReply reply) { try { return createFromNetwork(guid, reply.getTTL(), reply.getHops(), reply.getPayload()); } catch (BadPacketException bpe) { throw new IllegalArgumentException("Invalid QR", bpe); } } public QueryReply createWithNewAddress(byte [] ip, QueryReply reply) { if (Arrays.equals(ip, reply.getIPBytes())) return reply; byte [] payload = reply.getPayload().clone(); System.arraycopy(ip,0,payload,3,4); try { return createFromNetwork(reply.getGUID(), reply.getTTL(), reply.getHops(), payload); } catch (BadPacketException bpe) { throw new IllegalArgumentException("Invalid QR", bpe); } } /** * Copy constructor. * @param ggep new ggep byte [] to add to this reply * @return original reply but with the ggep in it. */ QueryReply createWithNewGGEP(QueryReplyImpl original, byte [] ggep) { int qhdOffset = original.getQHDOffset(); if (qhdOffset == -1) return original; // weird byte [] payload = original.getPayload(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write(original.getPayload(),0,qhdOffset); int length = ByteUtils.ubyte2int(payload[qhdOffset]); // some special cases: // length == 1 requires adding the second flag byte manually if (length == 1) { baos.write(2); byte flag = payload[qhdOffset+1]; flag |= QueryReplyImpl.GGEP_MASK; baos.write(flag); // control baos.write(flag); // flags } else { baos.write(length); // same length byte control = (byte)(payload[qhdOffset + 1] | QueryReplyImpl.GGEP_MASK); byte flags = (byte)(payload[qhdOffset + 2] | QueryReplyImpl.GGEP_MASK); baos.write(control); baos.write(flags); // copy the rest of the common area until start of GGEP baos.write(payload,qhdOffset+3, length - 2); } try { baos.write(ggep); } catch (IOException impossible){ return original; } if (original.getGGEPEnd() != -1) baos.write(payload, original.getGGEPEnd(), payload.length - original.getGGEPEnd()); else baos.write(payload, payload.length - 16, 16); // client guid byte [] newPayload = baos.toByteArray(); try { return createFromNetwork(original.getGUID(), original.getTTL(), original.getHops(), newPayload, original.getNetwork()); } catch (BadPacketException bad) { throw new RuntimeException(bad); } } public QueryReply createWithReturnPathInfo(QueryReply query, IpPort me, IpPort source) { QueryReplyImpl original = (QueryReplyImpl)query; byte [] payload = original.getPayload(); GGEP toAdd; if (original.getGGEPStart() != -1 && original.getGGEPEnd() != -1) { try { toAdd = new GGEP(payload,original.getGGEPStart()); } catch (BadGGEPBlockException bad) { return original; } } else toAdd = new GGEP(); addReturnPathInfo(me, source, original.getHops(), original.getTTL(), toAdd); return createWithNewGGEP(original, toAdd.toByteArray()); } private void addReturnPathInfo(IpPort me, IpPort source, byte hops, byte ttl, GGEP ggep) { String suffix = getReturnPathSuffix(ggep); if (suffix == null) return; if (me != null) { byte [] myAddr = new byte[6]; System.arraycopy(me.getInetAddress().getAddress(),0,myAddr,0,4); ByteUtils.short2beb((short)me.getPort(), myAddr, 4); ggep.put(GGEPKeys.GGEP_HEADER_RETURN_PATH_ME+suffix,myAddr); } byte [] theirAddr = new byte[6]; System.arraycopy(source.getInetAddress().getAddress(),0,theirAddr,0,4); ByteUtils.short2beb((short)source.getPort(), theirAddr, 4); ggep.put(GGEPKeys.GGEP_HEADER_RETURN_PATH_SOURCE+suffix,theirAddr); ggep.put(GGEPKeys.GGEP_HEADER_RETURN_PATH_HOPS+suffix,hops); ggep.put(GGEPKeys.GGEP_HEADER_RETURN_PATH_TTL+suffix,ttl); } /** * @param ggep the ggep we should add the return path info to * @return the suffix we should use, null if we can't add for some reason */ private String getReturnPathSuffix(GGEP ggep) { for (int i = 0; i < 100000; i++) { // large number to make it impractical to avoid if (ggep.hasKey(GGEPKeys.GGEP_HEADER_RETURN_PATH_ME+i) || ggep.hasKey(GGEPKeys.GGEP_HEADER_RETURN_PATH_SOURCE+i) || ggep.hasKey(GGEPKeys.GGEP_HEADER_RETURN_PATH_HOPS+i) || ggep.hasKey(GGEPKeys.GGEP_HEADER_RETURN_PATH_TTL+i)) continue; return String.valueOf(i); } return null; } /// The only two methods that actually end up constructing a QR! public QueryReply createFromNetwork(byte[] guid, byte ttl, byte hops, byte[] payload, Network network) throws BadPacketException { return new QueryReplyImpl(guid, ttl, hops, payload, network, networkInstanceUtils, networkManager, responseFactory); } protected QueryReply createInternal(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<? extends IpPort> proxies, SecurityToken securityToken) { return new QueryReplyImpl(guid, ttl, port, ip, speed, responses, clientGUID, xmlBytes, includeQHD, needsPush, isBusy, finishedUpload, measuredSpeed, supportsChat, supportsBH, isMulticastReply, supportsFWTransfer, proxies, securityToken, networkInstanceUtils, networkManager, responseFactory); } }