Author: toad
Date: 2006-12-02 22:20:34 +0000 (Sat, 02 Dec 2006)
New Revision: 11206

Added:
   trunk/freenet/src/freenet/clients/http/filter/JPEGFilter.java
Modified:
   trunk/freenet/src/freenet/clients/http/filter/ContentFilter.java
   trunk/freenet/src/freenet/clients/http/filter/DataFilterException.java
   trunk/freenet/src/freenet/clients/http/filter/GIFFilter.java
   trunk/freenet/src/freenet/clients/http/filter/PNGFilter.java
   trunk/freenet/src/freenet/support/io/CountedInputStream.java
Log:
Enable image filters (doh).
Add a detailed JPEG filter. (I got carried away...) This strips EXIF and 
comments, and is pretty thorough (although it doesn't parse *everything* e.g. 
the scan).

Modified: trunk/freenet/src/freenet/clients/http/filter/ContentFilter.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/filter/ContentFilter.java    
2006-12-02 21:29:00 UTC (rev 11205)
+++ trunk/freenet/src/freenet/clients/http/filter/ContentFilter.java    
2006-12-02 22:20:34 UTC (rev 11206)
@@ -37,20 +37,20 @@
                                "Plain text - not dangerous unless you include 
compromizing information",
                                true, "US-ASCII", null));

-               // GIF - probably safe - FIXME check this out, write filters 
+               // GIF - has a filter 
                register(new MIMEType("image/gif", "gif", new String[0], new 
String[0], 
                                true, false, new GIFFilter(), null, false, 
false, false, false, false, false,
                                "GIF image - probably not dangerous",
                                "GIF image - probably not dangerous but you 
should wipe any comments",
                                false, null, null));

-               // JPEG - probably safe - FIXME check this out, write filters
+               // JPEG - has a filter
                register(new MIMEType("image/jpeg", "jpeg", new String[0], new 
String[] { "jpg" },
-                               true, false, null, null, false, false, false, 
false, false, false,
+                               true, false, new JPEGFilter(true, true), null, 
false, false, false, false, false, false,
                                "JPEG image - probably not dangerous",
                                "JPEG image - probably not dangerous but can 
contain EXIF data", false, null, null));

-               // PNG - probably safe - FIXME check this out, write filters
+               // 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,
                                "PNG image - probably not dangerous",
@@ -157,10 +157,7 @@
                if(handler == null)
                        throw new UnknownContentTypeException(typeName);
                else {
-                       if(handler.safeToRead) {
-                               return new FilterOutput(data, typeName);
-                       }
-                       
+                       // Run the read filter if there is one.
                        if(handler.readFilter != null) {
                                if(handler.takesACharset && ((charset == null) 
|| (charset.length() == 0))) {
                                        charset = detectCharset(data, handler);
@@ -171,6 +168,11 @@
                                        type = type + "; charset="+charset;
                                return new FilterOutput(outputData, type);
                        }
+                       
+                       if(handler.safeToRead) {
+                               return new FilterOutput(data, typeName);
+                       }
+                       
                        handler.throwUnsafeContentTypeException();
                        return null;
                }

Modified: trunk/freenet/src/freenet/clients/http/filter/DataFilterException.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/filter/DataFilterException.java      
2006-12-02 21:29:00 UTC (rev 11205)
+++ trunk/freenet/src/freenet/clients/http/filter/DataFilterException.java      
2006-12-02 22:20:34 UTC (rev 11206)
@@ -38,5 +38,9 @@
        public String getRawTitle() {
                return rawTitle;
        }
+       
+       public String toString() {
+               return rawTitle;
+       }

 }

Modified: trunk/freenet/src/freenet/clients/http/filter/GIFFilter.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/filter/GIFFilter.java        
2006-12-02 21:29:00 UTC (rev 11205)
+++ trunk/freenet/src/freenet/clients/http/filter/GIFFilter.java        
2006-12-02 22:20:34 UTC (rev 11206)
@@ -21,8 +21,8 @@
 public class GIFFilter implements ContentDataFilter {

        static final String ERROR_MESSAGE = 
-               "The file you tried to fetch is not a GIF. It does not include 
a valid GIF header. "+
-               "It might be some other file format, and your browser may do 
something horrible with it, "+
+               "The file you tried to fetch is not a GIF. "+
+               "It might be some other file format, and your browser may do 
something dangerous with it, "+
                "therefore we have blocked it.";

        static final int HEADER_SIZE = 6;

Added: trunk/freenet/src/freenet/clients/http/filter/JPEGFilter.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/filter/JPEGFilter.java       
2006-12-02 21:29:00 UTC (rev 11205)
+++ trunk/freenet/src/freenet/clients/http/filter/JPEGFilter.java       
2006-12-02 22:20:34 UTC (rev 11206)
@@ -0,0 +1,369 @@
+/* This code is part of Freenet. It is distributed under the GNU General
+ * Public License, version 2 (or at your option any later version). See
+ * http://www.gnu.org/ for further details of the GPL. */
+package freenet.clients.http.filter;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.HashMap;
+
+import freenet.support.HTMLNode;
+import freenet.support.Logger;
+import freenet.support.io.Bucket;
+import freenet.support.io.BucketFactory;
+import freenet.support.io.CountedInputStream;
+
+/**
+ * Content filter for JPEG's.
+ * Just check the header.
+ * 
+ * http://www.obrador.com/essentialjpeg/headerinfo.htm
+ * Also the JFIF spec.
+ * Also http://cs.haifa.ac.il/~nimrod/Compression/JPEG/J6sntx2005.pdf
+ */
+public class JPEGFilter implements ContentDataFilter {
+
+       private final boolean deleteComments;
+       private final boolean deleteExif;
+       
+       JPEGFilter(boolean deleteComments, boolean deleteExif) {
+               this.deleteComments = deleteComments;
+               this.deleteExif = deleteExif;
+       }
+       
+       static final String ERROR_MESSAGE = 
+               "The file you tried to fetch is not a JPEG. "+
+               "It might be some other file format, and your browser may do 
something dangerous with it, "+
+               "therefore we have blocked it.";
+       
+       static final byte[] soi = new byte[] {
+               (byte)0xFF, (byte)0xD8 // Start of Image
+       };
+       static final byte[] app0 = new byte[] {
+               (byte)0xFF, (byte)0xE0 // APP0 (header)
+       };
+       static final byte[] identifier = new byte[] {
+               (byte)'J', (byte)'F', (byte)'I', (byte)'F', 0
+       };
+       static final byte[] extensionIdentifier = new byte[] {
+               (byte)'J', (byte)'F', (byte)'X', (byte)'X', 0
+       };
+       
+       public Bucket readFilter(Bucket data, BucketFactory bf, String charset,
+                       HashMap otherParams, FilterCallback cb) 
+       throws DataFilterException, IOException {
+               Bucket output = readFilter(data, bf, charset, otherParams, cb, 
deleteComments, deleteExif, null);
+               if(output != null)
+                       return output;
+               if(Logger.shouldLog(Logger.MINOR, this))
+                       Logger.minor(this, "Need to modify JPEG...");
+               Bucket filtered = bf.makeBucket(data.size());
+               return readFilter(data, bf, charset, otherParams, cb, 
deleteComments, deleteExif, new 
BufferedOutputStream(filtered.getOutputStream()));
+       }
+       
+       public Bucket readFilter(Bucket data, BucketFactory bf, String charset,
+                       HashMap otherParams, FilterCallback cb, boolean 
deleteComments, boolean deleteExif, OutputStream output) 
+       throws DataFilterException, IOException {
+               boolean logMINOR = Logger.shouldLog(Logger.MINOR, this);
+               long length = data.size();
+               boolean hadHeader = false;
+               if(length < 6) {
+                       throwError("Too short", "The file is too short to be a 
GIF.");
+               }
+               InputStream is = data.getInputStream();
+               BufferedInputStream bis = new BufferedInputStream(is);
+               CountedInputStream cis = new CountedInputStream(bis);
+               DataInputStream dis = new DataInputStream(cis);
+               try {
+                       assertHeader(dis, soi);
+                       if(output != null) output.write(soi);
+                       
+                       ByteArrayOutputStream baos = null;
+                       DataOutputStream dos = null;
+                       if(output != null) {
+                               baos = new ByteArrayOutputStream();
+                               dos = new DataOutputStream(baos);
+                       }
+                       
+                       // Check the chunks.
+                       
+                       boolean finished = false;
+                       int forceMarkerType = -1;
+                       while(!finished) {
+                               if(baos != null)
+                                       baos.reset();
+                               int markerType;
+                               if(forceMarkerType != -1) {
+                                       markerType = forceMarkerType;
+                                       forceMarkerType = -1;
+                               } else {
+                                       int markerStart = dis.read();
+                                       if(markerStart == -1) {
+                                               // No more chunks to scan.
+                                               break;
+                                       }
+                                       if(markerStart != 0xFF) {
+                                               throwError("Invalid marker", 
"The file includes an invalid marker "+Integer.toHexString(markerStart)+" and 
cannot be parsed further.");
+                                       }
+                                       if(baos != null) baos.write(0xFF);
+                                       markerType = dis.readUnsignedByte();
+                                       if(baos != null) baos.write(markerType);
+                               }
+                               long countAtStart = cis.count(); // After 
marker but before type
+                               int blockLength;
+                               if(markerType != 0xD9)
+                                       blockLength = dis.readUnsignedShort();
+                               else
+                                       blockLength = 0;
+                               if(markerType == 0xDB // quantisation table
+                                               || markerType == 0xC4 // 
huffman table
+                                               || markerType == 0xC0) { // 
start of frame
+                                       // Essential, non-terminal frames.
+                                       if(blockLength < 2)
+                                               throwError("Invalid frame 
length", "The file includes an invalid frame (length "+blockLength+").");
+                                       if(dos != null) {
+                                               byte[] buf = new 
byte[blockLength - 2];
+                                               dis.readFully(buf);
+                                               dos.write(buf);
+                                       } else
+                                               skipBytes(dis, blockLength - 2);
+                                       Logger.minor(this, "Essential frame 
type "+Integer.toHexString(markerType)+" length "+(blockLength-2)+" offset at 
end "+cis.count());
+                               } else if(markerType == 0xDA) {
+                                       // Start of scan marker
+                                       
+                                       // Copy marker
+                                       if(blockLength < 2)
+                                               throwError("Invalid frame 
length", "The file includes an invalid frame (length "+blockLength+").");
+                                       if(dos != null) {
+                                               byte[] buf = new 
byte[blockLength - 2];
+                                               dis.readFully(buf);
+                                               dos.write(buf);
+                                       } else
+                                               skipBytes(dis, blockLength - 2);
+                                       Logger.minor(this, "Copied 
start-of-frame marker length "+(blockLength-2));
+                                       
+                                       if(baos != null)
+                                               baos.writeTo(output); // will 
continue; at end
+                                       
+                                       // Now copy the scan itself
+                                       
+                                       int prevChar = -1;
+                                       while(true) {
+                                               int x = dis.read();
+                                               if(prevChar != -1 && output != 
null) {
+                                                       output.write(prevChar);
+                                               }
+                                               if(x == -1) {
+                                                       // Termination inside a 
scan; valid I suppose
+                                                       break;
+                                               }
+                                               if(prevChar == 0xFF && x != 0) {
+                                                       forceMarkerType = x;
+                                                       if(logMINOR)
+                                                               
Logger.minor(this, "Moved scan at "+cis.count()+", found a marker type 
"+Integer.toHexString(x));
+                                                       if(output != null) 
output.write(x);
+                                                       break; // End of scan, 
new marker
+                                               }
+                                               prevChar = x;
+                                       }
+                                       
+                                       continue; // Avoid writing the header 
twice
+                                       
+                               } else if(markerType == 0xE0) { // APP0
+                                       String type = 
readNullTerminatedAsciiString(dis);
+                                       if(baos != null) 
writeNullTerminatedString(baos, type);
+                                       if(type.equals("JFIF")) {
+                                               Logger.minor(this, "JFIF 
Header");
+                                               // File header
+                                               int majorVersion = 
dis.readUnsignedByte();
+                                               if(majorVersion != 1)
+                                                       throwError("Invalid 
header", "Unrecognized major version "+majorVersion+".");
+                                               if(dos != null) 
dos.write(majorVersion);
+                                               int minorVersion = 
dis.readUnsignedByte();
+                                               if(minorVersion > 2)
+                                                       throwError("Invalid 
header", "Unrecognized version 1."+minorVersion+".");
+                                               if(dos != null) 
dos.write(minorVersion);
+                                               int units = 
dis.readUnsignedByte();
+                                               if(units > 2)
+                                                       throwError("Invalid 
header", "Unrecognized units type "+units+".");
+                                               if(dos != null) {
+                                                       
dos.writeShort(dis.readShort()); // Copy Xdensity
+                                                       
dos.writeShort(dis.readShort()); // Copy Ydensity
+                                               } else {
+                                                       dis.readShort(); // 
Ignore Xdensity
+                                                       dis.readShort(); // 
Ignore Ydensity
+                                               }
+                                               int thumbX = 
dis.readUnsignedByte();
+                                               if(dos != null) 
dos.writeByte(thumbX);
+                                               int thumbY = 
dis.readUnsignedByte();
+                                               if(dos != null) 
dos.writeByte(thumbY);
+                                               int thumbLen = thumbX * thumbY 
* 3;
+                                               if(thumbLen > 
length-cis.count())
+                                                       throwError("Invalid 
header", "There should be "+thumbLen+" bytes of thumbnail but there are only 
"+(length-cis.count())+" bytes left in the file.");
+                                               if(dos != null) {
+                                                       byte[] buf = new 
byte[thumbLen];
+                                                       dis.readFully(buf);
+                                                       dos.write(buf);
+                                               } else 
+                                                       skipBytes(dis, 
thumbLen);
+                                       } else if(type.equals("JFXX")) {
+                                               // JFIF extension marker
+                                               int extensionCode = 
dis.readUnsignedByte();
+                                               if(extensionCode == 0x10 || 
extensionCode == 0x11 || extensionCode == 0x13) {
+                                                       // Alternate thumbnail, 
perfectly valid
+                                                       skipRest(blockLength, 
countAtStart, cis, dis, dos, "thumbnail frame");
+                                                       Logger.minor(this, 
"Thumbnail frame");
+                                               } else
+                                                       throwError("Unknown 
JFXX extension "+extensionCode, "The file contains an unknown JFXX extension.");
+                                       } else {
+                                               if(logMINOR)
+                                                       Logger.minor(this, 
"Dropping application-specific APP0 chunk named "+type);
+                                               // Application-specific 
extension
+                                               if(output == null) return null;
+                                               skipRest(blockLength, 
countAtStart, cis, dis, dos, "application-specific frame");
+                                               continue; // Don't write the 
frame.
+                                       }
+                               } else if(markerType == 0xE1) { // EXIF
+                                       if(output == null && deleteExif) return 
null;
+                                       if(deleteExif) {
+                                               if(logMINOR)
+                                                       Logger.minor(this, 
"Dropping EXIF data");
+                                               skipBytes(dis, blockLength - 2);
+                                               continue; // Don't write the 
frame
+                                       }
+                                       skipRest(blockLength, countAtStart, 
cis, dis, dos, "EXIF frame");
+                               } else if(markerType == 0xFE) {
+                                       // Comment
+                                       if(output == null && deleteComments) 
return null;
+                                       if(deleteComments) {
+                                               skipBytes(dis, blockLength - 2);
+                                               if(logMINOR)
+                                                       Logger.minor(this, 
"Dropping comment length "+(blockLength - 2)+'.');
+                                               continue; // Don't write the 
frame
+                                       }
+                                       skipRest(blockLength, countAtStart, 
cis, dis, dos, "comment");
+                               } else if(markerType == 0xD9) {
+                                       // End of image
+                                       if(dos != null) {
+                                               finished = true;
+                                       }
+                                       if(logMINOR)
+                                               Logger.minor(this, "End of 
image");
+                               } else {
+                                       // Delete frame
+                                       skipBytes(dis, blockLength - 2);
+                                       if(logMINOR)
+                                               Logger.minor(this, "Dropping 
unknown frame "+Integer.toHexString(markerType));
+                                       continue;
+                               }
+                               
+                               if(cis.count() != countAtStart + blockLength)
+                                       throwError("Invalid frame", "The length 
of the frame is incorrect (read "+
+                                                       
(cis.count()-countAtStart)+" bytes, frame length "+blockLength+" for type 
"+Integer.toHexString(markerType)+").");
+                               if(dos != null) {
+                                       // Write frame
+                                       baos.writeTo(output);
+                               }
+                       }
+                       
+                       // In future, maybe we will check the other chunks too.
+                       // In particular, we may want to delete, or filter, the 
comment blocks.
+                       // FIXME
+               } finally {
+                       dis.close();
+                       if(output != null) output.close();
+               }
+               return data;
+       }
+
+       private void writeNullTerminatedString(ByteArrayOutputStream baos, 
String type) throws IOException {
+               try {
+                       byte[] data = type.getBytes("ISO-8859-1"); // ascii, 
near enough
+                       baos.write(data);
+                       baos.write(0);
+               } catch (UnsupportedEncodingException e) {
+                       throw new Error(e);
+               }
+       }
+
+       private String readNullTerminatedAsciiString(DataInputStream dis) 
throws IOException {
+               StringBuffer sb = new StringBuffer();
+               while(true) {
+                       int x = dis.read();
+                       if(x == -1)
+                               throwError("Invalid extension frame", "Could 
not read an extension frame name.");
+                       if(x == 0) break;
+                       char c = (char) x; // ASCII
+                       if(x > 128 || (c < 32 && c != 10 && c != 13))
+                               throwError("Invalid extension frame name", 
"Non-ASCII character in extension frame name");
+                       sb.append(c);
+               }
+               return sb.toString();
+       }
+
+       private void skipRest(int blockLength, long countAtStart, 
CountedInputStream cis, DataInputStream dis, DataOutputStream dos, String 
thing) throws IOException {
+               // Skip the rest of the data
+               int skip = (int) (blockLength - (cis.count() - countAtStart));
+               if(skip < 0)
+                       throwError("Invalid "+thing, "The file includes an 
invalid "+thing+'.');
+               if(skip == 0) return;
+               if(dos != null) {
+                       byte[] buf = new byte[skip];
+                       dis.readFully(buf);
+                       dos.write(buf);
+               } else {
+                       skipBytes(dis, skip);
+               }
+       }
+
+       // FIXME factor this out somewhere ... an IOUtil class maybe
+       private void skipBytes(DataInputStream dis, int skip) throws 
IOException {
+               int skipped = 0;
+               while(skipped < skip) {
+                       long x = dis.skip(skip - skipped);
+                       if(x <= 0) {
+                               byte[] buf = new byte[Math.min(4096, skip - 
skipped)];
+                               dis.readFully(buf);
+                               skipped += buf.length;
+                       } else
+                               skipped += x;
+               }
+       }
+
+       private void assertHeader(DataInputStream dis, byte[] expected) throws 
IOException {
+               byte[] read = new byte[expected.length];
+               dis.read(read);
+               if(!Arrays.equals(read, expected))
+                       throwError("Invalid header", "The file does not start 
with a valid JPEG (JFIF) header.");
+       }
+
+       private void throwError(String shortReason, String reason) throws 
DataFilterException {
+               // Throw an exception
+               String message = ERROR_MESSAGE;
+               if(reason != null) message += ' ' + reason;
+               String msg = "Not a GIF";
+               if(shortReason != null)
+                       msg += " - " + shortReason;
+               DataFilterException e = new DataFilterException(shortReason, 
shortReason,
+                               "<p>"+message+"</p>", new 
HTMLNode("p").addChild("#", message));
+               if(Logger.shouldLog(Logger.NORMAL, this))
+                       Logger.normal(this, "Throwing "+e, e);
+               throw e;
+       }
+
+       public Bucket writeFilter(Bucket data, BucketFactory bf, String charset,
+                       HashMap otherParams, FilterCallback cb) throws 
DataFilterException,
+                       IOException {
+               // TODO Auto-generated method stub
+               return null;
+       }
+
+}

Modified: trunk/freenet/src/freenet/clients/http/filter/PNGFilter.java
===================================================================
--- trunk/freenet/src/freenet/clients/http/filter/PNGFilter.java        
2006-12-02 21:29:00 UTC (rev 11205)
+++ trunk/freenet/src/freenet/clients/http/filter/PNGFilter.java        
2006-12-02 22:20:34 UTC (rev 11206)
@@ -37,7 +37,7 @@
                                // Throw an exception
                                String message = 
                                        "The file you tried to fetch is not a 
PNG. It does not include a valid PNG header. "+
-                                       "It might be some other file format, 
and your browser may do something horrible with it, "+
+                                       "It might be some other file format, 
and your browser may do something dangerous with it, "+
                                        "therefore we have blocked it."; 
                                throw new DataFilterException("Not a PNG - 
invalid header", "Not a PNG - invalid header",
                                                "<p>"+message+"</p>", new 
HTMLNode("p").addChild("#", message));

Modified: trunk/freenet/src/freenet/support/io/CountedInputStream.java
===================================================================
--- trunk/freenet/src/freenet/support/io/CountedInputStream.java        
2006-12-02 21:29:00 UTC (rev 11205)
+++ trunk/freenet/src/freenet/support/io/CountedInputStream.java        
2006-12-02 22:20:34 UTC (rev 11206)
@@ -29,7 +29,7 @@
         return ret;
     }

-    public long skip(int n) throws IOException {
+    public long skip(long n) throws IOException {
        long l = in.skip(n);
        if(l > 0) count += l;
        return l;


Reply via email to