package com.limegroup.gnutella;
import static com.limegroup.gnutella.Constants.MAX_FILE_SIZE;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.StringTokenizer;
import org.limewire.collection.BitNumbers;
import org.limewire.collection.IntervalSet;
import org.limewire.core.settings.FilterSettings;
import org.limewire.core.settings.MessageSettings;
import org.limewire.io.BadGGEPPropertyException;
import org.limewire.io.ConnectableImpl;
import org.limewire.io.GGEP;
import org.limewire.io.InvalidDataException;
import org.limewire.io.IpPort;
import org.limewire.io.IpPortSet;
import org.limewire.io.NetworkInstanceUtils;
import org.limewire.io.NetworkUtils;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.service.ErrorService;
import org.limewire.util.ByteUtils;
import org.limewire.util.NameValue;
import org.limewire.util.StringUtils;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.limegroup.gnutella.altlocs.AltLocManager;
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.library.CreationTimeCache;
import com.limegroup.gnutella.library.FileDesc;
import com.limegroup.gnutella.library.IncompleteFileDesc;
import com.limegroup.gnutella.messages.GGEPKeys;
import com.limegroup.gnutella.messages.HUGEExtension;
import com.limegroup.gnutella.messages.IntervalEncoder;
import com.limegroup.gnutella.uploader.HTTPHeaderUtils;
import com.limegroup.gnutella.util.DataUtils;
import com.limegroup.gnutella.xml.LimeXMLDocument;
import com.limegroup.gnutella.xml.LimeXMLDocumentFactory;
import com.limegroup.gnutella.xml.LimeXMLNames;
@Singleton
public class ResponseFactoryImpl implements ResponseFactory {
private static final Log LOG = LogFactory.getLog(ResponseFactoryImpl.class);
/** The magic byte to use as extension separators. */
private static final byte EXT_SEPARATOR = 0x1c;
/** 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";
private final AltLocManager altLocManager;
private final Provider<CreationTimeCache> creationTimeCache;
private final IPFilter ipFilter;
private final NetworkInstanceUtils networkInstanceUtils;
private final LimeXMLDocumentFactory limeXMLDocumentFactory;
@Inject
public ResponseFactoryImpl(AltLocManager altLocManager,
Provider<CreationTimeCache> creationTimeCache, IPFilter ipFilter,
LimeXMLDocumentFactory limeXMLDocumentFactory, NetworkInstanceUtils networkInstanceUtils) {
this.altLocManager = altLocManager;
this.creationTimeCache = creationTimeCache;
this.ipFilter = ipFilter;
this.limeXMLDocumentFactory = limeXMLDocumentFactory;
this.networkInstanceUtils = networkInstanceUtils;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.ResponseFactory#createResponse(long, long, java.lang.String)
*/
public Response createResponse(long index, long size, String name, URN urn) {
return createResponse(index, size, name, -1, new UrnSet(urn), null, null, null);
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.ResponseFactory#createResponse(long, long, java.lang.String, com.limegroup.gnutella.xml.LimeXMLDocument)
*/
public Response createResponse(long index, long size, String name,
LimeXMLDocument doc, URN urn) {
return createResponse(index, size, name, -1, new UrnSet(urn), doc, null, null);
}
public Response createResponse(FileDesc fileDesc) {
return createResponse(fileDesc, false);
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.ResponseFactory#createResponse(com.limegroup.gnutella.FileDesc)
*/
public Response createResponse(FileDesc fd, boolean includeNMS1Urn) {
IntervalSet ranges = null;
boolean verified = false;
if (fd instanceof IncompleteFileDesc) {
IncompleteFileDesc ifd = (IncompleteFileDesc)fd;
ranges = new IntervalSet();
verified = ifd.loadResponseRanges(ranges);
}
GGEPContainer container = new GGEPContainer(getAsIpPorts(altLocManager
.getDirect(fd.getSHA1Urn())), creationTimeCache.get()
.getCreationTimeAsLong(fd.getSHA1Urn()), fd.getFileSize(), ranges,
verified, fd.getTTROOTUrn(), includeNMS1Urn ? fd.getNMS1Urn() : null);
LimeXMLDocument doc = getLimeXmlDoc(fd);
Response response = createResponse(fd.getIndex(), fd.getFileSize(),
fd.getFileName(), -1, fd.getUrns(), doc, container, null);
return response;
}
/**
* If the FileDesc has no XML documents, null is returned. If the FileDesc
* has one XML document, that document is returned. If the FileDesc
* has multiple XML documents, null is returned. The reasoning behind returning null
* when there are multiple XML docs is that presumably
* the query will be a 'rich' query, and we want to include only the schema
* that was in the query.
*/
private LimeXMLDocument getLimeXmlDoc(FileDesc fileDesc) {
List<LimeXMLDocument> docs = fileDesc.getLimeXMLDocuments();
if (docs.size() == 1) {
return docs.get(0);
}
return null;
}
/* (non-Javadoc)
* @see com.limegroup.gnutella.ResponseFactory#createFromStream(java.io.InputStream)
*/
public Response createFromStream(InputStream is) throws IOException {
// extract file index & size
long index = ByteUtils.uint2long(ByteUtils.leb2int(is));
long size = ByteUtils.uint2long(ByteUtils.leb2int(is));
int incomingNameByteArraySize;
if ((index & 0xFFFFFFFF00000000L) != 0)
throw new IOException("invalid index: " + index);
if (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);
}
incomingNameByteArraySize = baos.size();
String name = new String(baos.toByteArray(), "UTF-8");
checkFilename(name); // throws IOException
// 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.length == 0) {
if (is.available() < 16) {
throw new IOException("not enough room for the GUID");
}
return createResponse(index, size, name, incomingNameByteArraySize, null, null, null, null);
} else {
// now handle between-the-nulls
// \u001c is the HUGE v0.93 GEM delimiter
HUGEExtension huge = new HUGEExtension(rawMeta);
Set<URN> urns = huge.getURNS();
LimeXMLDocument doc = null;
for (String next : huge.getMiscBlocks()) {
doc = createXmlDocument(name, next);
if (doc != null)
break;
}
GGEPContainer ggep = getGGEP(huge.getGGEP(), size);
if (ggep.size64 > MAX_FILE_SIZE)
throw new IOException(" file too large " + ggep.size64);
if (ggep.size64 > Integer.MAX_VALUE)
size = ggep.size64;
urns = updateUrns(urns, ggep.nms1Urn);
return createResponse(index, size, name, incomingNameByteArraySize, urns, doc, ggep, rawMeta);
}
}
/**
* Updates <code>urns</code> of urns adding <code>nms1Urn</code> if
* if is not null.
*/
static Set<URN> updateUrns(Set<URN> urns, URN nms1Urn) {
if (nms1Urn == null) {
return urns;
}
if (!(urns instanceof UrnSet)) {
urns = new UrnSet(urns);
}
urns.add(nms1Urn);
return urns;
}
/**
* 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 extensions The raw unparsed extension bytes.
*/
// NOTE: not in the interface, but public so tests not in this package can use.
public Response createResponse(long index, long size, String name, int incomingNameByteArraySize,
Set<? extends URN> urns, LimeXMLDocument doc,
GGEPContainer ggepData, byte[] extensions) {
// make sure ggepData is correct.
if (ggepData == null) {
if (size <= Integer.MAX_VALUE)
ggepData = GGEPContainer.EMPTY;
else // large filesizes require GGEP now
ggepData = new GGEPContainer(null, -1L, size, null, false, null, null);
}
// build up extensions if it wasn't already!
if (extensions == null)
extensions = createExtBytes(urns, ggepData, size);
return new ResponseImpl(index, size, name, incomingNameByteArraySize, urns, doc,
ggepData.locations, ggepData.createTime, extensions, ggepData.ranges, ggepData.verified);
}
private void checkFilename(String name) throws IOException {
if (name.length() == 0) {
throw new IOException("empty name in response");
}
// sanity checks for filename
if (name.indexOf('/') != -1 || name.indexOf('\n') != -1 || name.indexOf('\r') != -1) {
throw new IOException("Illegal filename " + name + "contains one of [/\\n\\r]");
}
}
/**
* Constructs an xml string from the given extension string.
*
* @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 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();
assert first != null;
assert second != null;
first = first.toLowerCase(Locale.US);
second = second.toLowerCase(Locale.US);
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<NameValue<String>> values = new ArrayList<NameValue<String>>(3);
values.add(new NameValue<String>(LimeXMLNames.AUDIO_TITLE, name));
values.add(new NameValue<String>(LimeXMLNames.AUDIO_BITRATE, bitrate));
values.add(new NameValue<String>(LimeXMLNames.AUDIO_SECONDS, length));
return limeXMLDocumentFactory.createLimeXMLDocument(values,
LimeXMLNames.AUDIO_SCHEMA);
}
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 byte[] createExtBytes(Set<? extends URN> urns, GGEPContainer ggep, long size) {
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.
for (Iterator<? extends URN> iter = urns.iterator(); iter
.hasNext();) {
URN urn = iter.next();
assert urn != null : "Null URN";
if (!urn.isSHA1() && MessageSettings.TTROOT_IN_GGEP.getValue())
continue;
if (urn.isNMS1()) {
continue;
}
baos.write(StringUtils.toAsciiBytes(urn.toString()));
// 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())
addGGEP(baos, ggep, size);
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 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 Set<? extends IpPort> getAsIpPorts(
AlternateLocationCollection<DirectAltLoc> col) {
if (!col.hasAlternateLocations())
return Collections.emptySet();
long now = System.currentTimeMillis();
synchronized (col) {
Set<IpPort> endpoints = null;
int i = 0;
// Never send more alt-locs than another LimeWire peer would accept
final int maxLocations = Math.min(10,
FilterSettings.MAX_ALTS_PER_RESPONSE.getValue() - 1);
for (Iterator<DirectAltLoc> iter = col.iterator(); iter.hasNext()
&& i < maxLocations;) {
DirectAltLoc al = iter.next();
if (al.canBeSent(AlternateLocation.MESH_RESPONSE)) {
IpPort host = al.getHost();
if (!networkInstanceUtils.isMe(host)) {
if (endpoints == null)
endpoints = new IpPortSet();
endpoints.add(host);
i++;
al.send(now, AlternateLocation.MESH_RESPONSE);
}
} else if (!al.canBeSentAny())
iter.remove();
}
if (endpoints == null)
return Collections.emptySet();
else
return endpoints;
}
}
/**
* Adds a GGEP block with the specified alternate locations to the output
* stream.
*/
private void addGGEP(OutputStream out, GGEPContainer ggep, long size)
throws IOException {
if (ggep == null
|| (ggep.locations.size() == 0 &&
ggep.createTime <= 0 &&
ggep.size64 <= Integer.MAX_VALUE &&
ggep.ranges == null &&
ggep.ttroot == null &&
ggep.nms1Urn == null))
throw new IllegalArgumentException(
"null or empty locations and small size");
GGEP info = new GGEP(true);
if (ggep.locations.size() > 0) {
byte[] output = NetworkUtils.packIpPorts(ggep.locations);
info.put(GGEPKeys.GGEP_HEADER_ALTS, output);
BitNumbers bn = HTTPHeaderUtils.getTLSIndices(ggep.locations);
if (!bn.isEmpty())
info.put(GGEPKeys.GGEP_HEADER_ALTS_TLS, bn.toByteArray());
}
if (ggep.createTime > 0)
info.put(GGEPKeys.GGEP_HEADER_CREATE_TIME, ggep.createTime / 1000);
if (ggep.size64 > Integer.MAX_VALUE && ggep.size64 <= MAX_FILE_SIZE)
info.put(GGEPKeys.GGEP_HEADER_LARGE_FILE, ggep.size64);
if (ggep.ranges != null) {
IntervalEncoder.encode(size, info, ggep.ranges);
if (!ggep.verified)
info.put(GGEPKeys.GGEP_HEADER_PARTIAL_RESULT_UNVERIFIED);
}
if (ggep.ttroot != null && MessageSettings.TTROOT_IN_GGEP.getValue())
info.put(GGEPKeys.GGEP_HEADER_TTROOT,ggep.ttroot.getBytes());
if (ggep.nms1Urn != null) {
info.put(GGEPKeys.GGEP_HEADER_NMS1, ggep.nms1Urn.getBytes());
}
info.write(out);
}
/**
* Returns a <tt>Set</tt> of other endpoints described in one of the GGEP
* arrays.
*
* Default access for testing.
*/
GGEPContainer getGGEP(GGEP ggep, long size) {
if (ggep == null)
return GGEPContainer.EMPTY;
Set<? extends IpPort> locations = null;
long createTime = -1;
long size64 = size;
URN ttroot = null;
URN nms1Urn = null;
// if the block has a ALTS value, get it, parse it,
// and move to the next.
if (ggep.hasValueFor(GGEPKeys.GGEP_HEADER_ALTS)) {
byte[] tlsData = null;
if (ggep.hasValueFor(GGEPKeys.GGEP_HEADER_ALTS_TLS)) {
try {
tlsData = ggep.getBytes(GGEPKeys.GGEP_HEADER_ALTS_TLS);
} catch (BadGGEPPropertyException ignored) {
}
}
BitNumbers bn = tlsData == null ? null : new BitNumbers(tlsData);
try {
locations = parseLocations(bn, ggep
.getBytes(GGEPKeys.GGEP_HEADER_ALTS));
} catch (BadGGEPPropertyException bad) {
}
}
if (ggep.hasValueFor(GGEPKeys.GGEP_HEADER_CREATE_TIME)) {
try {
createTime = ggep.getLong(GGEPKeys.GGEP_HEADER_CREATE_TIME) * 1000;
} catch (BadGGEPPropertyException bad) {
}
}
if (ggep.hasValueFor(GGEPKeys.GGEP_HEADER_LARGE_FILE)) {
try {
size64 = ggep.getLong(GGEPKeys.GGEP_HEADER_LARGE_FILE);
} catch (BadGGEPPropertyException bad) {
}
}
if (ggep.hasValueFor(GGEPKeys.GGEP_HEADER_TTROOT)) {
try {
byte []tt = ggep.get(GGEPKeys.GGEP_HEADER_TTROOT);
ttroot = URN.createTTRootFromBytes(tt);
} catch (IOException bad){}
}
if (ggep.hasValueFor(GGEPKeys.GGEP_HEADER_NMS1)) {
try {
byte[] nms1 = ggep.get(GGEPKeys.GGEP_HEADER_NMS1);
nms1Urn = URN.createNMS1FromBytes(nms1);
} catch (IOException ie) {
LOG.debug("invalid non-metadata urn", ie);
}
}
boolean verified = false;
IntervalSet ranges = null;
try {
ranges = IntervalEncoder.decode(size64,ggep);
verified = !ggep.hasKey(GGEPKeys.GGEP_HEADER_PARTIAL_RESULT_UNVERIFIED);
} catch (BadGGEPPropertyException ignore){}
if (locations == null && createTime == -1 && size64 <= Integer.MAX_VALUE &&
ranges == null && ttroot == null && nms1Urn == null)
return GGEPContainer.EMPTY;
return new GGEPContainer(locations, createTime, size64, ranges, verified, ttroot, nms1Urn);
}
/**
* Returns a set of IpPorts corresponding to the IpPorts in data. If
* BitNumbers is non-null, the addresses in the index corresponding to any
* 'on' indexes in BitNumbers are considered tlsCapable. Whenever an invalid
* address is encountered, all further hosts are prevented from being TLS
* capable.
*/
private Set<? extends IpPort> parseLocations(BitNumbers tlsHosts,
byte[] data) {
Set<IpPort> locations = null;
if (data.length % 6 != 0)
return null;
int size = data.length / 6;
byte[] current = new byte[6];
for (int i = 0; i < size; i++) {
System.arraycopy(data, i * 6, current, 0, 6);
IpPort ipp;
try {
ipp = NetworkUtils.getIpPort(current, java.nio.ByteOrder.LITTLE_ENDIAN);
} catch (InvalidDataException ide) {
tlsHosts = null; // turn off TLS
continue;
}
// if we're me or banned, ignore.
if (!ipFilter.allow(ipp.getAddress()) || networkInstanceUtils.isMe(ipp))
continue;
if (locations == null)
locations = new IpPortSet();
// if this addr was TLS-capable, mark it as such.
if (tlsHosts != null && tlsHosts.isSet(i))
ipp = new ConnectableImpl(ipp, true);
locations.add(ipp);
}
return locations;
}
/**
* A container for information we're putting in/out of GGEP blocks.
*/
static final class GGEPContainer {
final Set<? extends IpPort> locations;
final long createTime;
final long size64;
static final GGEPContainer EMPTY = new GGEPContainer();
final IntervalSet ranges;
final boolean verified;
final URN ttroot;
private final URN nms1Urn;
private GGEPContainer() {
this(null, -1, 0, null, false, null, null);
}
/**
* @param nms1Urn can be null
*/
GGEPContainer(Set<? extends IpPort> locs, long create, long size64, IntervalSet ranges, boolean verified, URN ttroot,
URN nms1Urn) {
this.nms1Urn = nms1Urn;
if (locs == null)
locations = Collections.emptySet();
else
locations = Collections.unmodifiableSet(locs);
createTime = create;
this.size64 = size64;
this.ranges = ranges;
this.verified = verified;
this.ttroot = ttroot;
assert ttroot == null || ttroot.isTTRoot();
assert nms1Urn == null || nms1Urn.isNMS1();
}
boolean isEmpty() {
return locations.isEmpty() && createTime <= 0
&& size64 <= Integer.MAX_VALUE && ranges == null &&
(ttroot == null || !MessageSettings.TTROOT_IN_GGEP.getValue()) && nms1Urn == null;
}
}
}