/** * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. * Copyright (C) 2017 Dan Mulloy * * 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.Protocol; import com.comphenix.protocol.PacketType.Sender; import com.comphenix.protocol.events.ListeningWhitelist; import com.comphenix.protocol.events.PacketEvent; import com.comphenix.protocol.events.PacketListener; import com.comphenix.protocol.injector.netty.WirePacket; import com.comphenix.protocol.reflect.FuzzyReflection; import com.comphenix.protocol.reflect.accessors.Accessors; import com.comphenix.protocol.reflect.accessors.MethodAccessor; import com.comphenix.protocol.utility.MinecraftReflection; import org.bukkit.ChatColor; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; import org.bukkit.plugin.Plugin; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.logging.FileHandler; import java.util.logging.Formatter; import java.util.logging.Handler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; /** * Logs packets to a given stream * @author dmulloy2 */ public class PacketLogging implements CommandExecutor, PacketListener { public static final String NAME = "packetlog"; private static MethodAccessor HEX_DUMP; private List sendingTypes = new ArrayList<>(); private List receivingTypes = new ArrayList<>(); private ListeningWhitelist sendingWhitelist; private ListeningWhitelist receivingWhitelist; private Logger fileLogger; private LogLocation location = LogLocation.FILE; private final ProtocolManager manager; private final Plugin plugin; PacketLogging(Plugin plugin, ProtocolManager manager) { this.plugin = plugin; this.manager = manager; } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { PacketType type = null; try { if (args.length > 2) { Protocol protocol; try { protocol = Protocol.valueOf(args[0].toUpperCase()); } catch (IllegalArgumentException ex) { sender.sendMessage(ChatColor.RED + "Unknown protocol " + args[0]); return true; } Sender pSender; try { pSender = Sender.valueOf(args[1].toUpperCase()); } catch (IllegalArgumentException ex) { sender.sendMessage(ChatColor.RED + "Unknown sender: " + args[1]); return true; } try { try { // Try IDs first int id = Integer.parseInt(args[2]); type = PacketType.findCurrent(protocol, pSender, id); } catch (NumberFormatException ex) { // Check packet names String name = args[2]; for (PacketType packet : PacketType.values()) { if (packet.getProtocol() == protocol && packet.getSender() == pSender) { if (packet.name().equalsIgnoreCase(name)) { type = packet; break; } for (String className : packet.getClassNames()) { if (className.equalsIgnoreCase(name)) { type = packet; break; } } } } } } catch (IllegalArgumentException ex) { // RIP type = null; } if (type == null) { sender.sendMessage(ChatColor.RED + "Unknown packet: " + args[2]); return true; } if (args.length > 3) { if (args[3].equalsIgnoreCase("console")) { this.location = LogLocation.CONSOLE; } else { this.location = LogLocation.FILE; } } if (pSender == Sender.CLIENT) { if (receivingTypes.contains(type)) { receivingTypes.remove(type); } else { receivingTypes.add(type); } } else { if (sendingTypes.contains(type)) { sendingTypes.remove(type); } else { sendingTypes.add(type); } } startLogging(); sender.sendMessage(ChatColor.GREEN + "Now logging " + type.getPacketClass().getSimpleName()); return true; } sender.sendMessage(ChatColor.RED + "Invalid syntax: /packetlog [location]"); return true; } catch (Throwable ex) { sender.sendMessage(ChatColor.RED + "Failed to parse command: " + ex); return true; } } private void startLogging() { manager.removePacketListener(this); if (sendingTypes.isEmpty() && receivingTypes.isEmpty()) { return; } this.sendingWhitelist = ListeningWhitelist.newBuilder().types(sendingTypes).build(); this.receivingWhitelist = ListeningWhitelist.newBuilder().types(receivingTypes).build(); // Set up the file logger if it hasn't been already if (location == LogLocation.FILE && fileLogger == null) { fileLogger = Logger.getLogger("ProtocolLib-FileLogging"); for (Handler handler : fileLogger.getHandlers()) fileLogger.removeHandler(handler); fileLogger.setUseParentHandlers(false); try { File logFile = new File(plugin.getDataFolder(), "log.log"); FileHandler handler = new FileHandler(logFile.getAbsolutePath(), true); handler.setFormatter(new LogFormatter()); fileLogger.addHandler(handler); } catch (IOException ex) { plugin.getLogger().log(Level.SEVERE, "Failed to obtain log file:", ex); return; } } manager.addPacketListener(this); } @Override public void onPacketSending(PacketEvent event) { log(event); } @Override public void onPacketReceiving(PacketEvent event) { log(event); } // Here's where the magic happens private static String hexDump(byte[] bytes) throws IOException { try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { if (HEX_DUMP == null) { Class hexDumpClass = MinecraftReflection.getLibraryClass("org.apache.commons.io.HexDump"); HEX_DUMP = Accessors.getMethodAccessor(FuzzyReflection.fromClass(hexDumpClass) .getMethodByParameters("dump", byte[].class, long.class, OutputStream.class, int.class)); } HEX_DUMP.invoke(null, bytes, 0, output, 0); return new String(output.toByteArray(), StandardCharsets.UTF_8); } } private void log(PacketEvent event) { try { byte[] bytes = WirePacket.bytesFromPacket(event.getPacket()); String hexDump = hexDump(bytes); if (location == LogLocation.FILE) { fileLogger.log(Level.INFO, event.getPacketType() + ":"); fileLogger.log(Level.INFO, hexDump); fileLogger.log(Level.INFO, ""); } else { System.out.println(event.getPacketType() + ":"); System.out.println(hexDump); System.out.println(); } } catch (Throwable ex) { plugin.getLogger().log(Level.WARNING, "Failed to log packet " + event.getPacketType() + ":", ex); plugin.getLogger().log(Level.WARNING, "Clearing packet logger..."); sendingTypes.clear(); receivingTypes.clear(); startLogging(); } } @Override public ListeningWhitelist getSendingWhitelist() { return sendingWhitelist; } @Override public ListeningWhitelist getReceivingWhitelist() { return receivingWhitelist; } @Override public Plugin getPlugin() { return plugin; } private enum LogLocation { CONSOLE, FILE } private static class LogFormatter extends Formatter { private static final SimpleDateFormat DATE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); private static final String LINE_SEPARATOR = System.getProperty("line.separator"); private static final String FORMAT = "[{0}] {1}"; @Override public String format(LogRecord record) { final String string = formatMessage(record); return string.isEmpty() ? LINE_SEPARATOR : MessageFormat.format(FORMAT, DATE.format(record.getMillis()), string) + LINE_SEPARATOR; } } }