概要
本解説は1.7.x系を前提としています。
Bukkitで送受信されているパケットにプラグインで割り込み、改ざんします。
このプラグインにはBukkitAPIの他にnet.minecraftパッケージを必要とするので、
CraftBukkitをビルドパスに入れてください。
mavenを使用している場合は下記のように書き換えてください
<dependencies>
<dependency>
<groupId>org.bukkit</groupId>
<artifactId>craftbukkit</artifactId>
<version>1.7.2-R0.4-SNAPSHOT</version>
</dependency>
</dependencies>
割り込み
Nettyを使用してpacket_handlerに割り込みます。
それにあたって3つのクラスを作成します。
public class NettyInjector implements Listener {
private Plugin plugin;
HashMap<PlayerChannelHandler, Player> connection = new HashMap<>();
private Class EntityPlayer;
private Field EntityPlayer_playerConnection;
private Class PlayerConnection;
private Field PlayerConnection_networkManager;
private Class NetworkManager;
private Field NetworkManager_K;
private Field NetworkManager_M;
public NettyInjector(Plugin plugin){
this.plugin = plugin;
Bukkit.getServer().getPluginManager().registerEvents(this, plugin);
try {
EntityPlayer = Reflection.getCraftClass("EntityPlayer");
EntityPlayer_playerConnection = Reflection.getField(EntityPlayer, "playerConnection");
PlayerConnection = Reflection.getCraftClass("PlayerConnection");
PlayerConnection_networkManager = Reflection.getField(PlayerConnection, "networkManager");
NetworkManager = Reflection.getCraftClass("NetworkManager");
NetworkManager_K = Reflection.getField(NetworkManager, "k");
NetworkManager_M = Reflection.getField(NetworkManager, "m");
} catch (Exception e) {
e.printStackTrace();
}
}
public void inject(Player player) throws Exception{
Object entityPlayer = Reflection.getEntityPlayer(player);
Object networkManager = getNetworkManager(entityPlayer);
Channel channel = getChannel(networkManager);
PlayerChannelHandler pch = new PlayerChannelHandler();
if(channel.pipeline().get(PlayerChannelHandler.class) == null){
channel.pipeline().addBefore("packet_handler", "YourPluginName", pch);
connection.put(pch, player);
}
}
public void remove(final Player player) throws Exception{
Object entityPlayer = Reflection.getEntityPlayer(player);
Object networkManager = getNetworkManager(entityPlayer);
final Channel channel = getChannel(networkManager);
if(channel.pipeline().get(PlayerChannelHandler.class) != null){
channel.pipeline().remove(PlayerChannelHandler.class);
for (Map.Entry<PlayerChannelHandler, Player> es : connection.entrySet()) {
if (es.getValue().equals(player)) {
connection.remove(es.getKey());
}
}
}
}
@EventHandler
public void onPluginDisable(PluginDisableEvent e) throws Exception{
if(e.getPlugin().equals(plugin)){
for(Player player: Bukkit.getOnlinePlayers()) {
remove(player);
}
}
}
private Object getNetworkManager(Object entityPlayer) {
Object pc = Reflection.getFieldValue(EntityPlayer_playerConnection, entityPlayer);
return Reflection.getFieldValue(PlayerConnection_networkManager, pc);
}
private Channel getChannel(Object networkManager) {
Channel ch = null;
try {
ch = Reflection.getFieldValue(NetworkManager_K, networkManager);
} catch (Exception e) {
ch = Reflection.getFieldValue(NetworkManager_M, networkManager);
}
return ch;
}
@EventHandler
public void onPlayerJoin(PlayerJoinEvent e) throws Exception{
inject(e.getPlayer());
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent e) throws Exception{
remove(e.getPlayer());
}
@EventHandler
public void onPluginEnable(PluginEnableEvent e) throws Exception{
if(e.getPlugin().equals(plugin)){
for(Player player: Bukkit.getOnlinePlayers()){
inject(player);
}
}
}
}
public class Reflection {
public static String getPackageName(){
String packagename = "net.minecraft.server."+getCraftBukkitVersion();
return packagename;
}
public static String getCraftBukkitVersion(){
String version = Bukkit.getServer().getClass().getPackage().getName().replace(".", ",").split(",")[3];
return version;
}
public static Class<?> getCraftClass(String s) throws Exception {
Class<?> craftclass = Class.forName(getPackageName()+"."+s);
return craftclass;
}
public static Object getEntityPlayer(Player p) throws Exception{
Method getHandle = p.getClass().getMethod("getHandle");
return getHandle.invoke(p);
}
public static Object getFieldValue(Object instance, String fieldName) throws Exception {
Field field = instance.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(instance);
}
@SuppressWarnings("unchecked")
public static <T> T getFieldValue(Field field, Object obj) {
try {
return (T) field.get(obj);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static Field getField(Class<?> clazz, String fieldName) throws Exception {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
}
public static void setFieldValue(Field field, Object obj, Object value) {
try {
field.set(obj, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class PlayerChannelHandler extends ChannelDuplexHandler {
/**
* クライアントからパケットを受信したとき
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg.getClass().getSimpleName().equalsIgnoreCase("PacketPlayInTabComplete")){
String s = (String)Reflection.getFieldValue(msg, "a");
if(!(s.startsWith("/ver") || s.startsWith("/version"))){
super.channelRead(ctx, msg);
}
}else{
super.channelRead(ctx, msg);
}
}
/**
* サーバーからクライアントへパケットを送信するとき
* @param ctx
* @param msg
* @param promise
* @throws Exception
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
super.write(ctx, msg, promise);
}
}
3つのクラスを作成し終わったら、最後にonEnableに下記のように記述します
@Override
public void onEnable() {
new NettyInjector(this);
}
この記事の例ではPacketPlayInTabCompleteというタブ補完を行うときにサーバーに送られるパケットが**"/ver"または、"/version"**だった場合パケットを破棄するという処理を行っています。
どのパケットが何をしているかは下記をURL見てください。
https://github.com/Bukkit/mc-dev/tree/master/net/minecraft/server
http://wiki.vg/Protocol
下記の画像の状態でTABキーを押してもプラグインリストが出なければ成功です。
参考
https://github.com/Bukkit/mc-dev/blob/master/net/minecraft/server/PacketPlayInTabComplete.java
http://wiki.vg/Protocol#Tab-Complete