package com.limegroup.gnutella.messages.vendor; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import org.limewire.io.BadGGEPBlockException; import org.limewire.io.BadGGEPPropertyException; import org.limewire.io.GGEP; import org.limewire.io.GUID; import org.limewire.security.SecurityToken; import org.limewire.service.ErrorService; import org.limewire.util.ByteUtils; import com.limegroup.gnutella.messages.BadPacketException; import com.limegroup.gnutella.messages.GGEPKeys; /** In Vendor Message parlance, the "message type" of this VMP is "LIME/11". * This message acknowledges (ACKS) the guid contained in the message (i.e. A * sends B a message with GUID g, B can acknowledge this message by sending a * LimeACKVendorMessage to A with GUID g). It also contains the amount of * results the client wants. * <p> * This message must maintain backwards compatibility between successive * versions. This entails that any new features would grow the message * outward but shouldn't change the meaning of older fields. This could lead * to some issues (i.e. abandoning fields does not allow for older fields to * be reused) but since we don't expect major changes this is probably OK. * EXCEPTION: Version 1 is NEVER accepted. Only version's 2 and above are * recognized. * <p> * Note that this behavior of maintaining backwards compatibility is really * only necessary for UDP messages since in the UDP case there is probably no * MessagesSupportedVM exchange. * * @version 3 * * * Adds a security token to prevent clients from spoofing their IP and just sending * results back after a little while */ public final class LimeACKVendorMessage extends AbstractVendorMessage { public static final int VERSION = 3; public static final int OLD_VERSION = 2; private static final int PAYLOAD_MIN_LENGTH_V3 = derivePayloadV3(255, new byte[1]).length; /** * Constructs a new LimeACKVendorMessage with data from the network. */ LimeACKVendorMessage(byte[] guid, byte ttl, byte hops, int version, byte[] payload, Network network) throws BadPacketException { super(guid, ttl, hops, F_LIME_VENDOR_ID, F_LIME_ACK, version, payload, network); if (getVersion() == 1) throw new BadPacketException("UNSUPPORTED OLD VERSION"); if (getPayload().length < 1) throw new BadPacketException("UNSUPPORTED PAYLOAD LENGTH: " + getPayload().length); if ((getVersion() == OLD_VERSION) && (getPayload().length != 1)) throw new BadPacketException("VERSION 2 UNSUPPORTED PAYLOAD LEN: " + getPayload().length); if ((getVersion() == VERSION) && (getPayload().length < PAYLOAD_MIN_LENGTH_V3)) throw new BadPacketException("VERSION 3 should have a GGEP"); } /** * Constructs a new LimeACKVendorMessage to be sent out. * @param replyGUID The guid of the original query/reply that you want to * send reply info for. * @param numResults The number of results (0-255 inclusive) that you want * for this query. If you want more than 255 just send 255. */ public LimeACKVendorMessage(GUID replyGUID, int numResults) { super(F_LIME_VENDOR_ID, F_LIME_ACK, OLD_VERSION, derivePayload(numResults)); setGUID(replyGUID); } /** * Constructs a V3 LimeACKVendor message to be sent out. * @param securityToken the token to prevent spoofing. */ public LimeACKVendorMessage(GUID replyGUID, int numResults, SecurityToken securityToken) { super(F_LIME_VENDOR_ID, F_LIME_ACK, VERSION, derivePayloadV3(numResults,securityToken.getBytes())); setGUID(replyGUID); } /** @return an int (0-255) representing the amount of results that a host * wants for a given query (as specified by the guid of this message). */ public int getNumResults() { return ByteUtils.ubyte2int(getPayload()[0]); } /** * @return the security token of the message if it has one or <code>null</code> */ public SecurityToken getSecurityToken() { if (getVersion() > OLD_VERSION) { try { GGEP ggep = new GGEP(getPayload(), 1); if (ggep.hasValueFor(GGEPKeys.GGEP_HEADER_SECURE_OOB)) { // we return a oob query key, but cannot verify it when it is not from us return new UnknownSecurityToken(ggep.getBytes(GGEPKeys.GGEP_HEADER_SECURE_OOB)); } } catch (BadGGEPPropertyException corrupt) {} catch (BadGGEPBlockException e) {} } return null; } /** * Constructs the payload for a LimeACKVendorMessage with the given * number of results. */ private static byte[] derivePayload(int numResults) { if ((numResults < 0) || (numResults > 255)) throw new IllegalArgumentException("Number of results too big: " + numResults); byte[] payload = new byte[1]; byte[] bytes = new byte[2]; ByteUtils.short2leb((short) numResults, bytes, 0); payload[0] = bytes[0]; return payload; } private static byte[] derivePayloadV3(int numResults, byte[] securityTokenBytes) { if ((numResults <= 0) || (numResults > 255)) throw new IllegalArgumentException("Number of results too big: " + numResults); byte[] bytes = new byte[2]; ByteArrayOutputStream out = new ByteArrayOutputStream(); ByteUtils.short2leb((short) numResults, bytes, 0); out.write(bytes[0]); GGEP ggep = new GGEP(); ggep.put(GGEPKeys.GGEP_HEADER_SECURE_OOB, securityTokenBytes); try { ggep.write(out); } catch(IOException iox) { ErrorService.error(iox); // impossible. } return out.toByteArray(); } @Override public boolean equals(Object other) { if (other instanceof LimeACKVendorMessage) { LimeACKVendorMessage o = (LimeACKVendorMessage)other; GUID myGuid = new GUID(getGUID()); GUID otherGuid = new GUID(o.getGUID()); int otherResults = o.getNumResults(); return ((myGuid.equals(otherGuid)) && (getNumResults() == otherResults) && areEqualTokens(getSecurityToken(), o.getSecurityToken()) && super.equals(other)); } return false; } private final boolean areEqualTokens(SecurityToken t1, SecurityToken t2) { return t1 == t2 || (t1 != null && t2 != null && Arrays.equals(t1.getBytes(), t2.getBytes())); } /** Overridden purely for stats handling. */ @Override protected void writePayload(OutputStream out) throws IOException { super.writePayload(out); } @Override public String toString() { StringBuilder builder = new StringBuilder(super.toString()); builder.append(", num results: ").append(getNumResults()); builder.append(", security token: ").append(getSecurityToken()); return builder.toString(); } private static class UnknownSecurityToken implements SecurityToken { private final byte[] data; public UnknownSecurityToken(byte[] data) { this.data = data; } public byte[] getBytes() { return data; } public boolean isFor(TokenData data) { return false; } public void write(OutputStream out) throws IOException { out.write(data); } } }