Author: nextgens
Date: 2008-02-08 23:05:53 +0000 (Fri, 08 Feb 2008)
New Revision: 17725
Modified:
trunk/freenet/src/freenet/clients/http/filter/ContentFilter.java
trunk/freenet/src/freenet/clients/http/filter/PNGFilter.java
Log:
PNGFilter: improved filter stripping out comments, timestamps and invalid
chunks on demand
Modified: trunk/freenet/src/freenet/clients/http/filter/ContentFilter.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/filter/ContentFilter.java
2008-02-08 22:49:26 UTC (rev 17724)
+++ trunk/freenet/src/freenet/clients/http/filter/ContentFilter.java
2008-02-08 23:05:53 UTC (rev 17725)
@@ -54,7 +54,7 @@
// PNG - has a filter
register(new MIMEType("image/png", "png", new String[0], new
String[0],
- true, false, new PNGFilter(), null, false,
false, false, false, true, false,
+ true, false, new PNGFilter(false, false,
false)/* FIXME: reenable when they work */, null, false, false, false, false,
true, false,
l10n("imagePngReadAdvice"),
l10n("imagePngWriteAdvice"), false, null,
null));
Modified: trunk/freenet/src/freenet/clients/http/filter/PNGFilter.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/filter/PNGFilter.java
2008-02-08 22:49:26 UTC (rev 17724)
+++ trunk/freenet/src/freenet/clients/http/filter/PNGFilter.java
2008-02-08 23:05:53 UTC (rev 17725)
@@ -11,23 +11,73 @@
import java.util.HashMap;
import freenet.l10n.L10n;
+import freenet.support.CRC;
+import freenet.support.Fields;
import freenet.support.HTMLNode;
+import freenet.support.HexUtil;
+import freenet.support.Logger;
+import freenet.support.LoggerHook.InvalidThresholdException;
import freenet.support.api.Bucket;
import freenet.support.api.BucketFactory;
+import freenet.support.io.ArrayBucketFactory;
+import freenet.support.io.BucketTools;
import freenet.support.io.Closer;
+import freenet.support.io.FileBucket;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
/**
* Content filter for PNG's.
* This one just verifies that a PNG is valid, and throws if it isn't.
+ *
+ * It can strip the timestamp and "text(.)*" chunks if asked to
+ *
+ * FIXME: should be a whitelisting filter instead of a blacklisting one
*/
public class PNGFilter implements ContentDataFilter {
- static final byte[] pngHeader =
- { (byte)137, (byte)80, (byte)78, (byte)71, (byte)13, (byte)10,
(byte)26, (byte)10 };
-
+ private final boolean deleteText;
+ private final boolean deleteTimestamp;
+ private final boolean checkCRCs; // need a fair amount of memory
+ static final byte[] pngHeader =
+ {(byte) 137, (byte) 80, (byte) 78, (byte) 71, (byte) 13, (byte)
10, (byte) 26, (byte) 10};
+
+ PNGFilter(boolean deleteText, boolean deleteTimestamp, boolean
checkCRCs) {
+ this.deleteText = deleteText;
+ this.deleteTimestamp = deleteTimestamp;
+ this.checkCRCs = checkCRCs;
+ }
+
public Bucket readFilter(Bucket data, BucketFactory bf, String charset,
- HashMap otherParams, FilterCallback cb) throws
DataFilterException,
- IOException {
+ HashMap otherParams, FilterCallback cb) throws
DataFilterException,
+ IOException {
+ Bucket output = readFilter(data, bf, charset, otherParams, cb,
deleteText, deleteTimestamp, checkCRCs, null);
+ if(output != null)
+ return output;
+ if(Logger.shouldLog(Logger.MINOR, this))
+ Logger.minor(this, "Need to modify PNG...");
+ Bucket filtered = bf.makeBucket(data.size());
+ OutputStream os = new
BufferedOutputStream(filtered.getOutputStream());
+ try {
+ readFilter(data, bf, charset, otherParams, cb,
deleteText, deleteTimestamp, checkCRCs, os);
+ os.flush();
+ os.close();
+ } finally {
+ Closer.close(os);
+ }
+ return filtered;
+ }
+
+ public Bucket readFilter(Bucket data, BucketFactory bf, String charset,
+ HashMap otherParams, FilterCallback cb, boolean deleteText,
boolean deleteTimestamp, boolean checkCRCs, OutputStream output) throws
DataFilterException,
+ IOException {
+ boolean logMINOR = Logger.shouldLog(Logger.MINOR, this);
+ boolean logDEBUG = Logger.shouldLog(Logger.DEBUG, this);
InputStream is = null;
BufferedInputStream bis = null;
DataInputStream dis = null;
@@ -43,26 +93,170 @@
String message = l10n("invalidHeader");
String title = l10n("invalidHeaderTitle");
throw new DataFilterException(title, title,
- "<p>"+message+"</p>", new
HTMLNode("p").addChild("#", message));
+ "<p>" + message + "</p>", new
HTMLNode("p").addChild("#", message));
}
+
+ ByteArrayOutputStream baos = null;
+ DataOutputStream dos = null;
+ if(output != null) {
+ baos = new ByteArrayOutputStream();
+ dos = new DataOutputStream(baos);
+ output.write(pngHeader);
+ if(logMINOR)
+ Logger.minor(this, "Writing the PNG
header to the output bucket");
+ }
+
+ // Check the chunks :
+ // We should have an IHDR chunk at the beginning and a
IEND at the end
+ boolean finished = false;
+ boolean hasSeenIHDR = false;
+ boolean hasSeenIEND = false;
+
+ while(!finished) {
+ boolean skip = false;
+ if(baos != null)
+ baos.reset();
+ String chunkTypeString = null;
+ // Length of the chunk
+ byte[] lengthBytes = new byte[4];
+ if(dis.read(lengthBytes) < 4)
+ throw new IOException("The length of
the chunk is invalid!");
+
+ int length = ((lengthBytes[0] & 0xff) << 24) +
((lengthBytes[1] & 0xff) << 16) + ((lengthBytes[2] & 0xff) << 8) +
(lengthBytes[3] & 0xff);
+ if(logMINOR)
+ Logger.minor(this, "length " + length);
+ if(dos != null)
+ dos.write(lengthBytes);
+
+ // Type of the chunk : Should match [a-zA-Z]{4}
+ if(dis.read(lengthBytes) < 4)
+ throw new IOException("The name of the
chunk is invalid!");
+ StringBuffer sb = new StringBuffer();
+ byte[] chunkTypeBytes = new byte[4];
+ for(int i = 0; i < 4; i++) {
+ char val = (char) lengthBytes[i];
+ if((val >= 65 && val <= 99) || (val >=
97 && val <= 122)) {
+ chunkTypeBytes[i] =
lengthBytes[i];
+ sb.append(val);
+ } else
+ throw new IOException("The name
of the chunk is invalid!");
+ }
+ if(dos != null)
+ dos.write(chunkTypeBytes);
+ chunkTypeString = sb.toString();
+ if(logMINOR)
+ Logger.minor(this, "name " +
chunkTypeString);
+
+ // Content of the chunk
+ byte[] chunkData = new byte[length];
+ int readLength = dis.read(chunkData, 0, length);
+ if(readLength < length)
+ throw new IOException("The data in the
chunk '" + chunkTypeString + "' is " + readLength + " but should be " + length);
+ if(logMINOR)
+ if(logDEBUG)
+ Logger.minor(this, "data " +
(chunkData.length == 0 ? "null" : HexUtil.bytesToHex(chunkData)));
+ else
+ Logger.minor(this, "data " +
chunkData.length);
+ if(dos != null)
+ dos.write(chunkData);
+
+ // CRC of the chunk
+ if(dis.read(lengthBytes) < 4)
+ throw new IOException("The length of
the CRC is invalid!");
+ if(dos != null)
+ dos.write(lengthBytes);
+
+ if(checkCRCs) {
+ long readCRC = ((lengthBytes[0] & 0xff)
<< 24) + ((lengthBytes[1] & 0xff) << 16) + ((lengthBytes[2] & 0xff) << 8) +
(lengthBytes[3] & 0xff);
+ byte[] toCheck = new
byte[chunkTypeBytes.length + chunkData.length];
+ System.arraycopy(toCheck, 0,
chunkTypeBytes, 0, chunkTypeBytes.length);
+ System.arraycopy(toCheck, 0, chunkData,
0, chunkData.length);
+ long computedCRC = CRC.crc(toCheck);
+
+ if(readCRC != computedCRC) {
+ skip = true;
+ if(logMINOR)
+ Logger.minor(this, "CRC
of the chunk " + chunkTypeString + " doesn't match (" +
Long.toHexString(readCRC) + " but should be " + Long.toHexString(computedCRC) +
")!");
+ }
+ }
+
+ if(!skip && "IHDR".equals(chunkTypeString)) {
+ if(hasSeenIHDR)
+ throw new IOException("Two IHDR
chunks detected!!");
+ hasSeenIHDR = true;
+ }
+
+ if(!hasSeenIHDR)
+ throw new IOException("No IHDR chunk!");
+
+ if(!skip && "IEND".equals(chunkTypeString)) {
+ if(hasSeenIEND)
+ throw new IOException("Two IEND
chunks detected!!");
+ hasSeenIEND = true;
+ }
+
+ if(dis.available() < 1) {
+ if(!(hasSeenIEND && hasSeenIHDR))
+ throw new IOException("Missing
IEND or IHDR!");
+ finished = true;
+ }
+
+ if(deleteText &&
"text".equalsIgnoreCase(chunkTypeString))
+ skip = true;
+ else if(deleteTimestamp &&
"time".equalsIgnoreCase(chunkTypeString))
+ skip = true;
+
+ if(skip && output == null)
+ return null;
+ else if(!skip && output != null) {
+ if(logMINOR)
+ Logger.minor(this, "Writing " +
chunkTypeString + " (" + baos.size() + ") to the output bucket");
+ baos.writeTo(output);
+ }
+ }
+
dis.close();
+ if(dos != null) {
+ output.flush();
+ output.close();
+ }
} finally {
Closer.close(dis);
Closer.close(bis);
Closer.close(is);
+ Closer.close(output);
}
return data;
}
private String l10n(String key) {
- return L10n.getString("PNGFilter."+key);
+ return L10n.getString("PNGFilter." + key);
}
public Bucket writeFilter(Bucket data, BucketFactory bf, String charset,
- HashMap otherParams, FilterCallback cb) throws
DataFilterException,
- IOException {
+ HashMap otherParams, FilterCallback cb) throws
DataFilterException,
+ IOException {
// TODO Auto-generated method stub
return null;
}
+ public static void main(String arg[]) {
+ final File fin = new File("/tmp/test.png");
+ final File fout = new File("/tmp/test2.png");
+ fout.delete();
+ final Bucket data = new FileBucket(fin, true, false, false,
false, false);
+ final Bucket out = new FileBucket(fout, false, true, false,
false, false);
+ try {
+ Logger.setupStdoutLogging(Logger.MINOR, "");
+ ContentFilter.FilterOutput output =
ContentFilter.filter(data, new ArrayBucketFactory(), "image/png", new
URI("http://127.0.0.1:8888/"), null);
+ BucketTools.copy(output.data, out);
+ } catch(IOException e) {
+ System.out.println("Bucket error?: " + e.getMessage());
+ } catch(URISyntaxException e) {
+ System.out.println("Internal error: " + e.getMessage());
+ } catch(InvalidThresholdException e) {
+ } finally {
+ data.free();
+ }
+ }
}