package com.limegroup.gnutella;
import java.io.DataInputStream;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.WeakHashMap;
import com.limegroup.gnutella.http.HTTPConstants;
import com.limegroup.gnutella.http.HTTPHeaderValue;
import com.limegroup.gnutella.http.HTTPUtils;
import com.limegroup.gnutella.messages.BadPacketException;
import com.limegroup.gnutella.messages.IPPortCombo;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.util.IpPortImpl;
import com.limegroup.gnutella.util.IpPortSet;
import com.limegroup.gnutella.util.NetworkUtils;
/**
* a class that represents an endpoint behind one or more PushProxies.
* almost everything is immutable including the contents of the set.
*
* the network format this is serialized to is:
* byte 0 :
* - bits 0-2 how many push proxies we have (so max is 7)
* - bits 3-4 the version of the f2f transfer protocol this altloc supports
* - bits 5-7 other possible features.
* bytes 1-16 : the guid
* bytes 17-22: ip:port of the address, if known
* followed by 6 bytes per PushProxy
*
* If the size payload on the wire is HEADER+(#of proxies)*PROXY_SIZE then the pushloc
* does not carry in itself an external address. If the size is
* HEADER+(#of proxies+1)*PROXY_SIZE then the first 6 bytes is the ip:port of the
* external address.
*
* the http format this is serialized to is an ascii string consisting of
* ';'-delimited tokens. The first token is the client GUID represented in hex
* and is the only required token. The other tokens can be addresses of push proxies
* or various feature headers. At most one of the tokens should be the external ip and port
* of the firewalled node in a port:ip format. Currently the only feature header we
* parse is the fwawt header that contains the version number of the firewall to
* firewall transfer protocol supported by the altloc.
*
* A PE does not need to know the actual external address of the firewalled host,
* however without that knowledge we cannot do firewall-to-firewall transfer with
* the given host. Also, the RemoteFileDesc objects requires a valid IP for construction,
* so in the case we do not know the external address we return a BOGUS_IP.
*
* Examples:
*
* //altloc with 2 proxies that supports firewall transfer 1 :
*
* <ThisIsTheGUIDASDF>;fwt/1.0;20.30.40.50:60;1.2.3.4:5567
*
* //altloc with 1 proxy that doesn't support firewall transfer and external address:
*
* <ThisIsTHeGUIDasfdaa527>;1.2.3.4:5564;6346:2.3.4.5
*
* //altloc with 1 proxy that supports two features we don't know/care about :
*
* <ThisIsTHeGUIDasfdaa527>;someFeature/3.2;10.20.30.40:5564;otherFeature/0.4
*
* //altloc without any proxies that doesn't support any features
* // not very useful, but still valid
*
* <ThisIsTheGUIDasdf23457>
*/
public class PushEndpoint implements HTTPHeaderValue, IpPort {
public static final int HEADER_SIZE=17; //guid+# of proxies, maybe other things too
public static final int PROXY_SIZE=6; //ip:port
public static final int PLAIN=0x0; //no features for this PE
private static final int SIZE_MASK=0x7; //0000 0111
private static final int FWT_VERSION_MASK=0x18; //0001 1000
//the features mask does not clear the bits we do not understand
//because we may pass on the altloc to someone who does.
private static final int FEATURES_MASK=0xE0; //1110 0000
/**
* A mapping from GUID to a GUIDSetWrapper. This is used to ensure
* that all PE's will have access to the same PushProxies, even if
* multiple PE's exist for a single GUID. Because access to the proxies
* is referenced from this GUID_PROXY_MAP, the PE will always receive the
* most up-to-date set of proxies.
*
* Insertion to this map must be manually performed, to allow for temporary
* PE objects that are used to update pre-existing ones.
*
* There is no explicit removal from the map -- the Weak nature of it will
* automatically remove the key/value pairs when the key is garbage
* collected. For this reason, all PEs must reference the exact GUID
* object that is stored in the map -- to ensure that the map will not GC
* the GUID while it is still in use by a PE.
*
* The value is a GUIDSetWrapper (containing a WeakReference to the
* GUID key as well as the Set of proxies) so that subsequent PEs can
* reference the true key object. A WeakReference is used to allow
* GC'ing to still work and the map to ultimately remove unused keys.
*/
private static final Map GUID_PROXY_MAP =
Collections.synchronizedMap(new WeakHashMap());
static {
RouterService.schedule(new WeakCleaner(),30*1000,30*1000);
}
/**
* the client guid of the endpoint
*/
private final byte [] _clientGUID;
/**
* the guid as an object to avoid recreating
* If there are other PushEnpoint objects, they all will ultimately
* point to the same GUID object. This ensures that as long as
* there is at least one PE object for a remote host, the set of
* proxies will not be gc-ed.
*/
private GUID _guid;
/**
* the various features this PE supports.
*/
private final int _features;
/**
* the version of firewall to firewall transfer protocol
* this endpoint supports.
*/
private final int _fwtVersion;
/**
* the set of proxies this has immediately after creating the endpoint
* cleared after registering in the map. This is used only to
* hold the parsed proxies until they are put in the map.
*/
private Set _proxies;
/**
* the external address of this PE. Needed for firewall-to-firewall
* transfers, but can be null.
*/
private final IpPort _externalAddr;
/**
* @param guid the client guid
* @param proxies the push proxies for that host
*/
public PushEndpoint(byte [] guid, Set proxies,int features,int version) {
this(guid,proxies,features,version,null);
}
public PushEndpoint(byte [] guid, Set proxies,int features,int version,IpPort addr) {
_features = ((features & FEATURES_MASK) | (version << 3));
_fwtVersion=version;
_clientGUID=guid;
_guid = new GUID(_clientGUID);
if (proxies != null) {
if (proxies instanceof IpPortSet)
_proxies = Collections.unmodifiableSet(proxies);
else
_proxies = Collections.unmodifiableSet(new IpPortSet(proxies));
} else
_proxies = Collections.EMPTY_SET;
_externalAddr = addr;
}
public PushEndpoint(byte [] guid, Set proxies) {
this(guid,proxies,PLAIN,0);
}
/**
* creates a PushEndpoint without any proxies.
* not very useful but can happen.
*/
public PushEndpoint(byte [] guid) {
this(guid, Collections.EMPTY_SET);
}
/**
* creates a PushEndpoint from a String passed in http header exchange.
*/
public PushEndpoint(String httpString) throws IOException {
if (httpString.length() < 32 ||
httpString.indexOf(";") > 32)
throw new IOException("http string does not contain valid guid");
//the first token is the guid
String guidS=httpString.substring(0,32);
httpString = httpString.substring(32);
try {
_clientGUID = GUID.fromHexString(guidS);
} catch(IllegalArgumentException iae) {
throw new IOException(iae.getMessage());
}
_guid = new GUID(_clientGUID);
StringTokenizer tok = new StringTokenizer(httpString,";");
Set proxies = new IpPortSet();
int fwtVersion =0;
IpPort addr = null;
while(tok.hasMoreTokens() && proxies.size() < 4) {
String current = tok.nextToken().trim();
// see if this token is the fwt header
// if this token fails to parse we abort since we must know
// if the PE supports fwt or not.
if (current.startsWith(HTTPConstants.FW_TRANSFER)) {
fwtVersion = (int) HTTPUtils.parseFeatureToken(current);
continue;
}
// if its not the header, try to parse it as a push proxy
try {
proxies.add(parseIpPort(current));
continue;
}catch(IOException ohWell) {} //continue trying to parse port:ip
// if its not a push proxy, try to parse it as a port:ip
// only the first occurence of port:ip is parsed
if (addr==null) {
try {
addr = parsePortIp(current);
}catch(IOException notBad) {}
}
}
_proxies = Collections.unmodifiableSet(proxies);
_externalAddr=addr;
_fwtVersion=fwtVersion;
// its ok to use the _proxies and _size fields directly since altlocs created
// from http string do not need to change
_features = proxies.size() | (_fwtVersion << 3);
}
/**
* @return a byte-packed representation of this
*/
public byte [] toBytes() {
Set proxies = getProxies();
int payloadSize = getSizeBytes(proxies);
IpPort addr = getValidExternalAddress();
int FWTVersion = supportsFWTVersion();
if (addr != null && FWTVersion > 0)
payloadSize+=6;
byte [] ret = new byte[payloadSize];
toBytes(ret,0,proxies,addr,FWTVersion);
return ret;
}
/**
* creates a byte packet representation of this
* @param where the byte [] to serialize to
* @param offset the offset within that byte [] to serialize
*/
public void toBytes(byte [] where, int offset) {
toBytes(where, offset, getProxies(), getValidExternalAddress(),supportsFWTVersion());
}
private void toBytes(byte []where, int offset, Set proxies, IpPort address, int FWTVersion) {
int neededSpace = getSizeBytes(proxies);
if (address != null) {
if (FWTVersion > 0)
neededSpace+=6;
} else
FWTVersion = 0;
if (where.length-offset < neededSpace)
throw new IllegalArgumentException ("target array too small");
//store the number of proxies
where[offset] = (byte)(Math.min(4,proxies.size())
| getFeatures()
| FWTVersion << 3);
//store the guid
System.arraycopy(_clientGUID,0,where,++offset,16);
offset+=16;
//if we know the external address, store that too
//if its valid and not private and port is valid
if (address != null && FWTVersion > 0) {
byte [] addr = address.getInetAddress().getAddress();
int port = address.getPort();
System.arraycopy(addr,0,where,offset,4);
offset+=4;
ByteOrder.short2leb((short)port,where,offset);
offset+=2;
}
//store the push proxies
int i=0;
for (Iterator iter = proxies.iterator();iter.hasNext() && i < 4;) {
IpPort ppi = (IpPort) iter.next();
byte [] addr = ppi.getInetAddress().getAddress();
short port = (short)ppi.getPort();
System.arraycopy(addr,0,where,offset,4);
offset+=4;
ByteOrder.short2leb(port,where,offset);
offset+=2;
i++;
}
}
/**
*
* @return an IpPort representing our valid external
* address, or null if we don't have such.
*/
protected IpPort getValidExternalAddress() {
IpPort ret = getIpPort();
if (!NetworkUtils.isValidExternalIpPort(ret))
return null;
Assert.that(!ret.getAddress().equals(RemoteFileDesc.BOGUS_IP),"bogus ip address leaked");
return ret;
}
/**
* Constructs a PushEndpoint from binary representation
*/
public static PushEndpoint fromBytes(DataInputStream dais)
throws BadPacketException, IOException {
byte [] guid =new byte[16];
Set proxies = new IpPortSet();
IpPort addr = null;
int header = dais.read() & 0xFF;
// get the number of push proxies
int number = header & SIZE_MASK;
int features = header & FEATURES_MASK;
int version = (header & FWT_VERSION_MASK) >> 3;
dais.readFully(guid);
if (version > 0) {
byte [] host = new byte[6];
dais.readFully(host);
addr = IPPortCombo.getCombo(host);
if (addr.getAddress().equals(RemoteFileDesc.BOGUS_IP)) {
addr = null;
version = 0;
}
}
byte [] tmp = new byte[6];
for (int i = 0; i < number; i++) {
dais.readFully(tmp);
proxies.add(IPPortCombo.getCombo(tmp));
}
/** this adds the read set to the existing proxies */
PushEndpoint pe = new PushEndpoint(guid,proxies,features,version,addr);
pe.updateProxies(true);
return pe;
}
public byte [] getClientGUID() {
return _clientGUID;
}
/**
*
* @return a view of the current set of proxies.
*/
public Set getProxies() {
synchronized(this) {
if (_proxies!=null)
return _proxies;
}
GuidSetWrapper current = (GuidSetWrapper)GUID_PROXY_MAP.get(_guid);
if (current == null)
return Collections.EMPTY_SET;
return current.getProxies();
}
/**
* @param the set of proxies for this PE
* @return how many bytes a PE will use when serialized.
*/
public static int getSizeBytes(Set proxies) {
return HEADER_SIZE + Math.min(proxies.size(),4) * PROXY_SIZE;
}
/**
* @return which version of F2F transfers this PE supports.
* This always returns the most current version we know the PE supports
* unless it has never been put in the map.
*/
public int supportsFWTVersion() {
GuidSetWrapper current = (GuidSetWrapper)
GUID_PROXY_MAP.get(_guid);
int currentVersion = current == null ?
_fwtVersion : current.getFWTVersion();
return currentVersion;
}
/**
* Sets the fwt version supported for all PEs pointing to the
* given client guid.
*/
public static void setFWTVersionSupported(byte[] guid,int version){
GUID g = new GUID(guid);
GuidSetWrapper current = (GuidSetWrapper)
GUID_PROXY_MAP.get(g);
if (current!=null)
current.setFWTVersion(version);
}
public int hashCode() {
return _guid.hashCode();
}
public boolean equals(Object other) {
//this method ignores the version of firewall-to-firewall
//transfers supported, the features and the sets of proxies
if (other == null)
return false;
if (!(other instanceof PushEndpoint))
return false;
PushEndpoint o = (PushEndpoint)other;
//same guid
return _guid.equals(o._guid);
}
public String toString() {
String ret = "PE [FEATURES:"+getFeatures()+", FWT Version:"+supportsFWTVersion()+
", GUID:"+_guid+", address: "+
getAddress()+":"+getPort()+", proxies:{ ";
for (Iterator iter = getProxies().iterator();iter.hasNext();) {
IpPort ppi = (IpPort)iter.next();
ret = ret+ppi.getInetAddress()+":"+ppi.getPort()+" ";
}
ret = ret+ "}]";
return ret;
}
public String httpStringValue() {
StringBuffer httpString =new StringBuffer(_guid.toHexString()).append(";");
//if version is not 0, append it to the http string
int fwtVersion=supportsFWTVersion();
if (fwtVersion!=0) {
httpString.append(HTTPConstants.FW_TRANSFER)
.append("/")
.append(fwtVersion)
.append(";");
// append the external address of this endpoint if such exists
// and is valid, non-private and if the port is valid as well.
IpPort address = getValidExternalAddress();
if (address!=null) {
String addr = getAddress();
int port = getPort();
if (!addr.equals(RemoteFileDesc.BOGUS_IP) &&
NetworkUtils.isValidPort(port)){
httpString.append(port)
.append(":")
.append(addr)
.append(";");
}
}
}
int proxiesWritten=0;
for (Iterator iter = getProxies().iterator();
iter.hasNext() && proxiesWritten <4;) {
IpPort cur = (IpPort)iter.next();
httpString.append(NetworkUtils.ip2string(
cur.getInetAddress().getAddress()));
httpString.append(":").append(cur.getPort()).append(";");
proxiesWritten++;
}
//trim the ; at the end
httpString.deleteCharAt(httpString.length()-1);
return httpString.toString();
}
/**
* @return the various features this PE reports. This always
* returns the most current features, or the ones it was created with
* if they have never been updated.
*/
public int getFeatures() {
GuidSetWrapper current = (GuidSetWrapper)
GUID_PROXY_MAP.get(_guid);
int currentFeatures = current==null ? _features : current.getFeatures();
return currentFeatures & FEATURES_MASK;
}
/**
* updates the features of all PushEndpoints for the given guid
*/
public static void setFeatures(byte [] guid,int features) {
GUID g = new GUID(guid);
GuidSetWrapper current = (GuidSetWrapper)
GUID_PROXY_MAP.get(g);
if (current!=null)
current.setFeatures(features);
}
/**
* updates the external address of all PushEndpoints for the given guid
*/
public static void setAddr(byte [] guid, IpPort addr) {
GUID g = new GUID(guid);
GuidSetWrapper current = (GuidSetWrapper)
GUID_PROXY_MAP.get(g);
if (current!=null)
current.setIpPort(addr);
}
private IpPort getIpPort() {
GuidSetWrapper current = (GuidSetWrapper)
GUID_PROXY_MAP.get(_guid);
return current == null || current.getIpPort() == null ?
_externalAddr : current.getIpPort();
}
/**
* Implements the IpPort interface, returning a bogus ip if we don't know
* it.
*/
public String getAddress() {
IpPort addr = getIpPort();
return addr != null ? addr.getAddress() : RemoteFileDesc.BOGUS_IP;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.util.IpPort#getInetAddress()
*/
public InetAddress getInetAddress() {
IpPort addr = getIpPort();
return addr != null ? addr.getInetAddress() : null;
}
/**
* Implements the IpPort interface, returning a bogus port if we don't know it
*/
public int getPort() {
IpPort addr = getIpPort();
return addr != null ? addr.getPort() : 6346;
}
/**
* Updates either the PushEndpoint or the GUID_PROXY_MAP to ensure
* that GUID_PROXY_MAP has a reference to all live PE GUIDs and
* all live PE's reference the same GUID object as in GUID_PROXY_MAP.
*
* If this method is not called, the PE will know only about the set
* of proxies the remote host had when it was created. Otherwise it
* will point to the most recent known set.
*/
public synchronized void updateProxies(boolean good) {
GuidSetWrapper existing;
GUID guidRef = null;
synchronized(GUID_PROXY_MAP) {
existing = (GuidSetWrapper)GUID_PROXY_MAP.get(_guid);
// try to get a hard ref so that the mapping won't expire
if (existing!=null)
guidRef=existing.getGuid();
// if we do not have a mapping for this guid, or it just expired,
// add a new one atomically
// (we don't care about the proxies of the expired mapping)
if (existing == null || guidRef==null){
existing = new GuidSetWrapper(_guid,_features,_fwtVersion);
if (good)
existing.updateProxies(_proxies,true);
else
existing.updateProxies(Collections.EMPTY_SET,true);
GUID_PROXY_MAP.put(_guid,existing);
// clear the reference to the set
_proxies=null;
return;
}
}
// if we got here, means we did have a mapping. no need to
// hold the map mutex when updating just the set
existing.updateProxies(_proxies,good);
// make sure the PE points to the actual key guid
_guid = guidRef;
_proxies = null;
}
public PushEndpoint createClone() {
return new PushEndpoint(_guid.bytes(),
getProxies(),
getFeatures(),
supportsFWTVersion(),
getIpPort());
}
/**
* Overwrites the current known push proxies for the host specified
* in the httpString with the set contained in the httpString.
*
* @param guid the guid whose proxies to overwrite
* @param httpString comma-separated list of proxies
* @throws IOException if parsing of the http fails.
*/
public static void overwriteProxies(byte [] guid, String httpString)
throws IOException {
Set newSet = new HashSet();
StringTokenizer tok = new StringTokenizer(httpString,",");
while(tok.hasMoreTokens()) {
String proxy = tok.nextToken().trim();
try {
newSet.add(parseIpPort(proxy));
}catch(IOException ohWell){}
}
overwriteProxies(guid, newSet);
}
public static void overwriteProxies(byte[] guid, Set newSet) {
GUID g = new GUID(guid);
GuidSetWrapper wrapper;
synchronized(GUID_PROXY_MAP) {
wrapper = (GuidSetWrapper)GUID_PROXY_MAP.get(g);
if (wrapper==null) {
wrapper = new GuidSetWrapper(g);
GUID_PROXY_MAP.put(g, wrapper);
}
wrapper.overwriteProxies(newSet);
}
}
/**
*
* @param http a string representing an ip and port
* @return an object implementing PushProxyInterface
* @throws IOException parsing failed.
*/
private static IpPort parseIpPort(String http)
throws IOException{
int separator = http.indexOf(":");
//see if this is a valid ip:port address;
if (separator == -1 || separator!= http.lastIndexOf(":") ||
separator == http.length())
throw new IOException();
String host = http.substring(0,separator);
if (!NetworkUtils.isValidAddress(host) || NetworkUtils.isPrivateAddress(host))
throw new IOException();
String portS = http.substring(separator+1);
try {
int port = Integer.parseInt(portS);
if(!NetworkUtils.isValidPort(port))
throw new IOException();
IpPort ppc =
new IpPortImpl(host, port);
return ppc;
}catch(NumberFormatException notBad) {
throw new IOException(notBad.getMessage());
}
}
/**
* @param http a string representing a port and an ip
* @return an object implementing IpPort
* @throws IOException parsing failed.
*/
private static IpPort parsePortIp(String http) throws IOException{
int separator = http.indexOf(":");
//see if this is a valid ip:port address;
if (separator == -1 || separator!= http.lastIndexOf(":") ||
separator == http.length())
throw new IOException();
String portS = http.substring(0,separator);
int port =0;
try {
port = Integer.parseInt(portS);
if(!NetworkUtils.isValidPort(port))
throw new IOException();
}catch(NumberFormatException failed) {
throw new IOException(failed.getMessage());
}
String host = http.substring(separator+1);
if (!NetworkUtils.isValidAddress(host) || NetworkUtils.isPrivateAddress(host))
throw new IOException();
return new IpPortImpl(host,port);
}
private static class GuidSetWrapper {
private final WeakReference _guidRef;
private Set _proxies;
private int _features,_fwtVersion;
private IpPort _externalAddr;
GuidSetWrapper(GUID guid) {
this(guid,0,0);
}
GuidSetWrapper(GUID guid,int features, int version) {
_guidRef = new WeakReference(guid);
_features=features;
_fwtVersion=version;
}
synchronized void updateProxies(Set s, boolean add){
Set existing = new IpPortSet();
if (s == null)
s = _proxies;
if (_proxies!=null)
existing.addAll(_proxies);
if (add)
existing.addAll(s);
else
existing.removeAll(s);
overwriteProxies(existing);
}
synchronized void overwriteProxies(Set s) {
_proxies = Collections.unmodifiableSet(s);
}
synchronized Set getProxies() {
return _proxies != null ? _proxies : Collections.EMPTY_SET;
}
synchronized int getFeatures() {
return _features;
}
synchronized int getFWTVersion() {
return _fwtVersion;
}
synchronized void setFeatures(int features) {
_features=features;
}
synchronized void setFWTVersion(int version){
_fwtVersion=version;
}
synchronized void setIpPort(IpPort addr) {
_externalAddr = addr;
}
synchronized IpPort getIpPort() {
return _externalAddr;
}
GUID getGuid() {
return (GUID) _guidRef.get();
}
}
private static final class WeakCleaner implements Runnable {
public void run() {
GUID_PROXY_MAP.size();
}
}
}