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