diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/guava/Guava.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/guava/Guava.java new file mode 100644 index 00000000..c8fdbec7 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/guava/Guava.java @@ -0,0 +1,68 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.guava; + +import java.io.DataInputStream; +import java.util.Set; +import java.util.logging.Level; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.ProtocolLibrary; +import com.google.common.collect.Range; + +/** + * @author dmulloy2 + */ + +public class Guava { + private static GuavaCompat compat; + + private static GuavaCompat getCompat() { + if (compat == null) { + try { + Range.closed(1, 2); + return compat = new Guava17(); + } catch (Throwable ex) { + try { + ProtocolLibrary.log("Falling back to Guava 10 compat"); + Class clazz = Class.forName("com.comphenix.protocol.compat.guava.Guava10"); + return compat = (GuavaCompat) clazz.newInstance(); + } catch (Throwable ex1) { + ProtocolLibrary.getStaticLogger().log(Level.SEVERE, "Failed to create Guava 10 compat:", ex1); + } + } + } + + return compat; + } + + public static > Range closedRange(C lower, C upper) { + return getCompat().closedRange(lower, upper); + } + + public static > Range singleton(C singleton) { + return getCompat().singletonRange(singleton); + } + + public static Set toSet(Range range) { + return getCompat().toSet(range); + } + + public static DataInputStream addHeader(DataInputStream input, PacketType type) { + return getCompat().addHeader(input, type); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/guava/Guava17.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/guava/Guava17.java new file mode 100644 index 00000000..2c7bbb54 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/guava/Guava17.java @@ -0,0 +1,76 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.guava; + +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Set; + +import com.comphenix.protocol.PacketType; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.Range; +import com.google.common.io.ByteSource; + +/** + * @author dmulloy2 + */ + +public class Guava17 implements GuavaCompat { + + @Override + public > Range closedRange(C lower, C upper) { + return Range.closed(lower, upper); + } + + @Override + public > Range singletonRange(C singleton) { + return Range.singleton(singleton); + } + + @Override + public Set toSet(Range range) { + return ContiguousSet.create(range, DiscreteDomain.integers()); + } + + @Override + public DataInputStream addHeader(final DataInputStream input, final PacketType type) { + ByteSource header = new ByteSource() { + @Override + public InputStream openStream() throws IOException { + byte[] data = new byte[] { (byte) type.getLegacyId() }; + return new ByteArrayInputStream(data); + } + }; + + ByteSource data = new ByteSource() { + @Override + public InputStream openStream() throws IOException { + return input; + } + }; + + // Combine them into a single stream + try { + return new DataInputStream(ByteSource.concat(header, data).openStream()); + } catch (IOException e) { + throw new RuntimeException("Cannot add header.", e); + } + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/guava/GuavaCompat.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/guava/GuavaCompat.java new file mode 100644 index 00000000..a247cfb1 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/guava/GuavaCompat.java @@ -0,0 +1,38 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.guava; + +import java.io.DataInputStream; +import java.util.Set; + +import com.comphenix.protocol.PacketType; +import com.google.common.collect.Range; + +/** + * @author dmulloy2 + */ + +public interface GuavaCompat { + + > Range closedRange(C lower, C upper); + + > Range singletonRange(C singleton); + + Set toSet(Range range); + + DataInputStream addHeader(DataInputStream input, PacketType type); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/ChannelInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/ChannelInjector.java new file mode 100644 index 00000000..8311fb41 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/ChannelInjector.java @@ -0,0 +1,28 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty; + +import com.comphenix.protocol.injector.netty.Injector; + +/** + * @author dmulloy2 + */ + +public interface ChannelInjector extends Injector { + + WrappedChannel getChannel(); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/Netty.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/Netty.java new file mode 100644 index 00000000..f1830540 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/Netty.java @@ -0,0 +1,92 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.util.logging.Level; + +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.compat.netty.independent.IndependentNetty; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.injector.PacketFilterManager; +import com.comphenix.protocol.wrappers.WrappedServerPing.CompressedImage; + +/** + * @author dmulloy2 + */ + +public class Netty { + private static NettyCompat compat; + + private static NettyCompat getCompat() { + if (compat == null) { + try { + Class.forName("io.netty.buffer.ByteBuf"); + return compat = new IndependentNetty(); + } catch (Throwable ex) { + try { + ProtocolLibrary.log("Falling back to legacy Netty compat"); + Class clazz = Class.forName("com.comphenix.protocol.compat.netty.shaded.ShadedNetty"); + return compat = (NettyCompat) clazz.newInstance(); + } catch (Throwable ex1) { + ProtocolLibrary.getStaticLogger().log(Level.SEVERE, "Failed to create legacy netty compat:", ex1); + } + } + } + + return compat; + } + + public static WrappedByteBuf createPacketBuffer() { + return getCompat().createPacketBuffer(); + } + + public static WrappedByteBuf allocateUnpooled() { + return getCompat().allocateUnpooled(); + } + + public static Class getGenericFutureListenerArray() { + return getCompat().getGenericFutureListenerArray(); + } + + public static Class getChannelHandlerContext() { + return getCompat().getChannelHandlerContext(); + } + + public static String toEncodedText(CompressedImage image) { + return getCompat().toEncodedText(image); + } + + public static WrappedByteBuf decode(byte[] encoded) { + return getCompat().decode(encoded); + } + + public static ProtocolInjector getProtocolInjector(Plugin library, PacketFilterManager packetFilterManager, ErrorReporter reporter) { + return getCompat().getProtocolInjector(library, packetFilterManager, reporter); + } + + public static WrappedByteBuf packetReader(DataInputStream input) { + return getCompat().packetReader(input); + } + + public static WrappedByteBuf packetWriter(DataOutputStream output) { + return getCompat().packetWriter(output); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/NettyCompat.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/NettyCompat.java new file mode 100644 index 00000000..a4a65d82 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/NettyCompat.java @@ -0,0 +1,52 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty; + +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.wrappers.WrappedServerPing.CompressedImage; + + +/** + * @author dmulloy2 + */ + +public interface NettyCompat { + + WrappedByteBuf createPacketBuffer(); + + WrappedByteBuf allocateUnpooled(); + + Class getGenericFutureListenerArray(); + + Class getChannelHandlerContext(); + + String toEncodedText(CompressedImage image); + + WrappedByteBuf decode(byte[] encoded); + + ProtocolInjector getProtocolInjector(Plugin plugin, ListenerInvoker invoker, ErrorReporter reporter); + + WrappedByteBuf packetReader(DataInputStream input); + + WrappedByteBuf packetWriter(DataOutputStream output); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/ProtocolInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/ProtocolInjector.java new file mode 100644 index 00000000..6415d17d --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/ProtocolInjector.java @@ -0,0 +1,38 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty; + +import com.comphenix.protocol.injector.netty.ChannelListener; +import com.comphenix.protocol.injector.packet.PacketInjector; +import com.comphenix.protocol.injector.player.PlayerInjectionHandler; + +/** + * @author dmulloy2 + */ + +public interface ProtocolInjector extends ChannelListener { + + PlayerInjectionHandler getPlayerInjector(); + + PacketInjector getPacketInjector(); + + void setDebug(boolean debug); + + void inject(); + + void close(); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/WrappedByteBuf.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/WrappedByteBuf.java new file mode 100644 index 00000000..b7dda6f3 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/WrappedByteBuf.java @@ -0,0 +1,44 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +/** + * @author dmulloy2 + */ + +public interface WrappedByteBuf { + + void writeBytes(ObjectInputStream input, int id) throws IOException; + + Object getHandle(); + + int readableBytes(); + + void readBytes(ObjectOutputStream output, int readableBytes) throws IOException; + + void readBytes(byte[] data); + + void writeByte(byte b); + + void writeByte(int i); + + void writeBytes(byte[] bytes); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/WrappedChannel.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/WrappedChannel.java new file mode 100644 index 00000000..648d0fa1 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/WrappedChannel.java @@ -0,0 +1,26 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty; + +/** + * @author dmulloy2 + */ + +public interface WrappedChannel { + + void writeAndFlush(Object packet); +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/IndependentNetty.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/IndependentNetty.java new file mode 100644 index 00000000..c7925466 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/IndependentNetty.java @@ -0,0 +1,104 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.base64.Base64; +import io.netty.util.concurrent.GenericFutureListener; + +import java.io.DataInputStream; +import java.io.DataOutputStream; + +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.compat.netty.NettyCompat; +import com.comphenix.protocol.compat.netty.ProtocolInjector; +import com.comphenix.protocol.compat.netty.WrappedByteBuf; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.WrappedServerPing.CompressedImage; +import com.google.common.base.Charsets; + +/** + * @author dmulloy2 + */ + +public class IndependentNetty implements NettyCompat { + + @Override + public WrappedByteBuf createPacketBuffer() { + return getPacketDataSerializer(allocateUnpooled()); + } + + private WrappedByteBuf getPacketDataSerializer(WrappedByteBuf buffer) { + Class packetSerializer = MinecraftReflection.getPacketDataSerializerClass(); + + try { + return new NettyByteBuf((ByteBuf) packetSerializer.getConstructor(MinecraftReflection.getByteBufClass()) + .newInstance(buffer.getHandle())); + } catch (Exception e) { + throw new RuntimeException("Cannot construct packet serializer.", e); + } + } + + @Override + public WrappedByteBuf allocateUnpooled() { + return new NettyByteBuf(UnpooledByteBufAllocator.DEFAULT.buffer()); + } + + @Override + public Class getGenericFutureListenerArray() { + return GenericFutureListener[].class; + } + + @Override + public Class getChannelHandlerContext() { + return ChannelHandlerContext.class; + } + + @Override + public String toEncodedText(CompressedImage image) { + final ByteBuf buffer = Unpooled.wrappedBuffer(image.getDataCopy()); + String computed = "data:" + image.getMime() + ";base64," + + Base64.encode(buffer).toString(Charsets.UTF_8); + return computed; + } + + @Override + public WrappedByteBuf decode(byte[] encoded) { + return new NettyByteBuf(Base64.decode(Unpooled.wrappedBuffer(encoded))); + } + + @Override + public ProtocolInjector getProtocolInjector(Plugin plugin, ListenerInvoker invoker, ErrorReporter reporter) { + return new NettyProtocolInjector(plugin, invoker, reporter); + } + + @Override + public WrappedByteBuf packetReader(DataInputStream input) { + return new NettyByteBuf(NettyByteBufAdapter.packetReader(input)); + } + + @Override + public WrappedByteBuf packetWriter(DataOutputStream output) { + return new NettyByteBuf(NettyByteBufAdapter.packetWriter(output)); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyBootstrapList.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyBootstrapList.java new file mode 100644 index 00000000..17b55687 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyBootstrapList.java @@ -0,0 +1,234 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.Callable; + +import com.google.common.collect.Lists; + +public class NettyBootstrapList implements List { + private List delegate; + private ChannelHandler handler; + + /** + * Construct a new bootstrap list. + * @param delegate - the delegate. + * @param handler - the channel handler to add. + */ + public NettyBootstrapList(List delegate, ChannelHandler handler) { + this.delegate = delegate; + this.handler = handler; + + // Process all existing bootstraps + for (Object item : this) { + processElement(item); + } + } + + @Override + public synchronized boolean add(Object element) { + processElement(element); + return delegate.add(element); + } + + @Override + public synchronized boolean addAll(Collection collection) { + List copy = Lists.newArrayList(collection); + + // Process the collection before we pass it on + for (Object element : copy) { + processElement(element); + } + return delegate.addAll(copy); + } + + @Override + public synchronized Object set(int index, Object element) { + Object old = delegate.set(index, element); + + // Handle the old future, and the newly inserted future + if (old != element) { + unprocessElement(old); + processElement(element); + } + return old; + } + + /** + * Process a single element. + * @param element - the element. + */ + protected void processElement(Object element) { + if (element instanceof ChannelFuture) { + processBootstrap((ChannelFuture) element); + } + } + + /** + * Unprocess a single element. + * @param element - the element to unprocess. + */ + protected void unprocessElement(Object element) { + if (element instanceof ChannelFuture) { + unprocessBootstrap((ChannelFuture) element); + } + } + + /** + * Process a single channel future. + * @param future - the future. + */ + protected void processBootstrap(ChannelFuture future) { + // Important: Must be addFirst() + future.channel().pipeline().addFirst(handler); + } + + /** + * Revert any changes we made to the channel future. + * @param future - the future. + */ + protected void unprocessBootstrap(ChannelFuture future) { + final Channel channel = future.channel(); + + // For thread safety - see ChannelInjector.close() + channel.eventLoop().submit(new Callable() { + @Override + public Object call() throws Exception { + channel.pipeline().remove(handler); + return null; + } + }); + } + + /** + * Close and revert all changes. + */ + public synchronized void close() { + for (Object element : this) + unprocessElement(element); + } + + // Boiler plate + @Override + public synchronized int size() { + return delegate.size(); + } + + @Override + public synchronized boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return delegate.contains(o); + } + + @Override + public synchronized Iterator iterator() { + return delegate.iterator(); + } + + @Override + public synchronized Object[] toArray() { + return delegate.toArray(); + } + + @Override + public synchronized T[] toArray(T[] a) { + return delegate.toArray(a); + } + + @Override + public synchronized boolean remove(Object o) { + return delegate.remove(o); + } + + @Override + public synchronized boolean containsAll(Collection c) { + return delegate.containsAll(c); + } + + @Override + public synchronized boolean addAll(int index, Collection c) { + return delegate.addAll(index, c); + } + + @Override + public synchronized boolean removeAll(Collection c) { + return delegate.removeAll(c); + } + + @Override + public synchronized boolean retainAll(Collection c) { + return delegate.retainAll(c); + } + + @Override + public synchronized void clear() { + delegate.clear(); + } + + @Override + public synchronized Object get(int index) { + return delegate.get(index); + } + + @Override + public synchronized void add(int index, Object element) { + delegate.add(index, element); + } + + @Override + public synchronized Object remove(int index) { + return delegate.remove(index); + } + + @Override + public synchronized int indexOf(Object o) { + return delegate.indexOf(o); + } + + @Override + public synchronized int lastIndexOf(Object o) { + return delegate.lastIndexOf(o); + } + + @Override + public synchronized ListIterator listIterator() { + return delegate.listIterator(); + } + + @Override + public synchronized ListIterator listIterator(int index) { + return delegate.listIterator(index); + } + + @Override + public synchronized List subList(int fromIndex, int toIndex) { + return delegate.subList(fromIndex, toIndex); + } + // End boiler plate +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyByteBuf.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyByteBuf.java new file mode 100644 index 00000000..12c757a3 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyByteBuf.java @@ -0,0 +1,77 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.buffer.ByteBuf; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import com.comphenix.protocol.compat.netty.WrappedByteBuf; + +/** + * @author dmulloy2 + */ + +public class NettyByteBuf implements WrappedByteBuf { + private final ByteBuf handle; + + public NettyByteBuf(ByteBuf handle) { + this.handle = handle; + } + + @Override + public void writeBytes(ObjectInputStream input, int id) throws IOException { + handle.writeBytes(input, id); + } + + @Override + public Object getHandle() { + return handle; + } + + @Override + public int readableBytes() { + return handle.readableBytes(); + } + + @Override + public void readBytes(ObjectOutputStream output, int readableBytes) throws IOException { + handle.readBytes(output, readableBytes); + } + + @Override + public void readBytes(byte[] data) { + handle.readBytes(data); + } + + @Override + public void writeByte(byte b) { + handle.writeByte(b); + } + + @Override + public void writeByte(int i) { + handle.writeByte(i); + } + + @Override + public void writeBytes(byte[] bytes) { + handle.writeBytes(bytes); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyByteBufAdapter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyByteBufAdapter.java new file mode 100644 index 00000000..6d6889a5 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyByteBufAdapter.java @@ -0,0 +1,392 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.buffer.AbstractByteBuf; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.Channels; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.ScatteringByteChannel; +import java.nio.channels.WritableByteChannel; + +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.io.ByteStreams; + +/** + * Construct a ByteBuf around an input stream and an output stream. + *

+ * Note that as streams usually don't support seeking, this implementation will ignore + * all indexing in the byte buffer. + * @author Kristian + */ +public class NettyByteBufAdapter extends AbstractByteBuf { + private DataInputStream input; + private DataOutputStream output; + + // For modifying the reader or writer index + private static FieldAccessor READER_INDEX; + private static FieldAccessor WRITER_INDEX; + + private static final int CAPACITY = Short.MAX_VALUE; + + private NettyByteBufAdapter(DataInputStream input, DataOutputStream output) { + // Just pick a figure + super(CAPACITY); + this.input = input; + this.output = output; + + // Prepare accessors + try { + if (READER_INDEX == null) { + READER_INDEX = Accessors.getFieldAccessor(AbstractByteBuf.class.getDeclaredField("readerIndex")); + } + if (WRITER_INDEX == null) { + WRITER_INDEX = Accessors.getFieldAccessor(AbstractByteBuf.class.getDeclaredField("writerIndex")); + } + } catch (Exception e) { + throw new RuntimeException("Cannot initialize ByteBufAdapter.", e); + } + + // "Infinite" reading/writing + if (input == null) + READER_INDEX.set(this, Integer.MAX_VALUE); + if (output == null) + WRITER_INDEX.set(this, Integer.MAX_VALUE); + } + + /** + * Construct a new Minecraft packet serializer using the current byte buf adapter. + * @param input - the input stream. + * @return A packet serializer with a wrapped byte buf adapter. + */ + public static ByteBuf packetReader(DataInputStream input) { + return (ByteBuf) MinecraftReflection.getPacketDataSerializer(new NettyByteBufAdapter(input, null)); + } + + /** + * Construct a new Minecraft packet deserializer using the current byte buf adapter. + * @param output - the output stream. + * @return A packet serializer with a wrapped byte buf adapter. + */ + public static ByteBuf packetWriter(DataOutputStream output) { + return (ByteBuf) MinecraftReflection.getPacketDataSerializer(new NettyByteBufAdapter(null, output)); + } + + @Override + public int refCnt() { + return 1; + } + + @Override + public boolean release() { + return false; + } + + @Override + public boolean release(int paramInt) { + return false; + } + + @Override + protected byte _getByte(int paramInt) { + try { + return input.readByte(); + } catch (IOException e) { + throw new RuntimeException("Cannot read input.", e); + } + } + + @Override + protected short _getShort(int paramInt) { + try { + return input.readShort(); + } catch (IOException e) { + throw new RuntimeException("Cannot read input.", e); + } + } + + @Override + protected int _getUnsignedMedium(int paramInt) { + try { + return input.readUnsignedShort(); + } catch (IOException e) { + throw new RuntimeException("Cannot read input.", e); + } + } + + @Override + protected int _getInt(int paramInt) { + try { + return input.readInt(); + } catch (IOException e) { + throw new RuntimeException("Cannot read input.", e); + } + } + + @Override + protected long _getLong(int paramInt) { + try { + return input.readLong(); + } catch (IOException e) { + throw new RuntimeException("Cannot read input.", e); + } + } + + @Override + protected void _setByte(int index, int value) { + try { + output.writeByte(value); + } catch (IOException e) { + throw new RuntimeException("Cannot write output.", e); + } + } + + @Override + protected void _setShort(int index, int value) { + try { + output.writeShort(value); + } catch (IOException e) { + throw new RuntimeException("Cannot write output.", e); + } + } + + @Override + protected void _setMedium(int index, int value) { + try { + output.writeShort(value); + } catch (IOException e) { + throw new RuntimeException("Cannot write output.", e); + } + } + + @Override + protected void _setInt(int index, int value) { + try { + output.writeInt(value); + } catch (IOException e) { + throw new RuntimeException("Cannot write output.", e); + } + } + + @Override + protected void _setLong(int index, long value) { + try { + output.writeLong(value); + } catch (IOException e) { + throw new RuntimeException("Cannot write output.", e); + } + } + + @Override + public int capacity() { + return CAPACITY; + } + + @Override + public ByteBuf capacity(int paramInt) { + return this; + } + + @Override + public ByteBufAllocator alloc() { + return null; + } + + @Override + public ByteOrder order() { + return ByteOrder.LITTLE_ENDIAN; + } + + @Override + public ByteBuf unwrap() { + return null; + } + + @Override + public boolean isDirect() { + return false; + } + + @Override + public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { + try { + for (int i = 0; i < length; i++) { + dst.setByte(dstIndex + i, input.read()); + } + } catch (IOException e) { + throw new RuntimeException("Cannot read input.", e); + } + return this; + } + + @Override + public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { + try { + input.read(dst, dstIndex, length); + } catch (IOException e) { + throw new RuntimeException("Cannot read input.", e); + } + return this; + } + + @Override + public ByteBuf getBytes(int index, ByteBuffer dst) { + try { + dst.put(ByteStreams.toByteArray(input)); + } catch (IOException e) { + throw new RuntimeException("Cannot read input.", e); + } + return this; + } + + @Override + public ByteBuf getBytes(int index, OutputStream dst, int length) throws IOException { + ByteStreams.copy(ByteStreams.limit(input, length), dst); + return this; + } + + @Override + public int getBytes(int index, GatheringByteChannel out, int length) throws IOException { + byte[] data = ByteStreams.toByteArray(ByteStreams.limit(input, length)); + + out.write(ByteBuffer.wrap(data)); + return data.length; + } + + @Override + public ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length) { + byte[] buffer = new byte[length]; + src.getBytes(srcIndex, buffer); + + try { + output.write(buffer); + return this; + } catch (IOException e) { + throw new RuntimeException("Cannot write output.", e); + } + } + + @Override + public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) { + try { + output.write(src, srcIndex, length); + return this; + } catch (IOException e) { + throw new RuntimeException("Cannot write output.", e); + } + } + + @Override + public ByteBuf setBytes(int index, ByteBuffer src) { + try { + WritableByteChannel channel = Channels.newChannel(output); + + channel.write(src); + return this; + } catch (IOException e) { + throw new RuntimeException("Cannot write output.", e); + } + } + + @Override + public int setBytes(int index, InputStream in, int length) throws IOException { + InputStream limit = ByteStreams.limit(in, length); + ByteStreams.copy(limit, output); + return length - limit.available(); + } + + @Override + public int setBytes(int index, ScatteringByteChannel in, int length) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(length); + WritableByteChannel channel = Channels.newChannel(output); + + int count = in.read(buffer); + channel.write(buffer); + return count; + } + + @Override + public ByteBuf copy(int index, int length) { + throw new UnsupportedOperationException("Cannot seek in input stream."); + } + + @Override + public int nioBufferCount() { + return 0; + } + + @Override + public ByteBuffer nioBuffer(int paramInt1, int paramInt2) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuffer internalNioBuffer(int paramInt1, int paramInt2) { + return null; + } + + @Override + public ByteBuffer[] nioBuffers(int paramInt1, int paramInt2) { + return null; + } + + @Override + public boolean hasArray() { + return false; + } + + @Override + public byte[] array() { + return null; + } + + @Override + public int arrayOffset() { + return 0; + } + + @Override + public boolean hasMemoryAddress() { + return false; + } + + @Override + public long memoryAddress() { + return 0; + } + + @Override + public ByteBuf retain(int paramInt) { + return this; + } + + @Override + public ByteBuf retain() { + return this; + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyChannel.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyChannel.java new file mode 100644 index 00000000..dd95f04a --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyChannel.java @@ -0,0 +1,38 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.channel.Channel; + +import com.comphenix.protocol.compat.netty.WrappedChannel; + +/** + * @author dmulloy2 + */ + +public class NettyChannel implements WrappedChannel { + private final Channel channel; + + public NettyChannel(Channel channel) { + this.channel = channel; + } + + @Override + public void writeAndFlush(Object packet) { + channel.writeAndFlush(packet); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyChannelInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyChannelInjector.java new file mode 100644 index 00000000..5913dba7 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyChannelInjector.java @@ -0,0 +1,948 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.internal.TypeParameterMatcher; + +import java.lang.reflect.InvocationTargetException; +import java.net.Socket; +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.List; +import java.util.ListIterator; +import java.util.Map.Entry; +import java.util.NoSuchElementException; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentMap; + +import net.sf.cglib.proxy.Factory; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.PacketType.Protocol; +import com.comphenix.protocol.ProtocolLibrary; +import com.comphenix.protocol.compat.netty.ChannelInjector; +import com.comphenix.protocol.compat.netty.WrappedByteBuf; +import com.comphenix.protocol.compat.netty.WrappedChannel; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.events.ConnectionSide; +import com.comphenix.protocol.events.NetworkMarker; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.NetworkProcessor; +import com.comphenix.protocol.injector.netty.ChannelListener; +import com.comphenix.protocol.injector.netty.NettyNetworkMarker; +import com.comphenix.protocol.injector.netty.WirePacket; +import com.comphenix.protocol.injector.server.SocketInjector; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.VolatileField; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.comphenix.protocol.reflect.accessors.MethodAccessor; +import com.comphenix.protocol.utility.MinecraftFields; +import com.comphenix.protocol.utility.MinecraftMethods; +import com.comphenix.protocol.utility.MinecraftProtocolVersion; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.comphenix.protocol.wrappers.WrappedGameProfile; +import com.google.common.base.Preconditions; +import com.google.common.collect.MapMaker; + +/** + * Represents a channel injector. + * @author Kristian + */ +public class NettyChannelInjector extends ByteToMessageDecoder implements ChannelInjector { + public static final ReportType REPORT_CANNOT_INTERCEPT_SERVER_PACKET = new ReportType("Unable to intercept a written server packet."); + public static final ReportType REPORT_CANNOT_INTERCEPT_CLIENT_PACKET = new ReportType("Unable to intercept a read client packet."); + public static final ReportType REPORT_CANNOT_EXECUTE_IN_CHANNEL_THREAD = new ReportType("Cannot execute code in channel thread."); + public static final ReportType REPORT_CANNOT_FIND_GET_VERSION = new ReportType("Cannot find getVersion() in NetworkMananger"); + public static final ReportType REPORT_CANNOT_SEND_PACKET = new ReportType("Unable to send packet %s to %s"); + + /** + * Indicates that a packet has bypassed packet listeners. + */ + private static final PacketEvent BYPASSED_PACKET = new PacketEvent(NettyChannelInjector.class); + + // The login packet + private static Class PACKET_LOGIN_CLIENT = null; + private static FieldAccessor LOGIN_GAME_PROFILE = null; + + // Saved accessors + private static MethodAccessor DECODE_BUFFER; + private static MethodAccessor ENCODE_BUFFER; + private static FieldAccessor ENCODER_TYPE_MATCHER; + + // For retrieving the protocol + private static FieldAccessor PROTOCOL_ACCESSOR; + + // For retrieving the protocol version + private static MethodAccessor PROTOCOL_VERSION; + + // The factory that created this injector + private NettyInjectionFactory factory; + + // The player, or temporary player + private Player player; + private Player updated; + private String playerName; + + // The player connection + private Object playerConnection; + + // The current network manager and channel + private final Object networkManager; + private final Channel originalChannel; + private VolatileField channelField; + + // Known network markers + private ConcurrentMap packetMarker = new MapMaker().weakKeys().makeMap(); + + /** + * Indicate that this packet has been processed by event listeners. + *

+ * This must never be set outside the channel pipeline's thread. + */ + private PacketEvent currentEvent; + + /** + * A packet event that should be processed by the write method. + */ + private PacketEvent finalEvent; + + /** + * A flag set by the main thread to indiciate that a packet should not be processed. + */ + private final ThreadLocal scheduleProcessPackets = new ThreadLocal() { + @Override + protected Boolean initialValue() { + return true; + }; + }; + + // Other handlers + private ByteToMessageDecoder vanillaDecoder; + private MessageToByteEncoder vanillaEncoder; + + private Deque finishQueue = new ArrayDeque(); + + // The channel listener + private ChannelListener channelListener; + + // Processing network markers + private NetworkProcessor processor; + + // Closed + private boolean injected; + private boolean closed; + + /** + * Construct a new channel injector. + * @param player - the current player, or temporary player. + * @param networkManager - its network manager. + * @param channel - its channel. + * @param channelListener - a listener. + * @param factory - the factory that created this injector + */ + public NettyChannelInjector(Player player, Object networkManager, Channel channel, ChannelListener channelListener, NettyInjectionFactory factory) { + this.player = Preconditions.checkNotNull(player, "player cannot be NULL"); + this.networkManager = Preconditions.checkNotNull(networkManager, "networkMananger cannot be NULL"); + this.originalChannel = Preconditions.checkNotNull(channel, "channel cannot be NULL"); + this.channelListener = Preconditions.checkNotNull(channelListener, "channelListener cannot be NULL"); + this.factory = Preconditions.checkNotNull(factory, "factory cannot be NULL"); + this.processor = new NetworkProcessor(ProtocolLibrary.getErrorReporter()); + + // Get the channel field + this.channelField = new VolatileField(FuzzyReflection.fromObject(networkManager, true).getFieldByType("channel", Channel.class), + networkManager, true); + } + + /** + * Get the version of the current protocol. + * @return The version. + */ + @Override + public int getProtocolVersion() { + MethodAccessor accessor = PROTOCOL_VERSION; + if (accessor == null) { + try { + accessor = Accessors.getMethodAccessor(networkManager.getClass(), "getVersion"); + } catch (Throwable ex) { + } + } + + if (accessor != null) { + return (Integer) accessor.invoke(networkManager); + } else { + return MinecraftProtocolVersion.getCurrentVersion(); + } + } + + @Override + @SuppressWarnings("unchecked") + public boolean inject() { + synchronized (networkManager) { + if (closed) + return false; + if (originalChannel instanceof Factory) + return false; + if (!originalChannel.isActive()) + return false; + + // Main thread? We should synchronize with the channel thread, otherwise we might see a + // pipeline with only some of the handlers removed + if (Bukkit.isPrimaryThread()) { + // Just like in the close() method, we'll avoid blocking the main thread + executeInChannelThread(new Runnable() { + @Override + public void run() { + inject(); + } + }); + return false; // We don't know + } + + // Don't inject the same channel twice + if (findChannelHandler(originalChannel, NettyChannelInjector.class) != null) { + return false; + } + + // Get the vanilla decoder, so we don't have to replicate the work + vanillaDecoder = (ByteToMessageDecoder) originalChannel.pipeline().get("decoder"); + vanillaEncoder = (MessageToByteEncoder) originalChannel.pipeline().get("encoder"); + + if (vanillaDecoder == null) + throw new IllegalArgumentException("Unable to find vanilla decoder in " + originalChannel.pipeline() ); + if (vanillaEncoder == null) + throw new IllegalArgumentException("Unable to find vanilla encoder in " + originalChannel.pipeline() ); + patchEncoder(vanillaEncoder); + + if (DECODE_BUFFER == null) + DECODE_BUFFER = Accessors.getMethodAccessor(vanillaDecoder.getClass(), + "decode", ChannelHandlerContext.class, ByteBuf.class, List.class); + if (ENCODE_BUFFER == null) + ENCODE_BUFFER = Accessors.getMethodAccessor(vanillaEncoder.getClass(), + "encode", ChannelHandlerContext.class, Object.class, ByteBuf.class); + + // Intercept sent packets + MessageToByteEncoder protocolEncoder = new MessageToByteEncoder() { + @Override + protected void encode(ChannelHandlerContext ctx, Object packet, ByteBuf output) throws Exception { + if (packet instanceof WirePacket) { + // Special case for wire format + NettyChannelInjector.this.encodeWirePacket((WirePacket) packet, new NettyByteBuf(output)); + } else { + NettyChannelInjector.this.encode(ctx, packet, output); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object packet, ChannelPromise promise) throws Exception { + super.write(ctx, packet, promise); + NettyChannelInjector.this.finalWrite(ctx, packet, promise); + } + }; + + // Intercept recieved packets + ChannelInboundHandlerAdapter finishHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + // Execute context first + ctx.fireChannelRead(msg); + NettyChannelInjector.this.finishRead(ctx, msg); + } + }; + + ChannelHandlerAdapter exceptionHandler = new ChannelHandlerAdapter() { + @Override + public void exceptionCaught(ChannelHandlerContext context, Throwable ex) throws Exception { + if (ex instanceof ClosedChannelException) { + // Ignore + } else { + // TODO Actually handle exceptions? + System.err.println("[ProtocolLib] Encountered an uncaught exception in the channel pipeline:"); + ex.printStackTrace(); + } + } + }; + + // Insert our handlers - note that we effectively replace the vanilla encoder/decoder + originalChannel.pipeline().addBefore("decoder", "protocol_lib_decoder", this); + originalChannel.pipeline().addBefore("protocol_lib_decoder", "protocol_lib_finish", finishHandler); + originalChannel.pipeline().addAfter("encoder", "protocol_lib_encoder", protocolEncoder); + originalChannel.pipeline().addLast("protocol_lib_exception_handler", exceptionHandler); + + // Intercept all write methods + channelField.setValue(new NettyChannelProxy(originalChannel, MinecraftReflection.getPacketClass()) { + // Compatibility with Spigot 1.8 + private final NettyPipelineProxy pipelineProxy = new NettyPipelineProxy(originalChannel.pipeline(), this) { + @Override + public ChannelPipeline addBefore(String baseName, String name, ChannelHandler handler) { + // Correct the position of the decoder + if ("decoder".equals(baseName)) { + if (super.get("protocol_lib_decoder") != null && guessCompression(handler)) { + super.addBefore("protocol_lib_decoder", name, handler); + return this; + } + } + + return super.addBefore(baseName, name, handler); + } + }; + + @Override + public ChannelPipeline pipeline() { + return pipelineProxy; + } + + @Override + protected Callable onMessageScheduled(final Callable callable, FieldAccessor packetAccessor) { + final PacketEvent event = handleScheduled(callable, packetAccessor); + + // Handle cancelled events + if (event != null && event.isCancelled()) + return null; + + return new Callable() { + @Override + public T call() throws Exception { + T result = null; + + // This field must only be updated in the pipeline thread + currentEvent = event; + result = callable.call(); + currentEvent = null; + return result; + } + }; + } + + @Override + protected Runnable onMessageScheduled(final Runnable runnable, FieldAccessor packetAccessor) { + final PacketEvent event = handleScheduled(runnable, packetAccessor); + + // Handle cancelled events + if (event != null && event.isCancelled()) + return null; + + return new Runnable() { + @Override + public void run() { + currentEvent = event; + runnable.run(); + currentEvent = null; + } + }; + } + + protected PacketEvent handleScheduled(Object instance, FieldAccessor accessor) { + // Let the filters handle this packet + Object original = accessor.get(instance); + + // See if we've been instructed not to process packets + if (!scheduleProcessPackets.get()) { + NetworkMarker marker = getMarker(original); + + if (marker != null) { + PacketEvent result = new PacketEvent(NettyChannelInjector.class); + result.setNetworkMarker(marker); + return result; + } else { + return BYPASSED_PACKET; + } + } + PacketEvent event = processSending(original); + + if (event != null && !event.isCancelled()) { + Object changed = event.getPacket().getHandle(); + + // Change packet to be scheduled + if (original != changed) + accessor.set(instance, changed); + }; + return event != null ? event : BYPASSED_PACKET; + } + }); + + injected = true; + return true; + } + } + + /** + * Determine if the given object is a compressor or decompressor. + * @param handler - object to test. + * @return TRUE if it is, FALSE if not or unknown. + */ + private boolean guessCompression(ChannelHandler handler) { + String className = handler != null ? handler.getClass().getCanonicalName() : null; + return className.contains("Compressor") || className.contains("Decompressor"); + } + + /** + * Process a given message on the packet listeners. + * @param message - the message/packet. + * @return The resulting message/packet. + */ + private PacketEvent processSending(Object message) { + return channelListener.onPacketSending(NettyChannelInjector.this, message, getMarker(message)); + } + + /** + * This method patches the encoder so that it skips already created packets. + * @param encoder - the encoder to patch. + */ + private void patchEncoder(MessageToByteEncoder encoder) { + if (ENCODER_TYPE_MATCHER == null) { + ENCODER_TYPE_MATCHER = Accessors.getFieldAccessor(encoder.getClass(), "matcher", true); + } + ENCODER_TYPE_MATCHER.set(encoder, TypeParameterMatcher.get(MinecraftReflection.getPacketClass())); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (channelListener.isDebug()) + cause.printStackTrace(); + super.exceptionCaught(ctx, cause); + } + + protected void encodeWirePacket(WirePacket packet, WrappedByteBuf output) throws Exception { + packet.writeId(output); + packet.writeBytes(output); + } + + /** + * Encode a packet to a byte buffer, taking over for the standard Minecraft encoder. + * @param ctx - the current context. + * @param packet - the packet to encode to a byte array. + * @param output - the output byte array. + * @throws Exception If anything went wrong. + */ + protected void encode(ChannelHandlerContext ctx, Object packet, ByteBuf output) throws Exception { + NetworkMarker marker = null; + PacketEvent event = currentEvent; + + try { + // Skip every kind of non-filtered packet + if (!scheduleProcessPackets.get()) { + return; + } + + // This packet has not been seen by the main thread + if (event == null) { + Class clazz = packet.getClass(); + + // Schedule the transmission on the main thread instead + if (channelListener.hasMainThreadListener(clazz)) { + // Delay the packet + scheduleMainThread(packet); + packet = null; + + } else { + event = processSending(packet); + + // Handle the output + if (event != null) { + packet = !event.isCancelled() ? event.getPacket().getHandle() : null; + } + } + } + if (event != null) { + // Retrieve marker without accidentally constructing it + marker = NetworkMarker.getNetworkMarker(event); + } + + // Process output handler + if (packet != null && event != null && NetworkMarker.hasOutputHandlers(marker)) { + ByteBuf packetBuffer = ctx.alloc().buffer(); + ENCODE_BUFFER.invoke(vanillaEncoder, ctx, packet, packetBuffer); + + // Let each handler prepare the actual output + byte[] data = processor.processOutput(event, marker, getBytes(packetBuffer)); + + // Write the result + output.writeBytes(data); + packet = null; + + // Sent listeners? + finalEvent = event; + return; + } + } catch (Exception e) { + channelListener.getReporter().reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_INTERCEPT_SERVER_PACKET).callerParam(packet).error(e).build()); + } finally { + // Attempt to handle the packet nevertheless + if (packet != null) { + ENCODE_BUFFER.invoke(vanillaEncoder, ctx, packet, output); + finalEvent = event; + } + } + } + + /** + * Invoked when a packet has been written to the channel. + * @param ctx - current context. + * @param packet - the packet that has been written. + * @param promise - a promise. + */ + protected void finalWrite(ChannelHandlerContext ctx, Object packet, ChannelPromise promise) { + PacketEvent event = finalEvent; + + if (event != null) { + // Necessary to prevent infinite loops + finalEvent = null; + currentEvent = null; + + processor.invokePostEvent(event, NetworkMarker.getNetworkMarker(event)); + } + } + + private void scheduleMainThread(final Object packetCopy) { + // Don't use BukkitExecutors for this - it has a bit of overhead + Bukkit.getScheduler().scheduleSyncDelayedTask(factory.getPlugin(), new Runnable() { + @Override + public void run() { + invokeSendPacket(packetCopy); + } + }); + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuffer, List packets) throws Exception { + byteBuffer.markReaderIndex(); + DECODE_BUFFER.invoke(vanillaDecoder, ctx, byteBuffer, packets); + + try { + // Reset queue + finishQueue.clear(); + + for (ListIterator it = packets.listIterator(); it.hasNext(); ) { + Object input = it.next(); + Class packetClass = input.getClass(); + NetworkMarker marker = null; + + // Special case! + handleLogin(packetClass, input); + + if (channelListener.includeBuffer(packetClass)) { + byteBuffer.resetReaderIndex(); + marker = new NettyNetworkMarker(ConnectionSide.CLIENT_SIDE, getBytes(byteBuffer)); + } + + PacketEvent output = channelListener.onPacketReceiving(this, input, marker); + + // Handle packet changes + if (output != null) { + if (output.isCancelled()) { + it.remove(); + continue; + } else if (output.getPacket().getHandle() != input) { + it.set(output.getPacket().getHandle()); + } + + finishQueue.addLast(output); + } + } + } catch (Exception e) { + channelListener.getReporter().reportDetailed(this, + Report.newBuilder(REPORT_CANNOT_INTERCEPT_CLIENT_PACKET).callerParam(byteBuffer).error(e).build()); + } + } + + /** + * Invoked after our decoder. + * @param ctx - current context. + * @param msg - the current packet. + */ + protected void finishRead(ChannelHandlerContext ctx, Object msg) { + // Assume same order + PacketEvent event = finishQueue.pollFirst(); + + if (event != null) { + NetworkMarker marker = NetworkMarker.getNetworkMarker(event); + + if (marker != null) { + processor.invokePostEvent(event, marker); + } + } + } + + /** + * Invoked when we may need to handle the login packet. + * @param packetClass - the packet class. + * @param packet - the packet. + */ + protected void handleLogin(Class packetClass, Object packet) { + Class loginClass = PACKET_LOGIN_CLIENT; + FieldAccessor loginClient = LOGIN_GAME_PROFILE; + + // Initialize packet class and login + if (loginClass == null) { + loginClass = PacketType.Login.Client.START.getPacketClass(); + PACKET_LOGIN_CLIENT = loginClass; + } + if (loginClient == null) { + loginClient = Accessors.getFieldAccessor(PACKET_LOGIN_CLIENT, MinecraftReflection.getGameProfileClass(), true); + LOGIN_GAME_PROFILE = loginClient; + } + + // See if we are dealing with the login packet + if (loginClass.equals(packetClass)) { + // GameProfile profile = (GameProfile) loginClient.get(packet); + WrappedGameProfile profile = WrappedGameProfile.fromHandle(loginClient.get(packet)); + + // Save the channel injector + factory.cacheInjector(profile.getName(), this); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + + // See NetworkManager.channelActive(ChannelHandlerContext) for why + if (channelField != null) { + channelField.refreshValue(); + } + } + + /** + * Retrieve every byte in the given byte buffer. + * @param buffer - the buffer. + * @return The bytes. + */ + private byte[] getBytes(ByteBuf buffer) { + byte[] data = new byte[buffer.readableBytes()]; + + buffer.readBytes(data); + return data; + } + + /** + * Disconnect the current player. + * @param message - the disconnect message, if possible. + */ + private void disconnect(String message) { + // If we're logging in, we can only close the channel + if (playerConnection == null || player instanceof Factory) { + originalChannel.disconnect(); + } else { + // Call the disconnect method + try { + MinecraftMethods.getDisconnectMethod(playerConnection.getClass()). + invoke(playerConnection, message); + } catch (Exception e) { + throw new IllegalArgumentException("Unable to invoke disconnect method.", e); + } + } + } + + @Override + public void sendServerPacket(Object packet, NetworkMarker marker, boolean filtered) { + saveMarker(packet, marker); + + try { + scheduleProcessPackets.set(filtered); + invokeSendPacket(packet); + } finally { + scheduleProcessPackets.set(true); + } + } + + /** + * Invoke the sendPacket method in Minecraft. + * @param packet - the packet to send. + */ + private void invokeSendPacket(Object packet) { + // Attempt to send the packet with NetworkMarker.handle(), or the PlayerConnection if its active + try { + if (player instanceof Factory) { + MinecraftMethods.getNetworkManagerHandleMethod().invoke(networkManager, packet, new GenericFutureListener[0]); + } else { + MinecraftMethods.getSendPacketMethod().invoke(getPlayerConnection(), packet); + } + } catch (Throwable ex) { + ProtocolLibrary.getErrorReporter().reportWarning(factory.getPlugin(), + Report.newBuilder(REPORT_CANNOT_SEND_PACKET).messageParam(packet, playerName).error(ex).build()); + } + } + + @Override + public void recieveClientPacket(final Object packet) { + // TODO: Ensure the packet listeners are executed in the channel thread. + + // Execute this in the channel thread + Runnable action = new Runnable() { + @Override + public void run() { + try { + MinecraftMethods.getNetworkManagerReadPacketMethod().invoke(networkManager, null, packet); + } catch (Exception e) { + // Inform the user + ProtocolLibrary.getErrorReporter().reportMinimal(factory.getPlugin(), "recieveClientPacket", e); + } + } + }; + + // Execute in the worker thread + if (originalChannel.eventLoop().inEventLoop()) { + action.run(); + } else { + originalChannel.eventLoop().execute(action); + } + } + + @Override + public Protocol getCurrentProtocol() { + if (PROTOCOL_ACCESSOR == null) { + PROTOCOL_ACCESSOR = Accessors.getFieldAccessor( + networkManager.getClass(), MinecraftReflection.getEnumProtocolClass(), true); + } + return Protocol.fromVanilla((Enum) PROTOCOL_ACCESSOR.get(networkManager)); + } + + /** + * Retrieve the player connection of the current player. + * @return The player connection. + */ + private Object getPlayerConnection() { + if (playerConnection == null) { + playerConnection = MinecraftFields.getPlayerConnection(player); + } + return playerConnection; + } + + @Override + public NetworkMarker getMarker(Object packet) { + return packetMarker.get(packet); + } + + @Override + public void saveMarker(Object packet, NetworkMarker marker) { + if (marker != null) { + packetMarker.put(packet, marker); + } + } + + @Override + public Player getPlayer() { + if (player == null && playerName != null) { + return Bukkit.getPlayer(playerName); + } + + return player; + } + + /** + * Set the player instance. + * @param player - current instance. + */ + @Override + public void setPlayer(Player player) { + this.player = player; + this.playerName = player.getName(); + } + + /** + * Set the updated player instance. + * @param updated - updated instance. + */ + @Override + public void setUpdatedPlayer(Player updated) { + this.updated = updated; + this.playerName = updated.getName(); + } + + @Override + public boolean isInjected() { + return injected; + } + + /** + * Determine if this channel has been closed and cleaned up. + * @return TRUE if it has, FALSE otherwise. + */ + @Override + public boolean isClosed() { + return closed; + } + + @Override + public void close() { + if (!closed) { + closed = true; + + if (injected) { + channelField.revertValue(); + + // Calling remove() in the main thread will block the main thread, which may lead + // to a deadlock: + // http://pastebin.com/L3SBVKzp + // + // ProtocolLib executes this close() method through a PlayerQuitEvent in the main thread, + // which has implicitly aquired a lock on SimplePluginManager (see SimplePluginManager.callEvent(Event)). + // Unfortunately, the remove() method will schedule the removal on one of the Netty worker threads if + // it's called from a different thread, blocking until the removal has been confirmed. + // + // This is bad enough (Rule #1: Don't block the main thread), but the real trouble starts if the same + // worker thread happens to be handling a server ping connection when this removal task is scheduled. + // In that case, it may attempt to invoke an asynchronous ServerPingEvent (see PacketStatusListener) + // using SimplePluginManager.callEvent(). But, since this has already been locked by the main thread, + // we end up with a deadlock. The main thread is waiting for the worker thread to process the task, and + // the worker thread is waiting for the main thread to finish executing PlayerQuitEvent. + // + // TLDR: Concurrency is hard. + executeInChannelThread(new Runnable() { + @Override + public void run() { + String[] handlers = new String[] { + "protocol_lib_decoder", "protocol_lib_finish", "protocol_lib_encoder", "protocol_lib_exception_handler" + }; + + for (String handler : handlers) { + try { + originalChannel.pipeline().remove(handler); + } catch (NoSuchElementException e) { + // Ignore + } + } + } + }); + + // Clear cache + factory.invalidate(player); + + // Clear player instances + // Should help fix memory leaks + this.player = null; + this.updated = null; + } + } + } + + /** + * Execute a specific command in the channel thread. + *

+ * Exceptions are printed through the standard error reporter mechanism. + * @param command - the command to execute. + */ + private void executeInChannelThread(final Runnable command) { + originalChannel.eventLoop().execute(new Runnable() { + @Override + public void run() { + try { + command.run(); + } catch (Exception e) { + ProtocolLibrary.getErrorReporter().reportDetailed(NettyChannelInjector.this, + Report.newBuilder(REPORT_CANNOT_EXECUTE_IN_CHANNEL_THREAD).error(e).build()); + } + } + }); + } + + /** + * Find the first channel handler that is assignable to a given type. + * @param channel - the channel. + * @param clazz - the type. + * @return The first handler, or NULL. + */ + public static ChannelHandler findChannelHandler(Channel channel, Class clazz) { + for (Entry entry : channel.pipeline()) { + if (clazz.isAssignableFrom(entry.getValue().getClass())) { + return entry.getValue(); + } + } + return null; + } + + /** + * Represents a socket injector that foreards to the current channel injector. + * @author Kristian + */ + public static class ChannelSocketInjector implements SocketInjector { + private final NettyChannelInjector injector; + + public ChannelSocketInjector(NettyChannelInjector injector) { + this.injector = Preconditions.checkNotNull(injector, "injector cannot be NULL"); + } + + @Override + public Socket getSocket() throws IllegalAccessException { + return NettySocketAdapter.adapt((SocketChannel) injector.originalChannel); + } + + @Override + public SocketAddress getAddress() throws IllegalAccessException { + return injector.originalChannel.remoteAddress(); + } + + @Override + public void disconnect(String message) throws InvocationTargetException { + injector.disconnect(message); + } + + @Override + public void sendServerPacket(Object packet, NetworkMarker marker, boolean filtered) throws InvocationTargetException { + injector.sendServerPacket(packet, marker, filtered); + } + + @Override + public Player getPlayer() { + return injector.getPlayer(); + } + + @Override + public Player getUpdatedPlayer() { + return injector.updated; + } + + @Override + public void transferState(SocketInjector delegate) { + // Do nothing + } + + @Override + public void setUpdatedPlayer(Player updatedPlayer) { + injector.setPlayer(updatedPlayer); + } + + public NettyChannelInjector getChannelInjector() { + return injector; + } + } + + @Override + public WrappedChannel getChannel() { + return new NettyChannel(originalChannel); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyChannelProxy.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyChannelProxy.java new file mode 100644 index 00000000..dacae10a --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyChannelProxy.java @@ -0,0 +1,333 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelConfig; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelMetadata; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelProgressivePromise; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoop; +import io.netty.util.Attribute; +import io.netty.util.AttributeKey; + +import java.lang.reflect.Field; +import java.net.SocketAddress; +import java.util.Map; +import java.util.concurrent.Callable; + +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.google.common.collect.Maps; + +public abstract class NettyChannelProxy implements Channel { + // Mark that a certain object does not contain a message field + private static final FieldAccessor MARK_NO_MESSAGE = new FieldAccessor() { + @Override + public void set(Object instance, Object value) { } + @Override + public Object get(Object instance) { return null; } + @Override + public Field getField() { return null; }; + }; + + // Looking up packets in inner classes + private static Map, FieldAccessor> MESSAGE_LOOKUP = Maps.newConcurrentMap(); + + // The underlying channel + protected Channel delegate; + protected Class messageClass; + + // Event loop proxy + private transient NettyEventLoopProxy loopProxy; + + public NettyChannelProxy(Channel delegate, Class messageClass) { + this.delegate = delegate; + this.messageClass = messageClass; + } + + /** + * Invoked when a packet is scheduled for transmission in the event loop. + * @param callable - callable to schedule for execution. + * @param packetAccessor - accessor for modifying the packet in the callable. + * @return The callable that will be scheduled, or NULL to cancel. + */ + protected abstract Callable onMessageScheduled(Callable callable, FieldAccessor packetAccessor); + + /** + * Invoked when a packet is scheduled for transmission in the event loop. + * @param runnable - the runnable that contains a packet to be scheduled. + * @param packetAccessor - accessor for modifying the packet in the runnable. + * @return The runnable that will be scheduled, or NULL to cancel. + */ + protected abstract Runnable onMessageScheduled(Runnable runnable, FieldAccessor packetAccessor); + + @Override + public Attribute attr(AttributeKey paramAttributeKey) { + return delegate.attr(paramAttributeKey); + } + + @Override + public ChannelFuture bind(SocketAddress paramSocketAddress) { + return delegate.bind(paramSocketAddress); + } + + @Override + public ChannelPipeline pipeline() { + return delegate.pipeline(); + } + + @Override + public ChannelFuture connect(SocketAddress paramSocketAddress) { + return delegate.connect(paramSocketAddress); + } + + @Override + public ByteBufAllocator alloc() { + return delegate.alloc(); + } + + @Override + public ChannelPromise newPromise() { + return delegate.newPromise(); + } + + @Override + public EventLoop eventLoop() { + if (loopProxy == null) { + loopProxy = new NettyEventLoopProxy() { + @Override + protected EventLoop getDelegate() { + return delegate.eventLoop(); + } + + @Override + protected Runnable schedulingRunnable(final Runnable runnable) { + final FieldAccessor accessor = getMessageAccessor(runnable); + + if (accessor != null) { + Runnable result = onMessageScheduled(runnable, accessor);; + return result != null ? result : getEmptyRunnable(); + } + return runnable; + } + + @Override + protected Callable schedulingCallable(Callable callable) { + FieldAccessor accessor = getMessageAccessor(callable); + + if (accessor != null) { + Callable result = onMessageScheduled(callable, accessor);; + return result != null ? result : NettyEventLoopProxy.getEmptyCallable(); + } + return callable; + } + }; + } + return loopProxy; + } + + /** + * Retrieve a way to access the packet field of an object. + * @param value - the object. + * @return The packet field accessor, or NULL if not found. + */ + private FieldAccessor getMessageAccessor(Object value) { + Class clazz = value.getClass(); + FieldAccessor accessor = MESSAGE_LOOKUP.get(clazz); + + if (accessor == null) { + try { + accessor = Accessors.getFieldAccessor(clazz, messageClass, true); + } catch (IllegalArgumentException e) { + accessor = MARK_NO_MESSAGE; + } + // Save the result + MESSAGE_LOOKUP.put(clazz, accessor); + } + return accessor != MARK_NO_MESSAGE ? accessor : null; + } + + @Override + public ChannelFuture connect(SocketAddress paramSocketAddress1, + SocketAddress paramSocketAddress2) { + return delegate.connect(paramSocketAddress1, paramSocketAddress2); + } + + @Override + public ChannelProgressivePromise newProgressivePromise() { + return delegate.newProgressivePromise(); + } + + @Override + public Channel parent() { + return delegate.parent(); + } + + @Override + public ChannelConfig config() { + return delegate.config(); + } + + @Override + public ChannelFuture newSucceededFuture() { + return delegate.newSucceededFuture(); + } + + @Override + public boolean isOpen() { + return delegate.isOpen(); + } + + @Override + public ChannelFuture disconnect() { + return delegate.disconnect(); + } + + @Override + public boolean isRegistered() { + return delegate.isRegistered(); + } + + @Override + public ChannelFuture newFailedFuture(Throwable paramThrowable) { + return delegate.newFailedFuture(paramThrowable); + } + + @Override + public ChannelFuture close() { + return delegate.close(); + } + + @Override + public boolean isActive() { + return delegate.isActive(); + } + + @Override + @Deprecated + public ChannelFuture deregister() { + return delegate.deregister(); + } + + @Override + public ChannelPromise voidPromise() { + return delegate.voidPromise(); + } + + @Override + public ChannelMetadata metadata() { + return delegate.metadata(); + } + + @Override + public ChannelFuture bind(SocketAddress paramSocketAddress, + ChannelPromise paramChannelPromise) { + return delegate.bind(paramSocketAddress, paramChannelPromise); + } + + @Override + public SocketAddress localAddress() { + return delegate.localAddress(); + } + + @Override + public SocketAddress remoteAddress() { + return delegate.remoteAddress(); + } + + @Override + public ChannelFuture connect(SocketAddress paramSocketAddress, + ChannelPromise paramChannelPromise) { + return delegate.connect(paramSocketAddress, paramChannelPromise); + } + + @Override + public ChannelFuture closeFuture() { + return delegate.closeFuture(); + } + + @Override + public boolean isWritable() { + return delegate.isWritable(); + } + + @Override + public Channel flush() { + return delegate.flush(); + } + + @Override + public ChannelFuture connect(SocketAddress paramSocketAddress1, + SocketAddress paramSocketAddress2, ChannelPromise paramChannelPromise) { + return delegate.connect(paramSocketAddress1, paramSocketAddress2, paramChannelPromise); + } + + @Override + public Channel read() { + return delegate.read(); + } + + @Override + public Unsafe unsafe() { + return delegate.unsafe(); + } + + @Override + public ChannelFuture disconnect(ChannelPromise paramChannelPromise) { + return delegate.disconnect(paramChannelPromise); + } + + @Override + public ChannelFuture close(ChannelPromise paramChannelPromise) { + return delegate.close(paramChannelPromise); + } + + @Override + @Deprecated + public ChannelFuture deregister(ChannelPromise paramChannelPromise) { + return delegate.deregister(paramChannelPromise); + } + + @Override + public ChannelFuture write(Object paramObject) { + return delegate.write(paramObject); + } + + @Override + public ChannelFuture write(Object paramObject, ChannelPromise paramChannelPromise) { + return delegate.write(paramObject, paramChannelPromise); + } + + @Override + public ChannelFuture writeAndFlush(Object paramObject, ChannelPromise paramChannelPromise) { + return delegate.writeAndFlush(paramObject, paramChannelPromise); + } + + @Override + public ChannelFuture writeAndFlush(Object paramObject) { + return delegate.writeAndFlush(paramObject); + } + + @Override + public int compareTo(Channel o) { + return delegate.compareTo(o); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyEventLoopProxy.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyEventLoopProxy.java new file mode 100644 index 00000000..190d2af7 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyEventLoopProxy.java @@ -0,0 +1,259 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.ProgressivePromise; +import io.netty.util.concurrent.Promise; +import io.netty.util.concurrent.ScheduledFuture; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * An event loop proxy. + * @author Kristian. + */ +abstract class NettyEventLoopProxy implements EventLoop { + private static final Runnable EMPTY_RUNNABLE = new Runnable() { + @Override + public void run() { + // Do nothing + } + }; + private static final Callable EMPTY_CALLABLE = new Callable() { + @Override + public Object call() throws Exception { + return null; + }; + }; + + /** + * Retrieve the underlying event loop. + * @return The event loop. + */ + protected abstract EventLoop getDelegate(); + + /** + * Retrieve a callable that does nothing but return NULL. + * @return The empty callable. + */ + @SuppressWarnings("unchecked") + public static Callable getEmptyCallable() { + return (Callable) EMPTY_CALLABLE; + } + + /** + * Retrieve a runnable that does nothing. + * @return A NO-OP runnable. + */ + public static Runnable getEmptyRunnable() { + return EMPTY_RUNNABLE; + } + + /** + * Invoked when a runnable is being scheduled. + * @param runnable - the runnable that is scheduling. + * @return The runnable to schedule instead. Cannot be NULL. + */ + protected abstract Runnable schedulingRunnable(Runnable runnable); + + /** + * Invoked when a callable is being scheduled. + * @param runnable - the callable that is scheduling. + * @return The callable to schedule instead. Cannot be NULL. + */ + protected abstract Callable schedulingCallable(Callable callable); + + @Override + public void execute(Runnable command) { + getDelegate().execute(schedulingRunnable(command)); + } + + @Override + public Future submit(Callable action) { + return getDelegate().submit(schedulingCallable(action)); + } + + @Override + public Future submit(Runnable action, T arg1) { + return getDelegate().submit(schedulingRunnable(action), arg1); + } + + @Override + public Future submit(Runnable action) { + return getDelegate().submit(schedulingRunnable(action)); + } + + @Override + public ScheduledFuture schedule(Callable action, long arg1, TimeUnit arg2) { + return getDelegate().schedule(schedulingCallable(action), arg1, arg2); + } + + @Override + public ScheduledFuture schedule(Runnable action, long arg1, TimeUnit arg2) { + return getDelegate().schedule(schedulingRunnable(action), arg1, arg2); + } + + @Override + public ScheduledFuture scheduleAtFixedRate(Runnable action, long arg1, long arg2, TimeUnit arg3) { + return getDelegate().scheduleAtFixedRate(schedulingRunnable(action), arg1, arg2, arg3); + } + + @Override + public ScheduledFuture scheduleWithFixedDelay(Runnable action, long arg1, long arg2, TimeUnit arg3) { + return getDelegate().scheduleWithFixedDelay(schedulingRunnable(action), arg1, arg2, arg3); + } + + // Boiler plate: + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return getDelegate().awaitTermination(timeout, unit); + } + + @Override + public boolean inEventLoop() { + return getDelegate().inEventLoop(); + } + + @Override + public boolean inEventLoop(Thread arg0) { + return getDelegate().inEventLoop(arg0); + } + + @Override + public boolean isShutdown() { + return getDelegate().isShutdown(); + } + + @Override + public boolean isTerminated() { + return getDelegate().isTerminated(); + } + + @Override + public List> invokeAll(Collection> tasks) + throws InterruptedException { + return getDelegate().invokeAll(tasks); + } + + @Override + public List> invokeAll(Collection> tasks, long timeout, + TimeUnit unit) throws InterruptedException { + return getDelegate().invokeAll(tasks, timeout, unit); + } + + @Override + public T invokeAny(Collection> tasks) throws InterruptedException, + ExecutionException { + return getDelegate().invokeAny(tasks); + } + + @Override + public T invokeAny(Collection> tasks, long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException { + return getDelegate().invokeAny(tasks, timeout, unit); + } + + @Override + public boolean isShuttingDown() { + return getDelegate().isShuttingDown(); + } + + @Override + public Iterator iterator() { + return getDelegate().iterator(); + } + + @Override + public Future newFailedFuture(Throwable arg0) { + return getDelegate().newFailedFuture(arg0); + } + + @Override + public EventLoop next() { + return ((EventLoopGroup) getDelegate()).next(); + } + + @Override + public ProgressivePromise newProgressivePromise() { + return getDelegate().newProgressivePromise(); + } + + @Override + public Promise newPromise() { + return getDelegate().newPromise(); + } + + @Override + public Future newSucceededFuture(V arg0) { + return getDelegate().newSucceededFuture(arg0); + } + + @Override + public EventLoopGroup parent() { + return getDelegate().parent(); + } + + @Override + public ChannelFuture register(Channel arg0, ChannelPromise arg1) { + return getDelegate().register(arg0, arg1); + } + + @Override + public ChannelFuture register(Channel arg0) { + return getDelegate().register(arg0); + } + + @Override + public Future shutdownGracefully() { + return getDelegate().shutdownGracefully(); + } + + @Override + public Future shutdownGracefully(long arg0, long arg1, TimeUnit arg2) { + return getDelegate().shutdownGracefully(arg0, arg1, arg2); + } + + @Override + public Future terminationFuture() { + return getDelegate().terminationFuture(); + } + + @Override + @Deprecated + public void shutdown() { + getDelegate().shutdown(); + } + + @Override + @Deprecated + public List shutdownNow() { + return getDelegate().shutdownNow(); + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyInjectionFactory.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyInjectionFactory.java new file mode 100644 index 00000000..ea9115c8 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyInjectionFactory.java @@ -0,0 +1,239 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.channel.Channel; + +import java.util.concurrent.ConcurrentMap; + +import javax.annotation.Nonnull; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.compat.netty.independent.NettyChannelInjector.ChannelSocketInjector; +import com.comphenix.protocol.injector.netty.ChannelListener; +import com.comphenix.protocol.injector.netty.ClosedInjector; +import com.comphenix.protocol.injector.netty.Injector; +import com.comphenix.protocol.injector.server.SocketInjector; +import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.utility.MinecraftFields; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.collect.MapMaker; + +/** + * Represents an injector factory. + *

+ * Note that the factory will return {@link ClosedInjector} when the factory is closed. + * @author Kristian + */ +public class NettyInjectionFactory { + // This should work as long as the injectors are, uh, injected + private final ConcurrentMap playerLookup = new MapMaker().weakKeys().weakValues().makeMap(); + private final ConcurrentMap nameLookup = new MapMaker().weakValues().makeMap(); + + // Whether or not the factory is closed + private volatile boolean closed; + + // The current plugin + private final Plugin plugin; + + public NettyInjectionFactory(Plugin plugin) { + this.plugin = plugin; + } + + /** + * Retrieve the main plugin associated with this injection factory. + * @return The main plugin. + */ + public Plugin getPlugin() { + return plugin; + } + + /** + * Construct or retrieve a channel injector from an existing Bukkit player. + * @param player - the existing Bukkit player. + * @param channelListener - the listener. + * @return A new injector, an existing injector associated with this player, or a closed injector. + */ + @Nonnull + public Injector fromPlayer(Player player, ChannelListener listener) { + if (closed) + return new ClosedInjector(player); + Injector injector = playerLookup.get(player); + + // Find a temporary injector as well + if (injector == null) + injector = getTemporaryInjector(player); + if (injector != null && !injector.isClosed()) + return injector; + + Object networkManager = MinecraftFields.getNetworkManager(player); + + // Must be a temporary Bukkit player + if (networkManager == null) { + return fromName(player.getName(), player); + } + Channel channel = FuzzyReflection.getFieldValue(networkManager, Channel.class, true); + + // See if a channel has already been created + injector = (NettyChannelInjector) NettyChannelInjector.findChannelHandler(channel, NettyChannelInjector.class); + + if (injector != null) { + // Update the player instance + playerLookup.remove(injector.getPlayer()); + injector.setPlayer(player); + } else { + injector = new NettyChannelInjector(player, networkManager, channel, listener, this); + } + + // Cache injector and return + cacheInjector(player, injector); + return injector; + } + + /** + * Retrieve a cached injector from a name. + *

+ * The injector may be NULL if the plugin has been reloaded during a player login. + * @param address - the name. + * @return The cached injector, or a closed injector if it could not be found. + */ + public Injector fromName(String name, Player player) { + if (!closed) { + Injector injector = nameLookup.get(name); + + // We can only retrieve cached injectors + if (injector != null) { + // Update instance + injector.setUpdatedPlayer(player); + return injector; + } + } + return new ClosedInjector(player); + } + + /** + * Construct a new channel injector for the given channel. + * @param channel - the channel. + * @param playerFactory - a temporary player creator. + * @param channelListener - the listener. + * @param loader - the current (plugin) class loader. + * @return The channel injector, or a closed injector. + */ + @Nonnull + public Injector fromChannel(Channel channel, ChannelListener listener, TemporaryPlayerFactory playerFactory) { + if (closed) + return new ClosedInjector(null); + + Object networkManager = findNetworkManager(channel); + Player temporaryPlayer = playerFactory.createTemporaryPlayer(Bukkit.getServer()); + NettyChannelInjector injector = new NettyChannelInjector(temporaryPlayer, networkManager, channel, listener, this); + + // Initialize temporary player + TemporaryPlayerFactory.setInjectorInPlayer(temporaryPlayer, new ChannelSocketInjector(injector)); + return injector; + } + + /** + * Invalidate a cached injector. + * @param player - the associated player. + * @return The cached injector, or NULL if nothing was cached. + */ + public Injector invalidate(Player player) { + Injector injector = playerLookup.remove(player); + + nameLookup.remove(player.getName()); + return injector; + } + + /** + * Cache an injector by player. + * @param player - the player. + * @param injector - the injector to cache. + * @return The previously cached injector. + */ + public Injector cacheInjector(Player player, Injector injector) { + nameLookup.put(player.getName(), injector); + return playerLookup.put(player, injector); + } + + /** + * Cache an injector by name alone. + * @param name - the name to lookup. + * @param injector - the injector. + * @return The cached injector. + */ + public Injector cacheInjector(String name, Injector injector) { + return nameLookup.put(name, injector); + } + + /** + * Retrieve the associated channel injector. + * @param player - the temporary player, or normal Bukkit player. + * @return The associated injector, or NULL if this is a Bukkit player. + */ + private NettyChannelInjector getTemporaryInjector(Player player) { + SocketInjector injector = TemporaryPlayerFactory.getInjectorFromPlayer(player); + + if (injector != null) { + return ((ChannelSocketInjector) injector).getChannelInjector(); + } + return null; + } + + /** + * Find the network manager in a channel's pipeline. + * @param channel - the channel. + * @return The network manager. + */ + private Object findNetworkManager(Channel channel) { + // Find the network manager + Object networkManager = NettyChannelInjector.findChannelHandler(channel, MinecraftReflection.getNetworkManagerClass()); + + if (networkManager != null) + return networkManager; + throw new IllegalArgumentException("Unable to find NetworkManager in " + channel); + } + + /** + * Determine if the factory is closed. + *

+ * If it is, all new injectors will be closed by default. + * @return TRUE if it is closed, FALSE otherwise. + */ + public boolean isClosed() { + return closed; + } + + /** + * Close all injectors created by this factory, and cease the creation of new injections. + */ + public synchronized void close() { + if (!closed) { + closed = true; + + // Close everything + for (Injector injector : playerLookup.values()) + injector.close(); + for (Injector injector : nameLookup.values()) + injector.close(); + } + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyPipelineProxy.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyPipelineProxy.java new file mode 100644 index 00000000..f5c5c6c1 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyPipelineProxy.java @@ -0,0 +1,371 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.util.concurrent.EventExecutorGroup; + +import java.net.SocketAddress; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +/** + * A pipeline proxy. + * @author Kristian + */ +public class NettyPipelineProxy implements ChannelPipeline { + protected final ChannelPipeline pipeline; + protected final Channel channel; + + public NettyPipelineProxy(ChannelPipeline pipeline, Channel channel) { + this.pipeline = pipeline; + this.channel = channel; + } + + @Override + public ChannelPipeline addAfter(EventExecutorGroup arg0, String arg1, String arg2, ChannelHandler arg3) { + pipeline.addAfter(arg0, arg1, arg2, arg3); + return this; + } + + @Override + public ChannelPipeline addAfter(String arg0, String arg1, ChannelHandler arg2) { + pipeline.addAfter(arg0, arg1, arg2); + return this; + } + + @Override + public ChannelPipeline addBefore(EventExecutorGroup arg0, String arg1, String arg2, ChannelHandler arg3) { + pipeline.addBefore(arg0, arg1, arg2, arg3); + return this; + } + + @Override + public ChannelPipeline addBefore(String arg0, String arg1, ChannelHandler arg2) { + pipeline.addBefore(arg0, arg1, arg2); + return this; + } + + @Override + public ChannelPipeline addFirst(ChannelHandler... arg0) { + pipeline.addFirst(arg0); + return this; + } + + @Override + public ChannelPipeline addFirst(EventExecutorGroup arg0, ChannelHandler... arg1) { + pipeline.addFirst(arg0, arg1); + return this; + } + + @Override + public ChannelPipeline addFirst(EventExecutorGroup arg0, String arg1, ChannelHandler arg2) { + pipeline.addFirst(arg0, arg1, arg2); + return this; + } + + @Override + public ChannelPipeline addFirst(String arg0, ChannelHandler arg1) { + pipeline.addFirst(arg0, arg1); + return this; + } + + @Override + public ChannelPipeline addLast(ChannelHandler... arg0) { + pipeline.addLast(arg0); + return this; + } + + @Override + public ChannelPipeline addLast(EventExecutorGroup arg0, ChannelHandler... arg1) { + pipeline.addLast(arg0, arg1); + return this; + } + + @Override + public ChannelPipeline addLast(EventExecutorGroup arg0, String arg1, ChannelHandler arg2) { + pipeline.addLast(arg0, arg1, arg2); + return this; + } + + @Override + public ChannelPipeline addLast(String arg0, ChannelHandler arg1) { + pipeline.addLast(arg0, arg1); + return this; + } + + @Override + public ChannelFuture bind(SocketAddress arg0, ChannelPromise arg1) { + return pipeline.bind(arg0, arg1); + } + + @Override + public ChannelFuture bind(SocketAddress arg0) { + return pipeline.bind(arg0); + } + + @Override + public Channel channel() { + return channel; + } + + @Override + public ChannelFuture close() { + return pipeline.close(); + } + + @Override + public ChannelFuture close(ChannelPromise arg0) { + return pipeline.close(arg0); + } + + @Override + public ChannelFuture connect(SocketAddress arg0, ChannelPromise arg1) { + return pipeline.connect(arg0, arg1); + } + + @Override + public ChannelFuture connect(SocketAddress arg0, SocketAddress arg1, ChannelPromise arg2) { + return pipeline.connect(arg0, arg1, arg2); + } + + @Override + public ChannelFuture connect(SocketAddress arg0, SocketAddress arg1) { + return pipeline.connect(arg0, arg1); + } + + @Override + public ChannelFuture connect(SocketAddress arg0) { + return pipeline.connect(arg0); + } + + @Override + public ChannelHandlerContext context(ChannelHandler arg0) { + return pipeline.context(arg0); + } + + @Override + public ChannelHandlerContext context(Class arg0) { + return pipeline.context(arg0); + } + + @Override + public ChannelHandlerContext context(String arg0) { + return pipeline.context(arg0); + } + + // We have to call the depreciated methods to properly implement the proxy + @Override + public ChannelFuture deregister() { + return pipeline.deregister(); + } + + @Override + public ChannelFuture deregister(ChannelPromise arg0) { + return pipeline.deregister(arg0); + } + + @Override + public ChannelPipeline fireChannelUnregistered() { + pipeline.fireChannelUnregistered(); + return this; + } + + @Override + public ChannelFuture disconnect() { + return pipeline.disconnect(); + } + + @Override + public ChannelFuture disconnect(ChannelPromise arg0) { + return pipeline.disconnect(arg0); + } + + @Override + public ChannelPipeline fireChannelActive() { + pipeline.fireChannelActive(); + return this; + } + + @Override + public ChannelPipeline fireChannelInactive() { + pipeline.fireChannelInactive(); + return this; + } + + @Override + public ChannelPipeline fireChannelRead(Object arg0) { + pipeline.fireChannelRead(arg0); + return this; + } + + @Override + public ChannelPipeline fireChannelReadComplete() { + pipeline.fireChannelReadComplete(); + return this; + } + + @Override + public ChannelPipeline fireChannelRegistered() { + pipeline.fireChannelRegistered(); + return this; + } + + @Override + public ChannelPipeline fireChannelWritabilityChanged() { + pipeline.fireChannelWritabilityChanged(); + return this; + } + + @Override + public ChannelPipeline fireExceptionCaught(Throwable arg0) { + pipeline.fireExceptionCaught(arg0); + return this; + } + + @Override + public ChannelPipeline fireUserEventTriggered(Object arg0) { + pipeline.fireUserEventTriggered(arg0); + return this; + } + + @Override + public ChannelHandler first() { + return pipeline.first(); + } + + @Override + public ChannelHandlerContext firstContext() { + return pipeline.firstContext(); + } + + @Override + public ChannelPipeline flush() { + pipeline.flush(); + return this; + } + + @Override + public T get(Class arg0) { + return pipeline.get(arg0); + } + + @Override + public ChannelHandler get(String arg0) { + return pipeline.get(arg0); + } + + @Override + public Iterator> iterator() { + return pipeline.iterator(); + } + + @Override + public ChannelHandler last() { + return pipeline.last(); + } + + @Override + public ChannelHandlerContext lastContext() { + return pipeline.lastContext(); + } + + @Override + public List names() { + return pipeline.names(); + } + + @Override + public ChannelPipeline read() { + pipeline.read(); + return this; + } + + @Override + public ChannelPipeline remove(ChannelHandler arg0) { + pipeline.remove(arg0); + return this; + } + + @Override + public T remove(Class arg0) { + return pipeline.remove(arg0); + } + + @Override + public ChannelHandler remove(String arg0) { + return pipeline.remove(arg0); + } + + @Override + public ChannelHandler removeFirst() { + return pipeline.removeFirst(); + } + + @Override + public ChannelHandler removeLast() { + return pipeline.removeLast(); + } + + @Override + public ChannelPipeline replace(ChannelHandler arg0, String arg1, ChannelHandler arg2) { + pipeline.replace(arg0, arg1, arg2); + return this; + } + + @Override + public T replace(Class arg0, String arg1, ChannelHandler arg2) { + return pipeline.replace(arg0, arg1, arg2); + } + + @Override + public ChannelHandler replace(String arg0, String arg1, ChannelHandler arg2) { + return pipeline.replace(arg0, arg1, arg2); + } + + @Override + public Map toMap() { + return pipeline.toMap(); + } + + @Override + public ChannelFuture write(Object arg0, ChannelPromise arg1) { + return pipeline.write(arg0, arg1); + } + + @Override + public ChannelFuture write(Object arg0) { + return pipeline.write(arg0); + } + + @Override + public ChannelFuture writeAndFlush(Object arg0, ChannelPromise arg1) { + return pipeline.writeAndFlush(arg0, arg1); + } + + @Override + public ChannelFuture writeAndFlush(Object arg0) { + return pipeline.writeAndFlush(arg0); + } +} \ No newline at end of file diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyProtocolInjector.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyProtocolInjector.java new file mode 100644 index 00000000..90a880e2 --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettyProtocolInjector.java @@ -0,0 +1,438 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandler; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; + +import java.io.InputStream; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Set; + +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.compat.netty.ChannelInjector; +import com.comphenix.protocol.compat.netty.ProtocolInjector; +import com.comphenix.protocol.compat.netty.WrappedChannel; +import com.comphenix.protocol.concurrency.PacketTypeSet; +import com.comphenix.protocol.error.ErrorReporter; +import com.comphenix.protocol.error.Report; +import com.comphenix.protocol.error.ReportType; +import com.comphenix.protocol.events.ConnectionSide; +import com.comphenix.protocol.events.ListenerOptions; +import com.comphenix.protocol.events.NetworkMarker; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.events.PacketEvent; +import com.comphenix.protocol.injector.ListenerInvoker; +import com.comphenix.protocol.injector.netty.ChannelListener; +import com.comphenix.protocol.injector.netty.Injector; +import com.comphenix.protocol.injector.netty.NettyNetworkMarker; +import com.comphenix.protocol.injector.packet.PacketInjector; +import com.comphenix.protocol.injector.packet.PacketRegistry; +import com.comphenix.protocol.injector.player.PlayerInjectionHandler; +import com.comphenix.protocol.injector.server.TemporaryPlayerFactory; +import com.comphenix.protocol.injector.spigot.AbstractPacketInjector; +import com.comphenix.protocol.injector.spigot.AbstractPlayerHandler; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.VolatileField; +import com.comphenix.protocol.utility.MinecraftReflection; +import com.google.common.collect.Lists; + +public class NettyProtocolInjector implements ProtocolInjector { + public static final ReportType REPORT_CANNOT_INJECT_INCOMING_CHANNEL = new ReportType("Unable to inject incoming channel %s."); + + private volatile boolean injected; + private volatile boolean closed; + + // The temporary player factory + private TemporaryPlayerFactory playerFactory = new TemporaryPlayerFactory(); + private List bootstrapFields = Lists.newArrayList(); + + // The channel injector factory + private NettyInjectionFactory injectionFactory; + + // List of network managers + private volatile List networkManagers; + + // Different sending filters + private PacketTypeSet sendingFilters = new PacketTypeSet(); + private PacketTypeSet reveivedFilters = new PacketTypeSet(); + + // Packets that must be executed on the main thread + private PacketTypeSet mainThreadFilters = new PacketTypeSet(); + + // Which packets are buffered + private PacketTypeSet bufferedPackets = new PacketTypeSet(); + private ListenerInvoker invoker; + + // Handle errors + private ErrorReporter reporter; + private boolean debug; + + public NettyProtocolInjector(Plugin plugin, ListenerInvoker invoker, ErrorReporter reporter) { + this.injectionFactory = new NettyInjectionFactory(plugin); + this.invoker = invoker; + this.reporter = reporter; + } + + @Override + public boolean isDebug() { + return debug; + } + + /** + * Set whether or not the debug mode is enabled. + * @param debug - TRUE if it is, FALSE otherwise. + */ + @Override + public void setDebug(boolean debug) { + this.debug = debug; + } + + /** + * Inject into the spigot connection class. + */ + @Override + @SuppressWarnings("unchecked") + public synchronized void inject() { + if (injected) + throw new IllegalStateException("Cannot inject twice."); + try { + FuzzyReflection fuzzyServer = FuzzyReflection.fromClass(MinecraftReflection.getMinecraftServerClass()); + List serverConnectionMethods = fuzzyServer.getMethodListByParameters(MinecraftReflection.getServerConnectionClass(), new Class[] {}); + + // Get the server connection + Object server = fuzzyServer.getSingleton(); + Object serverConnection = null; + + for (Method method : serverConnectionMethods) { + try { + serverConnection = method.invoke(server); + + // Continue until we get a server connection + if (serverConnection != null) { + break; + } + } catch (Exception e) { + // Try the next though + e.printStackTrace(); + } + } + + // Handle connected channels + final ChannelInboundHandler endInitProtocol = new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) throws Exception { + try { + // This can take a while, so we need to stop the main thread from interfering + synchronized (networkManagers) { + injectionFactory.fromChannel(channel, NettyProtocolInjector.this, playerFactory).inject(); + } + } catch (Exception e) { + reporter.reportDetailed(NettyProtocolInjector.this, Report.newBuilder(REPORT_CANNOT_INJECT_INCOMING_CHANNEL). + messageParam(channel).error(e)); + } + } + }; + + // This is executed before Minecraft's channel handler + final ChannelInboundHandler beginInitProtocol = new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) throws Exception { + // Our only job is to add init protocol + channel.pipeline().addLast(endInitProtocol); + } + }; + + // Add our handler to newly created channels + final ChannelHandler connectionHandler = new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel channel = (Channel) msg; + + // Prepare to initialize ths channel + channel.pipeline().addFirst(beginInitProtocol); + ctx.fireChannelRead(msg); + } + }; + + // Get the current NetworkMananger list + networkManagers = (List) FuzzyReflection.fromObject(serverConnection, true). + invokeMethod(null, "getNetworkManagers", List.class, serverConnection); + + // Insert ProtocolLib's connection interceptor + bootstrapFields = getBootstrapFields(serverConnection); + + for (VolatileField field : bootstrapFields) { + final List list = (List) field.getValue(); + + // We don't have to override this list + if (list == networkManagers) { + continue; + } + + // Synchronize with each list before we attempt to replace them. + field.setValue(new NettyBootstrapList(list, connectionHandler)); + } + + injected = true; + + } catch (Exception e) { + throw new RuntimeException("Unable to inject channel futures.", e); + } + } + + @Override + public boolean hasListener(Class packetClass) { + return reveivedFilters.contains(packetClass) || sendingFilters.contains(packetClass); + } + + @Override + public boolean hasMainThreadListener(Class packetClass) { + return mainThreadFilters.contains(packetClass); + } + + @Override + public ErrorReporter getReporter() { + return reporter; + } + + /** + * Inject our packet handling into a specific player. + * @param player Player to inject into + */ + public void injectPlayer(Player player) { + injectionFactory.fromPlayer(player, this).inject(); + } + + private List getBootstrapFields(Object serverConnection) { + List result = Lists.newArrayList(); + + // Find and (possibly) proxy every list + for (Field field : FuzzyReflection.fromObject(serverConnection, true).getFieldListByType(List.class)) { + VolatileField volatileField = new VolatileField(field, serverConnection, true).toSynchronized(); + + @SuppressWarnings("unchecked") + List list = (List) volatileField.getValue(); + + if (list.size() == 0 || list.get(0) instanceof ChannelFuture) { + result.add(volatileField); + } + } + return result; + } + + /** + * Clean up any remaning injections. + */ + @Override + public synchronized void close() { + if (!closed) { + closed = true; + + for (VolatileField field : bootstrapFields) { + Object value = field.getValue(); + + // Undo the processed channels, if any + if (value instanceof NettyBootstrapList) { + ((NettyBootstrapList) value).close(); + } + field.revertValue(); + } + // Uninject all the players + injectionFactory.close(); + } + } + + @Override + public PacketEvent onPacketSending(Injector injector, Object packet, NetworkMarker marker) { + Class clazz = packet.getClass(); + + if (sendingFilters.contains(clazz) || marker != null) { + PacketContainer container = new PacketContainer(PacketRegistry.getPacketType(clazz), packet); + return packetQueued(container, injector.getPlayer(), marker); + } + + // Don't change anything + return null; + } + + @Override + public PacketEvent onPacketReceiving(Injector injector, Object packet, NetworkMarker marker) { + Class clazz = packet.getClass(); + + if (reveivedFilters.contains(clazz) || marker != null) { + PacketContainer container = new PacketContainer(PacketRegistry.getPacketType(clazz), packet); + return packetReceived(container, injector.getPlayer(), marker); + } + + // Don't change anything + return null; + } + + @Override + public boolean includeBuffer(Class packetClass) { + return bufferedPackets.contains(packetClass); + } + + /** + * Called to inform the event listeners of a queued packet. + * @param packet - the packet that is to be sent. + * @param receiver - the receiver of this packet. + * @return The packet event that was used. + */ + private PacketEvent packetQueued(PacketContainer packet, Player receiver, NetworkMarker marker) { + PacketEvent event = PacketEvent.fromServer(this, packet, marker, receiver); + + invoker.invokePacketSending(event); + return event; + } + + /** + * Called to inform the event listeners of a received packet. + * @param packet - the packet that has been receieved. + * @param sender - the client packet. + * @param marker - the network marker. + * @return The packet event that was used. + */ + private PacketEvent packetReceived(PacketContainer packet, Player sender, NetworkMarker marker) { + PacketEvent event = PacketEvent.fromClient(this, packet, marker, sender); + + invoker.invokePacketRecieving(event); + return event; + } + + // Server side + @Override + public PlayerInjectionHandler getPlayerInjector() { + return new AbstractPlayerHandler(sendingFilters) { + private ChannelListener listener = NettyProtocolInjector.this; + + @Override + public int getProtocolVersion(Player player) { + return injectionFactory.fromPlayer(player, listener).getProtocolVersion(); + } + + @Override + public void updatePlayer(Player player) { + injectionFactory.fromPlayer(player, listener).inject(); + } + + @Override + public void injectPlayer(Player player, ConflictStrategy strategy) { + injectionFactory.fromPlayer(player, listener).inject(); + } + + @Override + public boolean uninjectPlayer(InetSocketAddress address) { + // Ignore this too + return true; + } + + @Override + public void addPacketHandler(PacketType type, Set options) { + if (options != null && !options.contains(ListenerOptions.ASYNC)) + mainThreadFilters.addType(type); + super.addPacketHandler(type, options); + } + + @Override + public void removePacketHandler(PacketType type) { + mainThreadFilters.removeType(type); + super.removePacketHandler(type); + } + + @Override + public boolean uninjectPlayer(Player player) { + // Just let Netty clean this up + return true; + } + + @Override + public void sendServerPacket(Player receiver, PacketContainer packet, NetworkMarker marker, boolean filters) throws InvocationTargetException { + injectionFactory.fromPlayer(receiver, listener). + sendServerPacket(packet.getHandle(), marker, filters); + } + + @Override + public boolean hasMainThreadListener(PacketType type) { + return mainThreadFilters.contains(type); + } + + @Override + public void recieveClientPacket(Player player, Object mcPacket) throws IllegalAccessException, InvocationTargetException { + injectionFactory.fromPlayer(player, listener). + recieveClientPacket(mcPacket); + } + + @Override + public PacketEvent handlePacketRecieved(PacketContainer packet, InputStream input, byte[] buffered) { + // Ignore this + return null; + } + + @Override + public void handleDisconnect(Player player) { + injectionFactory.fromPlayer(player, listener).close(); + } + + @Override + public WrappedChannel getChannel(Player player) { + Injector injector = injectionFactory.fromPlayer(player, listener); + if (injector instanceof ChannelInjector) { + return ((ChannelInjector) injector).getChannel(); + } + + return null; + } + }; + } + + /** + * Retrieve a view of this protocol injector as a packet injector. + * @return The packet injector. + */ + // Client side + @Override + public PacketInjector getPacketInjector() { + return new AbstractPacketInjector(reveivedFilters) { + @Override + public PacketEvent packetRecieved(PacketContainer packet, Player client, byte[] buffered) { + NetworkMarker marker = buffered != null ? new NettyNetworkMarker(ConnectionSide.CLIENT_SIDE, buffered) : null; + injectionFactory.fromPlayer(client, NettyProtocolInjector.this). + saveMarker(packet.getHandle(), marker); + return packetReceived(packet, client, marker); + } + + @Override + public void inputBuffersChanged(Set set) { + bufferedPackets = new PacketTypeSet(set); + } + }; + } +} diff --git a/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettySocketAdapter.java b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettySocketAdapter.java new file mode 100644 index 00000000..cbf345ae --- /dev/null +++ b/ProtocolLib/src/main/java/com/comphenix/protocol/compat/netty/independent/NettySocketAdapter.java @@ -0,0 +1,265 @@ +/** + * ProtocolLib - Bukkit server library that allows access to the Minecraft protocol. + * Copyright (C) 2015 dmulloy2 + * + * 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.compat.netty.independent; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.socket.SocketChannel; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; + +/** + * This class wraps a Netty {@link Channel} in a {@link Socket}. It overrides + * all methods in {@link Socket} to ensure that calls are not mistakingly made + * to the unsupported super socket. All operations that can be sanely applied to + * a {@link Channel} are implemented here. Those which cannot will throw an + * {@link UnsupportedOperationException}. + */ +// Thanks MD5. :) +public class NettySocketAdapter extends Socket { + private final SocketChannel ch; + + private NettySocketAdapter(SocketChannel ch) { + this.ch = ch; + } + + public static NettySocketAdapter adapt(SocketChannel ch) { + return new NettySocketAdapter(ch); + } + + @Override + public void bind(SocketAddress bindpoint) throws IOException { + ch.bind(bindpoint).syncUninterruptibly(); + } + + @Override + public synchronized void close() throws IOException { + ch.close().syncUninterruptibly(); + } + + @Override + public void connect(SocketAddress endpoint) throws IOException { + ch.connect(endpoint).syncUninterruptibly(); + } + + @Override + public void connect(SocketAddress endpoint, int timeout) throws IOException { + ch.config().setConnectTimeoutMillis(timeout); + ch.connect(endpoint).syncUninterruptibly(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof NettySocketAdapter && ch.equals(((NettySocketAdapter) obj).ch); + } + + @Override + public java.nio.channels.SocketChannel getChannel() { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public InetAddress getInetAddress() { + return ch.remoteAddress().getAddress(); + } + + @Override + public InputStream getInputStream() throws IOException { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public boolean getKeepAlive() throws SocketException { + return ch.config().getOption(ChannelOption.SO_KEEPALIVE); + } + + @Override + public InetAddress getLocalAddress() { + return ch.localAddress().getAddress(); + } + + @Override + public int getLocalPort() { + return ch.localAddress().getPort(); + } + + @Override + public SocketAddress getLocalSocketAddress() { + return ch.localAddress(); + } + + @Override + public boolean getOOBInline() throws SocketException { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public OutputStream getOutputStream() throws IOException { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public int getPort() { + return ch.remoteAddress().getPort(); + } + + @Override + public synchronized int getReceiveBufferSize() throws SocketException { + return ch.config().getOption(ChannelOption.SO_RCVBUF); + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return ch.remoteAddress(); + } + + @Override + public boolean getReuseAddress() throws SocketException { + return ch.config().getOption(ChannelOption.SO_REUSEADDR); + } + + @Override + public synchronized int getSendBufferSize() throws SocketException { + return ch.config().getOption(ChannelOption.SO_SNDBUF); + } + + @Override + public int getSoLinger() throws SocketException { + return ch.config().getOption(ChannelOption.SO_LINGER); + } + + @Override + public synchronized int getSoTimeout() throws SocketException { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public boolean getTcpNoDelay() throws SocketException { + return ch.config().getOption(ChannelOption.TCP_NODELAY); + } + + @Override + public int getTrafficClass() throws SocketException { + return ch.config().getOption(ChannelOption.IP_TOS); + } + + @Override + public int hashCode() { + return ch.hashCode(); + } + + @Override + public boolean isBound() { + return ch.localAddress() != null; + } + + @Override + public boolean isClosed() { + return !ch.isOpen(); + } + + @Override + public boolean isConnected() { + return ch.isActive(); + } + + @Override + public boolean isInputShutdown() { + return ch.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return ch.isOutputShutdown(); + } + + @Override + public void sendUrgentData(int data) throws IOException { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public void setKeepAlive(boolean on) throws SocketException { + ch.config().setOption(ChannelOption.SO_KEEPALIVE, on); + } + + @Override + public void setOOBInline(boolean on) throws SocketException { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public synchronized void setReceiveBufferSize(int size) throws SocketException { + ch.config().setOption(ChannelOption.SO_RCVBUF, size); + } + + @Override + public void setReuseAddress(boolean on) throws SocketException { + ch.config().setOption(ChannelOption.SO_REUSEADDR, on); + } + + @Override + public synchronized void setSendBufferSize(int size) throws SocketException { + ch.config().setOption(ChannelOption.SO_SNDBUF, size); + } + + @Override + public void setSoLinger(boolean on, int linger) throws SocketException { + ch.config().setOption(ChannelOption.SO_LINGER, linger); + } + + @Override + public synchronized void setSoTimeout(int timeout) throws SocketException { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public void setTcpNoDelay(boolean on) throws SocketException { + ch.config().setOption(ChannelOption.TCP_NODELAY, on); + } + + @Override + public void setTrafficClass(int tc) throws SocketException { + ch.config().setOption(ChannelOption.IP_TOS, tc); + } + + @Override + public void shutdownInput() throws IOException { + throw new UnsupportedOperationException("Operation not supported on Channel wrapper."); + } + + @Override + public void shutdownOutput() throws IOException { + ch.shutdownOutput().syncUninterruptibly(); + } + + @Override + public String toString() { + return ch.toString(); + } +}