Skip to content

Instantly share code, notes, and snippets.

@SilverCory
Forked from aadnk/BootstrapList.java
Last active August 29, 2015 14:19
Show Gist options
  • Save SilverCory/a68eabb7149f30ba5b5b to your computer and use it in GitHub Desktop.
Save SilverCory/a68eabb7149f30ba5b5b to your computer and use it in GitHub Desktop.
package com.comphenix.example;
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;
import net.minecraft.util.io.netty.channel.Channel;
// Hopefully, CB won't version these as well
import net.minecraft.util.io.netty.channel.ChannelFuture;
import net.minecraft.util.io.netty.channel.ChannelHandler;
class BootstrapList implements List<Object> {
private List<Object> delegate;
private ChannelHandler handler;
/**
* Construct a new bootstrap list.
* @param delegate - the delegate.
* @param handler - the channel handler to add.
*/
public BootstrapList(List<Object> 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<? extends Object> collection) {
List<Object> 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<Object>() {
@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
public synchronized int size() {
return delegate.size();
}
public synchronized boolean isEmpty() {
return delegate.isEmpty();
}
public boolean contains(Object o) {
return delegate.contains(o);
}
public synchronized Iterator<Object> iterator() {
return delegate.iterator();
}
public synchronized Object[] toArray() {
return delegate.toArray();
}
public synchronized <T> T[] toArray(T[] a) {
return delegate.toArray(a);
}
public synchronized boolean remove(Object o) {
return delegate.remove(o);
}
public synchronized boolean containsAll(Collection<?> c) {
return delegate.containsAll(c);
}
public synchronized boolean addAll(int index, Collection<? extends Object> c) {
return delegate.addAll(index, c);
}
public synchronized boolean removeAll(Collection<?> c) {
return delegate.removeAll(c);
}
public synchronized boolean retainAll(Collection<?> c) {
return delegate.retainAll(c);
}
public synchronized void clear() {
delegate.clear();
}
public synchronized Object get(int index) {
return delegate.get(index);
}
public synchronized void add(int index, Object element) {
delegate.add(index, element);
}
public synchronized Object remove(int index) {
return delegate.remove(index);
}
public synchronized int indexOf(Object o) {
return delegate.indexOf(o);
}
public synchronized int lastIndexOf(Object o) {
return delegate.lastIndexOf(o);
}
public synchronized ListIterator<Object> listIterator() {
return delegate.listIterator();
}
public synchronized ListIterator<Object> listIterator(int index) {
return delegate.listIterator(index);
}
public synchronized List<Object> subList(int fromIndex, int toIndex) {
return delegate.subList(fromIndex, toIndex);
}
// End boiler plate
}
package com.comphenix.example;
import net.minecraft.util.io.netty.channel.Channel;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;
public class ExampleMod extends JavaPlugin implements Listener {
private NettyInjector injector = new NettyInjector() {
@Override
protected void injectChannel(Channel channel) {
channel.pipeline().addFirst(new JSONAPIChannelDecoder());
}
};
@Override
public void onEnable() {
injector.inject();
}
@Override
public void onDisable() {
injector.close();
}
}
package com.comphenix.example;
import java.util.List;
import java.util.NoSuchElementException;
import net.minecraft.util.io.netty.buffer.ByteBuf;
import net.minecraft.util.io.netty.channel.ChannelHandlerContext;
import net.minecraft.util.io.netty.channel.ChannelOption;
import net.minecraft.util.io.netty.channel.ChannelPipeline;
import net.minecraft.util.io.netty.handler.codec.ByteToMessageDecoder;
import net.minecraft.util.io.netty.handler.codec.http.HttpRequestDecoder;
import net.minecraft.util.io.netty.handler.codec.http.HttpResponseEncoder;
public class JSONAPIChannelDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> list) throws Exception {
// use 4 bytes to detect HTTP or abort
if (buf.readableBytes() < 4) {
return;
}
buf.retain(2);
final int magic1 = buf.getUnsignedByte(buf.readerIndex());
final int magic2 = buf.getUnsignedByte(buf.readerIndex() + 1);
final int magic3 = buf.getUnsignedByte(buf.readerIndex() + 2);
final int magic4 = buf.getUnsignedByte(buf.readerIndex() + 3);
ChannelPipeline p = ctx.channel().pipeline();
if (isHttp(magic1, magic2, magic3, magic4)) {
ByteBuf copy = buf.copy();
ctx.channel().config().setOption(ChannelOption.TCP_NODELAY, true);
try {
while (p.removeLast() != null);
} catch (NoSuchElementException e) {
}
p.addLast("decoder", new HttpRequestDecoder());
p.addLast("encoder", new HttpResponseEncoder());
p.addLast("handler", new JSONAPIHandler());
p.fireChannelRead(copy);
buf.release();
buf.release();
} else {
try {
p.remove(this);
} catch (NoSuchElementException e) {
// probably okay, it just needs to be off
System.out.println("NoSuchElementException");
}
buf.release();
buf.release();
}
}
private boolean isHttp(int magic1, int magic2, int magic3, int magic4) {
return magic1 == 'G' && magic2 == 'E' && magic3 == 'T' && magic4 == ' ' || // GET
magic1 == 'P' && magic2 == 'O' && magic3 == 'S' && magic4 == 'T' || // POST
magic1 == 'P' && magic2 == 'U' && magic3 == 'T' && magic4 == ' ' || // PUT
magic1 == 'H' && magic2 == 'E' && magic3 == 'A' && magic4 == 'D' || // HEAD
magic1 == 'O' && magic2 == 'P' && magic3 == 'T' && magic4 == 'I' || // OPTIONS
magic1 == 'P' && magic2 == 'A' && magic3 == 'T' && magic4 == 'C' || // PATCH
magic1 == 'D' && magic2 == 'E' && magic3 == 'L' && magic4 == 'E' || // DELETE
magic1 == 'T' && magic2 == 'R' && magic3 == 'C' && magic4 == 'C' || // TRACE
magic1 == 'C' && magic2 == 'O' && magic3 == 'N' && magic4 == 'N'; // CONNECT
}
}
package com.comphenix.example;
import com.google.common.base.Charsets;
import net.minecraft.util.io.netty.channel.ChannelFuture;
import net.minecraft.util.io.netty.channel.ChannelFutureListener;
import net.minecraft.util.io.netty.channel.ChannelHandlerContext;
import net.minecraft.util.io.netty.channel.ChannelPromise;
import net.minecraft.util.io.netty.channel.SimpleChannelInboundHandler;
import net.minecraft.util.io.netty.handler.codec.http.DefaultFullHttpResponse;
import net.minecraft.util.io.netty.handler.codec.http.FullHttpResponse;
import net.minecraft.util.io.netty.handler.codec.http.HttpRequest;
import net.minecraft.util.io.netty.handler.codec.http.HttpResponseStatus;
import net.minecraft.util.io.netty.handler.codec.http.HttpVersion;
class JSONAPIHandler extends SimpleChannelInboundHandler<HttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest request) throws Exception {
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
ChannelPromise promise = ctx.channel().newPromise();
response.content().writeBytes(Charsets.UTF_8.encode("Hello, world!"));
// Write the response, and close the channel
ctx.channel().writeAndFlush(response, promise);
promise.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
future.channel().close();
}
});
}
}
package com.comphenix.example;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import net.minecraft.util.io.netty.channel.Channel;
import net.minecraft.util.io.netty.channel.ChannelFuture;
import net.minecraft.util.io.netty.channel.ChannelHandler;
import net.minecraft.util.io.netty.channel.ChannelHandlerContext;
import net.minecraft.util.io.netty.channel.ChannelInboundHandler;
import net.minecraft.util.io.netty.channel.ChannelInboundHandlerAdapter;
import net.minecraft.util.io.netty.channel.ChannelInitializer;
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 abstract class NettyInjector {
// The temporary player factory
private List<VolatileField> bootstrapFields = Lists.newArrayList();
// List of network managers
private volatile List<Object> networkManagers;
private boolean injected;
private boolean closed;
/**
* Inject into the spigot connection class.
*/
@SuppressWarnings("unchecked")
public synchronized void inject() {
if (injected)
throw new IllegalStateException("Cannot inject twice.");
try {
FuzzyReflection fuzzyServer = FuzzyReflection.fromClass(MinecraftReflection.getMinecraftServerClass());
Method serverConnectionMethod = fuzzyServer.getMethodByParameters("getServerConnection", MinecraftReflection.getServerConnectionClass(), new Class[] {});
// Get the server connection
Object server = fuzzyServer.getSingleton();
Object serverConnection = serverConnectionMethod.invoke(server);
// Handle connected channels
final ChannelInboundHandler endInitProtocol = new ChannelInitializer<Channel>() {
@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) {
injectChannel(channel);
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
// This is executed before Minecraft's channel handler
final ChannelInboundHandler beginInitProtocol = new ChannelInitializer<Channel>() {
@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<Object>) FuzzyReflection.fromObject(serverConnection, true).
invokeMethod(null, "getNetworkManagers", List.class, serverConnection);
// Insert ProtocolLib's connection interceptor
bootstrapFields = getBootstrapFields(serverConnection);
for (VolatileField field : bootstrapFields) {
final List<Object> list = (List<Object>) 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 BootstrapList(list, connectionHandler));
}
injected = true;
} catch (Exception e) {
throw new RuntimeException("Unable to inject channel futures.", e);
}
}
/**
* Invoked when a channel is ready to be injected.
* @param channel - the channel to inject.
*/
protected abstract void injectChannel(Channel channel);
/**
* Retrieve a list of every field with a list of channel futures.
* @param serverConnection - the connection.
* @return List of fields.
*/
private List<VolatileField> getBootstrapFields(Object serverConnection) {
List<VolatileField> 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<Object> list = (List<Object>) volatileField.getValue();
if (list.size() == 0 || list.get(0) instanceof ChannelFuture) {
result.add(volatileField);
}
}
return result;
}
/**
* Clean up any remaning injections.
*/
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 BootstrapList) {
((BootstrapList) value).close();
}
field.revertValue();
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment