/* 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;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URI;
import freenet.client.FetchContext;
import freenet.client.FetchException;
import freenet.client.FetchResult;
import freenet.client.FetchWaiter;
import freenet.client.HighLevelSimpleClient;
import freenet.client.InsertBlock;
import freenet.client.InsertException;
import freenet.client.async.ClientGetter;
import freenet.keys.FreenetURI;
import freenet.l10n.NodeL10n;
import freenet.node.RequestClient;
import freenet.support.HTMLEncoder;
import freenet.support.HTMLNode;
import freenet.support.Logger;
import freenet.support.MultiValueTable;
import freenet.support.api.Bucket;
import freenet.support.api.HTTPRequest;
/**
* API similar to Servlets. Originally the reason for not using servlets was to support
* continuations, but that hasn't been implemented, and modern servlets do support continuations
* anyway. Also it was supposed to be simpler, which may not be true any more. Many plugins wrap
* their own API around this!
* FIXME consider using servlets.
*
* Important API complexity: The methods for handling the actual requests are synthetic:
*
* Methods are handled via handleMethodGET/POST/whatever ( URI uri, HTTPRequest request, ToadletContext ctx )
* Typically this throws IOException and ToadletContextClosedException.
*/
public abstract class Toadlet {
/** Handle a GET request.
* Other methods are accessed via handleMethodPOST etc, invoked via reflection. But all toadlets
* are expected to support GET.
* @param uri The URI being fetched.
* @param request The original HTTPRequest, convenient for e.g. fetching ?blah=blah parameters.
* @param ctx The request context. Mainly used for sending a reply; this identifies which
* request we are replying to. Also gives access to lots of important objects e.g. PageMaker. */
public abstract void handleMethodGET(URI uri, HTTPRequest request, ToadletContext ctx) throws ToadletContextClosedException, IOException, RedirectException;
public static final String HANDLE_METHOD_PREFIX = "handleMethod";
public abstract String path();
/**
* When displaying this Toadlet, the web interface should show the menu from which it was selected as opened, and mark the
* appropriate entry as selected in the menu. This function may return the Toadlet whose menu shall be opened and whose
* entry shall be marked as selected in the menu.
*
* It is necessary to have this function instead of just marking <code>Toadlet.this</code> as selected:
* Some Toadlets won't be added to a menu. They will be only accessible through other Toadlets. For example
* a Toadlet for deleting a single download might only be accessible through the Toadlet which shows all downloads.
* For still being able to figure out the menu entry through which those so-called invisible Toadlets where accessed,
* this function is necessary.
*
* @return <code>this</code> by default. Override the function to return something else for invisible Toadlets.
*/
public Toadlet showAsToadlet() {
return this;
}
/**
* Override to return true if the toadlet should handle POSTs that don't have the correct form
* password. Otherwise they will be rejected and not passed to the toadlet.
*/
public boolean allowPOSTWithoutPassword() {
return false;
}
protected Toadlet(HighLevelSimpleClient client) {
this.client = client;
}
final HighLevelSimpleClient client;
ToadletContainer container;
private String supportedMethodsCache;
private static String l10n(String key, String pattern, String value) {
return NodeL10n.getBase().getString("Toadlet."+key, new String[] { pattern }, new String[] { value });
}
private static String l10n(String key) {
return NodeL10n.getBase().getString("Toadlet."+key);
}
/**
* Which methods are supported by this Toadlet.
* Should return a string containing the methods supported, separated by commas
* For example: "GET, PUT" (in which case both 'handleMethodGET()' and 'handleMethodPUT()'
* must be implemented).
*
* IMPORTANT: This will discover inherited methods because of getMethod()
* below. If you do not want to expose a method implemented by a parent
* class, then *OVERRIDE THIS METHOD*.
*/
public final String findSupportedMethods() {
if (supportedMethodsCache == null) {
Method methlist[] = this.getClass().getMethods();
StringBuilder sb = new StringBuilder();
for (Method m: methlist) {
String name = m.getName();
if (name.startsWith(HANDLE_METHOD_PREFIX)) {
sb.append(name.substring(HANDLE_METHOD_PREFIX.length()));
sb.append(", ");
}
}
if (sb.length() >= 2) {
// remove last ", "
sb.deleteCharAt(sb.length()-1);
sb.deleteCharAt(sb.length()-1);
}
supportedMethodsCache = sb.toString();
}
return supportedMethodsCache;
}
/**
* Client calls from the above messages to run a Freenet request.
* This method may block (or suspend).
* @param maxSize Maximum length of returned content.
* @param clientContext Client context object. This should be the same for any group of related requests, but different
* for any two unrelated requests. Request selection round-robin's over these, within any priority and retry count class,
* and above the level of individual block fetches.
*/
FetchResult fetch(FreenetURI uri, long maxSize, RequestClient clientContext, FetchContext fctx) throws FetchException {
// For now, just run it blocking.
FetchWaiter fw = new FetchWaiter(clientContext);
@SuppressWarnings("unused")
ClientGetter getter = client.fetch(uri, 1, fw, fctx);
return fw.waitForCompletion();
}
/**
* Returns a default FetchContext
* @param maxSize The maximum allowable size of the fetch's result
* @return A default FetchContext
*/
FetchContext getFetchContext(long maxSize) {
//We want to retrieve a FetchContext we may override
return client.getFetchContext(maxSize);
}
FreenetURI insert(InsertBlock insert, String filenameHint, boolean getCHKOnly) throws InsertException {
// For now, just run it blocking.
insert.desiredURI.checkInsertURI();
return client.insert(insert, getCHKOnly, filenameHint);
}
/**
* Write an HTTP response, e.g. a page, an image, an error message, with no special headers.
* @param ctx The specific request to reply to.
* @param code The HTTP reply code to use.
* @param mimeType The MIME type of the data we are returning.
* @param desc The HTTP response description for the code.
* @param data The data to write as the response body.
* @param offset The offset within data of the first byte to send.
* @param length The number of bytes of data to send as the response body.
*/
protected void writeReply(ToadletContext ctx, int code, String mimeType, String desc, byte[] data, int offset, int length) throws ToadletContextClosedException, IOException {
ctx.sendReplyHeaders(code, desc, null, mimeType, length);
ctx.writeData(data, offset, length);
}
/**
* Write an HTTP response, e.g. a page, an image, an error message, with no special headers.
* @param ctx The specific request to reply to.
* @param code The HTTP reply code to use.
* @param mimeType The MIME type of the data we are returning.
* @param desc The HTTP response description for the code.
* @param data The Bucket which contains the reply data. This
* function assumes ownership of the Bucket, calling free()
* on it when done. If this behavior is undesired, callers
* can wrap their Bucket in a NoFreeBucket.
*
* @see freenet.support.io.NoFreeBucket
*/
protected void writeReply(ToadletContext ctx, int code, String mimeType, String desc, Bucket data) throws ToadletContextClosedException, IOException {
writeReply(ctx, code, mimeType, desc, null, data);
}
/**
* Write an HTTP response, e.g. a page, an image, an error message, possibly with custom
* headers, for example, we may want to send a redirect, or a file with a specified filename.
* @param ctx The specific request to reply to.
* @param code The HTTP reply code to use.
* @param mimeType The MIME type of the data we are returning.
* @param desc The HTTP response description for the code.
* @param headers The additional HTTP headers to send.
* @param data The Bucket which contains the reply data. This
* function assumes ownership of the Bucket, calling free()
* on it when done. If this behavior is undesired, callers
* can wrap their Bucket in a NoFreeBucket.
*
* @see freenet.support.io.NoFreeBucket
*/
protected void writeReply(ToadletContext context, int code, String mimeType, String desc, MultiValueTable<String, String> headers, Bucket data) throws ToadletContextClosedException, IOException {
context.sendReplyHeaders(code, desc, headers, mimeType, data.size());
context.writeData(data);
}
/**
* Write a text-based HTTP response, e.g. a page or an error message, with no special headers.
* @param ctx The specific request to reply to.
* @param code The HTTP reply code to use.
* @param mimeType The MIME type of the data we are returning.
* @param desc The HTTP response description for the code.
* @param reply The reply data, as a String (so only use this for text-based replies, e.g.
* HTML, plain text etc).
*/
protected void writeReply(ToadletContext ctx, int code, String mimeType, String desc, String reply) throws ToadletContextClosedException, IOException {
writeReply(ctx, code, mimeType, desc, null, reply, false);
}
/**
* Write an HTTP response as HTML.
* @param ctx The specific request to reply to.
* @param code The HTTP reply code to use.
* @param desc The HTTP response description for the code.
* @param reply The HTML page, as a String.
*/
protected void writeHTMLReply(ToadletContext ctx, int code, String desc, String reply) throws ToadletContextClosedException, IOException {
writeReply(ctx, code, "text/html; charset=utf-8", desc, null, reply, false);
}
/**
* Write an HTTP response as plain text.
* @param ctx The specific request to reply to.
* @param code The HTTP reply code to use.
* @param desc The HTTP response description for the code.
* @param reply The text of the page, as a String.
*/
protected void writeTextReply(ToadletContext ctx, int code, String desc, String reply) throws ToadletContextClosedException, IOException {
writeReply(ctx, code, "text/plain; charset=utf-8", desc, null, reply, true);
}
/**
* Write an HTTP response as HTML, possibly with custom headers, for example, we may want to
* send a redirect, or a file with a specified filename.
* @param ctx The specific request to reply to.
* @param code The HTTP reply code to use.
* @param desc The HTTP response description for the code.
* @param headers The additional HTTP headers to send.
* @param reply The HTML page, as a String.
*/
protected void writeHTMLReply(ToadletContext ctx, int code, String desc, MultiValueTable<String, String> headers, String reply) throws ToadletContextClosedException, IOException {
writeHTMLReply(ctx, code, desc, headers, reply, false);
}
/**
* Write an HTTP response as HTML, possibly with custom headers, for example, we may want to
* send a redirect, or a file with a specified filename.
* @param ctx The specific request to reply to.
* @param code The HTTP reply code to use.
* @param desc The HTTP response description for the code.
* @param headers The additional HTTP headers to send.
* @param reply The HTML page, as a String.
*/
protected void writeHTMLReply(ToadletContext ctx, int code, String desc, MultiValueTable<String, String> headers, String reply, boolean forceDisableJavascript) throws ToadletContextClosedException, IOException {
writeReply(ctx, code, "text/html; charset=utf-8", desc, headers, reply, forceDisableJavascript);
}
/**
* Write an HTTP response as plain text, possibly with custom headers, for example, we may want
* to send a redirect, or a file with a specified filename.
* @param ctx The specific request to reply to.
* @param code The HTTP reply code to use.
* @param desc The HTTP response description for the code.
* @param headers The additional HTTP headers to send.
* @param reply The text of the page, as a String.
*/
protected void writeTextReply(ToadletContext ctx, int code, String desc, MultiValueTable<String, String> headers, String reply) throws ToadletContextClosedException, IOException {
writeReply(ctx, code, "text/plain; charset=utf-8", desc, headers, reply, true);
}
protected void writeReply(ToadletContext context, int code, String mimeType, String desc, MultiValueTable<String, String> headers, String reply) throws ToadletContextClosedException, IOException {
writeReply(context, code, mimeType, desc, headers, reply, false);
}
protected void writeReply(ToadletContext context, int code, String mimeType, String desc, MultiValueTable<String, String> headers, String reply, boolean forceDisableJavascript) throws ToadletContextClosedException, IOException {
byte[] buffer = reply.getBytes("UTF-8");
writeReply(context, code, mimeType, desc, headers, buffer, 0, buffer.length, forceDisableJavascript);
}
/**
* Write a generated HTTP response, e.g. a page, an image, an error message, possibly with
* custom headers, for example, we may want to send a redirect, or a file with a specified
* filename. This should not be used for fproxy content i.e. content downloaded from Freenet.
* @param context The specific request to reply to.
* @param code The HTTP reply code to use.
* @param mimeType The MIME type of the data we are returning.
* @param desc The HTTP response description for the code.
* @param headers The additional HTTP headers to send.
* @param data The data to write as the response body.
* @param offset The offset within data of the first byte to send.
* @param length The number of bytes of data to send as the response body.
*/
private void writeReply(ToadletContext context, int code, String mimeType, String desc, MultiValueTable<String, String> headers, byte[] buffer, int startIndex, int length, boolean forceDisableJavascript) throws ToadletContextClosedException, IOException {
context.sendReplyHeaders(code, desc, headers, mimeType, length, forceDisableJavascript);
context.writeData(buffer, startIndex, length);
}
/**
* Do a permanent redirect (HTTP Status 301).
*
* This will write rudimentary HTML, but typically browsers will follow
* the Location header.
* TODO Refactor with writeTemporaryRedirect.
* @param ctx
* @param msg
* @param location
* @throws ToadletContextClosedException
* @throws IOException
*/
static void writePermanentRedirect(ToadletContext ctx, String msg, String location) throws ToadletContextClosedException, IOException {
MultiValueTable<String, String> mvt = new MultiValueTable<String, String>();
mvt.put("Location", location);
if(msg == null) msg = "";
else msg = HTMLEncoder.encode(msg);
String redirDoc =
"<html><head><title>"+msg+"</title></head><body><h1>" +
l10n("permRedirectWithReason", "reason", msg)+
"</h1><a href=\""+HTMLEncoder.encode(location)+"\">"+l10n("clickHere")+"</a></body></html>";
byte[] buf;
try {
buf = redirDoc.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e);
}
ctx.sendReplyHeaders(301, "Moved Permanently", mvt, "text/html; charset=UTF-8", buf.length);
ctx.writeData(buf, 0, buf.length);
}
/**
* Do a temporary redirect (HTTP Status 302).
*
* This will write rudimentary HTML, but typically browsers will follow
* the Location header.
* TODO Refactor with writePermanentRedirect.
* @param ctx
* @param msg Message to be used in HTML (not visible in general).
* @param location New location (URL)
* @throws ToadletContextClosedException
* @throws IOException
*/
protected void writeTemporaryRedirect(ToadletContext ctx, String msg, String location) throws ToadletContextClosedException, IOException {
MultiValueTable<String, String> mvt = new MultiValueTable<String, String>();
mvt.put("Location", location);
if(msg == null) msg = "";
else msg = HTMLEncoder.encode(msg);
String redirDoc =
"<html><head><title>"+msg+"</title></head><body><h1>" +
l10n("tempRedirectWithReason", "reason", msg)+
"</h1><a href=\""+HTMLEncoder.encode(location)+"\">" +
l10n("clickHere") + "</a></body></html>";
byte[] buf;
try {
buf = redirDoc.getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
throw new Error("Impossible: JVM doesn't support UTF-8: " + e, e);
}
ctx.sendReplyHeaders(302, "Found", mvt, "text/html; charset=UTF-8", buf.length);
ctx.writeData(buf, 0, buf.length);
}
/**
* Send a simple error page.
*/
protected void sendErrorPage(ToadletContext ctx, int code, String desc, String message) throws ToadletContextClosedException, IOException {
sendErrorPage(ctx, code, desc, new HTMLNode("#", message));
}
/**
* Send a slightly more complex error page.
*/
protected void sendErrorPage(ToadletContext ctx, int code, String desc, HTMLNode message) throws ToadletContextClosedException, IOException {
PageNode page = ctx.getPageMaker().getPageNode(desc, ctx);
HTMLNode pageNode = page.outer;
HTMLNode contentNode = page.content;
HTMLNode infoboxContent = ctx.getPageMaker().getInfobox("infobox-error", desc, contentNode, null, true);
infoboxContent.addChild(message);
infoboxContent.addChild("br");
infoboxContent.addChild("a", "href", ".", l10n("returnToPrevPage"));
infoboxContent.addChild("br");
addHomepageLink(infoboxContent);
writeHTMLReply(ctx, code, desc, pageNode.generate());
}
/**
* Send an error page from an exception.
* @param ctx The context object for this request.
* @param desc The title of the error page
* @param message The message to be sent to the user. The stack trace will follow.
* @param t The Throwable which caused the error.
* @throws IOException If there is an error writing the reply.
* @throws ToadletContextClosedException If the context has already been closed.
*/
protected void sendErrorPage(ToadletContext ctx, String desc, String message, Throwable t) throws ToadletContextClosedException, IOException {
PageNode page = ctx.getPageMaker().getPageNode(desc, ctx);
HTMLNode pageNode = page.outer;
HTMLNode contentNode = page.content;
HTMLNode infoboxContent = ctx.getPageMaker().getInfobox("infobox-error", desc, contentNode, null, true);
infoboxContent.addChild("#", message);
infoboxContent.addChild("br");
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
pw.println(t);
t.printStackTrace(pw);
pw.close();
// FIXME what is the modern (CSS/XHTML) equivalent of <pre>?
infoboxContent.addChild("pre", sw.toString());
infoboxContent.addChild("br");
infoboxContent.addChild("a", "href", ".", l10n("returnToPrevPage"));
addHomepageLink(infoboxContent);
writeHTMLReply(ctx, 500, desc, pageNode.generate());
}
/**
* @throws IOException See {@link #sendErrorPage(ToadletContext, int, String, String)}
* @throws ToadletContextClosedException See {@link #sendErrorPage(ToadletContext, int, String, String)}
*/
void sendUnauthorizedPage(ToadletContext ctx) throws ToadletContextClosedException, IOException {
sendErrorPage(ctx, 403, NodeL10n.getBase().getString("Toadlet.unauthorizedTitle"), NodeL10n.getBase().getString("Toadlet.unauthorized"));
}
protected void writeInternalError(Throwable t, ToadletContext ctx) throws ToadletContextClosedException, IOException {
Logger.error(this, "Caught "+t, t);
String msg = "<html><head><title>"+NodeL10n.getBase().getString("Toadlet.internalErrorTitle")+
"</title></head><body><h1>"+NodeL10n.getBase().getString("Toadlet.internalErrorPleaseReport")+"</h1><pre>";
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
while (t != null) {
t.printStackTrace(pw);
t = t.getCause();
}
pw.flush();
msg = msg + sw.toString() + "</pre></body></html>";
writeHTMLReply(ctx, 500, "Internal Error", msg);
}
protected static void addHomepageLink(HTMLNode content) {
content.addChild("a", new String[]{"href", "title"}, new String[]{"/", l10n("homepage")}, l10n("returnToNodeHomepage"));
}
/**
* Get the client impl. DO NOT call the blocking methods on it!!
* Just use it for configuration etc.
*/
protected HighLevelSimpleClient getClientImpl() {
return client;
}
}