package com.limegroup.gnutella.messages;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.StringTokenizer;
import java.util.LinkedList;
import java.util.List;
import java.util.Collections;
import java.util.Collection;
import com.limegroup.gnutella.Assert;
import com.limegroup.gnutella.ByteOrder;
import com.limegroup.gnutella.Endpoint;
import com.limegroup.gnutella.ErrorService;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.Statistics;
import com.limegroup.gnutella.UDPService;
import com.limegroup.gnutella.guess.QueryKey;
import com.limegroup.gnutella.settings.ApplicationSettings;
import com.limegroup.gnutella.statistics.DroppedSentMessageStatHandler;
import com.limegroup.gnutella.statistics.ReceivedErrorStat;
import com.limegroup.gnutella.statistics.SentMessageStatHandler;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.util.IpPortImpl;
import com.limegroup.gnutella.util.NetworkUtils;
/**
* A ping reply message, aka, "pong". This implementation provides a way
* to "mark" pongs as being from supernodes.
*/
public class PingReply extends Message implements Serializable, IpPort {
/**
* The list of extra ip/ports contained in this reply.
*/
private final List PACKED_IP_PORTS;
/**
* The list of extra ip/ports contained in this reply.
*/
private final List PACKED_UDP_HOST_CACHES;
/**
* The IP address to connect to if this is a UDP host cache.
* Null if this is not a UDP host cache.
*/
private final String UDP_CACHE_ADDRESS;
/**
* Constant for the number of ultrapeer slots for this host.
*/
private final int FREE_ULTRAPEER_SLOTS;
/**
* Constant for the number of free leaf slots for this host.
*/
private final int FREE_LEAF_SLOTS;
/**
* Constant for the standard size of the pong payload.
*/
public static final int STANDARD_PAYLOAD_SIZE = 14;
/** All the data. We extract the port, ip address, number of files,
* and number of kilobytes lazily. */
private final byte[] PAYLOAD;
/** The IP string as extracted from payload[2..5]. Cached to avoid
* allocations. LOCKING: obtain this' monitor. */
private final InetAddress IP;
/**
* Constant for the port number of this pong.
*/
private final int PORT;
/**
* The address this pong claims to be my external address
*/
private final InetAddress MY_IP;
/**
* The port this pong claims to be my external port
*/
private final int MY_PORT;
/**
* Constant for the number of shared files reported in the pong.
*/
private final long FILES;
/**
* Constant for the number of shared kilobytes reported in the pong.
*/
private final long KILOBYTES;
/**
* Constant int for the daily average uptime.
*/
private final int DAILY_UPTIME;
/**
* Constant for whether or not the remote node supports unicast.
*/
private final boolean SUPPORTS_UNICAST;
/**
* Constant for the vendor of the remote host.
*/
private final String VENDOR;
/**
* Constant for the major version number reported in the vendor block.
*/
private final int VENDOR_MAJOR_VERSION;
/**
* Constant for the minor version number reported in the vendor block.
*/
private final int VENDOR_MINOR_VERSION;
/**
* Constant for the query key reported for the pong.
*/
private final QueryKey QUERY_KEY;
/**
* Constant boolean for whether or not this pong contains any GGEP
* extensions.
*/
private final boolean HAS_GGEP_EXTENSION;
/**
* Cached constant for the vendor GGEP extension.
*/
private static final byte[] CACHED_VENDOR = new byte[5];
// performs any necessary static initialization of fields,
// such as the vendor GGEP extension
static {
// set 'LIME'
System.arraycopy(CommonUtils.QHD_VENDOR_NAME.getBytes(),
0, CACHED_VENDOR, 0,
CommonUtils.QHD_VENDOR_NAME.getBytes().length);
CACHED_VENDOR[4] = convertToGUESSFormat(CommonUtils.getMajorVersionNumber(),
CommonUtils.getMinorVersionNumber());
}
/**
* Constant for the locale
*/
private String CLIENT_LOCALE;
/**
* the number of free preferenced slots
*/
private int FREE_LOCALE_SLOTS;
/**
* Creates a new <tt>PingReply</tt> for this host with the specified
* GUID and ttl.
*
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
*/
public static PingReply create(byte[] guid, byte ttl) {
return create(guid, ttl, Collections.EMPTY_LIST);
}
/**
* Creates a new <tt>PingReply</tt> for this host with the specified
* GUID, TTL & packed hosts.
*/
public static PingReply create(byte[] guid, byte ttl, Collection hosts) {
return create(
guid,
ttl,
RouterService.getPort(),
RouterService.getAddress(),
(long)RouterService.getNumSharedFiles(),
(long)RouterService.getSharedFileSize()/1024,
RouterService.isSupernode(),
Statistics.instance().calculateDailyUptime(),
UDPService.instance().isGUESSCapable(),
ApplicationSettings.LANGUAGE.getValue().equals("") ?
ApplicationSettings.DEFAULT_LOCALE.getValue() :
ApplicationSettings.LANGUAGE.getValue(),
RouterService.getConnectionManager()
.getNumLimeWireLocalePrefSlots(),
hosts);
}
/**
* Creates a new PingReply for this host with the specified
* GUID, TTL & return address.
*/
public static PingReply create(byte[] guid, byte ttl, IpPort addr) {
return create(guid, ttl, addr, Collections.EMPTY_LIST);
}
/**
* Creates a new PingReply for this host with the specified
* GUID, TTL, return address & packed hosts.
*/
public static PingReply create(byte[] guid, byte ttl,
IpPort returnAddr, Collection hosts) {
GGEP ggep = newGGEP(Statistics.instance().calculateDailyUptime(),
RouterService.isSupernode(),
UDPService.instance().isGUESSCapable());
String locale = ApplicationSettings.LANGUAGE.getValue().equals("") ?
ApplicationSettings.DEFAULT_LOCALE.getValue() :
ApplicationSettings.LANGUAGE.getValue();
addLocale(ggep, locale, RouterService.getConnectionManager()
.getNumLimeWireLocalePrefSlots());
addAddress(ggep, returnAddr);
addPackedHosts(ggep, hosts);
return create(guid,
ttl,
RouterService.getPort(),
RouterService.getAddress(),
(long)RouterService.getNumSharedFiles(),
(long)RouterService.getSharedFileSize()/1024,
RouterService.isSupernode(),
ggep);
}
/**
* Creates a new <tt>PingReply</tt> for this host with the specified
* GUID, ttl, and query key.
*
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
* @param key the <tt>QueryKey</tt> for this reply
*/
public static PingReply createQueryKeyReply(byte[] guid, byte ttl,
QueryKey key) {
return create(guid, ttl,
RouterService.getPort(),
RouterService.getAddress(),
RouterService.getNumSharedFiles(),
RouterService.getSharedFileSize()/1024,
RouterService.isSupernode(),
qkGGEP(key));
}
/**
* Creates a new <tt>PingReply</tt> for this host with the specified
* GUID, ttl, and query key.
*
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
* @param key the <tt>QueryKey</tt> for this reply
*/
public static PingReply createQueryKeyReply(byte[] guid, byte ttl,
int port, byte[] ip,
long sharedFiles,
long sharedSize,
boolean ultrapeer,
QueryKey key) {
return create(guid, ttl,
port,
ip,
sharedFiles,
sharedSize,
ultrapeer,
qkGGEP(key));
}
/**
* Creates a new <tt>PingReply</tt> for an external node -- the data
* in the reply will not contain data for this node. In particular,
* the data fields are set to zero because we do not know these
* statistics for the other node.
*
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
* @param port the port the remote host is listening on
* @param address the address of the node
*/
public static PingReply
create(byte[] guid, byte ttl, int port, byte[] address) {
return create(guid, ttl, port, address, 0, 0, false, -1, false);
}
/**
* Creates a new <tt>PingReply</tt> for an external node -- the data
* in the reply will not contain data for this node. In particular,
* the data fields are set to zero because we do not know these
* statistics for the other node.
*
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
* @param port the port the remote host is listening on
* @param address the address of the node
* @param ultrapeer whether or not we should mark this node as
* being an Ultrapeer
*/
public static PingReply
createExternal(byte[] guid, byte ttl, int port, byte[] address,
boolean ultrapeer) {
return create(guid, ttl, port, address, 0, 0, ultrapeer, -1, false);
}
/**
* Creates a new <tt>PingReply</tt> for an external node -- the data
* in the reply will not contain data for this node. In particular,
* the data fields are set to zero because we do not know these
* statistics for the other node. This is primarily used for testing.
*
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
* @param port the port the remote host is listening on
* @param address the address of the node
* @param ultrapeer whether or not we should mark this node as
* being an Ultrapeer
*/
public static PingReply
createExternal(byte[] guid, byte ttl, int port, byte[] address,
int uptime,
boolean ultrapeer) {
return create(guid, ttl, port, address, 0, 0, ultrapeer, uptime, false);
}
/**
* Creates a new <tt>PingReply</tt> instance for a GUESS node. This
* method should only be called if the caller is sure that the given
* node is, in fact, a GUESS-capable node. This method is only used
* to create pongs for nodes other than ourselves.
*
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
* @param ep the <tt>Endpoint</tt> instance containing data about
* the remote host
*/
public static PingReply
createGUESSReply(byte[] guid, byte ttl, Endpoint ep)
throws UnknownHostException {
return create(guid, ttl,
ep.getPort(),
ep.getHostBytes(),
0, 0, true, -1, true);
}
/**
* Creates a new <tt>PingReply</tt> instance for a GUESS node. This
* method should only be called if the caller is sure that the given
* node is, in fact, a GUESS-capable node. This method is only used
* to create pongs for nodes other than ourselves. Given that this
* reply is for a remote node, we do not know the data for number of
* shared files, etc, and so leave it blank.
*
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
* @param port the port the remote host is listening on
* @param address the address of the node
*/
public static PingReply
createGUESSReply(byte[] guid, byte ttl, int port, byte[] address) {
return create(guid, ttl, port, address, 0, 0, true, -1, true);
}
/**
* Creates a new pong with the specified data -- used primarily for
* testing!
*
* @param guid the sixteen byte message GUID
* @param ttl the message TTL to use
* @param port my listening port. MUST fit in two signed bytes,
* i.e., 0 < port < 2^16.
* @param ip my listening IP address. MUST be in dotted-quad big-endian,
* format e.g. {18, 239, 0, 144}.
* @param files the number of files I'm sharing. Must fit in 4 unsigned
* bytes, i.e., 0 < files < 2^32.
* @param kbytes the total size of all files I'm sharing, in kilobytes.
* Must fit in 4 unsigned bytes, i.e., 0 < files < 2^32.
*/
public static PingReply
create(byte[] guid, byte ttl,
int port, byte[] ip, long files, long kbytes) {
return create(guid, ttl, port, ip, files, kbytes,
false, -1, false);
}
/**
* Creates a new ping from scratch with ultrapeer and daily uptime extension
* data.
*
* @param guid the sixteen byte message GUID
* @param ttl the message TTL to use
* @param port my listening port. MUST fit in two signed bytes,
* i.e., 0 < port < 2^16.
* @param ip my listening IP address. MUST be in dotted-quad big-endian,
* format e.g. {18, 239, 0, 144}.
* @param files the number of files I'm sharing. Must fit in 4 unsigned
* bytes, i.e., 0 < files < 2^32.
* @param kbytes the total size of all files I'm sharing, in kilobytes.
* Must fit in 4 unsigned bytes, i.e., 0 < files < 2^32.
* @param isUltrapeer true if this should be a marked ultrapeer pong,
* which sets kbytes to the nearest power of 2 not less than 8.
* @param dailyUptime my average daily uptime, in seconds, e.g.,
* 3600 for one hour per day. Negative values mean "don't know".
* GGEP extension blocks are allocated if dailyUptime is non-negative.
*/
public static PingReply
create(byte[] guid, byte ttl,
int port, byte[] ip, long files, long kbytes,
boolean isUltrapeer, int dailyUptime, boolean isGUESSCapable) {
return create(guid, ttl, port, ip, files, kbytes, isUltrapeer,
newGGEP(dailyUptime, isUltrapeer, isGUESSCapable));
}
/**
* Creates a new PingReply with the specified data.
*/
public static PingReply create(byte[] guid, byte ttl,
int port, byte[] ip, long files, long kbytes,
boolean isUltrapeer, int dailyUptime, boolean isGuessCapable,
String locale, int slots) {
return create(guid, ttl, port, ip, files, kbytes, isUltrapeer,
dailyUptime, isGuessCapable, locale, slots, Collections.EMPTY_LIST);
}
/**
* creates a new PingReply with the specified locale
*
* @param guid the sixteen byte message GUID
* @param ttl the message TTL to use
* @param port my listening port. MUST fit in two signed bytes,
* i.e., 0 < port < 2^16.
* @param ip my listening IP address. MUST be in dotted-quad big-endian,
* format e.g. {18, 239, 0, 144}.
* @param files the number of files I'm sharing. Must fit in 4 unsigned
* bytes, i.e., 0 < files < 2^32.
* @param kbytes the total size of all files I'm sharing, in kilobytes.
* Must fit in 4 unsigned bytes, i.e., 0 < files < 2^32.
* @param isUltrapeer true if this should be a marked ultrapeer pong,
* which sets kbytes to the nearest power of 2 not less than 8.
* @param dailyUptime my average daily uptime, in seconds, e.g.,
* 3600 for one hour per day. Negative values mean "don't know".
* GGEP extension blocks are allocated if dailyUptime is non-negative.
* @param isGuessCapable guess capable
* @param locale the locale
* @param slots the number of locale preferencing slots available
* @param hosts the hosts to pack into this PingReply
*/
public static PingReply
create(byte[] guid, byte ttl,
int port, byte[] ip, long files, long kbytes,
boolean isUltrapeer, int dailyUptime, boolean isGuessCapable,
String locale, int slots, Collection hosts) {
GGEP ggep = newGGEP(dailyUptime, isUltrapeer, isGuessCapable);
addLocale(ggep, locale, slots);
addPackedHosts(ggep, hosts);
return create(guid,
ttl,
port,
ip,
files,
kbytes,
isUltrapeer,
ggep);
}
/**
* Returns a new <tt>PingReply</tt> instance with all the same data
* as <tt>this</tt>, but with the specified GUID.
*
* @param guid the guid to use for the new <tt>PingReply</tt>
* @return a new <tt>PingReply</tt> instance with the specified GUID
* and all of the data from this <tt>PingReply</tt>
* @throws IllegalArgumentException if the guid is not 16 bytes or the input
* (this') format is bad
*/
public PingReply mutateGUID(byte[] guid) {
if (guid.length != 16)
throw new IllegalArgumentException("bad guid size: " + guid.length);
// i can't just call a new constructor, i have to recreate stuff
try {
return createFromNetwork(guid, getTTL(), getHops(), PAYLOAD);
}
catch (BadPacketException ioe) {
throw new IllegalArgumentException("Input pong was bad!");
}
}
/**
* Creates a new <tt>PingReply</tt> instance with the specified
* criteria.
*
* @return a new <tt>PingReply</tt> instance containing the specified
* data
*/
public static PingReply
create(byte[] guid, byte ttl, int port, byte[] ipBytes, long files,
long kbytes, boolean isUltrapeer, GGEP ggep) {
if(!NetworkUtils.isValidPort(port))
throw new IllegalArgumentException("invalid port: "+port);
if(!NetworkUtils.isValidAddress(ipBytes))
throw new IllegalArgumentException("invalid address: " +
NetworkUtils.ip2string(ipBytes));
InetAddress ip = null;
try {
ip = InetAddress.getByName(NetworkUtils.ip2string(ipBytes));
} catch (UnknownHostException e) {
throw new IllegalArgumentException(e.getMessage());
}
byte[] extensions = null;
if(ggep != null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ggep.write(baos);
} catch(IOException e) {
// this should not happen
ErrorService.error(e);
}
extensions = baos.toByteArray();
}
int length = STANDARD_PAYLOAD_SIZE +
(extensions == null ? 0 : extensions.length);
byte[] payload = new byte[length];
//It's ok if casting port, files, or kbytes turns negative.
ByteOrder.short2leb((short)port, payload, 0);
//payload stores IP in BIG-ENDIAN
payload[2]=ipBytes[0];
payload[3]=ipBytes[1];
payload[4]=ipBytes[2];
payload[5]=ipBytes[3];
ByteOrder.int2leb((int)files, payload, 6);
ByteOrder.int2leb((int) (isUltrapeer ? mark(kbytes) : kbytes),
payload,
10);
//Encode GGEP block if included.
if (extensions != null) {
System.arraycopy(extensions, 0,
payload, STANDARD_PAYLOAD_SIZE,
extensions.length);
}
return new PingReply(guid, ttl, (byte)0, payload, ggep, ip);
}
/**
* Creates a new <tt>PingReply</tt> instance from the network.
*
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
* @param hops the hops for this message
* @param payload the message payload
* @throws <tt>BadPacketException</tt> if the message is invalid for
* any reason
*/
public static PingReply
createFromNetwork(byte[] guid, byte ttl, byte hops, byte[] payload)
throws BadPacketException {
if(guid == null) {
throw new NullPointerException("null guid");
}
if(payload == null) {
throw new NullPointerException("null payload");
}
if (payload.length < STANDARD_PAYLOAD_SIZE) {
ReceivedErrorStat.PING_REPLY_INVALID_PAYLOAD.incrementStat();
throw new BadPacketException("invalid payload length");
}
int port = ByteOrder.ushort2int(ByteOrder.leb2short(payload,0));
if(!NetworkUtils.isValidPort(port)) {
ReceivedErrorStat.PING_REPLY_INVALID_PORT.incrementStat();
throw new BadPacketException("invalid port: "+port);
}
// this address may get updated if we have the UDPHC extention
// therefore it is checked after checking for that extention.
String ipString = NetworkUtils.ip2string(payload, 2);
InetAddress ip = null;
GGEP ggep = parseGGEP(payload);
if(ggep != null) {
if(ggep.hasKey(GGEP.GGEP_HEADER_VENDOR_INFO)) {
byte[] vendorBytes = null;
try {
vendorBytes = ggep.getBytes(GGEP.GGEP_HEADER_VENDOR_INFO);
} catch (BadGGEPPropertyException e) {
ReceivedErrorStat.PING_REPLY_INVALID_GGEP.incrementStat();
throw new BadPacketException("bad GGEP: "+vendorBytes);
}
if(vendorBytes.length < 4) {
ReceivedErrorStat.PING_REPLY_INVALID_VENDOR.incrementStat();
throw new BadPacketException("invalid vendor length: "+
vendorBytes.length);
}
}
if(ggep.hasKey(GGEP.GGEP_HEADER_CLIENT_LOCALE)) {
try {
byte[] clocale =
ggep.getBytes(GGEP.GGEP_HEADER_CLIENT_LOCALE);
}
catch(BadGGEPPropertyException e) {
ReceivedErrorStat.PING_REPLY_INVALID_GGEP.incrementStat();
throw new BadPacketException("GGEP error : creating from"
+ " network : client locale");
}
}
if(ggep.hasKey(GGEP.GGEP_HEADER_PACKED_IPPORTS)) {
byte[] data = null;
try {
data = ggep.getBytes(GGEP.GGEP_HEADER_PACKED_IPPORTS);
} catch(BadGGEPPropertyException bad) {
throw new BadPacketException(bad.getMessage());
}
if(data == null || data.length % 6 != 0)
throw new BadPacketException("invalid data");
}
if(ggep.hasKey(GGEP.GGEP_HEADER_PACKED_HOSTCACHES)) {
try {
ggep.getBytes(GGEP.GGEP_HEADER_PACKED_HOSTCACHES);
} catch(BadGGEPPropertyException bad) {
throw new BadPacketException(bad.getMessage());
}
}
if(ggep.hasKey(GGEP.GGEP_HEADER_UDP_HOST_CACHE)) {
try{
String dns = ggep.getString(GGEP.GGEP_HEADER_UDP_HOST_CACHE);
ip = InetAddress.getByName(dns);
ipString = ip.getHostAddress();
}catch(BadGGEPPropertyException ignored) {
}catch(UnknownHostException bad) {
throw new BadPacketException(bad.getMessage());
}
}
}
if(!NetworkUtils.isValidAddress(ipString)) {
ReceivedErrorStat.PING_REPLY_INVALID_ADDRESS.incrementStat();
throw new BadPacketException("invalid address: " + ipString);
}
if (ip==null) {
try {
ip = InetAddress.getByName(NetworkUtils.ip2string(payload, 2));
} catch (UnknownHostException e) {
throw new BadPacketException("bad IP:"+ipString+" "+e.getMessage());
}
}
return new PingReply(guid, ttl, hops, payload, ggep, ip);
}
/**
* Sole <tt>PingReply</tt> constructor. This establishes all ping
* reply invariants.
* @param guid the Globally Unique Identifier (GUID) for this message
* @param ttl the time to live for this message
* @param hops the hops for this message
* @param payload the message payload
*/
private PingReply(byte[] guid, byte ttl, byte hops, byte[] payload,
GGEP ggep, InetAddress ip) {
super(guid, Message.F_PING_REPLY, ttl, hops, payload.length);
PAYLOAD = payload;
PORT = ByteOrder.ushort2int(ByteOrder.leb2short(PAYLOAD,0));
FILES = ByteOrder.uint2long(ByteOrder.leb2int(PAYLOAD,6));
KILOBYTES = ByteOrder.uint2long(ByteOrder.leb2int(PAYLOAD,10));
IP = ip;
// GGEP parsing
//GGEP ggep = parseGGEP();
int dailyUptime = -1;
boolean supportsUnicast = false;
String vendor = "";
int vendorMajor = -1;
int vendorMinor = -1;
int freeLeafSlots = -1;
int freeUltrapeerSlots = -1;
QueryKey key = null;
String locale /** def. val from settings? */
= ApplicationSettings.DEFAULT_LOCALE.getValue();
int slots = -1; //-1 didn't get it.
InetAddress myIP=null;
int myPort=0;
List packedIPs = Collections.EMPTY_LIST;
List packedCaches = Collections.EMPTY_LIST;
String cacheAddress = null;
// TODO: the exceptions thrown here are messy
if(ggep != null) {
if(ggep.hasKey(GGEP.GGEP_HEADER_DAILY_AVERAGE_UPTIME)) {
try {
dailyUptime =
ggep.getInt(GGEP.GGEP_HEADER_DAILY_AVERAGE_UPTIME);
} catch(BadGGEPPropertyException e) {}
}
supportsUnicast =
ggep.hasKey(GGEP.GGEP_HEADER_UNICAST_SUPPORT);
if(ggep.hasKey(GGEP.GGEP_HEADER_VENDOR_INFO)) {
try {
byte[] bytes = ggep.getBytes(GGEP.GGEP_HEADER_VENDOR_INFO);
if(bytes.length >= 4)
vendor = new String(bytes, 0, 4);
if(bytes.length > 4) {
vendorMajor = bytes[4] >> 4;
vendorMinor = bytes[4] & 0xF;
}
} catch (BadGGEPPropertyException e) {}
}
if (ggep.hasKey(GGEP.GGEP_HEADER_QUERY_KEY_SUPPORT)) {
try {
byte[] bytes =
ggep.getBytes(GGEP.GGEP_HEADER_QUERY_KEY_SUPPORT);
if(QueryKey.isValidQueryKeyBytes(bytes))
key = QueryKey.getQueryKey(bytes, false);
} catch (BadGGEPPropertyException corrupt) {}
}
if(ggep.hasKey((GGEP.GGEP_HEADER_UP_SUPPORT))) {
try {
byte[] bytes = ggep.getBytes(GGEP.GGEP_HEADER_UP_SUPPORT);
if(bytes.length >= 3) {
freeLeafSlots = bytes[1];
freeUltrapeerSlots = bytes[2];
}
} catch (BadGGEPPropertyException e) {}
}
if(ggep.hasKey(GGEP.GGEP_HEADER_CLIENT_LOCALE)) {
try {
byte[] bytes = ggep.getBytes(GGEP.GGEP_HEADER_CLIENT_LOCALE);
if(bytes.length >= 2)
locale = new String(bytes, 0, 2);
if(bytes.length >= 3)
slots = ByteOrder.ubyte2int(bytes[2]);
} catch(BadGGEPPropertyException e) {}
}
if (ggep.hasKey(GGEP.GGEP_HEADER_IPPORT)) {
try{
byte[] data = ggep.getBytes(GGEP.GGEP_HEADER_IPPORT);
byte [] myip = new byte[4];
// only copy the addr if the data is atleast 6
// bytes (ip + port). that way isValidAddress
// will fail & we don't need to recheck the length
// when getting the port.
if(data.length >= 6)
System.arraycopy(data,0,myip,0,4);
if (NetworkUtils.isValidAddress(myip)) {
try{
myIP = NetworkUtils.getByAddress(myip);
myPort = ByteOrder.ushort2int(ByteOrder.leb2short(data,4));
if (NetworkUtils.isPrivateAddress(myIP) ||
!NetworkUtils.isValidPort(myPort) ) {
// liars, or we are behind a NAT and there is LAN outside
// either way we can't use it
myIP=null;
myPort=0;
}
}catch(UnknownHostException bad) {
//keep the ip address null and the port 0
}
}
}catch(BadGGEPPropertyException ignored) {}
}
if(ggep.hasKey(GGEP.GGEP_HEADER_UDP_HOST_CACHE)) {
cacheAddress = "";
try {
cacheAddress = ggep.getString(GGEP.GGEP_HEADER_UDP_HOST_CACHE);
} catch(BadGGEPPropertyException bad) {}
}
if(ggep.hasKey(GGEP.GGEP_HEADER_PACKED_IPPORTS)) {
try {
byte[] data = ggep.getBytes(GGEP.GGEP_HEADER_PACKED_IPPORTS);
packedIPs = NetworkUtils.unpackIps(data);
} catch(BadGGEPPropertyException bad) {
} catch(BadPacketException bpe) {}
}
if(ggep.hasKey(GGEP.GGEP_HEADER_PACKED_HOSTCACHES)) {
try {
String data = ggep.getString(GGEP.GGEP_HEADER_PACKED_HOSTCACHES);
packedCaches = listCaches(data);
} catch(BadGGEPPropertyException bad) {}
}
}
MY_IP = myIP;
MY_PORT = myPort;
HAS_GGEP_EXTENSION = ggep != null;
DAILY_UPTIME = dailyUptime;
SUPPORTS_UNICAST = supportsUnicast;
VENDOR = vendor;
VENDOR_MAJOR_VERSION = vendorMajor;
VENDOR_MINOR_VERSION = vendorMinor;
QUERY_KEY = key;
FREE_LEAF_SLOTS = freeLeafSlots;
FREE_ULTRAPEER_SLOTS = freeUltrapeerSlots;
CLIENT_LOCALE = locale;
FREE_LOCALE_SLOTS = slots;
if(cacheAddress != null && "".equals(cacheAddress))
UDP_CACHE_ADDRESS = getAddress();
else
UDP_CACHE_ADDRESS = cacheAddress;
PACKED_IP_PORTS = packedIPs;
PACKED_UDP_HOST_CACHES = packedCaches;
}
/** Returns the GGEP payload bytes to encode the given uptime */
private static GGEP newGGEP(int dailyUptime, boolean isUltrapeer,
boolean isGUESSCapable) {
GGEP ggep=new GGEP(true);
if (dailyUptime >= 0)
ggep.put(GGEP.GGEP_HEADER_DAILY_AVERAGE_UPTIME, dailyUptime);
if (isGUESSCapable && isUltrapeer) {
// indicate guess support
byte[] vNum = {
convertToGUESSFormat(CommonUtils.getGUESSMajorVersionNumber(),
CommonUtils.getGUESSMinorVersionNumber())};
ggep.put(GGEP.GGEP_HEADER_UNICAST_SUPPORT, vNum);
}
// indicate UP support
if (isUltrapeer)
addUltrapeerExtension(ggep);
// all pongs should have vendor info
ggep.put(GGEP.GGEP_HEADER_VENDOR_INFO, CACHED_VENDOR);
return ggep;
}
/** Returns the GGEP payload bytes to encode the given QueryKey */
private static GGEP qkGGEP(QueryKey queryKey) {
try {
GGEP ggep=new GGEP(true);
// get qk bytes....
ByteArrayOutputStream baos=new ByteArrayOutputStream();
queryKey.write(baos);
// populate GGEP....
ggep.put(GGEP.GGEP_HEADER_QUERY_KEY_SUPPORT, baos.toByteArray());
return ggep;
} catch (IOException e) {
//See above.
Assert.that(false, "Couldn't encode QueryKey" + queryKey);
return null;
}
}
/**
* Adds the locale GGEP.
*/
private static GGEP addLocale(GGEP ggep, String locale, int slots) {
byte[] payload = new byte[3];
byte[] s = locale.getBytes();
payload[0] = s[0];
payload[1] = s[1];
payload[2] = (byte)slots;
ggep.put(GGEP.GGEP_HEADER_CLIENT_LOCALE, payload);
return ggep;
}
/**
* Adds the address GGEP.
*/
private static GGEP addAddress(GGEP ggep, IpPort address) {
byte[] payload = new byte[6];
System.arraycopy(address.getInetAddress().getAddress(), 0, payload, 0, 4);
ByteOrder.short2leb((short)address.getPort(), payload, 4);
ggep.put(GGEP.GGEP_HEADER_IPPORT,payload);
return ggep;
}
/**
* Adds the packed hosts into this GGEP.
*/
private static GGEP addPackedHosts(GGEP ggep, Collection hosts) {
if(hosts == null || hosts.isEmpty())
return ggep;
ggep.put(GGEP.GGEP_HEADER_PACKED_IPPORTS, NetworkUtils.packIpPorts(hosts));
return ggep;
}
/**
* Adds the ultrapeer GGEP extension to the pong. This has the version of
* the Ultrapeer protocol that we support as well as the number of free
* leaf and Ultrapeer slots available.
*
* @param ggep the <tt>GGEP</tt> instance to add the extension to
*/
private static void addUltrapeerExtension(GGEP ggep) {
byte[] payload = new byte[3];
// put version
payload[0] = convertToGUESSFormat(CommonUtils.getUPMajorVersionNumber(),
CommonUtils.getUPMinorVersionNumber()
);
payload[1] = (byte) RouterService.getNumFreeLimeWireLeafSlots();
payload[2] = (byte) RouterService.getNumFreeLimeWireNonLeafSlots();
// add it
ggep.put(GGEP.GGEP_HEADER_UP_SUPPORT, payload);
}
/** puts major as the high order bits, minor as the low order bits.
* @exception IllegalArgumentException thrown if major/minor is greater
* than 15 or less than 0.
*/
private static byte convertToGUESSFormat(int major, int minor)
throws IllegalArgumentException {
if ((major < 0) || (minor < 0) || (major > 15) || (minor > 15))
throw new IllegalArgumentException();
// set major
int retInt = major;
retInt = retInt << 4;
// set minor
retInt |= minor;
return (byte) retInt;
}
/**
* Returns whether or not this pong is reporting any free slots on the
* remote host, either leaf or ultrapeer.
*
* @return <tt>true</tt> if the remote host has any free leaf or ultrapeer
* slots, otherwise <tt>false</tt>
*/
public boolean hasFreeSlots() {
return hasFreeLeafSlots() || hasFreeUltrapeerSlots();
}
/**
* Returns whether or not this pong is reporting free leaf slots on the
* remote host.
*
* @return <tt>true</tt> if the remote host has any free leaf slots,
* otherwise <tt>false</tt>
*/
public boolean hasFreeLeafSlots() {
return FREE_LEAF_SLOTS > 0;
}
/**
* Returns whether or not this pong is reporting free ultrapeer slots on
* the remote host.
*
* @return <tt>true</tt> if the remote host has any free ultrapeer slots,
* otherwise <tt>false</tt>
*/
public boolean hasFreeUltrapeerSlots() {
return FREE_ULTRAPEER_SLOTS > 0;
}
/**
* Accessor for the number of free leaf slots reported by the remote host.
* This will return -1 if the remote host did not include the necessary
* GGEP block reporting slots.
*
* @return the number of free leaf slots, or -1 if the remote host did not
* include this information
*/
public int getNumLeafSlots() {
return FREE_LEAF_SLOTS;
}
/**
* Accessor for the number of free ultrapeer slots reported by the remote
* host. This will return -1 if the remote host did not include the
* necessary GGEP block reporting slots.
*
* @return the number of free ultrapeer slots, or -1 if the remote host did
* not include this information
*/
public int getNumUltrapeerSlots() {
return FREE_ULTRAPEER_SLOTS;
}
protected void writePayload(OutputStream out) throws IOException {
out.write(PAYLOAD);
SentMessageStatHandler.TCP_PING_REPLIES.addMessage(this);
}
/**
* Accessor for the port reported in this pong.
*
* @return the port number reported in the pong
*/
public int getPort() {
return PORT;
}
/**
* Returns the ip field in standard dotted decimal format, e.g.,
* "127.0.0.1". The most significant byte is written first.
*/
public String getAddress() {
return IP.getHostAddress();
}
/**
* Returns the ip address bytes (MSB first)
*/
public byte[] getIPBytes() {
byte[] ip=new byte[4];
ip[0]=PAYLOAD[2];
ip[1]=PAYLOAD[3];
ip[2]=PAYLOAD[4];
ip[3]=PAYLOAD[5];
return ip;
}
/**
* Accessor for the number of files shared, as reported in the
* pong.
*
* @return the number of files reported shared
*/
public long getFiles() {
return FILES;
}
/**
* Accessor for the number of kilobytes shared, as reported in the
* pong.
*
* @return the number of kilobytes reported shared
*/
public long getKbytes() {
return KILOBYTES;
}
/** Returns the average daily uptime in seconds from the GGEP payload.
* If the pong did not report a daily uptime, returns -1.
*
* @return the daily uptime reported in the pong, or -1 if the uptime
* was not present or could not be read
*/
public int getDailyUptime() {
return DAILY_UPTIME;
}
/** Returns whether or not this host support unicast, GUESS-style
* queries.
*
* @return <tt>true</tt> if this host does support GUESS-style queries,
* otherwise <tt>false</tt>
*/
public boolean supportsUnicast() {
return SUPPORTS_UNICAST;
}
/** Returns the 4-character vendor string associated with this Pong.
*
* @return the 4-character vendor code reported in the pong, or the
* empty string if no vendor code was successfully read
*/
public String getVendor() {
return VENDOR;
}
/** Returns the major version number of the vendor returning this pong.
*
* @return the major version number of the vendor returning this pong,
* or -1 if the version could not be read
*/
public int getVendorMajorVersion() {
return VENDOR_MAJOR_VERSION;
}
/** Returns the minor version number of the vendor returning this pong.
*
* @return the minor version number of the vendor returning this pong,
* or -1 if the version could not be read
*/
public int getVendorMinorVersion() {
return VENDOR_MINOR_VERSION;
}
/** Returns the QueryKey (if any) associated with this pong. May be null!
*
* @return the <tt>QueryKey</tt> for this pong, or <tt>null</tt> if no
* key was specified
*/
public QueryKey getQueryKey() {
return QUERY_KEY;
}
/**
* Gets the list of packed IP/Ports.
*/
public List /* of IpPort */ getPackedIPPorts() {
return PACKED_IP_PORTS;
}
/**
* Gets a list of packed IP/Ports of UDP Host Caches.
*/
public List /* of IpPort */ getPackedUDPHostCaches() {
return PACKED_UDP_HOST_CACHES;
}
/**
* Returns whether or not this pong has a GGEP extension.
*
* @return <tt>true</tt> if the pong has a GGEP extension, otherwise
* <tt>false</tt>
*/
public boolean hasGGEPExtension() {
return HAS_GGEP_EXTENSION;
}
// TODO : change this to look for multiple GGEP block in the payload....
/** Ensure GGEP data parsed...if possible. */
private static GGEP parseGGEP(final byte[] PAYLOAD) {
//Return if this is a plain pong without space for GGEP. If
//this has bad GGEP data, multiple calls to
//parseGGEP will result in multiple parse attempts. While this is
//inefficient, it is sufficiently rare to not justify a parsedGGEP
//variable.
if (PAYLOAD.length <= STANDARD_PAYLOAD_SIZE)
return null;
try {
return new GGEP(PAYLOAD, STANDARD_PAYLOAD_SIZE, null);
} catch (BadGGEPBlockException e) {
return null;
}
}
// inherit doc comment from message superclass
public Message stripExtendedPayload() {
//TODO: if this is too slow, we can alias parts of this, as as the
//payload. In fact we could even return a subclass of PingReply that
//simply delegates to this.
byte[] newPayload=new byte[STANDARD_PAYLOAD_SIZE];
System.arraycopy(PAYLOAD, 0,
newPayload, 0,
STANDARD_PAYLOAD_SIZE);
return new PingReply(this.getGUID(), this.getTTL(), this.getHops(),
newPayload, null, IP);
}
/**
* Unzips data about UDP host caches & returns a list of'm.
*/
private List listCaches(String allCaches) {
List theCaches = new LinkedList();
StringTokenizer st = new StringTokenizer(allCaches, "\n");
while(st.hasMoreTokens()) {
String next = st.nextToken();
// look for possible features and ignore'm
int i = next.indexOf("&");
// basically ignore.
if(i != -1)
next = next.substring(0, i);
i = next.indexOf(":");
int port = 6346;
if(i == 0 || i == next.length()) {
continue;
} else if(i != -1) {
try {
port = Integer.valueOf(next.substring(i+1)).intValue();
} catch(NumberFormatException invalid) {
continue;
}
} else {
i = next.length(); // setup for i-1 below.
}
if(!NetworkUtils.isValidPort(port))
continue;
String host = next.substring(0, i);
try {
theCaches.add(new IpPortImpl(host, port));
} catch(UnknownHostException invalid) {
continue;
}
}
return Collections.unmodifiableList(theCaches);
}
////////////////////////// Pong Marking //////////////////////////
/**
* Returns true if this message is "marked", i.e., likely from an
* Ultrapeer.
*
* @return <tt>true</tt> if this pong is marked as an Ultrapeer pong,
* otherwise <tt>false</tt>
*/
public boolean isUltrapeer() {
//Returns true if kb is a power of two greater than or equal to eight.
long kb = getKbytes();
if (kb < 8)
return false;
return isPowerOf2(ByteOrder.long2int(kb));
}
public static boolean isPowerOf2(int x) { //package access for testability
if (x<=0)
return false;
else
return (x&(x - 1)) == 0;
}
// inherit doc comment
public void recordDrop() {
DroppedSentMessageStatHandler.TCP_PING_REPLIES.addMessage(this);
}
/** Marks the given kbytes field */
private static long mark(long kbytes) {
int x=ByteOrder.long2int(kbytes);
//Returns the power of two nearest to x. TODO3: faster algorithms are
//possible. At the least, you can do binary search. I imagine some bit
//operations can be done as well. This brute-force approach was
//generated with the help of the the following Python program:
//
// for i in xrange(0, 32):
// low=1<<i
// high=1<<(i+1)
// split=(low+high)/2
// print "else if (x<%d)" % split
// print " return %d; //1<<%d" % (low, i)
if (x<12)
return 8; //1<<3
else if (x<24)
return 16; //1<<4
else if (x<48)
return 32; //1<<5
else if (x<96)
return 64; //1<<6
else if (x<192)
return 128; //1<<7
else if (x<384)
return 256; //1<<8
else if (x<768)
return 512; //1<<9
else if (x<1536)
return 1024; //1<<10
else if (x<3072)
return 2048; //1<<11
else if (x<6144)
return 4096; //1<<12
else if (x<12288)
return 8192; //1<<13
else if (x<24576)
return 16384; //1<<14
else if (x<49152)
return 32768; //1<<15
else if (x<98304)
return 65536; //1<<16
else if (x<196608)
return 131072; //1<<17
else if (x<393216)
return 262144; //1<<18
else if (x<786432)
return 524288; //1<<19
else if (x<1572864)
return 1048576; //1<<20
else if (x<3145728)
return 2097152; //1<<21
else if (x<6291456)
return 4194304; //1<<22
else if (x<12582912)
return 8388608; //1<<23
else if (x<25165824)
return 16777216; //1<<24
else if (x<50331648)
return 33554432; //1<<25
else if (x<100663296)
return 67108864; //1<<26
else if (x<201326592)
return 134217728; //1<<27
else if (x<402653184)
return 268435456; //1<<28
else if (x<805306368)
return 536870912; //1<<29
else
return 1073741824; //1<<30
}
// overrides Object.toString
public String toString() {
return "PingReply("+getAddress()+":"+getPort()+
", free ultrapeers slots: "+hasFreeUltrapeerSlots()+
", free leaf slots: "+hasFreeLeafSlots()+
", vendor: "+VENDOR+" "+VENDOR_MAJOR_VERSION+"."+
VENDOR_MINOR_VERSION+
", "+super.toString()+
", locale : " + CLIENT_LOCALE + ")";
}
/**
* Implements <tt>IpPort</tt> interface. Returns the <tt>InetAddress</tt>
* for this host.
*
* @return the <tt>InetAddress</tt> for this host
*/
public InetAddress getInetAddress() {
return IP;
}
public InetAddress getMyInetAddress() {
return MY_IP;
}
public int getMyPort() {
return MY_PORT;
}
/**
* access the client_locale
*/
public String getClientLocale() {
return CLIENT_LOCALE;
}
public int getNumFreeLocaleSlots() {
return FREE_LOCALE_SLOTS;
}
/**
* Accessor for host cacheness.
*/
public boolean isUDPHostCache() {
return UDP_CACHE_ADDRESS != null;
}
/**
* Gets the UDP host cache address.
*/
public String getUDPCacheAddress() {
return UDP_CACHE_ADDRESS;
}
//Unit test: tests/com/limegroup/gnutella/messages/PingReplyTest
}