[freenet-cvs] r11206 - in trunk/freenet/src/freenet: clients/http/filter support/io

toad at freenetproject.org toad at freenetproject.org
Sat Dec 2 22:20:36 UTC 2006


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;




More information about the cvs mailing list