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();
}