package freenet.pluginmanager;
import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.UUID;
import freenet.client.HighLevelSimpleClient;
import freenet.client.async.PersistenceDisabledException;
import freenet.client.filter.FilterCallback;
import freenet.clients.fcp.FCPPluginConnection;
import freenet.clients.fcp.FCPPluginMessage;
import freenet.clients.http.PageMaker;
import freenet.clients.http.SessionManager;
import freenet.clients.http.ToadletContainer;
import freenet.config.SubConfig;
import freenet.node.Node;
import freenet.node.NodeClientCore;
import freenet.node.RequestStarter;
import freenet.support.HTMLNode;
import freenet.support.URIPreEncoder;
import freenet.support.api.HTTPRequest;
import freenet.support.plugins.helpers1.WebInterfaceToadlet;
public class PluginRespirator {
private static final ArrayList<SessionManager> sessionManagers = new ArrayList<SessionManager>(4);
/** For accessing Freenet: simple fetches and inserts, and the data you
* need (FetchContext etc) to start more complex ones. */
private final HighLevelSimpleClient hlsc;
/** For accessing the node. */
private final Node node;
private final FredPlugin plugin;
private final PluginInfoWrapper pi;
private final PluginStores stores;
private PluginStore store;
public PluginRespirator(Node node, PluginInfoWrapper pi) {
this.node = node;
this.hlsc = node.clientCore.makeClient(RequestStarter.INTERACTIVE_PRIORITY_CLASS, false, false);
this.plugin = pi.getPlugin();
this.pi = pi;
stores = node.clientCore.getPluginStores();
}
//public HighLevelSimpleClient getHLSimpleClient() throws PluginSecurityException {
public HighLevelSimpleClient getHLSimpleClient() {
return hlsc;
}
/** Get the node. Use this if you need access to low-level stuff, node config
* etc. */
public Node getNode(){
return node;
}
/** Create a GenericReadFilterCallback, which will filter URLs in
* exactly the same way as the node does when filtering a page.
* @param path The base URI for the page being filtered. Not necessarily
* a FreenetURI.
*/
public FilterCallback makeFilterCallback(String path) {
try {
return node.clientCore.createFilterCallback(URIPreEncoder.encodeURI(path), null);
} catch (URISyntaxException e) {
throw new Error(e);
}
}
/** Get the PageMaker. */
public PageMaker getPageMaker(){
ToadletContainer container = getToadletContainer();
if(container == null) return null;
return container.getPageMaker();
}
/**
* Add a valid form including the {@link NodeClientCore#formPassword}. See the JavaDoc there for an explanation of the purpose of this mechanism.
*
* <p><b>ATTENTION</b>: It is critically important to validate the form password when processing requests which "change the server state".
* Other words for this would be requests which change your database or "write" requests.
* Requests which only read values from the server don't have to validate the form password.</p>
*
* <p>To validate that the right password was received, use {@link WebInterfaceToadlet#isFormPassword(HTTPRequest)}.</p>
*
* @param parentNode The parent HTMLNode.
* @param target Where to post to.
* @param name The id/name of the form.
* @return The form's HTMLNode. */
public HTMLNode addFormChild(HTMLNode parentNode, String target, String name) {
HTMLNode formNode =
parentNode.addChild("form", new String[] { "action", "method", "enctype", "id", "name", "accept-charset" },
new String[] { target, "post", "multipart/form-data", name, name, "utf-8"} );
formNode.addChild("input", new String[] { "type", "name", "value" },
new String[] { "hidden", "formPassword", node.clientCore.formPassword });
return formNode;
}
/**
* Get a PluginTalker so you can talk with other plugins.
*
* @deprecated Use {@link #connectToOtherPlugin(String,
* FredPluginFCPMessageHandler.ClientSideFCPMessageHandler)} instead.
*/
@Deprecated
public PluginTalker getPluginTalker(FredPluginTalker fpt, String pluginname, String identifier) throws PluginNotFoundException {
return new PluginTalker(fpt, node, pluginname, identifier);
}
/**
* <i>NOTICE: This API is a rewrite of the whole code for plugin communication. It was added
* 2015-03, and for some time after that may change in ways which break backward compatibility.
* Thus any suggestions or pull requests for improvement of all involved interfaces and classes
* are welcome!<br>
* If you would not like to deal with adapting your plugins to possible changes, use the legacy
* {@link #getPluginTalker(FredPluginTalker, String, String)} API meanwhile.</i><br>
*
* <p>Creates a FCP client connection with another plugin which is a FCP server (= which
* implements interface {@link FredPluginFCPMessageHandler.ServerSideFCPMessageHandler}).<br>
* Currently, the remote plugin must run in the same node, but the fact that FCP is used lays
* foundation for a future implementation to allow you to connect to other plugins by network,
* no matter where they are running.</p>
*
* <h1>Disconnecting properly</h1>
* <p>The formally correct mechanism of disconnecting the returned {@link FCPPluginConnection}
* is to null out the strong reference to it. The node internally keeps a {@link ReferenceQueue}
* which allows it to detect the strong reference being nulled, which in turn makes the node
* clean up its internal structures.<br>
* Thus, you are encouraged to keep the returned {@link FCPPluginConnection} in memory and use
* it for as long as you need it. Notice that keeping it in memory won't block unloading of the
* server plugin. If the server plugin is unloaded, the send-functions will fail. To get
* reconnected once the server plugin is loaded again, you must obtain a fresh client connection
* from this function: Once an existing client connection is indicated as closed by a single
* call to a send function throwing {@link IOException}, it <b>must be</b> considered as dead
* for ever, reconnecting is not possible.<br>
* While this does seem like you do not have to take care about disconnection at all, you
* <b>must</b> make sure to not keep an excessive amount of {@link FCPPluginConnection} objects
* strongly referenced to ensure that this mechanism works. Especially notice that a
* {@link FCPPluginConnection} is safe and intended to be used for multiple messages, you should
* <b>not</b> obtain a fresh one for every message you send.<br>
* Also, you <b>should</b> make sure to periodically try to send a message over the
* {@link FCPPluginConnection} and check whether you receive a reply to check whether the
* connection still is alive: There is no other mechanism of indicating a closed connection to
* you than not getting back any reply to messages you send. So if your plugin does send
* messages very infrequently, and thus might keep a reference to a dead FCPPluginConnection for
* a long time, it might be indicated to create a "keepalive-loop" which sends "ping" messages
* periodically and reconnects if no "pong" message is received within a sane timeout. Whether a
* server plugin supports a special "ping" message or requires you to use another type of
* message as ping is left up to the implementation of the server plugin.</p>
*
* <h1>Performance</h1>
* <br>While you are formally connecting via FCP, there is no actual network connection being
* created. The FCP messages are passed-through directly as Java objects. Therefore, this
* mechanism should be somewhat efficient.<br>
* Thus, plugins should communicate via FCP instead of passing objects of their own Java
* classes even if they are running within the same node because this encourages implementation
* of FCP servers, which in turn allows people to write alternative user interfaces for plugins.
* <br/>Also, this will allow future changes to the node to make it able to run each plugin
* within its own node and only connect them via real networked FCP connections. This could be
* used for load balancing of plugins across multiple machines, CPU usage monitoring, sandboxing
* and other nice stuff.</p>
*
* @param pluginName
* The name of the main class of the plugin - that is the class which implements
* {@link FredPlugin}. See {@link PluginManager#getPluginInfoByClassName(String)}.
* @param messageHandler
* An object of your plugin which implements the
* {@link FredPluginFCPMessageHandler.ClientSideFCPMessageHandler} interface. Its purpose is
* to handle FCP messages which the remote plugin sends back to your plugin.
* @return
* A {@link FCPPluginConnection} representing the client connection.<br>
* Please do read the whole JavaDoc of this function to know how to use it properly. */
public FCPPluginConnection connectToOtherPlugin(String pluginName,
FredPluginFCPMessageHandler.ClientSideFCPMessageHandler messageHandler)
throws PluginNotFoundException {
if(messageHandler == null)
throw new NullPointerException("messageHandler must not be null");
// pluginName being null will be handled by createFCPPluginConnectionForIntraNodeFCP().
return node.clientCore.getFCPServer().createFCPPluginConnectionForIntraNodeFCP(pluginName,
messageHandler);
}
/**
* Allows FCP server plugins, that is plugins which implement
* {@link FredPluginFCPMessageHandler.ServerSideFCPMessageHandler}, to obtain an existing client
* {@link FCPPluginConnection} by its {@link UUID} - if the client is still connected.<br><br>
*
* May be used by servers which cannot store objects in memory, for example because they are
* using a database: An {@link UUID} can be serialized to disk, serialization would not be
* possible for a {@link FCPPluginConnection}.<br>
* Servers are however free to instead keep the {@link FCPPluginConnection} in memory, usage
* of this function is not mandatory.<br><br>
*
* <b>Must not</b> be used by client plugins: They shall instead keep a hard reference to the
* {@link FCPPluginConnection} in memory after they have received it from
* {@link #connectToOtherPlugin(String,
* FredPluginFCPMessageHandler.ClientSideFCPMessageHandler)}. If they did not keep a hard
* reference and only stored the ID, the {@link FCPPluginConnection} would be garbage collected
* and thus considered as disconnected.<br><br>
*
* Before you use this function, you <b>should definitely</b> also read the JavaDoc of
* {@link FredPluginFCPMessageHandler.ServerSideFCPMessageHandler#handlePluginFCPMessage(
* FCPPluginConnection, FCPPluginMessage)} for full instructions on how to handle the lifecycle
* of client connections and their disconnection.
*
* @see FredPluginFCPMessageHandler.ServerSideFCPMessageHandler#handlePluginFCPMessage(
* FCPPluginConnection, FCPPluginMessage)
* The message handler at FredPluginFCPMessageHandler.ServerSideFCPMessageHandler provides
* an explanation of when to use this.
* @param connectionID
* The connection's {@link UUID} as obtained by {@link FCPPluginConnection#getID()}.
* @return
* The client connection if it is still connected.
* @throws IOException
* If there has been no client connection with the given ID or if the client has
* disconnected already.<br>
* If this happens, you should consider the connection {@link UUID} as invalid forever and
* discard it. */
public FCPPluginConnection getPluginConnectionByID(UUID connectionID) throws IOException {
return node.clientCore.getFCPServer().getPluginConnectionByID(connectionID);
}
/** Get the ToadletContainer, which manages HTTP. You can then register
* toadlets on it, which allow integrating your plugin into the main
* menus, and are a more versatile interface than FredPluginHTTP. */
public ToadletContainer getToadletContainer() {
return node.clientCore.getToadletContainer();
}
/**
* Get a PluginStore that can be used by the plugin to put data in a database.
* The database used is the node's database, so all the encrypt/decrypt part
* is already automatically handled according to the physical security level.
* @param storeIdentifier PluginStore identifier, Plugin's name or some other identifier.
* @return PluginStore
* @throws DatabaseDisabledException
*/
public PluginStore getStore() throws PersistenceDisabledException {
synchronized(this) {
if(store != null) return store;
store = stores.loadPluginStore(this.plugin.getClass().getCanonicalName());
if(store == null)
store = new PluginStore();
return store;
}
}
/**
* This should be called by the plugin to store its PluginStore in the node's
* database.
* @param store Store to put.
* @param storeIdentifier Some string to identify the store, basically the plugin's name.
* @throws DatabaseDisabledException
*/
public void putStore(final PluginStore store) throws PersistenceDisabledException {
String name = this.plugin.getClass().getCanonicalName();
try {
stores.writePluginStore(name, store);
} catch (IOException e) {
System.err.println("Unable to write plugin data for "+name+" : "+e);
return;
}
}
/**
* Get a new session manager for use with the specified path.
* See {@link SessionManager} for a detailed explanation of what cookie paths are.
*
* The usage of the global "/" path is not allowed. You must use {@link getSessionManager(String cookieNamespace)}
* if you want your cookie to be valid in the "/" path.
*
* This function is synchronized on the SessionManager-list and therefore concurrency-proof.
*
* @Deprecated We want cookies to be valid in the "/" path for menus to work even if the user is not in the menu of the given
* plugin. Therefore, we should use cookie namespaces instead.
*/
@Deprecated
public SessionManager getSessionManager(URI cookiePath) {
synchronized(sessionManagers) {
for(SessionManager m : sessionManagers) {
if(m.getCookiePath().equals(cookiePath))
return m;
}
final SessionManager m = new SessionManager(cookiePath);
sessionManagers.add(m);
return m;
}
}
/**
* Get a new session manager for use with the global "/" cookie path and the given cookie namespace.
* See {@link SessionManager} for a detailed explanation of what cookie namespaces are.
*
* This function is synchronized on the SessionManager-list and therefore concurrency-proof.
*
* @param myCookieNamespace The name of the client application which uses this cookie.
*/
public SessionManager getSessionManager(String cookieNamespace) {
synchronized(sessionManagers) {
for(SessionManager m : sessionManagers) {
if(m.getCookieNamespace().equals(cookieNamespace))
return m;
}
final SessionManager m = new SessionManager(cookieNamespace);
sessionManagers.add(m);
return m;
}
}
/**
* Get the plugin's SubConfig. If the plugin does not implement
* FredPluginConfigurable, this will return null.
*/
public SubConfig getSubConfig() {
return pi.getSubConfig();
}
/**
* Force a write of the plugin's config file. If the plugin does
* not implement FredPluginConfigurable, don't expect magic to
* happen.
*/
public void storeConfig() {
pi.getConfig().store();
}
}