package com.comphenix.protocol; import com.comphenix.protocol.MultipleLinesPrompt.MultipleConversationCanceller; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.error.Report; import com.comphenix.protocol.error.ReportType; import com.comphenix.protocol.events.PacketEvent; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.conversations.Conversable; import org.bukkit.conversations.Conversation; import org.bukkit.conversations.ConversationAbandonedEvent; import org.bukkit.conversations.ConversationAbandonedListener; import org.bukkit.conversations.ConversationContext; import org.bukkit.conversations.ConversationFactory; import org.bukkit.plugin.Plugin; import javax.script.Invocable; import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; import javax.script.ScriptException; import java.util.Deque; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * A command to apply JavaScript filtering to the packet command. * * @author Kristian */ public class CommandFilter extends CommandBase { public static final ReportType REPORT_FALLBACK_ENGINE = new ReportType("Falling back to the Rhino engine."); public static final ReportType REPORT_CANNOT_LOAD_FALLBACK_ENGINE = new ReportType("Could not load Rhino either. Please upgrade your JVM or OS."); public static final ReportType REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE = new ReportType("Unable to initialize packages for JavaScript engine."); public static final ReportType REPORT_FILTER_REMOVED_FOR_ERROR = new ReportType("Removing filter %s for causing %s."); public static final ReportType REPORT_CANNOT_HANDLE_CONVERSATION = new ReportType("Cannot handle conversation."); public interface FilterFailedHandler{ /** * Invoked when a given filter has failed. * @param event - the packet event. * @param filter - the filter that failed. * @param ex - the failure. * @return TRUE to keep processing this filter, FALSE to remove it. */ boolean handle(PacketEvent event, Filter filter, Exception ex); } /** * Possible sub commands. * * @author Kristian */ private enum SubCommand { ADD, REMOVE } /** * A filter that will be used to process a packet event. * @author Kristian */ public static class Filter { private final String name; private final String predicate; private final Set packets; /** * Construct a new immutable filter. * @param name - the unique name of the filter. * @param predicate - the JavaScript predicate that will be used to filter packet events. * @param packets - a list of packet types this filter applies to. */ public Filter(String name, String predicate, Set packets) { this.name = name; this.predicate = predicate; this.packets = new HashSet<>(packets); } /** * Retrieve the unique name of the filter. * @return Unique name of the filter. */ public String getName() { return name; } /** * Retrieve the JavaScript predicate that will be used to filter packet events. * @return Predicate itself. */ public String getPredicate() { return predicate; } /** * Retrieve a copy of the set of packets this filter applies to. * @return Set of packets this filter applies to. */ public Set getRanges() { return new HashSet<>(packets); } /** * Determine whether or not a packet event needs to be passed to this filter. * @param event - the event to test. * @return TRUE if it does, FALSE otherwise. */ private boolean isApplicable(PacketEvent event) { return packets.contains(event.getPacketType()); } /** * Evaluate the current filter using the provided ScriptEngine as context. *

* This context may be modified with additional code. * @param context - the current script context. * @param event - the packet event to evaluate. * @return TRUE to pass this packet event on to the debug listeners, FALSE otherwise. * @throws ScriptException If the compilation failed or the filter is not valid. */ public boolean evaluate(ScriptEngine context, PacketEvent event) throws ScriptException { if (!isApplicable(event)) return true; // Ensure that the predicate has been compiled compile(context); try { Object result = ((Invocable) context).invokeFunction(name, event, event.getPacket().getHandle()); if (result instanceof Boolean) return (Boolean) result; else throw new ScriptException("Filter result wasn't a boolean: " + result); } catch (NoSuchMethodException e) { // Must be a fault with the script engine itself throw new IllegalStateException("Unable to compile " + name + " into current script engine.", e); } } /** * Force the compilation of a specific filter. * @param context - the current script context. * @throws ScriptException If the compilation failed. */ public void compile(ScriptEngine context) throws ScriptException { if (context.get(name) == null) { context.eval("var " + name + " = function(event, packet) {\n" + predicate); } } /** * Clean up all associated code from this filter in the provided script engine. * @param context - the current script context. */ public void close(ScriptEngine context) { context.put(name, null); } } private class CompilationSuccessCanceller implements MultipleConversationCanceller { @Override public boolean cancelBasedOnInput(ConversationContext context, String in) { throw new UnsupportedOperationException("Cannot cancel on the last line alone."); } @Override public void setConversation(Conversation conversation) { // Ignore } @Override public boolean cancelBasedOnInput(ConversationContext context, String currentLine, StringBuilder lines, int lineCount) { try { engine.eval("function(event, packet) {\n" + lines.toString()); // It compiles - accept the filter! return true; } catch (ScriptException e) { // We also have the function() line int realLineCount = lineCount + 1; // Only possible to recover from an error on the last line. return e.getLineNumber() < realLineCount; } } @Override public CompilationSuccessCanceller clone() { return new CompilationSuccessCanceller(); } } /** * Name of this command. */ public static final String NAME = "filter"; // Default error handler private FilterFailedHandler defaultFailedHandler; // Currently registered filters private final Map filters = new HashMap<>(); // Owner plugin private final Plugin plugin; // Whether the command is enabled private final ProtocolConfig config; // Script engine private ScriptEngine engine; private boolean uninitialized; public CommandFilter(ErrorReporter reporter, Plugin plugin, ProtocolConfig config) { super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 2); this.plugin = plugin; this.config = config; // Tell the filter system to initialize the script first chance it gets this.uninitialized = true; } private void initializeScript() { try { // First attempt initializeEngine(); // Oh for .. if (!isInitialized()) { throw new ScriptException("A JavaScript engine could not be found."); } else { plugin.getLogger().info("Loaded command filter engine."); } } catch (ScriptException e1) { // It's not a huge deal printPackageWarning(e1); if (!config.getScriptEngineName().equals("rhino")) { reporter.reportWarning(this, Report.newBuilder(REPORT_FALLBACK_ENGINE)); config.setScriptEngineName("rhino"); config.saveAll(); try { initializeEngine(); if (!isInitialized()) { reporter.reportWarning(this, Report.newBuilder(REPORT_CANNOT_LOAD_FALLBACK_ENGINE)); } } catch (ScriptException e2) { // And again .. printPackageWarning(e2); } } } } private void printPackageWarning(ScriptException e) { reporter.reportWarning(this, Report.newBuilder(REPORT_PACKAGES_UNSUPPORTED_IN_ENGINE).error(e)); } /** * Initialize the current configured engine. * @throws ScriptException If we are unable to import packages. */ private void initializeEngine() throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); engine = manager.getEngineByName(config.getScriptEngineName()); // Import useful packages if (engine != null) { engine.eval("importPackage(org.bukkit);"); engine.eval("importPackage(com.comphenix.protocol.reflect);"); } } /** * Determine if the filter engine has been successfully initialized. * @return TRUE if it has, FALSE otherwise. */ public boolean isInitialized() { return engine != null; } private FilterFailedHandler getDefaultErrorHandler() { // No need to create a new object every time if (defaultFailedHandler == null) { defaultFailedHandler = new FilterFailedHandler() { @Override public boolean handle(PacketEvent event, Filter filter, Exception ex) { reporter.reportMinimal(plugin, "filterEvent(PacketEvent)", ex, event); reporter.reportWarning(this, Report.newBuilder(REPORT_FILTER_REMOVED_FOR_ERROR).messageParam(filter.getName(), ex.getClass().getSimpleName()) ); return false; } }; } return defaultFailedHandler; } /** * Determine whether to pass the given packet event to the packet listeners. *

* Uses a default filter failure handler that simply prints the error message and removes the filter. * @param event - the event. * @return TRUE if we should, FALSE otherwise. */ public boolean filterEvent(PacketEvent event) { return filterEvent(event, getDefaultErrorHandler()); } /** * Determine whether to pass the given packet event to the packet listeners. * @param event - the event. * @param handler - failure handler. * @return TRUE if we should, FALSE otherwise. */ public boolean filterEvent(PacketEvent event, FilterFailedHandler handler) { for (Iterator it = filters.values().iterator(); it.hasNext(); ) { Filter filter = it.next(); try { if (!filter.evaluate(engine, event)) { return false; } } catch (Exception ex) { if (!handler.handle(event, filter, ex)) { it.remove(); } } } // Pass! return true; } /** * Initialize the script engine if necessary. */ private void checkScriptStatus() { // Start the engine if (uninitialized) { uninitialized = false; initializeScript(); } } /* * Description: Adds or removes a simple packet filter. Usage: / add|remove name [packet IDs] */ @Override protected boolean handleCommand(CommandSender sender, String[] args) { checkScriptStatus(); if (!config.isDebug()) { sender.sendMessage(ChatColor.RED + "Debug mode must be enabled in the configuration first!"); return true; } if (!isInitialized()) { sender.sendMessage(ChatColor.RED + "JavaScript engine was not present. Filter system is disabled."); return true; } final SubCommand command = parseCommand(args, 0); final String name = args[1]; final String lowerCaseName = name.toLowerCase(); switch (command) { case ADD: // Never overwrite an existing filter if(filters.containsKey(lowerCaseName)) { sender.sendMessage(ChatColor.RED + "Filter " + name + " already exists. Remove it first."); return true; } // Prepare the input to the packet type parser Deque rangeArguments = toQueue(args, 2); final PacketTypeParser parser = new PacketTypeParser(); final Set packets = parser.parseTypes(rangeArguments, PacketTypeParser.DEFAULT_MAX_RANGE); sender.sendMessage("Enter filter program ('}' to complete or CANCEL):"); // Make sure we can use the conversable interface if (sender instanceof Conversable) { final MultipleLinesPrompt prompt = new MultipleLinesPrompt(new CompilationSuccessCanceller(), "function(event, packet) {"); new ConversationFactory(plugin). withFirstPrompt(prompt). withEscapeSequence("CANCEL"). withLocalEcho(false). addConversationAbandonedListener(new ConversationAbandonedListener() { @Override public void conversationAbandoned(ConversationAbandonedEvent event) { try { final Conversable whom = event.getContext().getForWhom(); if (event.gracefulExit()) { final String predicate = prompt.removeAccumulatedInput(event.getContext()); final Filter filter = new Filter(name, predicate, packets); // Print the last line as well whom.sendRawMessage(prompt.getPromptText(event.getContext())); try { // Force early compilation filter.compile(engine); filters.put(lowerCaseName, filter); whom.sendRawMessage(ChatColor.GOLD + "Added filter " + name); } catch (ScriptException e) { e.printStackTrace(); whom.sendRawMessage(ChatColor.GOLD + "Compilation error: " + e.getMessage()); } } else { // Too bad whom.sendRawMessage(ChatColor.RED + "Cancelled filter."); } } catch (Exception e) { reporter.reportDetailed(this, Report.newBuilder(REPORT_CANNOT_HANDLE_CONVERSATION).error(e).callerParam(event) ); } } }). buildConversation((Conversable) sender). begin(); } else { sender.sendMessage(ChatColor.RED + "Only console and players are supported!"); } break; case REMOVE: final Filter filter = filters.get(lowerCaseName); // See if it exists before we remove it if (filter != null) { filter.close(engine); filters.remove(lowerCaseName); sender.sendMessage(ChatColor.GOLD + "Removed filter " + name); } else { sender.sendMessage(ChatColor.RED + "Unable to find a filter by the name " + name); } break; } return true; } private SubCommand parseCommand(String[] args, int index) { String text = args[index].toUpperCase(); try { return SubCommand.valueOf(text); } catch (IllegalArgumentException e) { throw new IllegalArgumentException(text + " is not a valid sub command. Must be add or remove.", e); } } }