site

(djk)
2011-01-26: Use Freenet ContentFilter.

Use Freenet ContentFilter.

diff --git a/build.xml b/build.xml
--- a/build.xml
+++ b/build.xml
@@ -19,14 +19,7 @@
     </javac>
   </target>
 
-  <target name="compile" depends="compile.alien.src">
-    <mkdir dir="${classes}"/>
-    <javac srcdir="${src}" destdir="${classes}" debug="true">
-    <compilerarg line="-encoding utf8"/>
-    </javac>
-  </target>
-
-  <target name="compile.plugin.src" depends="compile">
+  <target name="check.for.freenet.jar">
     <mkdir dir="${alien.libs}"/>
     <fail message="No freenet.jar! Copy freenet.jar into: ${alien.libs}">
       <condition>
@@ -37,7 +30,19 @@
         </not>
       </condition>
     </fail>
+  </target>
 
+  <target name="compile" depends="check.for.freenet.jar, compile.alien.src">
+    <mkdir dir="${classes}"/>
+    <javac srcdir="${src}" destdir="${classes}" debug="true">
+        <compilerarg line="-encoding utf8"/>
+        <classpath>
+        <pathelement location="${alien.libs}/freenet.jar"/>
+        </classpath>
+    </javac>
+  </target>
+
+  <target name="compile.plugin.src" depends="check.for.freenet.jar, compile">
     <mkdir dir="${classes}"/>
     <javac srcdir="${plugin.src}" destdir="${classes}" debug="true">
       <compilerarg line="-encoding utf8"/>
diff --git a/plugin/src/fniki/plugin/Fniki.java b/plugin/src/fniki/plugin/Fniki.java
--- a/plugin/src/fniki/plugin/Fniki.java
+++ b/plugin/src/fniki/plugin/Fniki.java
@@ -90,9 +90,6 @@ public class Fniki implements FredPlugin
             // doesn't contain a hidden field with the freenet per boot form password.
             wikiApp.setFormPassword(pr.getNode().clientCore.formPassword);
 
-            // I couldn't get application/x-www-form-urlencoded forms to work.
-            wikiApp.setUseMultiPartForms(true);
-
             mWikiApp = wikiApp;
 
         } catch (IOException ioe) {
diff --git a/script/jfniki.sh b/script/jfniki.sh
--- a/script/jfniki.sh
+++ b/script/jfniki.sh
@@ -29,9 +29,9 @@ export set JAR_NAME="jfniki.jar"
 # If you want to move it to somewhere else, modify the line below.
 #export set JAR_PATH="${0%%/*}/../build/jar"
 export set SCRIPT_DIR=`dirname $0`
+
 export set JAR_PATH="${SCRIPT_DIR}/../build/jar"
 export set JAR_FILE="${JAR_PATH}/${JAR_NAME}"
-
 if [ ! -f ${JAR_FILE} ];
 then
     export set JAR_PATH=${SCRIPT_DIR}
@@ -49,7 +49,30 @@ then
     fi
 fi
 
-echo "Using jar file: ${JAR_FILE}"
+echo "Using fniki.jar: ${JAR_FILE}"
+echo
+
+export set FN_JAR_NAME="freenet.jar"
+export set FN_JAR_PATH="${SCRIPT_DIR}/../alien/libs"
+export set FN_JAR_FILE="${FN_JAR_PATH}/${FN_JAR_NAME}"
+if [ ! -f ${FN_JAR_FILE} ];
+then
+    export set FN_JAR_PATH=${SCRIPT_DIR}
+    export set FN_JAR_FILE="${FN_JAR_PATH}/${FN_JAR_NAME}"
+    if [ ! -f ${FN_JAR_FILE} ];
+    then
+        echo "Looked in:"
+        echo "${SCRIPT_DIR}/../alien/libs/${FN_JAR_NAME}"
+        echo "and"
+        echo "${FN_JAR_FILE}"
+        echo
+        echo "but still can't find the freenet.jar file!"
+        echo "Not sure what's going on. :-("
+        exit -1
+    fi
+fi
+
+echo "Using freenet.jar: ${FN_JAR_FILE}"
 echo
 
 # FCP configuration
@@ -68,7 +91,7 @@ export set FPROXY_PREFIX="http://127.0.0
 
 export set JAVA_CMD="java"
 
-${JAVA_CMD} -jar ${JAR_FILE} \
+${JAVA_CMD} -classpath ${JAR_FILE}:${FN_JAR_FILE} fniki.standalone.ServeHttp \
     ${LISTEN_PORT} \
     ${FCP_HOST} \
     ${FCP_PORT} \
diff --git a/src/fniki/freenet/filter/ContentFilterFactory.java b/src/fniki/freenet/filter/ContentFilterFactory.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/freenet/filter/ContentFilterFactory.java
@@ -0,0 +1,34 @@
+/* Factory to make ContentFilter instances.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+
+package fniki.freenet.filter;
+
+import fniki.wiki.ContentFilter;
+
+// Keep Freenet gook out of my code.
+public class ContentFilterFactory {
+    public static ContentFilter create(String containerPrefix, String fproxyPrefix) {
+        return new WikiContentFilter(containerPrefix, fproxyPrefix);
+    }
+}
diff --git a/src/fniki/freenet/filter/WikiContentFilter.java b/src/fniki/freenet/filter/WikiContentFilter.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/freenet/filter/WikiContentFilter.java
@@ -0,0 +1,149 @@
+/* ContentFilter implemented using freenet.client.filter.ContentFilter.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+package fniki.freenet.filter;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import freenet.client.filter.FilterCallback;
+import freenet.client.filter.CommentException;
+import freenet.client.filter.UnsafeContentTypeException;
+import freenet.client.filter.HTMLFilter.ParsedTag;
+
+import fniki.wiki.ContentFilter;
+import fniki.wiki.ServerErrorException;
+
+class WikiContentFilter implements ContentFilter, FilterCallback  {
+    private String mContainerPrefix;
+    private String mFproxyPrefix;
+    private final static String UTF8 = "UTF-8";
+    private static class FilterTrippedException extends RuntimeException {
+        FilterTrippedException() { super("Freenet content filter tripped!"); }
+    }
+
+    private static void filterTripped() {
+        throw new FilterTrippedException();
+    }
+
+    protected WikiContentFilter(String containerPrefix, String fproxyPrefix) {
+        mContainerPrefix = containerPrefix;
+        mFproxyPrefix = fproxyPrefix;
+    }
+
+    /**
+     * Process a URI.
+     * If it cannot be turned into something sufficiently safe, then return null.
+     * @param overrideType Force the return type.
+     * @throws CommentException If the URI is nvalid or unacceptable in some way.
+     */
+    public String processURI(String uri, String overrideType) throws CommentException {
+        System.err.println("processURI(0): " + uri + " : " + overrideType);
+        if (!(uri.startsWith(mContainerPrefix) || uri.startsWith(mFproxyPrefix))) {
+            System.err.println("processURI(0): REJECTED URI");
+            filterTripped();
+            return null;
+        }
+
+        return uri;
+    }
+
+    /**
+     * Process a URI.
+     * If it cannot be turned into something sufficiently safe, then return null.
+     * @param overrideType Force the return type.
+     * @throws CommentException If the URI is nvalid or unacceptable in some way.
+     */
+    public String processURI(String uri, String overrideType, boolean noRelative, boolean inline) throws CommentException {
+        // DCI: understand these parameters!
+        System.err.println("processURI(1): " + uri + " : " + overrideType + " : " + noRelative + " : " + inline);
+        return processURI(uri, overrideType);
+    }
+
+    /**
+     * Process a base URI in the page. Not only is this filtered, it affects all
+     * relative uri's on the page.
+     */
+    public String onBaseHref(String baseHref) {
+        System.err.println("processBaseRef: " + baseHref);
+        filterTripped();
+        return null;
+    }
+    /**
+     * Process plain-text. Notification only; can't modify.
+     * Type can be null, or can correspond, for example to HTML tag name around text
+     * (for example: "title").
+     *
+     * Note that the string will have been fed through the relevant decoder if
+     * necessary (e.g. HTMLDecoder). It must be re-encoded if it is sent out as
+     * text to a browser.
+     */
+    public void onText(String s, String type) {}
+
+    /**
+     * Process a form on the page.
+     * @param method The form sending method. Normally GET or POST.
+     * @param action The URI to send the form to.
+     * @return The new action URI, or null if the form is not allowed.
+     * @throws CommentException
+     */
+    public String processForm(String method, String action) throws CommentException {
+        if (!(action.startsWith(mContainerPrefix) || action.startsWith(mFproxyPrefix))) {
+            System.err.println("processForm: REJECTED URI");
+            filterTripped();
+            return null;
+        }
+        return action;
+    }
+    /**
+     * Process a tag. If it needs changing, then return the changed
+     * HTML, if not, then return null;
+     * @param pt - The tag to be replaced
+     * @return The new tag, or null, if it doesn't need changing
+     * */
+    public String processTag(ParsedTag pt) { return null; }
+    ////////////////////////////////////////////////////////////
+
+    public String filter(String html) throws ServerErrorException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        try {
+            freenet.client.filter.ContentFilter.FilterStatus status =
+                freenet.client.filter.ContentFilter.filter(new ByteArrayInputStream(html.getBytes(UTF8)),
+                                                           baos, "text/html",
+                                                           UTF8,
+                                                           this);
+            return new String(baos.toByteArray(), UTF8);
+        } catch (UnsafeContentTypeException ucte) {
+            ucte.printStackTrace();
+            throw new ServerErrorException("BUG: Generated dangerous html(0)??? But we caught it :-)");
+        } catch (FilterTrippedException fte) {
+            fte.printStackTrace();
+            throw new ServerErrorException("BUG: Generated dangerous html(1)??? But we caught it :-)");
+        } catch (IOException ioe) {
+            ioe.printStackTrace();
+            throw new ServerErrorException("BUG: IOException validating page???");
+        }
+    }
+}
diff --git a/src/fniki/standalone/FnikiContextHandler.java b/src/fniki/standalone/FnikiContextHandler.java
--- a/src/fniki/standalone/FnikiContextHandler.java
+++ b/src/fniki/standalone/FnikiContextHandler.java
@@ -25,6 +25,7 @@
 package fniki.standalone;
 
 import java.io.IOException;
+import java.io.ByteArrayOutputStream;
 import net.freeutils.httpserver.HTTPServer;
 
 import fniki.wiki.Query;
@@ -43,13 +44,53 @@ public class FnikiContextHandler impleme
 
     private static class WikiQuery implements Query {
         private final HTTPServer.Request mParent;
+        private final String mSaveText;
+        private final String mSavePage;
 
-        WikiQuery(HTTPServer.Request parent) {
-            mParent = parent;
+        // Hmmmm... can't figure out any other way to know when part is done.
+        private final String readAsUtf8(HTTPServer.MultipartIterator.Part part) throws IOException {
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+            while (part.body.available() > 0) { // Do better? Does it matter?
+                int oneByte = part.body.read();
+                if (oneByte == -1) {
+                    throw new IOException("Unexpected EOF???");
+                }
+                baos.write(oneByte);
+            }
+
+            return new String(baos.toByteArray(), "utf8");
         }
 
+
+        WikiQuery(HTTPServer.Request parent) throws IOException {
+            mParent = parent;
+
+            String saveText = null;
+            String savePage = null;
+            if (parent.getHeaders().getParams("Content-Type").
+                containsKey("multipart/form-data")) {
+                HTTPServer.MultipartIterator iter = new HTTPServer.MultipartIterator(parent);
+                while (iter.hasNext()) {
+                    HTTPServer.MultipartIterator.Part part = iter.next();
+                    if (part.name.equals("savetext")) {
+                        saveText = readAsUtf8(part);
+                    } else if (part.name.equals("savepage")) {
+                        savePage = readAsUtf8(part);
+                    }
+                }
+                parent.consumeBody();
+            }
+            mSaveText = saveText;
+            mSavePage = savePage;
+        }
         public boolean containsKey(String paramName) {
             try {
+                if (paramName.equals("savetext")) {
+                    return mSaveText != null;
+                } else if (paramName.equals("savepage")) {
+                    return mSavePage != null;
+                }
                 return mParent.getParams().containsKey(paramName);
             } catch (IOException ioe) {
                 return false;
@@ -58,6 +99,11 @@ public class FnikiContextHandler impleme
 
         public String get(String paramName) {
             try {
+                if (paramName.equals("savetext")) {
+                    return mSaveText;
+                } else if (paramName.equals("savepage")) {
+                    return mSavePage;
+                }
                 return mParent.getParams().get(paramName);
             } catch (IOException ioe) {
                 return null;
diff --git a/src/fniki/wiki/ContentFilter.java b/src/fniki/wiki/ContentFilter.java
new file mode 100644
--- /dev/null
+++ b/src/fniki/wiki/ContentFilter.java
@@ -0,0 +1,29 @@
+/* Interface to check generated pages for anonymity leaks.
+ *
+ * Copyright (C) 2010, 2011 Darrell Karbott
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.0 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: djk@isFiaD04zgAgnrEC5XJt1i4IE7AkNPqhBG5bONi6Yks
+ *
+ *  This file was developed as component of
+ * "fniki" (a wiki implementation running over Freenet).
+ */
+package fniki.wiki;
+
+// Thin wrapper around the Freenet content filter.
+public interface ContentFilter {
+    String filter(String html) throws ServerErrorException;
+}
diff --git a/src/fniki/wiki/WikiApp.java b/src/fniki/wiki/WikiApp.java
--- a/src/fniki/wiki/WikiApp.java
+++ b/src/fniki/wiki/WikiApp.java
@@ -47,6 +47,8 @@ import fniki.wiki.child.QueryError;
 import fniki.wiki.child.Submitting;
 import fniki.wiki.child.WikiContainer;
 
+import fniki.freenet.filter.ContentFilterFactory;
+
 // Aggregates a bunch of other Containers and runs UI state machine.
 public class WikiApp implements ChildContainer, WikiContext {
     // Delegate to implement link, image and macro handling in wikitext.
@@ -71,10 +73,15 @@ public class WikiApp implements ChildCon
 
     private ArchiveManager mArchiveManager;
 
+    private ContentFilter mFilter;
     private String mFproxyPrefix = "http://127.0.0.1:8888/";
     private boolean mAllowImages = true;
     private String mFormPassword;
-    private boolean mUseMultiPartForms;
+
+    // final because it is called from the ctor.
+    private final void resetContentFilter() {
+        mFilter = ContentFilterFactory.create(mFproxyPrefix, containerPrefix());
+    }
 
     public WikiApp(ArchiveManager archiveManager) {
         mParserDelegate = new LocalParserDelegate(this, archiveManager);
@@ -91,6 +98,8 @@ public class WikiApp implements ChildCon
 
         mState = mWikiContainer;
         mArchiveManager = archiveManager;
+
+        resetContentFilter();
     }
 
     public void setFproxyPrefix(String value) {
@@ -98,6 +107,7 @@ public class WikiApp implements ChildCon
             throw new IllegalArgumentException("Expected a value starting with 'http' and ending with '/'");
         }
         mFproxyPrefix = value;
+        resetContentFilter();
     }
 
     public void setAllowImages(boolean value) {
@@ -108,10 +118,6 @@ public class WikiApp implements ChildCon
         mFormPassword = value;
     }
 
-    public void setUseMultiPartForms(boolean value) {
-        mUseMultiPartForms = value;
-    }
-
     private ChildContainer setState(WikiContext context, ChildContainer container) {
         if (mState == container) {
             return mState;
@@ -267,7 +273,8 @@ public class WikiApp implements ChildCon
         try {
             ChildContainer childContainer = routeRequest(context);
             System.err.println("Request routed to: " + childContainer.getClass().getName());
-            return childContainer.handle(context);
+
+            return mFilter.filter(childContainer.handle(context));
         } catch (ChildContainerException cce) {
             // Normal, used to do redirection.
             throw cce;
@@ -399,11 +406,6 @@ public class WikiApp implements ChildCon
             return containerPrefix();
         } else if (keyName.equals("form_password") && mFormPassword != null) {
             return mFormPassword;
-        } else if (keyName.equals("form_encoding")) {
-            if (mUseMultiPartForms) {
-                return "multipart/form-data";
-            }
-            return "application/x-www-form-urlencoded";
         }
 
         return defaultValue;
diff --git a/src/fniki/wiki/child/WikiContainer.java b/src/fniki/wiki/child/WikiContainer.java
--- a/src/fniki/wiki/child/WikiContainer.java
+++ b/src/fniki/wiki/child/WikiContainer.java
@@ -112,16 +112,20 @@ public class WikiContainer implements Ch
 
     private String handleSave(WikiContext context, Query form) throws ChildContainerException, IOException {
         // Name is included in the query data.
+        System.err.println("handleSave -- ENTERED");
         String name = form.get("savepage");
         String wikiText = form.get("savetext");
 
+        System.err.println("handleSave --got params");
         if (name == null || wikiText == null) {
             context.raiseAccessDenied("Couldn't parse parameters from POST.");
         }
 
         System.err.println("Writing: " + name);
         context.getStorage().putPage(name, wikiText);
+        System.err.println("Raising redirect!");
         context.raiseRedirect(context.makeLink("/" + name), "Redirecting...");
+        System.err.println("SOMETHING WENT WRONG!");
         return "unreachable code";
     }
 
@@ -210,7 +214,13 @@ public class WikiContainer implements Ch
                       "\" enctype=\"");
 
         // IMPORTANT: Only multipart/form-data encoding works in plugins.
-        buffer.append(context.getString("form_encoding", "application/x-www-form-urlencoded"));
+        // IMPORTANT: Must be multipart/form-date even for standalone because
+        //            the Freenet ContentFilter rewrites the encoding in all forms
+        //            to this value.
+        buffer.append("multipart/form-data");
+
+        System.err.println("Sending form encoding: " + context.getString("form_encoding", "application/x-www-form-urlencoded"));
+
 
         buffer.append("\" accept-charset=\"UTF-8\">\n");
 
@@ -229,6 +239,7 @@ public class WikiContainer implements Ch
         buffer.append("</textarea>\n");
         buffer.append("<br><input type=submit value=\"Save\">\n");
         buffer.append("<input type=hidden name=formPassword value=\"");
+
         // IMPORTANT: Required by Freenet Plugin.
         buffer.append(context.getString("form_password", "FORM_PASSWORD_NOT_SET")); // DCI: % encode?
         buffer.append("\"/>\n");