Created
February 20, 2014 21:47
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
}); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
This is really, really useful to a lot of people, including myself. 😜