package com.limegroup.gnutella; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.net.MalformedURLException; import java.net.URL; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import com.util.LOG; import com.limegroup.gnutella.altlocs.AlternateLocation; import com.limegroup.gnutella.downloader.URLRemoteFileDesc; import com.limegroup.gnutella.http.HTTPConstants; import com.limegroup.gnutella.util.DataUtils; import com.limegroup.gnutella.util.IntervalSet; import com.limegroup.gnutella.util.IpPort; import com.limegroup.gnutella.util.NetworkUtils; import com.limegroup.gnutella.xml.LimeXMLDocument; /** * A reference to a single file on a remote machine. In this respect * RemoteFileDesc is similar to a URL, but it contains Gnutella- * specific data as well, such as the server's 16-byte GUID.<p> * * This class is serialized to disk as part of the downloads.dat file. Hence * you must be very careful before making any changes. Deleting or changing the * types of fields is DISALLOWED. Adding field a F is acceptable as long as the * readObject() method of this initializes F to a reasonable value when * reading from older files where the fields are not present. This is exactly * what we do with _urns and _browseHostEnabled. On the other hand, older * version of LimeWire will simply discard any extra fields F if reading from a * newer serialized file. */ public class RemoteFileDesc implements Serializable { private static final long serialVersionUID = 6619479308616716538L; private static final int COPY_INDEX = Integer.MAX_VALUE; /** bogus IP we assign to RFDs whose real ip is unknown */ public static final String BOGUS_IP = "1.1.1.1"; private final String _host; private final int _port; private final String _filename; private final long _index; private final byte[] _clientGUID; private final int _speed; private final int _size; private final boolean _chatEnabled; private final int _quality; private final boolean _replyToMulticast; private Set /* of URN*/ _urns; /** * Boolean indicating whether or not the remote host has browse host * enabled. */ private boolean _browseHostEnabled; private boolean _firewalled; private String _vendor; /** * Whether or not the remote host supports HTTP/1.1 * This is purposely NOT IMMUTABLE. Before we connect, * we can only assume the remote host supports HTTP/1.1 by * looking at the set of URNs. If any exist, we assume * HTTP/1.1 is supported (because URNs were added to Gnutella * after HTTP/1.1). Once we connect, this value is set to * be whatever the host reports in the response line. * * When deserializing, this value may be wrong for older download.dat * files. (Older versions will always set this to false, because * the field did not exist.) To counter that, when deserializing, * if this is false, we set it to true if any URNs are present. */ private boolean _http11; /** * The <tt>PushEndpoint</tt> for this RFD. * if null, the rfd is not behind a push proxy. */ private transient PushEndpoint _pushAddr; /** * The list of available ranges. * This is NOT SERIALIZED. */ private transient IntervalSet _availableRanges = null; /** * The number of times this download has failed while attempting * to transfer data. */ private transient int _failedCount = 0; /** * The earliest time to retry this host in milliseconds since 01-01-1970 */ private transient long _earliestRetryTime = 0; /** * The cached hash code for this RFD. */ private transient int _hashCode = 0; /** * The cached RemoteHostData for this rfd. */ private transient RemoteHostData _hostData = null; /** * Whether or not this RFD is/was used for downloading. */ private transient boolean _isDownloading = false; /** * The creation time of this file. */ private transient long _creationTime; /** * Constructs a new RemoteFileDesc exactly like the other one, * but with a different remote host. * * It is okay to use the same internal structures * for URNs because the Set is immutable. */ public RemoteFileDesc(RemoteFileDesc rfd, IpPort ep) { this( ep.getAddress(), // host ep.getPort(), // port COPY_INDEX, // index (unknown) rfd.getFileName(), // filename rfd.getSize(), // filesize DataUtils.EMPTY_GUID, // client GUID 0, // speed false, // chat capable 2, // quality false, // browse hostable rfd.getUrns(), // urns false, // reply to MCast false, // is firewalled AlternateLocation.ALT_VENDOR, // vendor System.currentTimeMillis(), // timestamp Collections.EMPTY_SET, // push proxies rfd.getCreationTime(), // creation time 0); // firewalled transfer } /** * Constructs a new RemoteFileDesc exactly like the other one, * but with a different push proxy host. Will be handy when processing * head pongs. */ public RemoteFileDesc(RemoteFileDesc rfd, PushEndpoint pe){ this( rfd.getHost(), // host - ignored rfd.getPort(), // port -ignored COPY_INDEX, // index (unknown) rfd.getFileName(), // filename rfd.getSize(), // filesize rfd.getSpeed(), // speed false, // chat capable rfd.getQuality(), // quality false, // browse hostable rfd.getUrns(), // urns false, // reply to MCast true, // is firewalled AlternateLocation.ALT_VENDOR, // vendor System.currentTimeMillis(), // timestamp rfd.getCreationTime(), // creation time pe); } /** * Constructs a new RemoteFileDesc with metadata. * * @param host the host's ip * @param port the host's port * @param index the index of the file that the client sent * @param filename the name of the file * @param size the completed size of this file * @param clientGUID the unique identifier of the client * @param speed the speed of the connection * @param chat true if the location is chattable * @param quality the quality of the connection, where 0 is the * worst and 3 is the best. (This is the same system as in the * GUI but on a 0 to N-1 scale.) * @param browseHost specifies whether or not the remote host supports * browse host * @param xmlDoc the <tt>LimeXMLDocument</tt> for the response * @param urns the <tt>Set</tt> of <tt>URN</tt>s for the file * @param replyToMulticast true if its from a reply to a multicast query * @param firewalled true if the host is firewalled * @param vendor the vendor of the remote host * @param timestamp the time this RemoteFileDesc was instantiated * @param proxies the push proxies for this host * @param createTime the network-wide creation time of this file * @throws <tt>IllegalArgumentException</tt> if any of the arguments are * not valid * @throws <tt>NullPointerException</tt> if the host argument is * <tt>null</tt> or if the file name is <tt>null</tt> */ public RemoteFileDesc(String host, int port, long index, String filename, int size, byte[] clientGUID, int speed, boolean chat, int quality, boolean browseHost, Set urns, boolean replyToMulticast, boolean firewalled, String vendor, long timestamp, Set proxies, long createTime) { this(host, port, index, filename, size, clientGUID, speed, chat, quality, browseHost, urns, replyToMulticast, firewalled, vendor, timestamp, proxies, createTime, 0); } /** * Constructs a new RemoteFileDesc with metadata. * * @param host the host's ip * @param port the host's port * @param index the index of the file that the client sent * @param filename the name of the file * @param clientGUID the unique identifier of the client * @param speed the speed of the connection * @param chat true if the location is chattable * @param quality the quality of the connection, where 0 is the * worst and 3 is the best. (This is the same system as in the * GUI but on a 0 to N-1 scale.) * @param xmlDocs the array of XML documents pertaining to this file * @param browseHost specifies whether or not the remote host supports * browse host * @param xmlDoc the <tt>LimeXMLDocument</tt> for the response * @param urns the <tt>Set</tt> of <tt>URN</tt>s for the file * @param replyToMulticast true if its from a reply to a multicast query * * @throws <tt>IllegalArgumentException</tt> if any of the arguments are * not valid * @throws <tt>NullPointerException</tt> if the host argument is * <tt>null</tt> or if the file name is <tt>null</tt> */ public RemoteFileDesc(String host, int port, long index, String filename, int size, byte[] clientGUID, int speed, boolean chat, int quality, boolean browseHost, Set urns, boolean replyToMulticast, boolean firewalled, String vendor, long timestamp, Set proxies, long createTime, int FWTVersion) { this(host,port,index,filename,size,speed,chat,quality,browseHost, urns,replyToMulticast,firewalled,vendor,timestamp,createTime, clientGUID == null? null : new PushEndpoint(clientGUID, proxies,PushEndpoint.PLAIN,FWTVersion)); } public RemoteFileDesc(String host, int port, long index, String filename, int size,int speed,boolean chat, int quality, boolean browseHost, Set urns, boolean replyToMulticast, boolean firewalled, String vendor,long timestamp,long createTime, PushEndpoint pe) { if(!NetworkUtils.isValidPort(port)) { throw new IllegalArgumentException("invalid port: "+port); } if((speed & 0xFFFFFFFF00000000L) != 0) { throw new IllegalArgumentException("invalid speed: "+speed); } if(filename == null) { throw new NullPointerException("null filename"); } if(filename.equals("")) { throw new IllegalArgumentException("cannot accept empty string file name"); } if((size & 0xFFFFFFFF00000000L) != 0) { throw new IllegalArgumentException("invalid size: "+size); } if((index & 0xFFFFFFFF00000000L) != 0) { throw new IllegalArgumentException("invalid index: "+index); } if(host == null) { throw new NullPointerException("null host"); } _speed = speed; _host = host; _port = port; _index = index; _filename = filename; _size = size; _pushAddr=pe; if (pe!=null) _clientGUID=pe.getClientGUID(); else _clientGUID=null; _chatEnabled = chat; _quality = quality; _browseHostEnabled = browseHost; _replyToMulticast = replyToMulticast; _firewalled = firewalled; _vendor = vendor; _creationTime = createTime; if(urns == null) { _urns = Collections.EMPTY_SET; } else { _urns = Collections.unmodifiableSet(urns); } _http11 = ( !_urns.isEmpty() ); } private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); //Older downloads.dat files do not have _urns, so _urns will be null //(the default Java value). Hence we also initialize //_browseHostEnabled. See class overview for more details. if(_urns == null) { _urns = Collections.EMPTY_SET; _browseHostEnabled= false; } else { // It seems that the Urn Set has some java.io.Files // inserted into it. See: // http://bugs.limewire.com:8080/bugs/searching.jsp?disp1=l&disp2=c&disp3=o&disp4=j&l=141&c=188&m=694_223 // Here we check for this case and remove the offending object. HashSet newUrns = null; Iterator iter = _urns.iterator(); while(iter.hasNext()) { Object next = iter.next(); if(!(next instanceof URN)) { if(newUrns == null) { newUrns = new HashSet(); newUrns.addAll(_urns); } newUrns.remove(next); } } if(newUrns != null) { _urns = Collections.unmodifiableSet(newUrns); } } // http11 must be set manually, because older clients did not have this // field but did have urns. _http11 = ( _http11 || !_urns.isEmpty() ); } /** * Accessor for HTTP11. * * @return Whether or not we think this host supports HTTP11. */ public boolean isHTTP11() { return _http11; } /** * Mutator for HTTP11. Should be set after connecting. */ public void setHTTP11(boolean http11) { _http11 = http11; } /** * Returns true if this is a partial source */ public boolean isPartialSource() { return (_availableRanges != null); } /** * @return whether this rfd points to myself. */ public boolean isMe() { return needsPush() ? Arrays.equals(_clientGUID,RouterService.getMyGUID()) : NetworkUtils.isMe(getHost(),getPort()); } /** * Accessor for the available ranges. */ public IntervalSet getAvailableRanges() { return (IntervalSet)_availableRanges.clone(); } /** * Mutator for the available ranges. */ public void setAvailableRanges(IntervalSet availableRanges) { this._availableRanges = availableRanges; } /** * updates the push address of the rfd to a new one. * This should be done only to update the set of push proxies, * features or FWT capability. */ public void setPushAddress(PushEndpoint pe) { if (!Arrays.equals(pe.getClientGUID(),this._clientGUID)) throw new IllegalArgumentException("different clientGUID"); this._pushAddr=pe; } /** * Returns the current failed count. */ public int getFailedCount() { return _failedCount; } /** * Increments the failed count by one. */ public void incrementFailedCount() { _failedCount++; } /** * Resets the failed count back to zero. */ public void resetFailedCount() { _failedCount = 0; } /** * Determines whether or not this RemoteFileDesc was created * from an alternate location. */ public boolean isFromAlternateLocation() { return "ALT".equals(_vendor); } /** * @return true if this host is still busy and should not be retried */ public boolean isBusy() { return (System.currentTimeMillis() < _earliestRetryTime); } /** * @return time to wait until this host will be ready to be retried * in seconds */ public int getWaitTime() { return (isBusy() ? (int) (_earliestRetryTime - System.currentTimeMillis())/1000 + 1: 0 ); } /** * Mutator for _earliestRetryTime. * @param seconds number of seconds to wait before retrying */ public void setRetryAfter(int seconds) { if(LOG.isDebugEnabled()) LOG.debug("setting retry after to be [" + seconds + "] seconds for " + this); _earliestRetryTime = System.currentTimeMillis() + seconds*1000; } /** * The creation time of this file. */ public long getCreationTime() { return _creationTime; } /** * Sets this RFD as downloading. */ public void setDownloading(boolean dl) { _isDownloading = dl; } /** * Determines if this RFD is downloading. * * @return whether or not this is downloading */ public boolean isDownloading() { return _isDownloading; } /** * Accessor for the host ip with this file. * * @return the host ip with this file */ public final String getHost() {return _host;} /** * Accessor for the port of the host with this file. * * @return the file name for the port of the host */ public final int getPort() {return _port;} /** * Accessor for the index this file, which can be <tt>null</tt>. * * @return the file name for this file, which can be <tt>null</tt> */ public final long getIndex() {return _index;} /** * Accessor for the size in bytes of this file. * * @return the size in bytes of this file */ public final int getSize() {return _size;} /** * Accessor for the file name for this file, which can be <tt>null</tt>. * * @return the file name for this file, which can be <tt>null</tt> */ public final String getFileName() {return _filename;} /** * Accessor for the client guid for this file, which can be <tt>null</tt>. * * @return the client guid for this file, which can be <tt>null</tt> */ public final byte[] getClientGUID() {return _clientGUID;} /** * Accessor for the speed of the host with this file, which can be * <tt>null</tt>. * * @return the speed of the host with this file, which can be * <tt>null</tt> */ public final int getSpeed() {return _speed;} public final String getVendor() {return _vendor;} public final boolean chatEnabled() {return _chatEnabled;} public final boolean browseHostEnabled() {return _browseHostEnabled;} /** * Returns the "quality" of the remote file in terms of firewalled status, * whether or not the remote host has open slots, etc. * * @return the current "quality" of the remote file in terms of the * determined likelihood of the request succeeding */ public final int getQuality() {return _quality;} /** * Accessor for the <tt>Set</tt> of URNs for this <tt>RemoteFileDesc</tt>. * * @return the <tt>Set</tt> of URNs for this <tt>RemoteFileDesc</tt> */ public final Set getUrns() { return _urns; } /** * Accessor for the SHA1 URN for this <tt>RemoteFileDesc</tt>. * * @return the SHA1 <tt>URN</tt> for this <tt>RemoteFileDesc</tt>, or * <tt>null</tt> if there is none */ public final URN getSHA1Urn() { Iterator iter = _urns.iterator(); while(iter.hasNext()) { URN urn = (URN)iter.next(); // defensively check against null values added. if(urn == null) continue; if(urn.isSHA1()) { return urn; } } return null; } /** * Returns an <tt>URL</tt> instance for this <tt>RemoteFileDesc</tt>. * * @return an <tt>URL</tt> instance for this <tt>RemoteFileDesc</tt> */ public URL getUrl() { try { String fileName = ""; URN urn = getSHA1Urn(); if(urn == null) { fileName = "/get/"+_index+"/"+_filename; } else { fileName = HTTPConstants.URI_RES_N2R+urn.httpStringValue(); } return new URL("http", _host, _port, fileName); } catch(MalformedURLException e) { return null; } } /** * Determines whether or not this RFD was a reply to a multicast query. * * @return <tt>true</tt> if this RFD was in reply to a multicast query, * otherwise <tt>false</tt> */ public final boolean isReplyToMulticast() { return _replyToMulticast; } /** * Determines whether or not this host reported a private address. * * @return <tt>true</tt> if the address for this host is private, * otherwise <tt>false</tt>. If the address is unknown, returns * <tt>true</tt> * * TODO:: use InetAddress in this class for the host so that we don't * have to go through the process of creating one each time we check * it it's a private address */ public final boolean isPrivate() { return NetworkUtils.isPrivateAddress(_host); } /** * Accessor for the <tt>Set</tt> of <tt>PushProxyInterface</tt>s for this * file -- can be empty, but is guaranteed to be non-null. * * @return the <tt>Set</tt> of proxy hosts that will accept push requests * for this host -- can be empty */ public final Set getPushProxies() { if (_pushAddr!=null) return _pushAddr.getProxies(); else return Collections.EMPTY_SET; } /** * @return whether this RFD supports firewall-to-firewall transfer. * For this to be true we need to have some push proxies, indication that * the host supports FWT and we need to know that hosts' external address. */ public final boolean supportsFWTransfer() { if (_host.equals(BOGUS_IP) || !NetworkUtils.isValidAddress(_host) || NetworkUtils.isPrivateAddress(_host)) return false; return _pushAddr == null ? false : _pushAddr.supportsFWTVersion() > 0; } /** * Creates the _hostData lazily and uses as necessary */ public final RemoteHostData getRemoteHostData() { if(_hostData == null) _hostData = new RemoteHostData(_host, _port, _clientGUID); return _hostData; } /** * @return true if I am not a multicast host and have a hash. * also, if I am firewalled I must have at least one push proxy, * otherwise my port and address need to be valid. */ public final boolean isAltLocCapable() { boolean ret = getSHA1Urn() != null && !_replyToMulticast; if (_firewalled) ret = ret && _pushAddr!=null && _pushAddr.getProxies().size() > 0; else ret= ret && NetworkUtils.isValidPort(_port) && !NetworkUtils.isPrivateAddress(_host) && NetworkUtils.isValidAddress(_host); return ret; } /** * * @return whether a push should be sent tho this rfd. */ public boolean needsPush() { //if replying to multicast, do a push. if ( isReplyToMulticast() ) return true; //Return true if rfd is private or unreachable if (isPrivate()) { // Don't do a push for magnets in case you are in a private network. // Note to Sam: This doesn't mean that isPrivate should be true. if (this instanceof URLRemoteFileDesc) return false; else // Otherwise obey push rule for private rfds. return true; } else if (!NetworkUtils.isValidPort(getPort())) return true; // make sure we have some push proxies. else return _firewalled && _pushAddr!=null; } /** * * @return the push address. */ public PushEndpoint getPushAddr() { return _pushAddr; } /** * Overrides <tt>Object.equals</tt> to return instance equality * based on the equality of all <tt>RemoteFileDesc</tt> fields. * * @return <tt>true</tt> if all of fields of this * <tt>RemoteFileDesc</tt> instance are equal to all of the * fields of the specified object, and <tt>false</tt> if this * is not the case, or if the specified object is not a * <tt>RemoteFileDesc</tt>. * * Dynamic values such as _http11, and _availableSources * are not checked here, as they can change and still be considered * the same "remote file". * * The _host field may be equal for many firewalled locations; * therefore it is necessary that we distinguish those by their * client GUIDs */ public boolean equals(Object o) { if(o == this) return true; if (! (o instanceof RemoteFileDesc)) return false; RemoteFileDesc other=(RemoteFileDesc)o; if (! (nullEquals(_host, other._host) && (_port==other._port)) ) return false; if (_size != other._size) return false; if ( (_clientGUID ==null) != (other._clientGUID==null) ) return false; if ( _clientGUID!= null && ! ( Arrays.equals(_clientGUID,other._clientGUID))) return false; if (_urns.isEmpty() && other._urns.isEmpty()) return nullEquals(_filename, other._filename); else return urnSetEquals(_urns, other._urns); } private boolean nullEquals(Object one, Object two) { return one == null ? two == null : one.equals(two); } private boolean urnSetEquals(Set one, Set two) { for (Iterator iter = one.iterator(); iter.hasNext(); ) { if (two.contains(iter.next())) { return true; } } return false; } private boolean byteArrayEquals(byte[] one, byte[] two) { return one == null ? two == null : Arrays.equals(one, two); } /** * 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" RemoteFileDescs * 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() { if(_hashCode == 0) { int result = 17; result = (37* result)+_host.hashCode(); result = (37* result)+_port; result = (37* result)+_size; result = (37* result)+_urns.hashCode(); if (_clientGUID!=null) result = (37* result)+_clientGUID.hashCode(); _hashCode = result; } return _hashCode; } public String toString() { return ("<"+getHost()+":"+getPort()+", " +getFileName().toLowerCase()+">"); } }