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");