/* 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.fcp; import java.io.IOException; import freenet.clients.fcp.FCPPluginConnection.SendDirection; import freenet.node.FSParseException; import freenet.node.Node; import freenet.pluginmanager.PluginManager; import freenet.pluginmanager.PluginNotFoundException; import freenet.pluginmanager.PluginTalker; import freenet.support.SimpleFieldSet; /** * This class parses the network format for a FCP message which is send from a FCP client * to a FCP server plugin.<br> * It is the inverse of {@link FCPPluginServerMessage} which produces the on-network format of * server to client messages.<br> * * There is a similar class {@link FCPPluginMessage} which serves as a container of FCP plugin * messages which are produced and consumed by the actual server and client plugin implementations. * Consider this class here as an internal representation of FCP plugin messages used solely * for parsing client-to-server messages, while the other one is the external representation used * for both server and client messages. * Also notice that interface {@link FCPPluginConnection} consumes only objects of type * {@link FCPPluginMessage}, not of this class here: As the external representation to both server * and client, it does not care whether a message is from the server or the client, and is * only interested in the external representation {@link FCPPluginMessage} of the message.<br><br> * * ATTENTION: The on-network name of this message is different: It is {@value #NAME}. The class * previously had the same name but it was decided to rename it to fix the name clash with the * aforementioned external representation class {@link FCPPluginMessage}. To stay backward * compatible, it was decided to keep the raw network message name as is.<br> * TODO: Would it technically be possible to add a second name to the on-network data so we * can get rid of the old name after a transition period?<br><br> * * @link FCPPluginConnection * FCPPluginConnection gives an overview of how plugin messaging works in general. * @link FCPPluginConnectionImpl * FCPPluginConnectionImpl gives an overview of the internal code paths which messages take. * @author saces * @author xor (xor@freenetproject.org) * * FCPPluginMessage * Identifer=me * PluginName=plugins.HelloFCP.HelloFCP * Param.Itemname1=value1 * Param.Itemname2=value2 * ... * * EndMessage * or * DataLength=datasize * Data * <datasize> bytes of data * */ public class FCPPluginClientMessage extends DataCarryingMessage { /** * On-network format name of the message. * * ATTENTION: This one is different to the class name. For an explanation, see the class-level * JavaDoc {@link FCPPluginClientMessage}. */ public static final String NAME = "FCPPluginMessage"; public static final String PARAM_PREFIX = "Param"; /** @see FCPPluginMessage#identifier */ private final String identifier; /** @see PluginManager#getPluginFCPServer(String) */ private final String pluginname; /** @see FCPPluginMessage#data */ private final long dataLength; /** @see FCPPluginMessage#params */ private final SimpleFieldSet plugparams; /** @see FCPPluginMessage#success */ private final Boolean success; /** @see FCPPluginMessage#errorCode */ private final String errorCode; /** @see FCPPluginMessage#errorMessage */ private final String errorMessage; FCPPluginClientMessage(SimpleFieldSet fs) throws MessageInvalidException { identifier = fs.get("Identifier"); if(identifier == null) throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, NAME + " must contain a Identifier field", null, false); pluginname = fs.get("PluginName"); if(pluginname == null) throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, NAME + " must contain a PluginName field", identifier, false); boolean havedata = "Data".equals(fs.getEndMarker()); String dataLengthString = fs.get("DataLength"); if(!havedata && (dataLengthString != null)) throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "A nondata message can't have a DataLength field", identifier, false); if(havedata) { if (dataLengthString == null) throw new MessageInvalidException(ProtocolErrorMessage.MISSING_FIELD, "Need DataLength on a Datamessage", identifier, false); try { dataLength = Long.parseLong(dataLengthString, 10); } catch (NumberFormatException e) { throw new MessageInvalidException(ProtocolErrorMessage.ERROR_PARSING_NUMBER, "Error parsing DataLength field: "+e.getMessage(), identifier, false); } } else { dataLength = -1; } SimpleFieldSet maybePlugparams = fs.subset(PARAM_PREFIX); // subset() will return null if the subset is empty. To make server code more robust, we // hand out an empty mock SimpleFieldSet in that case. plugparams = maybePlugparams != null ? maybePlugparams : new SimpleFieldSet(true); if(fs.get("Success") != null) { try { success = fs.getBoolean("Success"); } catch(FSParseException e) { throw new MessageInvalidException(ProtocolErrorMessage.INVALID_FIELD, "Success must be a boolean (yes, no, true or false)", identifier, false); } } else { success = null; } if(success != null && success == false) { errorCode = fs.get("ErrorCode"); errorMessage = errorCode != null ? fs.get("ErrorMessage") : null; } else { errorCode = errorMessage = null; } } @Override String getIdentifier() { return identifier; } @Override boolean isGlobal() { return false; } @Override long dataLength() { return dataLength; } @Override public SimpleFieldSet getFieldSet() { return null; } @Override public String getName() { return NAME; } protected FCPPluginMessage constructFCPPluginMessage() { return FCPPluginMessage.constructRawMessage(null, identifier, plugparams, this.bucket, success, errorCode, errorMessage); } @Override public void run(final FCPConnectionHandler handler, final Node node) throws MessageInvalidException { // There are 2 code paths for deploying plugin messages: // 1. The new interface FCPPluginConnection. This is only available if the plugin implements // the new interface FredPluginFCPMessageHandler.ServerSideFCPMessageHandler // 2. The old class PluginTalker. This is available if the plugin implements the old // interface FredPluginFCP. // We first try code path 1 by doing FCPConnectionHandler.getFCPPluginConnection(): That // function will only yield a result if the new interface is implemented. // If that fails, we try the old code path of PluginTalker, which will fail if the plugin // also does not implement the old interface and thus is no FCP server at all. // If both fail, we finally send a MessageInvalidException. FCPPluginConnection serverConnection = null; try { serverConnection = handler.getFCPPluginConnection(pluginname); } catch (PluginNotFoundException e1) { // Do not send an error yet: Allow plugins which only implement the old interface to // keep working. // TODO: Once we remove class PluginTalker, we should throw here as we do below. } if(serverConnection != null) { FCPPluginMessage message = constructFCPPluginMessage(); // Call this here instead of in the above try{} because the above // handler.getFCPPluginConnection() might also throw IOException in the future and we // don't want to mix that up with the one whose reason is that the plugin does not // support the new interface: In the case of send() throwing, it would indicate that the // plugin DOES support the new interface but was unloaded meanwhile. So we can exit the // function then, we don't have to try the old interface. try { serverConnection.send(SendDirection.ToServer, message); } catch (IOException e) { throw new MessageInvalidException(ProtocolErrorMessage.NO_SUCH_PLUGIN, pluginname + " not found or is not a FCPPlugin", identifier, false); } return; } // Now follows the legacy code PluginTalker pt; try { pt = new PluginTalker(node, handler, pluginname, identifier, handler.hasFullAccess()); } catch (PluginNotFoundException e) { throw new MessageInvalidException(ProtocolErrorMessage.NO_SUCH_PLUGIN, pluginname + " not found or is not a FCPPlugin", identifier, false); } pt.send(plugparams, this.bucket); } }