diff --git a/src/freenet/client/DefaultMIMETypes.java b/src/freenet/client/DefaultMIMETypes.java index 6f754eab35..0615915681 100644 --- a/src/freenet/client/DefaultMIMETypes.java +++ b/src/freenet/client/DefaultMIMETypes.java @@ -571,7 +571,7 @@ public synchronized static short byName(String s) { addMIMEType((short)439, "audio/x-realaudio", "ra"); addMIMEType((short)440, "audio/x-scpls", "pls"); addMIMEType((short)441, "audio/x-sd2", "sd2"); - addMIMEType((short)442, "audio/x-wav", "wav"); + addMIMEType((short)442, "audio/vnd.wave", "wav"); addMIMEType((short)443, "chemical/x-pdb", "pdb"); addMIMEType((short)444, "chemical/x-xyz", "xyz"); addMIMEType((short)445, "image/cgm"); @@ -752,9 +752,6 @@ public synchronized static short byName(String s) { addMIMEType((short)620, "audio/ogg", "oga"); addMIMEType((short)621, "audio/flac", "flac"); addMIMEType((short)622, "image/webp", "webp"); - addMIMEType((short)623, "image/avif", "avif"); - addMIMEType((short)624, "image/heic", "heic"); - addMIMEType((short)625, "image/heif", "heif"); } /** Guess a MIME type from a filename. diff --git a/src/freenet/client/filter/ContentFilter.java b/src/freenet/client/filter/ContentFilter.java index 3ae493ca55..d41c165679 100644 --- a/src/freenet/client/filter/ContentFilter.java +++ b/src/freenet/client/filter/ContentFilter.java @@ -56,6 +56,7 @@ public static void init() { l10n("textPlainReadAdvice"), true, "US-ASCII", null, false)); + // Images // GIF - has a filter register(new FilterMIMEType("image/gif", "gif", new String[0], new String[0], true, false, new GIFFilter(), false, false, false, false, false, false, @@ -74,7 +75,6 @@ true, false, new PNGFilter(true, true, true), false, false, false, false, true, l10n("imagePngReadAdvice"), false, null, null, false)); - // BMP - has a filter // Reference: http://filext.com/file-extension/BMP register(new FilterMIMEType("image/bmp", "bmp", new String[] { "image/x-bmp","image/x-bitmap","image/x-xbitmap","image/x-win-bitmap","image/x-windows-bmp","image/ms-bmp","image/x-ms-bmp","application/bmp","application/x-bmp","application/x-win-bitmap" }, new String[0], @@ -88,6 +88,7 @@ true, false, new WebPFilter(), false, false, false, false, true, false, l10n("imageWebPReadAdvice"), false, null, null, false)); + // Audio /* Ogg - has a filter * Xiph's container format. Contains one or more logical bitstreams. * Each type of bitstream will likely require additional processing, @@ -123,6 +124,11 @@ false, false, new M3UFilter(), false, false, false, false, false, false, register(new FilterMIMEType("audio/mpeg", "mp3", new String[] {"audio/mp3", "audio/x-mp3", "audio/x-mpeg", "audio/mpeg3", "audio/x-mpeg3", "audio/mpg", "audio/x-mpg", "audio/mpegaudio"}, new String[0], true, false, new MP3Filter(), true, true, false, true, false, false, l10n("audioMP3ReadAdvice"), false, null, null, false)); + + // WAV - has a filter + register(new FilterMIMEType("audio/vnd.wave", "wav", new String[] {"audio/x-wav", "audio/wav", "audio/wave"}, + new String[0], true, false, new WAVFilter(), true, true, false, true, false, false, + l10n("audioWAVReadAdvice"), false, null, null, false)); // ICO needs filtering. // Format is not the same as BMP iirc. @@ -532,24 +538,26 @@ public static boolean startsWith(byte[] data, byte[] cmp, int length) { } public static String mimeTypeForSrc(String uriold) { - String uriPath = uriold.contains("?") - ? uriold.split("\\?")[0] - : uriold; - String subMimetype; - if (uriPath.endsWith(".m3u") || uriPath.endsWith(".m3u8")) { - subMimetype = "audio/mpegurl"; -} else if (uriPath.endsWith(".flac")) { - subMimetype = "audio/flac"; -} else if (uriPath.endsWith(".oga")) { - subMimetype = "audio/ogg"; -} else if (uriPath.endsWith(".ogv")) { - subMimetype = "video/ogg"; -} else if (uriPath.endsWith(".ogg")) { - subMimetype = "application/ogg"; - } else { // force mp3 for anything we do not know - subMimetype = "audio/mpeg"; - } - return subMimetype; + String uriPath = uriold.contains("?") + ? uriold.split("\\?")[0] + : uriold; + String subMimetype; + if (uriPath.endsWith(".m3u") || uriPath.endsWith(".m3u8")) { + subMimetype = "audio/mpegurl"; + } else if (uriPath.endsWith(".flac")) { + subMimetype = "audio/flac"; + } else if (uriPath.endsWith(".oga")) { + subMimetype = "audio/ogg"; + } else if (uriPath.endsWith(".ogv")) { + subMimetype = "video/ogg"; + } else if (uriPath.endsWith(".ogg")) { + subMimetype = "application/ogg"; + } else if (uriPath.endsWith(".wav")) { + subMimetype = "audio/vnd.wave"; + } else { // force mp3 for anything we do not know + subMimetype = "audio/mpeg"; + } + return subMimetype; } public static class FilterStatus { diff --git a/src/freenet/client/filter/WAVFilter.java b/src/freenet/client/filter/WAVFilter.java new file mode 100644 index 0000000000..c5d45dc21c --- /dev/null +++ b/src/freenet/client/filter/WAVFilter.java @@ -0,0 +1,131 @@ +package freenet.client.filter; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.Map; + +import freenet.l10n.NodeL10n; + +public class WAVFilter extends RIFFFilter { + // RFC 2361 + private final int WAVE_FORMAT_UNKNOWN = 0; + private final int WAVE_FORMAT_PCM = 1; + private final int WAVE_FORMAT_IEEE_FLOAT = 3; + private final int WAVE_FORMAT_ALAW = 6; + private final int WAVE_FORMAT_MULAW = 7; + // Header sizes (https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html) + // fmt header without cbSize field + private final int FMT_SIZE_BASIC = 16; + // fmt header with cbSize = 0 + private final int FMT_SIZE_cbSize = 18; + // fmt header with cbSize and extensions + private final int FMT_SIZE_cbSize_extension = 40; + + @Override + protected byte[] getChunkMagicNumber() { + return new byte[] {'W', 'A', 'V', 'E'}; + } + + private static final class WAVFilterContext { + boolean hasfmt = false; + boolean hasdata = false; + int nSamplesPerSec = 0; + int nChannels = 0; + int nBlockAlign = 0; + int wBitsPerSample = 0; + int format = 0; + } + + @Override + protected Object createContext() { + return new WAVFilterContext(); + } + + @Override + protected void readFilterChunk(byte[] ID, int size, Object context, DataInputStream input, DataOutputStream output, + String charset, Map otherParams, String schemeHostAndPort, FilterCallback cb) + throws DataFilterException, IOException { + WAVFilterContext ctx = (WAVFilterContext)context; + if(ID[0] == 'f' && ID[1] == 'm' && ID[2] == 't' && ID[3] == ' ') { + if(ctx.hasfmt) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Unexpected fmt chunk was encountered"); + } + if(size != FMT_SIZE_BASIC && size != FMT_SIZE_cbSize && size != FMT_SIZE_cbSize_extension) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "fmt chunk size is invalid"); + } + ctx.format = Short.reverseBytes(input.readShort()); + if(ctx.format != WAVE_FORMAT_PCM && ctx.format != WAVE_FORMAT_IEEE_FLOAT && ctx.format != WAVE_FORMAT_ALAW && ctx.format != WAVE_FORMAT_MULAW) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "WAV file uses a not yet supported format"); + } + ctx.nChannels = Short.reverseBytes(input.readShort()); + output.write(ID); + writeLittleEndianInt(output, size); + output.writeInt((Short.reverseBytes((short) ctx.format) << 16) | Short.reverseBytes((short) ctx.nChannels)); + ctx.nSamplesPerSec = readLittleEndianInt(input); + writeLittleEndianInt(output, ctx.nSamplesPerSec); + int nAvgBytesPerSec = readLittleEndianInt(input); + writeLittleEndianInt(output, nAvgBytesPerSec); + ctx.nBlockAlign = Short.reverseBytes(input.readShort()); + ctx.wBitsPerSample = Short.reverseBytes(input.readShort()); + output.writeInt((Short.reverseBytes((short) ctx.nBlockAlign) << 16) | Short.reverseBytes((short) ctx.wBitsPerSample)); + ctx.hasfmt = true; + if(size > FMT_SIZE_BASIC) { + short cbSize = Short.reverseBytes(input.readShort()); + if(cbSize + FMT_SIZE_cbSize != size) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "fmt chunk size is invalid"); + } + output.writeShort(Short.reverseBytes(cbSize)); + } + if(size > FMT_SIZE_cbSize) { + // wValidBitsPerSample, dwChannelMask, and SubFormat GUID + passthroughBytes(input, output, FMT_SIZE_cbSize_extension - FMT_SIZE_cbSize); + } + // Further checks + if((ctx.format == WAVE_FORMAT_ALAW || ctx.format == WAVE_FORMAT_MULAW) && ctx.wBitsPerSample != 8) { + // These formats are 8-bit + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Unexpected bits per sample value"); + } + return; + } + if(!ctx.hasfmt) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "Unexpected header chunk was encountered, instead of fmt chunk"); + } + if(ID[0] == 'd' && ID[1] == 'a' && ID[2] == 't' && ID[3] == 'a') { + // audio data + output.write(ID); + writeLittleEndianInt(output, size); + passthroughBytes(input, output, size); + if((size & 1) != 0) { // Add padding if necessary + output.writeByte(input.readByte()); + } + ctx.hasdata = true; + } else if(ID[0] == 'f' && ID[1] == 'a' && ID[2] == 'c' && ID[3] == 't') { + if(size < 4) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "fact chunk must contain at least 4 bytes"); + } + // Just dwSampleLength (Number of samples) here, pass through + output.write(ID); + writeLittleEndianInt(output, size); + passthroughBytes(input, output, size); + if((size & 1) != 0) { // Add padding if necessary + output.writeByte(input.readByte()); + } + } else { + // Unknown block + writeJunkChunk(input, output, size); + } + } + + @Override + protected void EOFCheck(Object context) throws DataFilterException { + WAVFilterContext ctx = (WAVFilterContext)context; + if(!ctx.hasfmt || !ctx.hasdata) { + throw new DataFilterException(l10n("invalidTitle"), l10n("invalidTitle"), "WAV file is missing fmt chunk or data chunk"); + } + } + + private static String l10n(String key) { + return NodeL10n.getBase().getString("WAVFilter."+key); + } +} diff --git a/src/freenet/l10n/freenet.l10n.en.properties b/src/freenet/l10n/freenet.l10n.en.properties index 9e3b11a52c..036cc20690 100644 --- a/src/freenet/l10n/freenet.l10n.en.properties +++ b/src/freenet/l10n/freenet.l10n.en.properties @@ -270,6 +270,7 @@ ContentDataFilter.warningUnknownCharsetTitle=Warning: Unknown character set (${c ContentFilter.applicationPdfReadAdvice=Adobe(R) PDF document - VERY DANGEROUS! ContentFilter.audioM3UReadAdvice=MP3 music/audio file - probably not dangerous but can contain metadata which might include URLs of unencrypted content; the filter will strip these out ContentFilter.audioMP3ReadAdvice=MP3 music/audio file - probably not dangerous but can contain metadata which might include URLs of unencrypted content; the filter will strip these out +ContentFilter.audioWAVReadAdvice=WAV music/audio file - probably not dangerous. ContentFilter.EOFMessage=Unexpected end of file ContentFilter.EOFDescription=The filter needed more data from the file you were accessing than was available. The file may be malformed or corrupted. ContentFilter.audioFLACReadAdvice=FLAC audio format - Dangerous. May contain off Freenet links to album art. If followed, these links can harm anonymity. @@ -2248,6 +2249,7 @@ UserAlertsToadlet.title=Status messages UserAlertsToadlet.noMessages=No messages VorbisBitstreamFilter.MalformedTitle=Malformed Vorbis Bitstream VorbisBitstreamFilter.MalformedMessage=The Vorbis bitstream is not correctly formatted, and could not be properly validated. +WAVFilter.invalidTitle=Invalid WAV file WebPFilter.animUnsupportedTitle=WebP animation is currently not supported WebPFilter.animUnsupported=WebP animation is currently not supported by the filter, because it could contain frames using the lossless encoding. WebP lossless format has known buffer overflow exploit. When viewed on unpatched browsers and applications, it can damage the security of the system. Therefore, the content filter cannot ensure the safety of this animation. WebPFilter.alphUnsupportedTitle=WebP alpha channel with lossless compression is currently not supported diff --git a/test/freenet/client/filter/WAVFilterTest.java b/test/freenet/client/filter/WAVFilterTest.java new file mode 100644 index 0000000000..4399ec058c --- /dev/null +++ b/test/freenet/client/filter/WAVFilterTest.java @@ -0,0 +1,89 @@ +package freenet.client.filter; + +import static freenet.client.filter.ResourceFileUtil.resourceToBucket; +import static org.junit.Assert.*; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import org.junit.Test; + +import freenet.support.api.Bucket; +import freenet.support.io.ArrayBucket; +import freenet.support.io.BucketTools; + +/** + * Unit test for (parts of) {@link WAVFilter}. + */ +public class WAVFilterTest { + + @Test + public void testValidWAV() throws IOException { + Bucket input = resourceToBucket("./wav/test.wav"); + Bucket output = filterWAV(input, null); + + //Filter should return the original + assertEquals("Input and output should be the same length", input.size(), output.size()); + assertArrayEquals("Input and output are not identical", BucketTools.toByteArray(input), BucketTools.toByteArray(output)); + } + + // This file is WebP, not WAV! + @Test + public void testAnotherFile() throws IOException { + Bucket input = resourceToBucket("./webp/test.webp"); + filterWAV(input, DataFilterException.class); + } + + // There is just a JUNK chunk in the file + @Test + public void testFileJustJUNK() throws IOException { + ByteBuffer buf = ByteBuffer.allocate(28) + .order(ByteOrder.LITTLE_ENDIAN) + .put(new byte[]{'R', 'I', 'F', 'F'}) + .putInt(20 /* file size */) + .put(new byte[]{'W', 'A', 'V', 'E'}) + .put(new byte[]{'J', 'U', 'N', 'K'}) + .putInt(7 /* chunk size */) + .putLong(0); + + Bucket input = new ArrayBucket(buf.array()); + filterWAV(input, DataFilterException.class); + } + + // There is just a fmt chunk in the file, but no audio data + @Test + public void testFileNoData() throws IOException { + ByteBuffer buf = ByteBuffer.allocate(36) + .order(ByteOrder.LITTLE_ENDIAN) + .put(new byte[]{'R', 'I', 'F', 'F'}) + .putInt(28 /* file size */) + .put(new byte[]{'W', 'A', 'V', 'E'}) + .put(new byte[]{'f', 'm', 't', ' '}) + .putInt(16 /* chunk size */) + .put(new byte[]{1, 0, 2, 0}) //format, nChannels + .putInt(44100) // nSamplesPerSec + .putInt(44100 * 4) // nAvgBytesPerSec + .put(new byte[]{4, 0, 16, 0}); // nBlockAlign, wBitsPerSample + + Bucket input = new ArrayBucket(buf.array()); + filterWAV(input, DataFilterException.class); + } + + private Bucket filterWAV(Bucket input, Class expected) throws IOException { + WAVFilter objWAVFilter = new WAVFilter(); + Bucket output = new ArrayBucket(); + try ( + InputStream inStream = input.getInputStream(); + OutputStream outStream = output.getOutputStream() + ) { + if (expected != null) { + assertThrows(expected, () -> objWAVFilter.readFilter(inStream, outStream, "", null, null, null)); + } else { + objWAVFilter.readFilter(inStream, outStream, "", null, null, null); + } + } + return output; + } +} diff --git a/test/freenet/client/filter/wav/test.wav b/test/freenet/client/filter/wav/test.wav new file mode 100644 index 0000000000..b3127d1e24 Binary files /dev/null and b/test/freenet/client/filter/wav/test.wav differ