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