package net.glowstone.net.query;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.DatagramPacket;
import net.glowstone.GlowServer;
import net.glowstone.entity.GlowPlayer;
import org.bukkit.plugin.Plugin;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
/**
* Class for handling UDP packets according to the minecraft server query protocol.
* @see QueryServer
* @see <a href="http://wiki.vg/Query">Protocol Specifications</a>
*/
public class QueryHandler extends SimpleChannelInboundHandler<DatagramPacket> {
private static final byte ACTION_HANDSHAKE = 9;
private static final byte ACTION_STATS = 0;
/**
* The {@link QueryServer} this handler belongs to.
*/
private QueryServer queryServer;
/**
* Whether the a plugin list should be included in responses.
*/
private boolean showPlugins;
public QueryHandler(QueryServer queryServer, boolean showPlugins) {
this.queryServer = queryServer;
this.showPlugins = showPlugins;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
GlowServer.logger.log(Level.SEVERE, "Error in query handling", cause);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket msg) throws Exception {
ByteBuf buf = msg.content();
if (buf.readableBytes() < 7) {
return;
}
int magic = buf.readUnsignedShort();
byte type = buf.readByte();
int sessionId = buf.readInt();
if (magic != 0xFEFD) {
return;
}
if (type == ACTION_HANDSHAKE) {
handleHandshake(ctx, msg, sessionId);
} else if (type == ACTION_STATS) {
if (buf.readableBytes() < 4) {
return;
}
int token = buf.readInt();
if (queryServer.verifyChallengeToken(msg.sender(), token)) {
if (buf.readableBytes() == 4) {
handleFullStats(ctx, msg, sessionId);
} else {
handleBasicStats(ctx, msg, sessionId);
}
}
}
if (buf.refCnt() > 0) {
buf.release(buf.refCnt());
}
}
private void handleHandshake(ChannelHandlerContext ctx, DatagramPacket packet, int sessionId) {
int challengeToken = queryServer.generateChallengeToken(packet.sender());
ByteBuf out = ctx.alloc().buffer();
out.writeByte(ACTION_HANDSHAKE);
out.writeInt(sessionId);
writeString(out, String.valueOf(challengeToken));
ctx.write(new DatagramPacket(out, packet.sender()));
if (out.refCnt() > 0) {
out.release();
}
}
private void handleBasicStats(ChannelHandlerContext ctx, DatagramPacket packet, int sessionId) {
GlowServer server = queryServer.getServer();
ByteBuf buf = ctx.alloc().buffer();
buf.writeByte(ACTION_STATS);
buf.writeInt(sessionId);
writeString(buf, server.getMotd());
writeString(buf, "SMP");
writeString(buf, server.getWorlds().get(0).getName());
writeString(buf, String.valueOf(server.getOnlinePlayers().size()));
writeString(buf, String.valueOf(server.getMaxPlayers()));
buf.order(ByteOrder.LITTLE_ENDIAN).writeShort(server.getPort());
writeString(buf, getIpString());
ctx.write(new DatagramPacket(buf, packet.sender()));
if (buf.refCnt() > 0) {
buf.release(buf.refCnt());
}
}
private void handleFullStats(ChannelHandlerContext ctx, DatagramPacket packet, int sessionId) {
GlowServer server = queryServer.getServer();
StringBuilder plugins = new StringBuilder("Glowstone ").append(server.getVersion()).append(" on Bukkit ").append(server.getBukkitVersion());
if (showPlugins) {
char delim = ':';
for (Plugin plugin : server.getPluginManager().getPlugins()) {
plugins.append(delim).append(' ').append(plugin.getDescription().getFullName());
delim = ';';
}
}
Map<String, Object> data = new LinkedHashMap<>();
data.put("hostname", server.getMotd());
data.put("gametype", "SMP");
data.put("game_id", "MINECRAFT");
data.put("version", GlowServer.GAME_VERSION);
data.put("plugins", plugins);
data.put("map", server.getWorlds().get(0).getName());
data.put("numplayers", server.getOnlinePlayers().size());
data.put("maxplayers", server.getMaxPlayers());
data.put("hostport", server.getPort());
data.put("hostip", getIpString());
ByteBuf buf = ctx.alloc().buffer();
buf.writeByte(ACTION_STATS);
buf.writeInt(sessionId);
// constant: splitnum\x00\x80\x00
buf.writeBytes(new byte[]{0x73, 0x70, 0x6C, 0x69, 0x74, 0x6E, 0x75, 0x6D, 0x00, (byte) 0x80, 0x00});
for (Entry<String, Object> e : data.entrySet()) {
writeString(buf, e.getKey());
writeString(buf, String.valueOf(e.getValue()));
}
buf.writeByte(0);
// constant: \x01player_\x00\x00
buf.writeBytes(new byte[]{0x01, 0x70, 0x6C, 0x61, 0x79, 0x65, 0x72, 0x5F, 0x00, 0x00});
for (GlowPlayer player : server.getOnlinePlayers()) {
writeString(buf, player.getName());
}
buf.writeByte(0);
ctx.write(new DatagramPacket(buf, packet.sender()));
if (buf.refCnt() > 0) {
buf.release(buf.refCnt());
}
}
private void writeString(ByteBuf out, String str) {
out.writeBytes(str.getBytes(StandardCharsets.UTF_8)).writeByte(0);
}
private String getIpString() {
String ip = queryServer.getServer().getIp();
return ip.isEmpty() ? "127.0.0.1" : ip;
}
}