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 );
+ }
+}