Skip to content

Instantly share code, notes, and snippets.

@aadnk
Created February 20, 2014 21:47
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save aadnk/9123919 to your computer and use it in GitHub Desktop.
Save aadnk/9123919 to your computer and use it in GitHub Desktop.
Handling HTTP requests to port 25556 in Minecraft. This requires ProtocolLib (for now), but could be rewritten to avoid it.
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();
}
}
}
}
@CyanFrost
Copy link

This is really, really useful to a lot of people, including myself. 😜

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment