package com.limegroup.gnutella.messages; import java.io.IOException; import java.io.OutputStream; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import com.limegroup.gnutella.ByteOrder; import com.limegroup.gnutella.util.COBSUtil; import com.limegroup.gnutella.util.NameValue; import com.limegroup.gnutella.util.IOUtils; /** * A mutable GGEP extension block. A GGEP block can be thought of as a * collection of key/value pairs. A key (extension header) cannot be greater * than 15 bytes. The value (extension data) can be 0 to 2^24-1 bytes. Values * can be formatted as a number, boolean, or generic blob of binary data. If * necessary (e.g., for query replies), GGEP will COBS-encode values to remove * null bytes. The order of the extensions is immaterial. Extensions supported * by LimeWire have keys specified in this class (prefixed by GGEP_HEADER...) */ public class GGEP { /** The extension header (key) for Browse Host. */ public static final String GGEP_HEADER_BROWSE_HOST = "BH"; /** The extension header (key) for average daily uptime. */ public static final String GGEP_HEADER_DAILY_AVERAGE_UPTIME = "DU"; /** The extension header (key) for unicast protocol support. */ public static final String GGEP_HEADER_UNICAST_SUPPORT = "GUE"; /** The extension header (key) for vendor info. */ public static final String GGEP_HEADER_VENDOR_INFO = "VC"; /** The extension header (key) for Ultrapeer support. */ public static final String GGEP_HEADER_UP_SUPPORT = "UP"; /** The extension header (key) for QueryKey support. */ public static final String GGEP_HEADER_QUERY_KEY_SUPPORT = "QK"; /** The extension header (key) for QueryKey support. */ public static final String GGEP_HEADER_MULTICAST_RESPONSE = "MCAST"; /** The extension header (key) for PushProxy support. */ public static final String GGEP_HEADER_PUSH_PROXY = "PUSH"; /** The extension header (key) for AlternateLocation support */ public static final String GGEP_HEADER_ALTS = "ALT"; /** The extention header (key) for IpPort request */ public static final String GGEP_HEADER_IPPORT="IP"; /** The extension header (key) for UDP HostCache pongs. */ public static final String GGEP_HEADER_UDP_HOST_CACHE = "UDPHC"; /** The extension header (key) for indicating support for packed ip/ports & udp host caches. */ public static final String GGEP_HEADER_SUPPORT_CACHE_PONGS = "SCP"; /** The extension header (key) for packed IP/Ports */ public static final String GGEP_HEADER_PACKED_IPPORTS="IPP"; /** The extension header (key) for packed UDP Host Caches */ public static final String GGEP_HEADER_PACKED_HOSTCACHES="PHC"; /** The extension header (key) for SHA1 urns. */ public static final String GGEP_HEADER_SHA1 = "S1"; /** The extension header (key) to determine if a SHA1 is valid. */ public static final String GGEP_HEADER_SHA1_VALID = "SV"; /** * The extension header (key) for a feature query. * This is 'WH' for legacy reasons, because 'What is New' was the first. */ public static final String GGEP_HEADER_FEATURE_QUERY = "WH"; /** The extension header disabling OOB proxying. */ public static final String GGEP_HEADER_NO_PROXY = "NP"; /** The extension header (key) for MetaType query support */ public static final String GGEP_HEADER_META = "M"; /** The extension header (key) for client locale */ public static final String GGEP_HEADER_CLIENT_LOCALE = "LOC"; /** The extension header (key) for creation time */ public static final String GGEP_HEADER_CREATE_TIME = "CT"; /** The extension header (key) for Firewalled Transfer support in Hits. */ public static final String GGEP_HEADER_FW_TRANS = "FW"; /** The extension header (key) indicating the GGEP block is the 'secure' block. */ public static final String GGEP_HEADER_SECURE_BLOCK = "SB"; /** The extension header (key) indiciating the value has a signature in it. */ public static final String GGEP_HEADER_SIGNATURE = "SIG"; /** The maximum size of a extension header (key). */ public static final int MAX_KEY_SIZE_IN_BYTES = 15; /** The maximum size of a extension data (value). */ public static final int MAX_VALUE_SIZE_IN_BYTES = 262143; /** The GGEP prefix. A GGEP block will start with this byte value. */ public static final byte GGEP_PREFIX_MAGIC_NUMBER = (byte) 0xC3; /** * The collection of key/value pairs. Rep. rationale: arrays of bytes are * convenient for values since they're easy to convert to numbers or * strings. But strings are conventient for keys since they define hashCode * and equals. */ private final Map /*String->byte[]*/ _props = new TreeMap(); /** * False iff this should COBS encode values to prevent null bytes. * Default is false, to be conservative. */ public boolean notNeedCOBS=false; /** * Cached hash code value to avoid calculating the hash code from the * map each time. */ private volatile int hashCode = 0; //////////////////// Encoding/Decoding (Map <==> byte[]) /////////////////// /** * Creates a new empty GGEP block. Typically this is used for outgoing * messages and mutated before encoding. * * @param notNeedCOBS true if nulls are allowed in extension values;false if * this should activate COBS encoding if necessary to remove null bytes. */ public GGEP(boolean notNeedCOBS) { this.notNeedCOBS=notNeedCOBS; } /** * Creates a new empty GGEP block. Typically this is used for outgoing * messages and mutated before encoding. This does do COBS encoding. */ public GGEP() { this(false); } /** * Constructs a new GGEP message with the given bytes & offset. */ public GGEP(byte[] data, int offset) throws BadGGEPBlockException { this(data, offset, null); } /** * Constructs a GGEP instance based on the GGEP block beginning at * messageBytes[beginOffset]. * @param messageBytes The bytes of the message. * @param beginOffset The begin index of the GGEP prefix. * @param endOffset If you want to get the offset where the GGEP block * ends (more precisely, one above the ending index), then send me a * int[1]. I'll put the endOffset in endOffset[0]. If you don't care, * null will do.... * @exception BadGGEPBlockException Thrown if the block could not be parsed * correctly. */ public GGEP(byte[] messageBytes, final int beginOffset, int[] endOffset) throws BadGGEPBlockException { if (messageBytes.length < 4) throw new BadGGEPBlockException(); // all GGEP blocks start with this prefix.... if (messageBytes[beginOffset] != GGEP_PREFIX_MAGIC_NUMBER) throw new BadGGEPBlockException(); boolean onLastExtension = false; int currIndex = beginOffset + 1; while (!onLastExtension) { // process extension header flags // bit order is interpreted as 76543210 try { sanityCheck(messageBytes[currIndex]); } catch (ArrayIndexOutOfBoundsException malformedInput) { throw new BadGGEPBlockException(); } onLastExtension = isLastExtension(messageBytes[currIndex]); boolean encoded = isEncoded(messageBytes[currIndex]); boolean compressed = isCompressed(messageBytes[currIndex]); int headerLen = deriveHeaderLength(messageBytes[currIndex]); // get the extension header currIndex++; String extensionHeader = null; try { extensionHeader = new String(messageBytes, currIndex, headerLen); } catch (StringIndexOutOfBoundsException inputIsMalformed) { throw new BadGGEPBlockException(); } // get the data length currIndex += headerLen; int[] toIncrement = new int[1]; final int dataLength = deriveDataLength(messageBytes, currIndex, toIncrement); byte[] extensionData = null; currIndex+=toIncrement[0]; if (dataLength > 0) { // ok, data is present, get it.... byte[] data = new byte[dataLength]; try { System.arraycopy(messageBytes, currIndex, data, 0, dataLength); } catch (ArrayIndexOutOfBoundsException malformedInput) { throw new BadGGEPBlockException(); } if (encoded) { try { data = COBSUtil.cobsDecode(data); } catch (IOException badCobsEncoding) { throw new BadGGEPBlockException("Bad COBS Encoding"); } } if (compressed) { try { data = IOUtils.inflate(data); } catch(IOException badData) { throw new BadGGEPBlockException("Bad compressed data"); } } extensionData = data; currIndex += dataLength; } // ok, everything checks out, just slap it in the hashmapper... if(compressed) _props.put(extensionHeader, new NeedsCompression(extensionData)); else _props.put(extensionHeader, extensionData); } if ((endOffset != null) && (endOffset.length > 0)) endOffset[0] = currIndex; } /** * Merges the other's GGEP with this' GGEP. */ public void merge(GGEP other) { _props.putAll(other._props); } private void sanityCheck(byte headerFlags) throws BadGGEPBlockException { // the 4th bit in the header's first byte must be 0. if ((headerFlags & 0x10) != 0) throw new BadGGEPBlockException(); } private boolean isLastExtension(byte headerFlags) { boolean retBool = false; // the 8th bit in the header's first byte, when set, indicates that // this header is the last.... if ((headerFlags & 0x80) != 0) retBool = true; return retBool; } private boolean isEncoded(byte headerFlags) { boolean retBool = false; // the 7th bit in the header's first byte, when set, indicates that // this header is the encoded with COBS if ((headerFlags & 0x40) != 0) retBool = true; return retBool; } private boolean isCompressed(byte headerFlags) { boolean retBool = false; // the 6th bit in the header's first byte, when set, indicates that // this header is the compressed with deflate if ((headerFlags & 0x20) != 0) retBool = true; return retBool; } private int deriveHeaderLength(byte headerFlags) throws BadGGEPBlockException { int retInt = 0; // bits 0-3 give the length of the extension header (1-15) retInt = headerFlags & 0x0F; if (retInt == 0) throw new BadGGEPBlockException(); return retInt; } /** @param increment a int array of size >0. i'll put the number of bytes * devoted to data storage in increment[0]. */ private int deriveDataLength(byte[] buff, int beginOffset, int increment[]) throws BadGGEPBlockException { int length = 0, iterations = 0; // the length is stored in at most 3 bytes.... final int MAX_ITERATIONS = 3; byte currByte; do { try { currByte = buff[beginOffset++]; } catch (ArrayIndexOutOfBoundsException malformedInput) { throw new BadGGEPBlockException(); } length = (length << 6) | (currByte & 0x3f); if (++iterations > MAX_ITERATIONS) throw new BadGGEPBlockException(); } while (0x40 != (currByte & 0x40)); increment[0] = iterations; return length; } /** Writes this GGEP instance as a properly formatted GGEP Block. * @param out This GGEP instance is written to out. * @exception IOException Thrown if had error writing to out. */ public void write(OutputStream out) throws IOException { if (getHeaders().size() > 0) { // start with the magic prefix out.write(GGEP_PREFIX_MAGIC_NUMBER); Iterator headers = getHeaders().iterator(); // for each header, write the GGEP header and data while (headers.hasNext()) { String currHeader = (String) headers.next(); byte[] currData = get(currHeader); int dataLen = 0; boolean shouldEncode = shouldCOBSEncode(currData); boolean shouldCompress = shouldCompress(currHeader); if (currData != null) { if (shouldCompress) { currData = IOUtils.deflate(currData); if(currData.length > MAX_VALUE_SIZE_IN_BYTES) throw new IllegalArgumentException("value for [" + currHeader + "] too large after compression"); } if (shouldEncode) currData = COBSUtil.cobsEncode(currData); dataLen = currData.length; } writeHeader(currHeader, dataLen, !headers.hasNext(), out, shouldEncode, shouldCompress); if (dataLen > 0) out.write(currData); } } } private final boolean shouldCOBSEncode(byte[] data) { // if nulls are allowed from construction time and if nulls are present // in the data... return (!notNeedCOBS && containsNull(data)); } private final boolean shouldCompress(String header) { return (_props.get(header) instanceof NeedsCompression); } private void writeHeader(String header, final int dataLen, boolean isLast, OutputStream out, boolean isEncoded, boolean isCompressed) throws IOException { // 1. WRITE THE HEADER FLAGS // in the future, when we actually encode and compress, this code should // still work. well, the code that deals with the header flags, that // is, you'll still need to encode/compress boolean shouldCompress = false; int flags = 0x00; if (isLast) flags |= 0x80; if (isEncoded) flags |= 0x40; if (isCompressed) flags |= 0x20; flags |= header.getBytes().length; out.write(flags); // 2. WRITE THE HEADER out.write(header.getBytes()); // 3. WRITE THE DATA LEN // possibly 3 bytes int toWrite; int begin = dataLen & 0x3F000; if (dataLen > 0x00000fff) { begin = begin >> 12; // relevant bytes at the bottom now... toWrite = 0x80 | begin; out.write(toWrite); } int middle = dataLen & 0xFC0; if (dataLen > 0x0000003f) { middle = middle >> 6; // relevant bytes at the bottom now... toWrite = 0x80 | middle; out.write(toWrite); } int end = dataLen & 0x3F; // shut off everything except last 6 bits... toWrite = 0x40 | end; out.write(toWrite); } ////////////////////////// Key/Value Mutators and Accessors //////////////// /** * Adds all the specified key/value pairs. * TODO: Allow a value to be compressed. */ public void putAll(List /* of NameValue */ fields) throws IllegalArgumentException { for(Iterator i = fields.iterator(); i.hasNext(); ) { NameValue next = (NameValue)i.next(); String key = next.getName(); Object value = next.getValue(); if(value == null) put(key); else if(value instanceof byte[]) put(key, (byte[])value); else if(value instanceof String) put(key, (String)value); else if(value instanceof Integer) put(key, ((Integer)value).intValue()); else if(value instanceof Long) put(key, ((Long)value).longValue()); else throw new IllegalArgumentException("Unknown value: " + value); } } /** * Adds a key with data that should be compressed. */ public void putCompressed(String key, byte[] value) throws IllegalArgumentException { validateKey(key); //validateValue(value); // done when writing. TODO: do here? _props.put(key, new NeedsCompression(value)); } /** * Adds a key with raw byte value. * @param key the name of the GGEP extension, whose length should be between * 1 and 15, inclusive * @param value the GGEP extension data * @exception IllegalArgumentException key is of an illegal length; * or value contains a null bytes, null bytes are disallowed, and if you * didn't allow nulls at construction but has nulls */ public void put(String key, byte[] value) throws IllegalArgumentException { validateKey(key); validateValue(value); _props.put(key, value); } /** * Adds a key with string value, using the default character encoding. * @param key the name of the GGEP extension, whose length should be between * 1 and 15, inclusive * @param value the GGEP extension data * @exception IllegalArgumentException key is of an illegal length; * or value contains a null bytes, null bytes are disallowed, if you * didn't allow nulls at construction but has nulls */ public void put(String key, String value) throws IllegalArgumentException { put(key, value==null ? null : value.getBytes()); } /** * Adds a key with integer value. * @param key the name of the GGEP extension, whose length should be between * 1 and 15, inclusive * @param value the GGEP extension data, which should be an unsigned integer * @exception IllegalArgumentException key is of an illegal length; or value * is negative; or value contains a null bytes, null bytes are disallowed, * and COBS encoding is not supported */ public void put(String key, int value) throws IllegalArgumentException { if (value<0) //TODO: ? throw new IllegalArgumentException("Negative value"); put(key, ByteOrder.int2minLeb(value)); } /** * Adds a key with long value. * @param key the name of the GGEP extension, whose length should be between * 1 and 15, inclusive * @param value the GGEP extension data, which should be an unsigned long * @exception IllegalArgumentException key is of an illegal length; or value * is negative; or value contains a null bytes, null bytes are disallowed, * and COBS encoding is not supported */ public void put(String key, long value) throws IllegalArgumentException { if (value<0) //TODO: ? throw new IllegalArgumentException("Negative value"); put(key, ByteOrder.long2minLeb(value)); } /** * Adds a key without any value. * @param key the name of the GGEP extension, whose length should be between * 1 and 15, inclusive * @exception IllegalArgumentException key is of an illegal length. */ public void put(String key) throws IllegalArgumentException { put(key, (byte[])null); } /** * Returns the value for a key, as raw bytes. * @param key the name of the GGEP extension * @return the GGEP extension data associated with the key * @exception BadGGEPPropertyException extension not found, was corrupt, * or has no associated data. Note that BadGGEPPropertyException is * is always thrown for extensions with no data; use hasKey instead. */ public byte[] getBytes(String key) throws BadGGEPPropertyException { byte[] ret= get(key); if (ret==null) throw new BadGGEPPropertyException(); return ret; } /** * Returns the value for a key, as a string. * @param key the name of the GGEP extension * @return the GGEP extension data associated with the key * @exception BadGGEPPropertyException extension not found, was corrupt, * or has no associated data. Note that BadGGEPPropertyException is * is always thrown for extensions with no data; use hasKey instead. */ public String getString(String key) throws BadGGEPPropertyException { return new String(getBytes(key)); } /** * Returns the value for a key, as an integer * @param key the name of the GGEP extension * @return the GGEP extension data associated with the key * @exception BadGGEPPropertyException extension not found, was corrupt, * or has no associated data. Note that BadGGEPPropertyException is * is always thrown for extensions with no data; use hasKey instead. */ public int getInt(String key) throws BadGGEPPropertyException { byte[] bytes=getBytes(key); if (bytes.length<1) throw new BadGGEPPropertyException("No bytes"); if (bytes.length>4) throw new BadGGEPPropertyException("Integer too big"); return ByteOrder.leb2int(bytes, 0, bytes.length); } /** * Returns the value for a key as a long. * @param key the name of the GGEP extension * @return the GGEP extension data associated with the key * @exception BadGGEPPropertyException extension not found, was corrupt, * or has no associated data. Note that BadGGEPPropertyException is * is always thrown for extensions with no data; use hasKey instead. */ public long getLong(String key) throws BadGGEPPropertyException { byte[] bytes=getBytes(key); if (bytes.length<1) throw new BadGGEPPropertyException("No bytes"); if (bytes.length>8) throw new BadGGEPPropertyException("Integer too big"); return ByteOrder.leb2long(bytes, 0, bytes.length); } /** * Returns whether this has the given key. * @param key the name of the GGEP extension * @return true if this has a key */ public boolean hasKey(String key) { return _props.containsKey(key); } /** * Returns the set of keys. * @return a set of all the GGEP extension header name in this, each * as a String. */ public Set getHeaders() { return _props.keySet(); } /** * Gets the byte[] data from props. */ public byte[] get(String key) { Object value = _props.get(key); if(value instanceof NeedsCompression) return ((NeedsCompression)value).data; else return (byte[])value; } private void validateKey(String key) throws IllegalArgumentException { byte[] bytes=key.getBytes(); if ((key == null) || key.equals("") || (bytes.length > MAX_KEY_SIZE_IN_BYTES) || containsNull(bytes)) throw new IllegalArgumentException(); } private void validateValue(byte[] value) throws IllegalArgumentException { if (value==null) return; if (value.length>MAX_VALUE_SIZE_IN_BYTES) throw new IllegalArgumentException(); } private boolean containsNull(byte[] bytes) { if (bytes != null) { for (int i = 0; i < bytes.length; i++) if (bytes[i] == 0x0) return true; } return false; } //////////////////////////////// Miscellany /////////////////////////////// /** @return True if the two Maps that represent header/data pairs are * equivalent. */ public boolean equals(Object o) { if(o == this) return true; if (! (o instanceof GGEP)) return false; //This is O(n lg n) time with n keys. It would be great if we could //just check that the trees are isomorphic. I don't think this code is //really used anywhere, however. return this.subset((GGEP)o) && ((GGEP)o).subset(this); } /** Returns true if this is a subset of other, e.g., all of this' keys * can be found in OTHER with the same value. */ private boolean subset(GGEP other) { for (Iterator iter=this._props.keySet().iterator(); iter.hasNext(); ) { String key=(String)iter.next(); byte[] v1= this.get(key); byte[] v2= other.get(key); //Remember that v1 and v2 can be null. if ((v1==null) != (v2==null)) return false; if (v1!=null && !Arrays.equals(v1, v2)) return false; } return true; } // overrides Object.hashCode to be consistent with equals public int hashCode() { if(hashCode == 0) { hashCode = 37 * _props.hashCode(); } return hashCode; } /** * Marker class that wraps a byte[] value, if that value * is going to require compression upon write. */ private static class NeedsCompression { final byte[] data; NeedsCompression(byte[] data) { this.data = data; } } }