/* * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. * Copyright (C) 2012 Kristian S. Stangeland * * This program 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 of * the License, or (at your option) any later version. * * This program 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 program; * if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA * 02111-1307 USA */ package com.comphenix.protocol; import com.comphenix.protocol.PacketType.Sender; import com.comphenix.protocol.concurrency.PacketTypeSet; import com.comphenix.protocol.error.ErrorReporter; import com.comphenix.protocol.error.ReportType; import com.comphenix.protocol.events.ListeningWhitelist; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.utility.ChatExtensions; import com.comphenix.protocol.utility.HexDumper; import com.google.common.collect.MapMaker; import org.bukkit.ChatColor; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.WeakHashMap; import java.util.logging.Level; import java.util.logging.Logger; /** * Handles the "packet" debug command. * * @author Kristian */ class CommandPacket extends CommandBase { public static final ReportType REPORT_CANNOT_SEND_MESSAGE = new ReportType("Cannot send chat message."); private enum SubCommand { ADD, REMOVE, NAMES, PAGE } /** * Name of this command. */ public static final String NAME = "packet"; /** * Number of lines per page. */ public static final int PAGE_LINE_COUNT = 9; private final Plugin plugin; private final Logger logger; private final ProtocolManager manager; private final ChatExtensions chatter; // The main parser private final PacketTypeParser typeParser = new PacketTypeParser(); // Paged message private final Map> pagedMessage = new WeakHashMap>(); // Current registered packet types private final PacketTypeSet packetTypes = new PacketTypeSet(); private final PacketTypeSet extendedTypes = new PacketTypeSet(); // Compare listeners private final PacketTypeSet compareTypes = new PacketTypeSet(); private final Map originalPackets = new MapMaker().weakKeys().makeMap(); // The packet listener private PacketListener listener; // Compare listener private PacketListener compareListener; // Filter packet events private final CommandFilter filter; public CommandPacket(ErrorReporter reporter, Plugin plugin, Logger logger, CommandFilter filter, ProtocolManager manager) { super(reporter, CommandBase.PERMISSION_ADMIN, NAME, 1); this.plugin = plugin; this.logger = logger; this.manager = manager; this.filter = filter; this.chatter = new ChatExtensions(manager); } /** * Send a message without invoking the packet listeners. * @param receiver - the player to send it to. * @param message - the message to send. * @return TRUE if the message was sent successfully, FALSE otherwise. */ public void sendMessageSilently(CommandSender receiver, String message) { chatter.sendMessageSilently(receiver, message); } /** * Broadcast a message without invoking any packet listeners. * @param message - message to send. * @param permission - permission required to receieve the message. NULL to target everyone. */ public void broadcastMessageSilently(String message, String permission) { chatter.broadcastMessageSilently(message, permission); } private void printPage(CommandSender sender, int pageIndex) { List paged = pagedMessage.get(sender); // Make sure the player has any pages if (paged != null) { int lastPage = ((paged.size() - 1) / PAGE_LINE_COUNT) + 1; for (int i = PAGE_LINE_COUNT * (pageIndex - 1); i < PAGE_LINE_COUNT * pageIndex; i++) { if (i < paged.size()) { sendMessageSilently(sender, " " + paged.get(i)); } } // More data? if (pageIndex < lastPage) { sendMessageSilently(sender, "Send /packet page " + (pageIndex + 1) + " for the next page."); } } else { sendMessageSilently(sender, ChatColor.RED + "No pages found."); } } /* * Description: Adds or removes a simple packet listener. Usage: / add|remove client|server|both [ID start] [ID stop] [detailed] */ @Override protected boolean handleCommand(CommandSender sender, String[] args) { try { Deque arguments = new ArrayDeque<>(Arrays.asList(args)); SubCommand subCommand = parseCommand(arguments); // Commands with different parameters if (subCommand == SubCommand.PAGE) { if (args.length <= 1) { sendMessageSilently(sender, ChatColor.RED + "Must specify a page index."); return true; } int page = Integer.parseInt(args[1]); if (page > 0) printPage(sender, page); else sendMessageSilently(sender, ChatColor.RED + "Page index must be greater than zero."); return true; } Set types = typeParser.parseTypes(arguments, PacketTypeParser.DEFAULT_MAX_RANGE); Boolean detailed = parseBoolean(arguments, "detailed"); Boolean compare = parseBoolean(arguments, "compare"); // Notify user if (typeParser.getLastProtocol() == null) { sender.sendMessage(ChatColor.YELLOW + "Warning: Missing protocol (PLAY, etc) - assuming legacy IDs."); } if (arguments.size() > 0) { throw new IllegalArgumentException("Cannot parse " + arguments); } // The last elements are optional if (detailed == null) { detailed = false; } if (compare == null) { compare = false; } else { // This is implied - we cannot compare content without going detailed detailed = true; } // Perform commands if (subCommand == SubCommand.ADD) { // The add command is dangerous - don't default on the connection side if (args.length == 1) { sender.sendMessage(ChatColor.RED + "Please specify a connection side."); return false; } executeAddCommand(sender, types, detailed, compare); } else if (subCommand == SubCommand.REMOVE) { executeRemoveCommand(sender, types); } else if (subCommand == SubCommand.NAMES) { executeNamesCommand(sender, types); } } catch (NumberFormatException e) { sendMessageSilently(sender, ChatColor.RED + "Cannot parse number: " + e.getMessage()); } catch (IllegalArgumentException e) { sendMessageSilently(sender, ChatColor.RED + e.getMessage()); } return true; } private void executeAddCommand(CommandSender sender, Set addition, boolean detailed, boolean compare) { packetTypes.addAll(addition); // Also mark these types as "detailed" if (detailed) { extendedTypes.addAll(addition); } // Whether or not to compare the packet with the initial state if (compare) { compareTypes.addAll(addition); } updatePacketListener(); sendMessageSilently(sender, ChatColor.YELLOW + "Added listener " + getWhitelistInfo(listener)); } private void executeRemoveCommand(CommandSender sender, Set removal) { packetTypes.removeAll(removal); extendedTypes.removeAll(removal); compareTypes.removeAll(removal); updatePacketListener(); sendMessageSilently(sender, ChatColor.YELLOW + "Removing packet types."); } private void executeNamesCommand(CommandSender sender, Set types) { List messages = new ArrayList<>(); // Print the equivalent name of every given ID for (PacketType type : types) { messages.add(ChatColor.YELLOW + type.toString()); } if (sender instanceof Player && messages.size() > PAGE_LINE_COUNT) { // Divide the messages into chuncks pagedMessage.put(sender, messages); printPage(sender, 1); } else { // Just print the damn thing for (String message : messages) { sendMessageSilently(sender, message); } } } /** * Retrieve whitelist information about a given listener. * @param listener - the given listener. * @return Whitelist information. */ private String getWhitelistInfo(PacketListener listener) { boolean sendingEmpty = ListeningWhitelist.isEmpty(listener.getSendingWhitelist()); boolean receivingEmpty = ListeningWhitelist.isEmpty(listener.getReceivingWhitelist()); if (!sendingEmpty && !receivingEmpty) return String.format("Sending: %s, Receiving: %s", listener.getSendingWhitelist(), listener.getReceivingWhitelist()); else if (!sendingEmpty) return listener.getSendingWhitelist().toString(); else if (!receivingEmpty) return listener.getReceivingWhitelist().toString(); else return "[None]"; } private Set filterTypes(Set types, Sender sender) { final Set result = new HashSet<>(); for (PacketType type : types) { if (type.getSender() == sender) { result.add(type); } } return result; } public PacketListener createPacketListener(Set type) { final ListeningWhitelist serverList = ListeningWhitelist.newBuilder(). types(filterTypes(type, Sender.SERVER)). gamePhaseBoth(). monitor(). build(); final ListeningWhitelist clientList = ListeningWhitelist.newBuilder(serverList). types(filterTypes(type, Sender.CLIENT)). monitor(). build(); return new PacketListener() { @Override public void onPacketSending(PacketEvent event) { if (filter.filterEvent(event)) { printInformation(event); } } @Override public void onPacketReceiving(PacketEvent event) { if (filter.filterEvent(event)) { printInformation(event); } } private void printInformation(PacketEvent event) { String verb = event.isServerPacket() ? "Sent" : "Received"; String format = event.isServerPacket() ? "%s %s to %s" : "%s %s from %s"; String shortDescription = String.format(format, event.isCancelled() ? "Cancelled" : verb, event.getPacketType(), event.getPlayer().getName() ); // Detailed will print the packet's content too if (extendedTypes.contains(event.getPacketType())) { try { String original = originalPackets.remove(event); // Also print original if (original != null) { logger.info("Initial packet:\n" + original + " -> "); } logger.info(shortDescription + ":\n" + HexDumper.getPacketDescription( event.getPacket()) ); } catch (IllegalAccessException e) { logger.log(Level.WARNING, "Unable to use reflection.", e); } } else { logger.info(shortDescription + "."); } } @Override public ListeningWhitelist getSendingWhitelist() { return serverList; } @Override public ListeningWhitelist getReceivingWhitelist() { return clientList; } @Override public Plugin getPlugin() { return plugin; } }; } public PacketListener createCompareListener(Set type) { final ListeningWhitelist serverList = ListeningWhitelist.newBuilder(). types(filterTypes(type, Sender.SERVER)). gamePhaseBoth(). lowest(). build(); final ListeningWhitelist clientList = ListeningWhitelist.newBuilder(serverList). types(filterTypes(type, Sender.CLIENT)). lowest(). build(); return new PacketListener() { @Override public void onPacketSending(PacketEvent event) { savePacketState(event); } @Override public void onPacketReceiving(PacketEvent event) { savePacketState(event); } /** * Save the original value. * @param event - the event with the packet to save. */ private void savePacketState(PacketEvent event) { try { originalPackets.put(event, HexDumper.getPacketDescription(event.getPacket())); } catch (IllegalAccessException e) { throw new RuntimeException("Cannot read packet.", e); } } @Override public ListeningWhitelist getSendingWhitelist() { return serverList; } @Override public ListeningWhitelist getReceivingWhitelist() { return clientList; } @Override public Plugin getPlugin() { return plugin; } }; } public PacketListener updatePacketListener() { if (listener != null) { manager.removePacketListener(listener); } if (compareListener != null) { manager.removePacketListener(compareListener); } // Register the new listeners listener = createPacketListener(packetTypes.values()); compareListener = createCompareListener(compareTypes.values()); manager.addPacketListener(listener); manager.addPacketListener(compareListener); return listener; } private SubCommand parseCommand(Deque arguments) { return SubCommand.valueOf(arguments.remove().toUpperCase()); } }