package com.limegroup.gnutella.messages; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.io.Serializable; import java.util.Iterator; import com.limegroup.gnutella.Assert; import com.limegroup.gnutella.ByteOrder; import com.limegroup.gnutella.GUID; import com.limegroup.gnutella.messages.vendor.VendorMessage; import com.limegroup.gnutella.routing.RouteTableMessage; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.settings.MessageSettings; import com.limegroup.gnutella.statistics.ReceivedErrorStat; import com.limegroup.gnutella.udpconnect.UDPConnectionMessage; import com.limegroup.gnutella.util.DataUtils; /** * A Gnutella message (packet). This class is abstract; subclasses * implement specific messages such as search requests.<p> * * All messages have message IDs, function IDs, TTLs, hops taken, and * data length. Messages come in two flavors: requests (ping, search) * and replies (pong, search results). Message are mostly immutable; * only the TTL, hops, and priority field can be changed. */ public abstract class Message implements Serializable, Comparable { //Functional IDs defined by Gnutella protocol. public static final byte F_PING = (byte)0x0; public static final byte F_PING_REPLY = (byte)0x1; public static final byte F_PUSH = (byte)0x40; public static final byte F_QUERY = (byte)0x80; public static final byte F_QUERY_REPLY = (byte)0x81; public static final byte F_ROUTE_TABLE_UPDATE = (byte)0x30; public static final byte F_VENDOR_MESSAGE = (byte)0x31; public static final byte F_VENDOR_MESSAGE_STABLE = (byte)0x32; public static final byte F_UDP_CONNECTION = (byte)0x41; public static final int N_UNKNOWN = -1; public static final int N_TCP = 1; public static final int N_UDP = 2; public static final int N_MULTICAST = 3; /** * Cached soft max ttl -- if the TTL+hops is greater than SOFT_MAX, * the TTL is set to SOFT_MAX-hops. */ public static final byte SOFT_MAX = ConnectionSettings.SOFT_MAX.getValue(); /** Same as GUID.makeGUID. This exists for backwards compatibility. */ public static byte[] makeGuid() { return GUID.makeGuid(); } ////////////////////////// Instance Data ////////////////////// private byte[] guid; private final byte func; /* We do not support TTLs > 2^7, nor do we support packets * of length > 2^31 */ private byte ttl; private byte hops; private int length; /** Priority for flow-control. Lower numbers mean higher priority.NOT * written to network. */ private int priority=0; /** Time this was created. Not written to network. */ private final long creationTime=System.currentTimeMillis(); /** * The network that this was received on or is going to be sent to. */ private final int network; /** Rep. invariant */ protected void repOk() { Assert.that(guid.length==16); Assert.that(func==F_PING || func==F_PING_REPLY || func==F_PUSH || func==F_QUERY || func==F_QUERY_REPLY || func==F_VENDOR_MESSAGE || func == F_VENDOR_MESSAGE_STABLE, "Bad function code"); if (func==F_PUSH) Assert.that(length==26, "Bad push length: "+length); Assert.that(ttl>=0, "Negative TTL: "+ttl); Assert.that(hops>=0, "Negative hops: "+hops); Assert.that(length>=0, "Negative length: "+length); } ////////////////////// Constructors and Producers ///////////////// /** * @requires func is a valid functional id (i.e., 0, 1, 64, 128, 129), * 0 &<;= ttl, 0 &<;= length (i.e., high bit not used) * @effects Creates a new message with the following data. * The GUID is set appropriately, and the number of hops is set to 0. */ protected Message(byte func, byte ttl, int length) { this(func, ttl, length, N_UNKNOWN); } protected Message(byte func, byte ttl, int length, int network) { this(makeGuid(), func, ttl, (byte)0, length, network); } /** * Same as above, but caller specifies TTL and number of hops. * This is used when reading packets off network. */ protected Message(byte[] guid, byte func, byte ttl, byte hops, int length) { this(guid, func, ttl, hops, length, N_UNKNOWN); } /** * Same as above, but caller specifies the network. * This is used when reading packets off network. */ protected Message(byte[] guid, byte func, byte ttl, byte hops, int length, int network) { if(guid.length != 16) { throw new IllegalArgumentException("invalid guid length: "+guid.length); } this.guid=guid; this.func=func; this.ttl=ttl; this.hops=hops; this.length=length; this.network = network; //repOk(); } /** * Reads a Gnutella message from the specified input stream. The returned * message can be any one of the recognized Gnutella message, such as * queries, query hits, pings, pongs, etc. * * @param in the <tt>InputStream</tt> instance containing message data * @return a new Gnutella message instance * @throws <tt>BadPacketException</tt> if the message is not considered * valid for any reason * @throws <tt>IOException</tt> if there is any IO problem reading the * message */ public static Message read(InputStream in) throws BadPacketException, IOException { return Message.read(in, new byte[23], N_UNKNOWN, SOFT_MAX); } /** * @modifies in * @effects reads a packet from the network and returns it as an * instance of a subclass of Message, unless one of the following happens: * <ul> * <li>No data is available: returns null * <li>A bad packet is read: BadPacketException. The client should be * able to recover from this. * <li>A major problem occurs: IOException. This includes reading packets * that are ridiculously long and half-completed messages. The client * is not expected to recover from this. * </ul> */ public static Message read(InputStream in, byte softMax) throws BadPacketException, IOException { return Message.read(in, new byte[23], N_UNKNOWN, softMax); } /** * @modifies in * @effects reads a packet from the network and returns it as an * instance of a subclass of Message, unless one of the following happens: * <ul> * <li>No data is available: returns null * <li>A bad packet is read: BadPacketException. The client should be * able to recover from this. * <li>A major problem occurs: IOException. This includes reading packets * that are ridiculously long and half-completed messages. The client * is not expected to recover from this. * </ul> */ public static Message read(InputStream in, int network) throws BadPacketException, IOException { return Message.read(in, new byte[23], network, SOFT_MAX); } /** * @requires buf.length==23 * @effects exactly like Message.read(in), but buf is used as scratch for * reading the header. This is an optimization that lets you avoid * repeatedly allocating 23-byte arrays. buf may be used when this returns, * but the contents are not guaranteed to contain any useful data. */ public static Message read(InputStream in, byte[] buf, byte softMax) throws BadPacketException, IOException { return Message.read(in, buf, N_UNKNOWN, softMax); } /** * Reads a message using the specified buffer & network and the default * soft max. */ public static Message read(InputStream in, int network, byte[] buf) throws BadPacketException, IOException { return Message.read(in, buf, network, SOFT_MAX); } /** * @param network the network this was received from. * @requires buf.length==23 * @effects exactly like Message.read(in), but buf is used as scratch for * reading the header. This is an optimization that lets you avoid * repeatedly allocating 23-byte arrays. buf may be used when this returns, * but the contents are not guaranteed to contain any useful data. */ public static Message read(InputStream in, byte[] buf, int network, byte softMax) throws BadPacketException, IOException { //1. Read header bytes from network. If we timeout before any // data has been read, return null instead of throwing an // exception. for (int i=0; i<23; ) { int got; try { got=in.read(buf, i, 23-i); } catch (InterruptedIOException e) { //have we read any of the message yet? if (i==0) return null; else throw e; } if (got==-1) { ReceivedErrorStat.CONNECTION_CLOSED.incrementStat(); throw new IOException("Connection closed."); } i+=got; } //2. Unpack. int length=ByteOrder.leb2int(buf,19); //2.5 If the length is hopelessly off (this includes lengths > // than 2^31 bytes, throw an irrecoverable exception to // cause this connection to be closed. if (length<0 || length > MessageSettings.MAX_LENGTH.getValue()) { ReceivedErrorStat.INVALID_LENGTH.incrementStat(); throw new IOException("Unreasonable message length: "+length); } //3. Read rest of payload. This must be done even for bad // packets, so we can resume reading packets. byte[] payload=null; if (length!=0) { payload=new byte[length]; for (int i=0; i<length; ) { int got=in.read(payload, i, length-i); if (got==-1) { ReceivedErrorStat.CONNECTION_CLOSED.incrementStat(); throw new IOException("Connection closed."); } i+=got; } } else { payload = DataUtils.EMPTY_BYTE_ARRAY; } return createMessage(buf, payload, softMax, network); } /** * Creates a message based on the header & payload. * The header, starting at headerOffset, MUST be >= 19 bytes. * Additional headers bytes will be ignored and the byte[] will be discarded. * (Note that the header is normally 23 bytes, but we don't need the last 4 here.) * The payload MUST be a unique byte[] of that payload. Nothing can write into or change the byte[]. */ public static Message createMessage(byte[] header, byte[] payload, byte softMax, int network) throws BadPacketException, IOException { if(header.length < 19) throw new IllegalArgumentException("header must be >= 19 bytes."); //4. Check values. These are based on the recommendations from the // GnutellaDev page. This also catches those TTLs and hops whose // high bit is set to 0. byte func=header[16]; byte ttl=header[17]; byte hops=header[18]; byte hardMax = (byte)14; if (hops<0) { ReceivedErrorStat.INVALID_HOPS.incrementStat(); throw new BadPacketException("Negative (or very large) hops"); } else if (ttl<0) { ReceivedErrorStat.INVALID_TTL.incrementStat(); throw new BadPacketException("Negative (or very large) TTL"); } else if ((hops > softMax) && (func != F_QUERY_REPLY) && (func != F_PING_REPLY)) { ReceivedErrorStat.HOPS_EXCEED_SOFT_MAX.incrementStat(); throw new BadPacketException("func: " + func + ", ttl: " + ttl + ", hops: " + hops); } else if (ttl+hops > hardMax) { ReceivedErrorStat.HOPS_AND_TTL_OVER_HARD_MAX.incrementStat(); throw new BadPacketException("TTL+hops exceeds hard max; probably spam"); } else if ((ttl+hops > softMax) && (func != F_QUERY_REPLY) && (func != F_PING_REPLY)) { ttl=(byte)(softMax - hops); //overzealous client; //readjust accordingly Assert.that(ttl>=0); //should hold since hops<=softMax ==> //new ttl>=0 } // Delayed GUID allocation byte[] guid=new byte[16]; for (int i=0; i<16; i++) //TODO3: can optimize guid[i]=header[i]; //Dispatch based on opcode. int length = payload.length; switch (func) { //TODO: all the length checks should be encapsulated in the various //constructors; Message shouldn't know anything about the various //messages except for their function codes. I've started this //refactoring with PushRequest and PingReply. case F_PING: if (length>0) //Big ping return new PingRequest(guid,ttl,hops,payload); return new PingRequest(guid,ttl,hops); case F_PING_REPLY: return PingReply.createFromNetwork(guid, ttl, hops, payload); case F_QUERY: if (length<3) break; return QueryRequest.createNetworkQuery( guid, ttl, hops, payload, network); case F_QUERY_REPLY: if (length<26) break; return new QueryReply(guid,ttl,hops,payload,network); case F_PUSH: return new PushRequest(guid,ttl,hops,payload, network); case F_ROUTE_TABLE_UPDATE: //The exact subclass of RouteTableMessage returned depends on //the variant stored within the payload. So leave it to the //static read(..) method of RouteTableMessage to actually call //the right constructor. return RouteTableMessage.read(guid, ttl, hops, payload); case F_VENDOR_MESSAGE: return VendorMessage.deriveVendorMessage(guid, ttl, hops, payload, network); case F_VENDOR_MESSAGE_STABLE: return VendorMessage.deriveVendorMessage(guid, ttl, hops, payload, network); case F_UDP_CONNECTION: return UDPConnectionMessage.createMessage( guid, ttl, hops, payload); } ReceivedErrorStat.INVALID_CODE.incrementStat(); throw new BadPacketException("Unrecognized function code: "+func); } /** * Writes a message quickly, without using temporary buffers or crap. */ public void writeQuickly(OutputStream out) throws IOException { out.write(guid, 0, 16); out.write(func); out.write(ttl); out.write(hops); ByteOrder.int2leb(length, out); writePayload(out); } /** * Writes a message out, using the buffer as the temporary header. */ public void write(OutputStream out, byte[] buf) throws IOException { for (int i=0; i<16; i++) //TODO3: can optimize buf[i]=guid[i]; buf[16]=func; buf[17]=ttl; buf[18]=hops; ByteOrder.int2leb(length, buf, 19); out.write(buf); writePayload(out); } /** * @modifies out * @effects Writes an encoding of this to out. Does NOT flush out. */ public void write(OutputStream out) throws IOException { write(out, new byte[23]); } /** @modifies out * @effects writes the payload specific data to out (the stuff * following the header). Does NOT flush out. */ protected abstract void writePayload(OutputStream out) throws IOException; /** * @effects Writes given extension string to given stream, adding * delimiter if necessary, reporting whether next call should add * delimiter. ext may be null or zero-length, in which case this is noop */ protected boolean writeGemExtension(OutputStream os, boolean addPrefixDelimiter, byte[] extBytes) throws IOException { if (extBytes == null || (extBytes.length == 0)) { return addPrefixDelimiter; } if(addPrefixDelimiter) { os.write(0x1c); } os.write(extBytes); return true; // any subsequent extensions should have delimiter } /** * @effects Writes given extension string to given stream, adding * delimiter if necessary, reporting whether next call should add * delimiter. ext may be null or zero-length, in which case this is noop */ protected boolean writeGemExtension(OutputStream os, boolean addPrefixDelimiter, String ext) throws IOException { if (ext != null) return writeGemExtension(os, addPrefixDelimiter, ext.getBytes()); else return writeGemExtension(os, addPrefixDelimiter, new byte[0]); } /** * @effects Writes each extension string in exts to given stream, * adding delimiters as necessary. exts may be null or empty, in * which case this is noop */ protected boolean writeGemExtensions(OutputStream os, boolean addPrefixDelimiter, Iterator iter) throws IOException { if (iter == null) { return addPrefixDelimiter; } while(iter.hasNext()) { addPrefixDelimiter = writeGemExtension(os, addPrefixDelimiter, iter.next().toString()); } return addPrefixDelimiter; // will be true is anything at all was written } /** * @effects utility function to read null-terminated byte[] from stream */ protected byte[] readNullTerminatedBytes(InputStream is) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int i; while ((is.available()>0)&&(i=is.read())!=0) { baos.write(i); } return baos.toByteArray(); } //////////////////////////////////////////////////////////////////// public int getNetwork() { return network; } public boolean isMulticast() { return network == N_MULTICAST; } public boolean isUDP() { return network == N_UDP; } public boolean isTCP() { return network == N_TCP; } public boolean isUnknownNetwork() { return network == N_UNKNOWN; } public byte[] getGUID() { return guid; } public byte getFunc() { return func; } public byte getTTL() { return ttl; } /** * If ttl is less than zero, throws IllegalArgumentException. Otherwise sets * this TTL to the given value. This is useful when you want certain messages * to travel less than others. * @modifies this' TTL */ public void setTTL(byte ttl) throws IllegalArgumentException { if (ttl < 0) throw new IllegalArgumentException("invalid TTL: "+ttl); this.ttl = ttl; } /** * Sets the guid for this message. Is needed, when we want to cache * query replies or other messages, and change the GUID as per the * request * @param guid The guid to be set */ protected void setGUID(GUID guid) { this.guid = guid.bytes(); } /** * If the hops is less than zero, throws IllegalArgumentException. * Otherwise sets this hops to the given value. This is useful when you * want certain messages to look as if they've travelled further. * @modifies this' hops */ public void setHops(byte hops) throws IllegalArgumentException { if(hops < 0) throw new IllegalArgumentException("invalid hops: " + hops); this.hops = hops; } public byte getHops() { return hops; } /** Returns the length of this' payload, in bytes. */ public int getLength() { return length; } /** Updates length of this' payload, in bytes. */ protected void updateLength(int l) { length=l; } /** Returns the total length of this, in bytes */ public int getTotalLength() { //Header is 23 bytes. return 23+length; } /** @modifies this * @effects increments hops, decrements TTL if > 0, and returns the * OLD value of TTL. */ public byte hop() { hops++; if (ttl>0) return ttl--; else return ttl; } /** * Returns the system time (i.e., the result of System.currentTimeMillis()) * this was instantiated. */ public long getCreationTime() { return creationTime; } /** Returns this user-defined priority. Lower values are higher priority. */ public int getPriority() { return priority; } /** Set this user-defined priority for flow-control purposes. Lower values * are higher priority. */ public void setPriority(int priority) { this.priority=priority; } /** * Returns a message identical to this but without any extended (typically * GGEP) data. Since Message's are mostly immutable, the returned message * may alias parts of this; in fact the returned message could even be this. * The caveat is that the hops and TTL field of Message can be mutated for * efficiency reasons. Hence you must not call hop() on either this or the * returned value. Typically this is not a problem, as hop() is called * before forwarding/broadcasting a message. * * @return an instance of this without any dangerous extended payload */ public abstract Message stripExtendedPayload(); /** * Returns a negative value if this is of lesser priority than message, * positive value if of higher priority, or zero if of same priority. * Remember that lower priority numbers mean HIGHER priority. * * @exception ClassCastException message not an instance of Message */ public int compareTo(Object message) { Message m=(Message)message; return m.getPriority() - this.getPriority(); } public String toString() { return "{guid="+(new GUID(guid)).toString() +", ttl="+ttl +", hops="+hops +", priority="+getPriority()+"}"; } /** * Records the dropping of this message in statistics. */ public abstract void recordDrop(); }