diff --git a/api/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java b/api/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java index 8eea1adf..fde06cb2 100644 --- a/api/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java +++ b/api/src/main/java/net/md_5/bungee/api/config/ListenerInfo.java @@ -61,4 +61,12 @@ public class ListenerInfo * server (force default server). */ private final boolean pingPassthrough; + /** + * What port to run udp query on. + */ + private final int queryPort; + /** + * Whether to enable udp query. + */ + private final boolean queryEnabled; } diff --git a/config/pom.xml b/config/pom.xml index 27467217..8c7e4cff 100644 --- a/config/pom.xml +++ b/config/pom.xml @@ -17,7 +17,7 @@ BungeeCord-Config Generic java configuration API intended for use with BungeeCord - + org.yaml diff --git a/pom.xml b/pom.xml index f2438781..d55ac3e2 100644 --- a/pom.xml +++ b/pom.xml @@ -42,6 +42,7 @@ event protocol proxy + query diff --git a/proxy/pom.xml b/proxy/pom.xml index 71552b75..3f80db79 100644 --- a/proxy/pom.xml +++ b/proxy/pom.xml @@ -53,6 +53,12 @@ ${project.version} compile + + net.md-5 + bungeecord-query + ${project.version} + compile + net.sf.trove4j trove4j diff --git a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java index d0dccb9e..506915a3 100644 --- a/proxy/src/main/java/net/md_5/bungee/BungeeCord.java +++ b/proxy/src/main/java/net/md_5/bungee/BungeeCord.java @@ -60,6 +60,7 @@ import net.md_5.bungee.protocol.packet.DefinedPacket; import net.md_5.bungee.protocol.packet.Packet3Chat; import net.md_5.bungee.protocol.packet.PacketFAPluginMessage; import net.md_5.bungee.protocol.Vanilla; +import net.md_5.bungee.query.RemoteQuery; import net.md_5.bungee.tab.Custom; import net.md_5.bungee.util.CaseInsensitiveMap; import org.fusesource.jansi.AnsiConsole; @@ -269,6 +270,26 @@ public class BungeeCord extends ProxyServer .group( eventLoops ) .localAddress( info.getHost() ) .bind().addListener( listener ); + + if ( info.isQueryEnabled() ) + { + ChannelFutureListener bindListener = new ChannelFutureListener() + { + @Override + public void operationComplete(ChannelFuture future) throws Exception + { + if ( future.isSuccess() ) + { + listeners.add( future.channel() ); + getLogger().info( "Started query on " + future.channel().localAddress() ); + } else + { + getLogger().log( Level.WARNING, "Could not bind to host " + future.channel().remoteAddress(), future.cause() ); + } + } + }; + new RemoteQuery( this, info ).start( new InetSocketAddress( info.getHost().getAddress(), info.getQueryPort() ), eventLoops, bindListener ); + } } } diff --git a/proxy/src/main/java/net/md_5/bungee/config/YamlConfig.java b/proxy/src/main/java/net/md_5/bungee/config/YamlConfig.java index 89d248dd..253c76b7 100644 --- a/proxy/src/main/java/net/md_5/bungee/config/YamlConfig.java +++ b/proxy/src/main/java/net/md_5/bungee/config/YamlConfig.java @@ -215,7 +215,10 @@ public class YamlConfig implements ConfigurationAdapter boolean setLocalAddress = get( "bind_local_address", true, val ); boolean pingPassthrough = get( "ping_passthrough", false, val ); - ListenerInfo info = new ListenerInfo( address, motd, maxPlayers, tabListSize, defaultServer, fallbackServer, forceDefault, forced, value.clazz, setLocalAddress, pingPassthrough ); + boolean query = get( "guery_enabled", false, val ); + int queryPort = get( "query_port", 25577, val ); + + ListenerInfo info = new ListenerInfo( address, motd, maxPlayers, tabListSize, defaultServer, fallbackServer, forceDefault, forced, value.clazz, setLocalAddress, pingPassthrough, queryPort, query ); ret.add( info ); } diff --git a/query/nb-configuration.xml b/query/nb-configuration.xml new file mode 100644 index 00000000..7e465924 --- /dev/null +++ b/query/nb-configuration.xml @@ -0,0 +1,31 @@ + + + + + + project + NEW_LINE + NEW_LINE + NEW_LINE + true + true + true + true + true + true + true + true + true + true + + diff --git a/query/pom.xml b/query/pom.xml new file mode 100644 index 00000000..f86726ce --- /dev/null +++ b/query/pom.xml @@ -0,0 +1,35 @@ + + + 4.0.0 + + + net.md-5 + bungeecord-parent + 1.6.4-SNAPSHOT + ../pom.xml + + + net.md-5 + bungeecord-query + 1.6.4-SNAPSHOT + jar + + BungeeCord-Query + Minecraft query implementation based on the BungeeCord API. + + + + io.netty + netty-transport + ${netty.version} + compile + + + net.md-5 + bungeecord-api + ${project.version} + compile + + + diff --git a/query/src/main/java/net/md_5/bungee/query/QueryHandler.java b/query/src/main/java/net/md_5/bungee/query/QueryHandler.java new file mode 100644 index 00000000..adfc4a63 --- /dev/null +++ b/query/src/main/java/net/md_5/bungee/query/QueryHandler.java @@ -0,0 +1,145 @@ +package net.md_5.bungee.query; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.socket.DatagramPacket; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.TimeUnit; +import lombok.RequiredArgsConstructor; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.config.ListenerInfo; +import net.md_5.bungee.api.connection.ProxiedPlayer; + +@RequiredArgsConstructor +public class QueryHandler extends SimpleChannelInboundHandler +{ + + private final ProxyServer bungee; + private final ListenerInfo listener; + /*========================================================================*/ + private final Random random = new Random(); + private final Map sessions = new HashMap<>(); + + private void writeShort(ByteBuf buf, int s) + { + buf.order( ByteOrder.LITTLE_ENDIAN ).writeShort( s ); + } + + private void writeNumber(ByteBuf buf, int i) + { + writeString( buf, Integer.toString( i ) ); + } + + private void writeString(ByteBuf buf, String s) + { + for ( char c : s.toCharArray() ) + { + buf.writeChar( c ); + } + buf.writeByte( 0x00 ); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception + { + super.channelActive( ctx ); //To change body of generated methods, choose Tools | Templates. + } + + @Override + protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception + { + ByteBuf in = msg.content(); + if ( in.readUnsignedByte() != 0xFE && in.readUnsignedByte() != 0xFD ) + { + throw new IllegalStateException( "Incorrect magic!" ); + } + + ByteBuf out = ctx.alloc().buffer(); + AddressedEnvelope response = new DatagramPacket( out, msg.sender() ); + + byte type = in.readByte(); + int sessionId = in.readInt(); + + if ( type == 0x09 ) + { + out.writeByte( 0x09 ); + out.writeInt( sessionId ); + + int challengeToken = random.nextInt(); + sessions.put( challengeToken, System.currentTimeMillis() ); + + writeNumber( out, challengeToken ); + } + + if ( type == 0x00 ) + { + int challengeToken = out.readInt(); + Long session = sessions.get( challengeToken ); + if ( session == null || System.currentTimeMillis() - session > TimeUnit.SECONDS.toMillis( 30 ) ) + { + throw new IllegalStateException( "No session!" ); + } + + out.writeByte( 0x00 ); + out.writeInt( sessionId ); + + if ( in.readableBytes() == 0 ) + { + // Short response + writeString( out, listener.getMotd() ); // MOTD + writeString( out, "SMP" ); // Game Type + writeString( out, "BungeeCord_Proxy" ); // World Name + writeNumber( out, bungee.getOnlineCount() ); // Online Count + writeNumber( out, listener.getMaxPlayers() ); // Max Players + writeShort( out, listener.getHost().getPort() ); // Port + writeString( out, listener.getHost().getHostString() ); // IP + } else if ( in.readableBytes() == 8 ) + { + // Long Response + out.writeBytes( new byte[ 11 ] ); + Map data = new HashMap<>(); + + data.put( "hostname", listener.getMotd() ); + data.put( "gametype", "SMP" ); + // Start Extra Info + data.put( "game_id", "MINECRAFT" ); + data.put( "version", bungee.getGameVersion() ); + // data.put( "plugins",""); + // End Extra Info + data.put( "map", "BungeeCord_Proxy" ); + data.put( "numplayers", Integer.toString( bungee.getOnlineCount() ) ); + data.put( "maxplayers", Integer.toString( listener.getMaxPlayers() ) ); + data.put( "hostport", Integer.toString( listener.getHost().getPort() ) ); + data.put( "hostip", listener.getHost().getHostString() ); + + for ( Map.Entry entry : data.entrySet() ) + { + writeString( out, entry.getKey() ); + writeString( out, entry.getValue() ); + + } + out.writeByte( 0x00 ); // Null + + // Padding + out.writeBytes( new byte[ 10 ] ); + // Player List + for ( ProxiedPlayer p : bungee.getPlayers() ) + { + writeString( out, p.getName() ); + } + out.writeByte( 0x00 ); // Null + } else + { + // Error! + throw new IllegalStateException( "Invalid data request packet" ); + } + } + + ctx.writeAndFlush( response ); + } +} diff --git a/query/src/main/java/net/md_5/bungee/query/RemoteQuery.java b/query/src/main/java/net/md_5/bungee/query/RemoteQuery.java new file mode 100644 index 00000000..011c722e --- /dev/null +++ b/query/src/main/java/net/md_5/bungee/query/RemoteQuery.java @@ -0,0 +1,29 @@ +package net.md_5.bungee.query; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import java.net.InetSocketAddress; +import lombok.RequiredArgsConstructor; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.config.ListenerInfo; + +@RequiredArgsConstructor +public class RemoteQuery +{ + + private final ProxyServer bungee; + private final ListenerInfo listener; + + public void start(InetSocketAddress address, EventLoopGroup eventLoop, ChannelFutureListener future) + { + new Bootstrap() + .channel( NioDatagramChannel.class ) + .group( eventLoop ) + .handler( new QueryHandler( bungee, listener ) ) + .localAddress( address ) + .bind().addListener( future ); + } +}