Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add WAV content filter #982

Open
wants to merge 3 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions src/freenet/client/DefaultMIMETypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down Expand Up @@ -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.
Expand Down
46 changes: 27 additions & 19 deletions src/freenet/client/filter/ContentFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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],
Expand All @@ -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,
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down
131 changes: 131 additions & 0 deletions src/freenet/client/filter/WAVFilter.java
Original file line number Diff line number Diff line change
@@ -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<String, String> 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);
}
}
2 changes: 2 additions & 0 deletions src/freenet/l10n/freenet.l10n.en.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
89 changes: 89 additions & 0 deletions test/freenet/client/filter/WAVFilterTest.java
Original file line number Diff line number Diff line change
@@ -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<? extends Exception> 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;
}
}
Binary file added test/freenet/client/filter/wav/test.wav
Binary file not shown.
Loading