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


Reply via email to