package com.limegroup.gnutella.malware;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;
import org.limewire.bittorrent.bencoding.Token;
import org.limewire.core.settings.FilterSettings;
import org.limewire.io.IOUtils;
import org.limewire.io.InvalidDataException;
import org.limewire.logging.Log;
import org.limewire.logging.LogFactory;
import org.limewire.util.BEncoder;
import org.limewire.util.Base32;
import org.limewire.util.StringUtils;
/**
* Encoder/decoder for the base32-encoded, deflated, bencoded string that
* defines mime types for the FileExtensionChecker.
*
* The bencoded data consists of a map with three keys, "m", "o", and "p",
* which point to three lists of equal length. Each element of the "m" list is
* a lowercase UTF-8 string containing the name of a mime type. The
* corresponding element in the "o" list is a list of integers, which are
* offsets from the beginning (positive values) or end (negative values) of the
* file. The corresponding element in the "p" list is a list of byte patterns
* that occur at those offsets in files of the given mime type.
*
* A file is misnamed if it has a mime type that indicates a dangerous file
* type but does not have one of the extensions allowed for that type.
*/
class MimeTypeEncoder {
private static final Log LOG =
LogFactory.getLog(MimeTypeEncoder.class);
private final MimeType[] mimeTypes;
MimeTypeEncoder() {
MimeType[] mt;
try {
mt = decodeSetting(FilterSettings.MIME_TYPES.get());
} catch(InvalidDataException e) {
mt = new MimeType[0];
}
mimeTypes = mt;
}
/**
* Returns the mime type of a file, or null if the mime type is unknown.
*/
String getMimeType(File file) {
RandomAccessFile f = null;
try {
long length = file.length();
f = new RandomAccessFile(file, "r");
for(MimeType mimeType : mimeTypes) {
boolean matches = true;
for(int i = 0; i < mimeType.offsets.length; i++) {
long offset = mimeType.offsets[i];
int patternLength = mimeType.patterns[i].length;
// Negative offsets are relative to the end of the file
if(offset < 0)
offset = length - offset;
if(offset < 0 || offset + patternLength > length) {
matches = false;
break;
}
f.seek(offset);
byte[] buf = new byte[patternLength];
f.readFully(buf);
if(!Arrays.equals(buf, mimeType.patterns[i])) {
matches = false;
break;
}
}
if(matches) {
if(LOG.isDebugEnabled())
LOG.debug("Mime type " + mimeType.name);
return mimeType.name;
}
}
LOG.debug("Unknown mime type");
return null;
} catch(IOException e) {
LOG.debug("Error reading file", e);
return null;
} finally {
IOUtils.close(f);
}
}
/**
* Encodes a string defining mime types.
*/
static String encodeSetting(MimeType[] types) throws InvalidDataException {
List<String> names = new ArrayList<String>();
List<List<Long>> offsets = new ArrayList<List<Long>>();
List<List<byte[]>> patterns = new ArrayList<List<byte[]>>();
HashMap<String, List<?>> payload = new HashMap<String, List<?>>();
payload.put("m", names);
payload.put("o", offsets);
payload.put("p", patterns);
for(MimeType m : types) {
names.add(m.name);
offsets.add(Arrays.asList(m.offsets));
patterns.add(Arrays.asList(m.patterns));
}
ByteArrayOutputStream zipped = new ByteArrayOutputStream();
DeflaterOutputStream zip = new DeflaterOutputStream(zipped);
try {
BEncoder.getEncoder(zip, true, false, "UTF-8").encodeDict(payload);
zip.flush();
} catch(IOException e) {
LOG.error("Error encoding setting", e);
throw new InvalidDataException(e);
} finally {
IOUtils.close(zip);
}
return Base32.encode(zipped.toByteArray());
}
/**
* Decodes a string defining mime types.
*/
static MimeType[] decodeSetting(String setting) throws InvalidDataException {
if(setting.isEmpty())
return new MimeType[0];
InflaterInputStream unzip = null;
ReadableByteChannel byteChannel = null;
try {
ByteArrayInputStream zipped =
new ByteArrayInputStream(Base32.decode(setting));
unzip = new InflaterInputStream(zipped);
byteChannel = Channels.newChannel(unzip);
Object bencoded = Token.parse(byteChannel, "UTF-8");
if(bencoded == null)
throw new InvalidDataException("No bencoded object");
Map<?, ?> map = (Map)bencoded;
List<?> m = (List)map.get("m");
List<?> o = (List)map.get("o");
List<?> p = (List)map.get("p");
if(m == null || o == null || p == null)
throw new InvalidDataException("Missing key");
int length = m.size();
if(o.size() != length || p.size() != length)
throw new InvalidDataException("List lengths differ");
MimeType[] types = new MimeType[length];
for(int i = 0; i < length; i++) {
String name = StringUtils.getUTF8String((byte[])m.get(i));
if(LOG.isDebugEnabled())
LOG.debug("Name: " + name);
List<?> offsets = (List)o.get(i);
List<?> patterns = (List)p.get(i);
if(offsets.size() != patterns.size())
throw new InvalidDataException("List lengths differ");
// Don't allow empty offset lists
if(offsets.isEmpty())
throw new InvalidDataException("Empty offset list");
Long[] off = new Long[offsets.size()];
byte[][] pat = new byte[patterns.size()][];
for(int j = 0; j < off.length; j++) {
off[j] = (Long)offsets.get(j);
pat[j] = (byte[])patterns.get(j);
if(LOG.isDebugEnabled())
LOG.debug("Offset: " + off[j] + ", pattern: " +
StringUtils.toHexString(pat[j]));
}
types[i] = new MimeType(name, off, pat);
}
return types;
} catch(ClassCastException e) {
LOG.debug("Error decoding setting", e);
throw new InvalidDataException(e);
} catch(Exception e) {
LOG.debug("Error decoding setting", e);
throw new InvalidDataException(e);
} finally {
IOUtils.close(byteChannel);
IOUtils.close(unzip);
}
}
}