package net.md_5.bungee.query; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; 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.net.InetAddress; import java.util.LinkedHashMap; import java.util.Map; import java.util.Random; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import lombok.Data; 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<DatagramPacket> { private final ProxyServer bungee; private final ListenerInfo listener; /*========================================================================*/ private final Random random = new Random(); private final Cache<InetAddress, QuerySession> sessions = CacheBuilder.newBuilder().expireAfterWrite( 30, TimeUnit.SECONDS).build(); private void writeShort(ByteBuf buf, int s) { buf.writeShortLE( 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.writeByte( c ); } buf.writeByte( 0x00 ); } @Override protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception { ByteBuf in = msg.content(); if ( in.readUnsignedByte() != 0xFE || in.readUnsignedByte() != 0xFD ) { bungee.getLogger().log( Level.WARNING, "Query - Incorrect magic!: {0}", msg.sender() ); return; } 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( msg.sender().getAddress(), new QuerySession( challengeToken, System.currentTimeMillis() ) ); writeNumber( out, challengeToken ); } if ( type == 0x00 ) { int challengeToken = in.readInt(); QuerySession session = sessions.getIfPresent( msg.sender().getAddress() ); if ( session == null || session.getToken() != challengeToken ) { 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() == 4 ) { // Long Response out.writeBytes( new byte[] { 0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, (byte) 0x80, 0x00 } ); Map<String, String> data = new LinkedHashMap<>(); 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<String, String> entry : data.entrySet() ) { writeString( out, entry.getKey() ); writeString( out, entry.getValue() ); } out.writeByte( 0x00 ); // Null // Padding writeString( out, "\01player_\00" ); // 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 ); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { bungee.getLogger().log( Level.WARNING, "Error whilst handling query packet from " + ctx.channel().remoteAddress(), cause ); } @Data private static class QuerySession { private final int token; private final long time; } }