package com.limegroup.gnutella;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.UnknownHostException;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.StringTokenizer;
import com.limegroup.gnutella.altlocs.AlternateLocation;
import com.limegroup.gnutella.altlocs.AlternateLocationCollection;
import com.limegroup.gnutella.altlocs.DirectAltLoc;
import com.limegroup.gnutella.filters.IPFilter;
import com.limegroup.gnutella.metadata.AudioMetaData;
import com.limegroup.gnutella.messages.BadGGEPPropertyException;
import com.limegroup.gnutella.messages.GGEP;
import com.limegroup.gnutella.messages.HUGEExtension;
import com.limegroup.gnutella.search.HostData;
import com.limegroup.gnutella.util.IpPort;
import com.limegroup.gnutella.util.NameValue;
import com.limegroup.gnutella.util.DataUtils;
import com.limegroup.gnutella.util.NetworkUtils;
import com.limegroup.gnutella.xml.LimeXMLDocument;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Log;
/**
* A single result from a query reply message. (In hindsight, "Result" would
* have been a better name.) Besides basic file information, responses can
* include metadata.
*
* Response was originally intended to be immutable, but it currently includes
* mutator methods for metadata; these will be removed in the future.
*/
public class Response {
private static final Log LOG = LogFactory.getLog(Response.class);
/**
* The magic byte to use as extension separators.
*/
private static final byte EXT_SEPARATOR = 0x1c;
/**
* The maximum number of alternate locations to include in responses
* in the GGEP block
*/
private static final int MAX_LOCATIONS = 10;
/** Both index and size must fit into 4 unsigned bytes; see
* constructor for details. */
private final long index;
private final long size;
/**
* The bytes for the name string, guaranteed to be non-null.
*/
private final byte[] nameBytes;
/** The name of the file matching the search. This does NOT
* include the double null terminator.
*/
private final String name;
/** The document representing the XML in this response. */
private LimeXMLDocument document;
/**
* The <tt>Set</tt> of <tt>URN</tt> instances for this <tt>Response</tt>,
* as specified in HUGE v0.94. This is guaranteed to be non-null,
* although it is often empty.
*/
private final Set urns;
/**
* The bytes between the nulls for the <tt>Response</tt>, as specified
* in HUGE v0.94. This is guaranteed to be non-null, although it can be
* an empty array.
*/
private final byte[] extBytes;
/**
* The cached RemoteFileDesc created from this Response.
*/
private volatile RemoteFileDesc cachedRFD;
/**
* The container for extra GGEP data.
*/
private final GGEPContainer ggepData;
/**
* Constant for the KBPS string to avoid constructing it too many
* times.
*/
private static final String KBPS = "kbps";
/**
* Constant for kHz to string to avoid excessive string construction.
*/
private static final String KHZ = "kHz";
/** Creates a fresh new response.
*
* @requires index and size can fit in 4 unsigned bytes, i.e.,
* 0 <= index, size < 2^32
*/
public Response(long index, long size, String name) {
this(index, size, name, null, null, null, null);
}
/**
* Creates a new response with parsed metadata. Typically this is used
* to respond to query requests.
* @param doc the metadata to include
*/
public Response(long index, long size, String name, LimeXMLDocument doc) {
this(index, size, name, null, doc, null, null);
}
/**
* Constructs a new <tt>Response</tt> instance from the data in the
* specified <tt>FileDesc</tt>.
*
* @param fd the <tt>FileDesc</tt> containing the data to construct
* this <tt>Response</tt> -- must not be <tt>null</tt>
*/
public Response(FileDesc fd) {
this(fd.getIndex(), fd.getFileSize(), fd.getFileName(),
fd.getUrns(), null,
new GGEPContainer(
getAsEndpoints(RouterService.getAltlocManager().getDirect(fd.getSHA1Urn())),
CreationTimeCache.instance().getCreationTimeAsLong(fd.getSHA1Urn())),
null);
}
/**
* Overloaded constructor that allows the creation of Responses with
* meta-data and a <tt>Set</tt> of <tt>URN</tt> instances. This
* is the primary constructor that establishes all of the class's
* invariants, does any necessary parameter validation, etc.
*
* If extensions is non-null, it is used as the extBytes instead
* of creating them from the urns and locations.
*
* @param index the index of the file referenced in the response
* @param size the size of the file (in bytes)
* @param name the name of the file
* @param urns the <tt>Set</tt> of <tt>URN</tt> instances associated
* with the file
* @param doc the <tt>LimeXMLDocument</tt> instance associated with
* the file
* @param endpoints a collection of other locations on this network
* that will have this file
* @param extensions The raw unparsed extension bytes.
*/
private Response(long index, long size, String name,
Set urns, LimeXMLDocument doc,
GGEPContainer ggepData, byte[] extensions) {
if( (index & 0xFFFFFFFF00000000L)!=0 )
throw new IllegalArgumentException("invalid index: " + index);
// see note in createFromStream about Integer.MAX_VALUE
if (size > Integer.MAX_VALUE || size < 0)
throw new IllegalArgumentException("invalid size: " + size);
this.index=index;
this.size=size;
if (name == null)
this.name = "";
else
this.name = name;
byte[] temp = null;
try {
temp = this.name.getBytes("UTF-8");
} catch(UnsupportedEncodingException namex) {
//b/c this should never happen, we will show and error
//if it ever does for some reason.
ErrorService.error(namex);
}
this.nameBytes = temp;
if (urns == null)
this.urns = Collections.EMPTY_SET;
else
this.urns = Collections.unmodifiableSet(urns);
if(ggepData == null)
this.ggepData = GGEPContainer.EMPTY;
else
this.ggepData = ggepData;
if (extensions != null)
this.extBytes = extensions;
else
this.extBytes = createExtBytes(this.urns, this.ggepData);
this.document = doc;
}
/**
* Factory method for instantiating individual responses from an
* <tt>InputStream</tt> instance.
*
* @param is the <tt>InputStream</tt> to read from
* @throws <tt>IOException</tt> if there are any problems reading from
* or writing to the stream
*/
public static Response createFromStream(InputStream is)
throws IOException {
// extract file index & size
long index=ByteOrder.uint2long(ByteOrder.leb2int(is));
long size=ByteOrder.uint2long(ByteOrder.leb2int(is));
if( (index & 0xFFFFFFFF00000000L)!=0 )
throw new IOException("invalid index: " + index);
// must use Integer.MAX_VALUE instead of mask because
// this value is later converted to an int, so we want
// to ensure that when it's converted it doesn't become
// negative.
if (size > Integer.MAX_VALUE || size < 0)
throw new IOException("invalid size: " + size);
//The file name is terminated by a null terminator.
// A second null indicates the end of this response.
// Gnotella & others insert meta-information between
// these null characters. So we have to handle this.
// See http://gnutelladev.wego.com/go/
// wego.discussion.message?groupId=139406&
// view=message&curMsgId=319258&discId=140845&
// index=-1&action=view
// Extract the filename
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int c;
while((c=is.read())!=0) {
if(c == -1)
throw new IOException("EOF before null termination");
baos.write(c);
}
String name = new String(baos.toByteArray(), "UTF-8");
if(name.length() == 0) {
throw new IOException("empty name in response");
}
// Extract extra info, if any
baos.reset();
while((c=is.read())!=0) {
if(c == -1)
throw new IOException("EOF before null termination");
baos.write(c);
}
byte[] rawMeta = baos.toByteArray();
if(rawMeta == null || rawMeta.length == 0) {
if(is.available() < 16) {
throw new IOException("not enough room for the GUID");
}
return new Response(index,size,name);
} else {
// now handle between-the-nulls
// \u001c is the HUGE v0.93 GEM delimiter
HUGEExtension huge = new HUGEExtension(rawMeta);
Set urns = huge.getURNS();
LimeXMLDocument doc = null;
Iterator iter = huge.getMiscBlocks().iterator();
while (iter.hasNext() && doc == null)
doc = createXmlDocument(name, (String)iter.next());
GGEPContainer ggep = GGEPUtil.getGGEP(huge.getGGEP());
return new Response(index, size, name,
urns, doc, ggep, rawMeta);
}
}
/**
* Constructs an xml string from the given extension sting.
*
* @param name the name of the file to construct the string for
* @param ext an individual between-the-nulls string (note that there
* can be multiple between-the-nulls extension strings with HUGE)
* @return the xml formatted string, or the empty string if the
* xml could not be parsed
*/
private static LimeXMLDocument createXmlDocument(String name, String ext) {
StringTokenizer tok = new StringTokenizer(ext);
// if there aren't the expected number of tokens, simply
// return the empty string
if(tok.countTokens() < 2)
return null;
String first = tok.nextToken();
String second = tok.nextToken();
if (first != null)
first = first.toLowerCase();
if (second != null)
second = second.toLowerCase();
String length="";
String bitrate="";
boolean bearShare1 = false;
boolean bearShare2 = false;
boolean gnotella = false;
if(second.startsWith(KBPS))
bearShare1 = true;
else if (first.endsWith(KBPS))
bearShare2 = true;
if(bearShare1){
bitrate = first;
}
else if (bearShare2){
int j = first.indexOf(KBPS);
bitrate = first.substring(0,j);
}
if(bearShare1 || bearShare2){
while(tok.hasMoreTokens())
length=tok.nextToken();
//OK we have the bitrate and the length
}
else if (ext.endsWith(KHZ)){//Gnotella
gnotella = true;
length=first;
//extract the bitrate from second
int i=second.indexOf(KBPS);
if(i>-1)//see if we can find the bitrate
bitrate = second.substring(0,i);
else//not gnotella, after all...some other format we do not know
gnotella=false;
}
// make sure these are valid numbers.
try {
Integer.parseInt(bitrate);
Integer.parseInt(length);
} catch(NumberFormatException nfe) {
return null;
}
if(bearShare1 || bearShare2 || gnotella) {//some metadata we understand
List values = new ArrayList(3);
values.add(new NameValue("audios__audio__title__", name));
values.add(new NameValue("audios__audio__bitrate__", bitrate));
values.add(new NameValue("audios__audio__seconds__", length));
return new LimeXMLDocument(values, AudioMetaData.schemaURI);
}
return null;
}
/**
* Helper method that creates an array of bytes for the specified
* <tt>Set</tt> of <tt>URN</tt> instances. The bytes are written
* as specified in HUGE v 0.94.
*
* @param urns the <tt>Set</tt> of <tt>URN</tt> instances to use in
* constructing the byte array
*/
private static byte[] createExtBytes(Set urns, GGEPContainer ggep) {
try {
if( isEmpty(urns) && ggep.isEmpty() )
return DataUtils.EMPTY_BYTE_ARRAY;
ByteArrayOutputStream baos = new ByteArrayOutputStream();
if( !isEmpty(urns) ) {
// Add the extension for URNs, if any.
Iterator iter = urns.iterator();
while (iter.hasNext()) {
URN urn = (URN)iter.next();
Assert.that(urn!=null, "Null URN");
baos.write(urn.toString().getBytes());
// If there's another URN, add the separator.
if (iter.hasNext()) {
baos.write(EXT_SEPARATOR);
}
}
// If there's ggep data, write the separator.
if( !ggep.isEmpty() )
baos.write(EXT_SEPARATOR);
}
// It is imperitive that GGEP is added LAST.
// That is because GGEP can contain 0x1c (EXT_SEPARATOR)
// within it, which would cause parsing problems
// otherwise.
if(!ggep.isEmpty())
GGEPUtil.addGGEP(baos, ggep);
return baos.toByteArray();
} catch (IOException impossible) {
ErrorService.error(impossible);
return DataUtils.EMPTY_BYTE_ARRAY;
}
}
/**
* Utility method to know if a set is empty or null.
*/
private static boolean isEmpty(Set set) {
return set == null || set.isEmpty();
}
/**
* Utility method for converting the non-firewalled elements of an
* AlternateLocationCollection to a smaller set of endpoints.
*/
private static Set getAsEndpoints(AlternateLocationCollection col) {
if( col == null || !col.hasAlternateLocations() )
return Collections.EMPTY_SET;
long now = System.currentTimeMillis();
synchronized(col) {
Set endpoints = null;
int i = 0;
for(Iterator iter = col.iterator();
iter.hasNext() && i < MAX_LOCATIONS;) {
Object o = iter.next();
if (!(o instanceof DirectAltLoc))
continue;
DirectAltLoc al = (DirectAltLoc)o;
if (al.canBeSent(AlternateLocation.MESH_RESPONSE)) {
IpPort host = al.getHost();
if( !NetworkUtils.isMe(host) ) {
if (endpoints == null)
endpoints = new HashSet();
if (!(host instanceof Endpoint))
host = new Endpoint(host.getAddress(),host.getPort());
endpoints.add( host );
i++;
al.send(now, AlternateLocation.MESH_RESPONSE);
}
} else if (!al.canBeSentAny())
iter.remove();
}
return endpoints == null ? Collections.EMPTY_SET : endpoints;
}
}
/**
* Like writeToArray(), but writes to an OutputStream.
*/
public void writeToStream(OutputStream os) throws IOException {
ByteOrder.int2leb((int)index, os);
ByteOrder.int2leb((int)size, os);
for (int i = 0; i < nameBytes.length; i++)
os.write(nameBytes[i]);
//Write first null terminator.
os.write(0);
// write HUGE v0.93 General Extension Mechanism extensions
// (currently just URNs)
for (int i = 0; i < extBytes.length; i++)
os.write(extBytes[i]);
//add the second null terminator
os.write(0);
}
/**
* Sets this' metadata.
* @param meta the parsed XML metadata
*/
public void setDocument(LimeXMLDocument doc) {
document = doc;
}
/**
*/
public int getLength() {
// must match same number of bytes writeToArray() will write
return 8 + // index and size
nameBytes.length +
1 + // null
extBytes.length +
1; // final null
}
/**
* Returns the index for the file stored in this <tt>Response</tt>
* instance.
*
* @return the index for the file stored in this <tt>Response</tt>
* instance
*/
public long getIndex() {
return index;
}
/**
* Returns the size of the file for this <tt>Response</tt> instance
* (in bytes).
*
* @return the size of the file for this <tt>Response</tt> instance
* (in bytes)
*/
public long getSize() {
return size;
}
/**
* Returns the name of the file for this response. This is guaranteed
* to be non-null, but it could be the empty string.
*
* @return the name of the file for this response
*/
public String getName() {
return name;
}
/**
* Returns this' metadata.
*/
public LimeXMLDocument getDocument() {
return document;
}
/**
* Returns an immutable <tt>Set</tt> of <tt>URN</tt> instances for
* this <tt>Response</tt>.
*
* @return an immutable <tt>Set</tt> of <tt>URN</tt> instances for
* this <tt>Response</tt>, guaranteed to be non-null, although the
* set could be empty
*/
public Set getUrns() {
return urns;
}
/**
* Returns an immutabe <tt>Set</tt> of <tt>Endpoint</tt> that
* contain the same file described in this <tt>Response</tt>.
*
* @return an immutabe <tt>Set</tt> of <tt>Endpoint</tt> that
* contain the same file described in this <tt>Response</tt>,
* guaranteed to be non-null, although the set could be empty
*/
public Set getLocations() {
return ggepData.locations;
}
/**
* Returns the create time.
*/
public long getCreateTime() {
return ggepData.createTime;
}
byte[] getExtBytes() {
return extBytes;
}
/**
* Returns this Response as a RemoteFileDesc.
*/
public RemoteFileDesc toRemoteFileDesc(HostData data){
if(cachedRFD != null &&
cachedRFD.getPort() == data.getPort() &&
cachedRFD.getHost().equals(data.getIP()))
return cachedRFD;
else {
RemoteFileDesc rfd = new RemoteFileDesc(
data.getIP(),
data.getPort(),
getIndex(),
getName(),
(int)getSize(),
data.getClientGUID(),
data.getSpeed(),
data.isChatEnabled(),
data.getQuality(),
data.isBrowseHostEnabled(),
getDocument(),
getUrns(),
data.isReplyToMulticastQuery(),
data.isFirewalled(),
data.getVendorCode(),
System.currentTimeMillis(),
data.getPushProxies(),
getCreateTime(),
data.getFWTVersionSupported()
);
cachedRFD = rfd;
return rfd;
}
}
/**
* Overrides equals to check that these two responses are equal.
* Raw extension bytes are not checked, because they may be
* extensions that do not change equality, such as
* otherLocations.
*/
public boolean equals(Object o) {
if(o == this) return true;
if (! (o instanceof Response))
return false;
Response r=(Response)o;
return getIndex() == r.getIndex() &&
getSize() == r.getSize() &&
getName().equals(r.getName()) &&
((getDocument() == null) ? (r.getDocument() == null) :
getDocument().equals(r.getDocument())) &&
getUrns().equals(r.getUrns());
}
public int hashCode() {
//Good enough for the moment
// TODO:: IMPROVE THIS HASHCODE!!
return getName().hashCode()+(int)getSize()+(int)getIndex();
}
/**
* Overrides Object.toString to print out a more informative message.
*/
public String toString() {
return ("index: "+index+"\r\n"+
"size: "+size+"\r\n"+
"name: "+name+"\r\n"+
"xml document: "+document+"\r\n"+
"urns: "+urns);
}
/**
* Utility class for handling GGEP blocks in the per-file section
* of QueryHits.
*/
private static class GGEPUtil {
/**
* Private constructor so it never gets constructed.
*/
private GGEPUtil() {}
/**
* Adds a GGEP block with the specified alternate locations to the
* output stream.
*/
static void addGGEP(OutputStream out, GGEPContainer ggep)
throws IOException {
if( ggep == null || (ggep.locations.size() == 0 && ggep.createTime <= 0))
throw new NullPointerException("null or empty locations");
GGEP info = new GGEP();
if(ggep.locations.size() > 0) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
for(Iterator i = ggep.locations.iterator(); i.hasNext();) {
try {
Endpoint ep = (Endpoint)i.next();
baos.write(ep.getHostBytes());
ByteOrder.short2leb((short)ep.getPort(), baos);
} catch(UnknownHostException uhe) {
continue;
}
}
} catch(IOException impossible) {
ErrorService.error(impossible);
}
info.put(GGEP.GGEP_HEADER_ALTS, baos.toByteArray());
}
if(ggep.createTime > 0)
info.put(GGEP.GGEP_HEADER_CREATE_TIME, ggep.createTime / 1000);
info.write(out);
}
/**
* Returns a <tt>Set</tt> of other endpoints described
* in one of the GGEP arrays.
*/
static GGEPContainer getGGEP(GGEP ggep) {
if (ggep == null)
return GGEPContainer.EMPTY;
Set locations = null;
long createTime = -1;
// if the block has a ALTS value, get it, parse it,
// and move to the next.
if (ggep.hasKey(GGEP.GGEP_HEADER_ALTS)) {
try {
locations = parseLocations(ggep.getBytes(GGEP.GGEP_HEADER_ALTS));
} catch (BadGGEPPropertyException bad) {}
}
if(ggep.hasKey(GGEP.GGEP_HEADER_CREATE_TIME)) {
try {
createTime = ggep.getLong(GGEP.GGEP_HEADER_CREATE_TIME) * 1000;
} catch(BadGGEPPropertyException bad) {}
}
return (locations == null && createTime == -1) ?
GGEPContainer.EMPTY : new GGEPContainer(locations, createTime);
}
private static Set parseLocations(byte[] locBytes) {
Set locations = null;
IPFilter ipFilter = IPFilter.instance();
if (locBytes.length % 6 == 0) {
for (int j = 0; j < locBytes.length; j += 6) {
int port = ByteOrder.ushort2int(ByteOrder.leb2short(locBytes, j+4));
if (!NetworkUtils.isValidPort(port))
continue;
byte[] ip = new byte[4];
ip[0] = locBytes[j];
ip[1] = locBytes[j + 1];
ip[2] = locBytes[j + 2];
ip[3] = locBytes[j + 3];
if (!NetworkUtils.isValidAddress(ip) ||
!ipFilter.allow(ip) ||
NetworkUtils.isMe(ip, port))
continue;
if (locations == null)
locations = new HashSet();
locations.add(new Endpoint(ip, port));
}
}
return locations;
}
}
/**
* A container for information we're putting in/out of GGEP blocks.
*/
static final class GGEPContainer {
final Set locations;
final long createTime;
private static final GGEPContainer EMPTY = new GGEPContainer();
private GGEPContainer() {
this(null, -1);
}
GGEPContainer(Set locs, long create) {
locations = locs == null ? Collections.EMPTY_SET : locs;
createTime = create;
}
boolean isEmpty() {
return locations.isEmpty() && createTime <= 0;
}
}
}