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); } } }