package com.limegroup.gnutella.altlocs; import java.io.IOException; import java.net.URL; import java.util.Collections; import java.util.StringTokenizer; import com.limegroup.gnutella.Endpoint; import com.limegroup.gnutella.ErrorService; import com.limegroup.gnutella.PushEndpoint; import com.limegroup.gnutella.RemoteFileDesc; import com.limegroup.gnutella.RouterService; import com.limegroup.gnutella.URN; import com.limegroup.gnutella.filters.IP; import com.limegroup.gnutella.http.HTTPHeaderValue; import com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.settings.UploadSettings; import com.limegroup.gnutella.util.IpPort; import com.limegroup.gnutella.util.IpPortForSelf; import com.limegroup.gnutella.util.NetworkUtils; /** * This class encapsulates the data for an alternate resource location, as * specified in HUGE v0.93. This also provides utility methods for such * operations as comparing alternate locations based on the date they were * stored. * * Firewalled hosts can also be alternate locations, although the format is * slightly different. */ public abstract class AlternateLocation implements HTTPHeaderValue, Comparable { /** * The vendor to use. */ public static final String ALT_VENDOR = "ALT"; /** * The three types of medium altlocs travel through */ public static final int MESH_PING = 0; public static final int MESH_LEGACY = 1; public static final int MESH_RESPONSE = 2; /** * Constant for the sha1 urn for this <tt>AlternateLocation</tt> -- * can be <tt>null</tt>. */ protected final URN SHA1_URN; /** * Constant for the string to display as the httpStringValue. */ private String DISPLAY_STRING; /** * Cached hash code that is lazily initialized. */ protected volatile int hashCode = 0; /** * LOCKING: obtain this' monitor while changing/accessing _count and * _demoted as multiple threads could be accessing them. */ /** * maintins a count of how many times this alternate location has been seen. * A value of 0 means this alternate location was failed one more time that * it has succeeded. Newly created AlternateLocations start out wit a value * of 1. */ protected volatile int _count = 0; /** * Two counter objects to keep track of altloc expiration */ private final Average legacy, ping, response; ////////////////////////"Constructors"////////////////////////////// /** * Constructs a new <tt>AlternateLocation</tt> instance based on the * specified string argument. * * @param location a string containing a single alternate location, * including a full URL for a file and an optional date * @throws <tt>IOException</tt> if there is any problem constructing * the new instance from the specified string, or if the <tt<location</tt> * argument is either null or the empty string -- we could (should?) * throw NullPointerException here, but since we're already forcing the * caller to catch IOException, we might as well throw in in both cases */ public static AlternateLocation create(final String location) throws IOException { if(location == null || location.equals("")) throw new IOException("null or empty location"); URL url = AlternateLocation.createUrl(location); URN sha1 = URN.createSHA1UrnFromURL(url); return new DirectAltLoc(url,sha1); } /** * Constructs a new <tt>AlternateLocation</tt> instance based on the * specified string argument and URN. The location created this way * assumes the name "ALT" for the file. * * @param location a string containing one of the following: * "http://my.address.com:port#/uri-res/N2R?urn:sha:SHA1LETTERS" or * "1.2.3.4[:6346]" or * http representation of a PushEndpoint. * * If the first is given, then the SHA1 in the string MUST match * the SHA1 given. * * @param good whether the proxies contained in the string representation * should be added to or removed from the current set of proxies * * @throws <tt>IOException</tt> if there is any problem constructing * the new instance. */ public static AlternateLocation create(final String location, final URN urn) throws IOException { if(location == null || location.equals("")) throw new IOException("null or empty location"); if(urn == null) throw new IOException("null URN."); // Case 1. Old-Style direct alt loc. if(location.toLowerCase().startsWith("http")) { URL url = createUrl(location); URN sha1 = URN.createSHA1UrnFromURL(url); AlternateLocation al = new DirectAltLoc(url,sha1); if(!al.SHA1_URN.equals(urn)) throw new IOException("mismatched URN"); return al; } // Case 2. Direct Alt Loc if (location.indexOf(";")==-1) { IpPort addr = AlternateLocation.createUrlFromMini(location, urn); return new DirectAltLoc(addr, urn); } //Case 3. Push Alt loc PushEndpoint pe = new PushEndpoint(location); return new PushAltLoc(pe,urn); } /** * Creates a new <tt>AlternateLocation</tt> for the data stored in * a <tt>RemoteFileDesc</tt>. * * @param rfd the <tt>RemoteFileDesc</tt> to use in creating the * <tt>AlternateLocation</tt> * @return a new <tt>AlternateLocation</tt> * @throws <tt>IOException</tt> if the <tt>rfd</tt> does not contain * a valid urn or if it's a private address * @throws <tt>NullPointerException</tt> if the <tt>rfd</tt> is * <tt>null</tt> * @throws <tt>IOException</tt> if the port is invalid */ public static AlternateLocation create(final RemoteFileDesc rfd) throws IOException { if(rfd == null) throw new NullPointerException("cannot accept null RFD"); URN urn = rfd.getSHA1Urn(); if(urn == null) throw new NullPointerException("cannot accept null URN"); int port = rfd.getPort(); if (!rfd.needsPush()) { return new DirectAltLoc(new Endpoint(rfd.getHost(),rfd.getPort()), urn); } else { PushEndpoint copy; if (rfd.getPushAddr() != null) copy = rfd.getPushAddr(); else copy = new PushEndpoint(rfd.getClientGUID(),Collections.EMPTY_SET,0,0,null); return new PushAltLoc(copy,urn); } } /** * Creates a new <tt>AlternateLocation</tt> for a file stored locally * with the specified <tt>URN</tt>. * * Note: the altloc created this way does not know the name of the file. * * @param urn the <tt>URN</tt> of the locally stored file */ public static AlternateLocation create(URN urn) { if(urn == null) throw new NullPointerException("null sha1"); try { // We try to guess whether we are firewalled or not. If the node // has just started up and has not yet received an incoming connection // our best bet is to see if we have received a connection in the past. // // However it is entirely possible that we have received connection in // the past but are firewalled this session, so if we are connected // we see if we received a conn this session only. boolean open; if (RouterService.isConnected()) open = RouterService.acceptedIncomingConnection(); else open = ConnectionSettings.EVER_ACCEPTED_INCOMING.getValue(); if (open && NetworkUtils.isValidExternalIpPort(IpPortForSelf.instance())) return new DirectAltLoc(urn); else return new PushAltLoc(urn); }catch(IOException bad) { ErrorService.error(bad); return null; } } protected AlternateLocation(URN sha1) throws IOException { if(sha1 == null) throw new IOException("null sha1"); SHA1_URN=sha1; legacy = new Average(); ping = new Average(); response = new Average(); } //////////////////////////////accessors//////////////////////////// /** * Accessor for the SHA1 urn for this <tt>AlternateLocation</tt>. * <p> * @return the SHA1 urn for the this <tt>AlternateLocation</tt> */ public URN getSHA1Urn() { return SHA1_URN; } /** * Accessor to find if this has been demoted */ public synchronized int getCount() { return _count; } /** * package access, accessor to the value of _demoted */ public abstract boolean isDemoted(); ////////////////////////////Mesh utility methods//////////////////////////// public String httpStringValue() { if (DISPLAY_STRING == null) DISPLAY_STRING = generateHTTPString(); return DISPLAY_STRING; } /** * Creates a new <tt>RemoteFileDesc</tt> from this AlternateLocation * * @param size the size of the file for the new <tt>RemoteFileDesc</tt> * -- this is necessary to make sure the download bucketing works * correctly * @return new <tt>RemoteFileDesc</tt> based off of this, or * <tt>null</tt> if the <tt>RemoteFileDesc</tt> could not be created */ public abstract RemoteFileDesc createRemoteFileDesc(int size); /** * * @return whether this is an alternate location pointing to myself. */ public abstract boolean isMe(); /** * increment the count. * @see demote */ public synchronized void increment() { _count++; } /** * package access for demoting this. */ abstract void demote(); /** * package access for promoting this. */ abstract void promote(); /** * could return null */ public abstract AlternateLocation createClone(); public synchronized void send(long now, int meshType) { switch(meshType) { case MESH_LEGACY : legacy.send(now);return; case MESH_PING : ping.send(now);return; case MESH_RESPONSE : response.send(now);return; default : throw new IllegalArgumentException("unknown mesh type"); } } public synchronized boolean canBeSent(int meshType) { switch(meshType) { case MESH_LEGACY : if (!UploadSettings.EXPIRE_LEGACY.getValue()) return true; return legacy.canBeSent(UploadSettings.LEGACY_BIAS.getValue(), UploadSettings.LEGACY_EXPIRATION_DAMPER.getValue()); case MESH_PING : if (!UploadSettings.EXPIRE_PING.getValue()) return true; return ping.canBeSent(UploadSettings.PING_BIAS.getValue(), UploadSettings.PING_EXPIRATION_DAMPER.getValue()); case MESH_RESPONSE : if (!UploadSettings.EXPIRE_RESPONSE.getValue()) return true; return response.canBeSent(UploadSettings.RESPONSE_BIAS.getValue(), UploadSettings.RESPONSE_EXPIRATION_DAMPER.getValue()); default : throw new IllegalArgumentException("unknown mesh type"); } } public synchronized boolean canBeSentAny() { return canBeSent(MESH_LEGACY) || canBeSent(MESH_PING) || canBeSent(MESH_RESPONSE); } synchronized void resetSent() { ping.reset(); legacy.reset(); response.reset(); } ///////////////////////////////helpers//////////////////////////////// /** * Creates a new <tt>URL</tt> instance based on the URL specified in * the alternate location header. * * @param locationHeader the alternate location header from an HTTP * header * @return a new <tt>URL</tt> instance for the URL in the alternate * location header * @throws <tt>IOException</tt> if the url could not be extracted from * the header in the expected format * @throws <tt>MalformedURLException</tt> if the enclosed URL is not * formatted correctly */ private static URL createUrl(final String locationHeader) throws IOException { String locHeader = locationHeader.toLowerCase(); //Doesn't start with http? Bad. if(!locHeader.startsWith("http")) throw new IOException("invalid location: " + locationHeader); //Had multiple http's in it? Bad. if(locHeader.lastIndexOf("http://") > 4) throw new IOException("invalid location: " + locationHeader); String urlStr = AlternateLocation.removeTimestamp(locHeader); URL url = new URL(urlStr); String host = url.getHost(); // Invalid host? Bad. if(host == null || host.equals("")) throw new IOException("invalid location: " + locationHeader); // If no port, fake it at 80. if(url.getPort()==-1) url = new URL("http",url.getHost(),80,url.getFile()); return url; } /** * Creates a new <tt>URL</tt> based on the IP and port in the location * The location MUST be a dotted IP address. */ private static IpPort createUrlFromMini(final String location, URN urn) throws IOException { int port = location.indexOf(':'); final String loc = (port == -1 ? location : location.substring(0, port)); //Use the IP class as a quick test to make sure it numeric try { new IP(loc); } catch(IllegalArgumentException iae) { throw new IOException("invalid location: " + location); } //But, IP still could have passed if it thought there was a submask if( loc.indexOf('/') != -1 ) throw new IOException("invalid location: " + location); //Then make sure it's a valid IP addr. if(!NetworkUtils.isValidAddress(loc)) throw new IOException("invalid location: " + location); if( port == -1 ) port = 6346; // default port if not included. else { // Not enough room for a port. if(location.length() < port+1) throw new IOException("invalid location: " + location); try { port = Short.parseShort(location.substring(port+1)); } catch(NumberFormatException nfe) { throw new IOException("invalid location: " + location); } } if(!NetworkUtils.isValidPort(port)) throw new IOException("invalid port: " + port); return new Endpoint(loc,port); } /** * Removes the timestamp from an alternate location header. This will * remove the timestamp from an alternate location header string that * includes the header name, or from an alternate location string that * only contains the alternate location header value. * * @param locationHeader the string containing the full header, or only * the header value * @return the same string as supplied in the <tt>locationHeader</tt> * argument, but with the timestamp removed */ private static String removeTimestamp(final String locationHeader) { StringTokenizer st = new StringTokenizer(locationHeader); int numToks = st.countTokens(); if(numToks == 1) { return locationHeader; } String curTok = null; for(int i=0; i<numToks; i++) { curTok = st.nextToken(); } int tsIndex = locationHeader.indexOf(curTok); if(tsIndex == -1) return null; return locationHeader.substring(0, tsIndex); } /////////////////////Object's overridden methods//////////////// /** * Overrides the equals method to accurately compare * <tt>AlternateLocation</tt> instances. <tt>AlternateLocation</tt>s * are equal if their <tt>URL</tt>s are equal. * * @param obj the <tt>Object</tt> instance to compare to * @return <tt>true</tt> if the <tt>URL</tt> of this * <tt>AlternateLocation</tt> is equal to the <tt>URL</tt> * of the <tt>AlternateLocation</tt> location argument, * and otherwise returns <tt>false</tt> */ public boolean equals(Object obj) { if(obj == this) return true; if(!(obj instanceof AlternateLocation)) return false; AlternateLocation other = (AlternateLocation)obj; return SHA1_URN.equals(other.SHA1_URN); } /** * The idea is that this is smaller than any AlternateLocation who has a * greater value of _count. There is one exception to this rule -- a demoted * AlternateLocation has a higher value irrespective of count. * <p> * This is because we want to have a sorted set of AlternateLocation where * any demoted AlternateLocation is put at the end of the list * because it probably does not work. * <p> * Further we want to get AlternateLocations with smaller counts to be * propogated more, since this will serve to get better load balancing of * uploader. */ public int compareTo(Object obj) { AlternateLocation other = (AlternateLocation) obj; int ret = _count - other._count; if(ret!=0) return ret; return ret; } protected abstract String generateHTTPString(); /** * Overrides the hashCode method of Object to meet the contract of * hashCode. Since we override equals, it is necessary to also * override hashcode to ensure that two "equal" alternate locations * return the same hashCode, less we unleash unknown havoc on the * hash-based collections. * * @return a hash code value for this object */ public int hashCode() { return 17*37+this.SHA1_URN.hashCode(); } private static class Average { /** The number of times this altloc was given out */ private int numTimes; /** The average time in ms between giving out the altloc */ private double average; /** The last time the altloc was given out */ private long lastSentTime; /** The last calculated threshold, -1 if dirty */ private double cachedTreshold = -1; public void send(long now) { if (lastSentTime == 0) lastSentTime = now; average = ( (average * numTimes) + (now - lastSentTime) ) / ++numTimes; lastSentTime = now; cachedTreshold = -1; } public boolean canBeSent(float bias, float damper) { if (numTimes < 2 || average == 0) return true; if (cachedTreshold == -1) cachedTreshold = Math.abs(Math.log(average) / Math.log(damper)); return numTimes < cachedTreshold * bias; } public void reset() { numTimes = 0; average = 0; lastSentTime = 0; } } }