/* * This file is part of LanternServer, licensed under the MIT License (MIT). * * Copyright (c) LanternPowered <https://www.lanternpowered.org> * Copyright (c) SpongePowered <https://www.spongepowered.org> * Copyright (c) contributors * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the Software), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /* * Copyright (c) 2011-2014 Glowstone - Tad Hardesty * Copyright (c) 2010-2011 Lightstone - Graham Edgecombe * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.lanternpowered.server.network.query; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.channel.socket.DatagramPacket; import org.lanternpowered.server.LanternServer; import org.lanternpowered.server.game.LanternGame; import org.spongepowered.api.Platform; import org.spongepowered.api.Sponge; import org.spongepowered.api.command.CommandSource; import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.event.SpongeEventFactory; import org.spongepowered.api.event.cause.Cause; import org.spongepowered.api.event.server.query.QueryServerEvent; import org.spongepowered.api.plugin.PluginContainer; import org.spongepowered.api.world.World; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.stream.Collectors; /** * Class for handling UDP packets according to the minecraft server query protocol. * @see QueryServer * @see <a href="http://wiki.vg/Query">Protocol Specifications</a> */ 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; QueryHandler(QueryServer queryServer, boolean showPlugins) { this.queryServer = queryServer; this.showPlugins = showPlugins; } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { this.queryServer.getGame().getLogger().error("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 (this.queryServer.verifyChallengeToken(msg.sender(), token)) { if (buf.readableBytes() == 4) { this.handleFullStats(ctx, msg, sessionId); } else { this.handleBasicStats(ctx, msg, sessionId); } } } } 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())); } private void handleBasicStats(ChannelHandlerContext ctx, DatagramPacket packet, int sessionId) { LanternServer server = this.queryServer.getGame().getServer(); // TODO: Find out how to support the size and max size properties final QueryServerEvent.Basic event = SpongeEventFactory.createQueryServerEventBasic( Cause.source(ctx.channel().remoteAddress()).build(), (InetSocketAddress) ctx.channel().localAddress(), "SMP", this.getWorldName(), server.getMotd().toPlain(), server.getMaxPlayers(), Integer.MAX_VALUE, server.getOnlinePlayers().size(), 0); Sponge.getEventManager().post(event); final InetSocketAddress address = event.getAddress(); ByteBuf buf = ctx.alloc().buffer(); buf.writeByte(ACTION_STATS); buf.writeInt(sessionId); writeString(buf, event.getMotd()); writeString(buf, event.getGameType()); writeString(buf, event.getMap()); writeString(buf, String.valueOf(event.getPlayerCount())); writeString(buf, String.valueOf(event.getMaxPlayerCount())); buf.writeShortLE(address.getPort()); writeString(buf, address.getHostString()); ctx.write(new DatagramPacket(buf, packet.sender())); } private void handleFullStats(ChannelHandlerContext ctx, DatagramPacket packet, int sessionId) { LanternGame game = this.queryServer.getGame(); Platform platform = game.getPlatform(); final StringBuilder plugins = new StringBuilder() .append(platform.getImplementation().getName()) .append(" ") .append(platform.getImplementation().getVersion()) .append(" on ") .append(platform.getApi().getName()) .append(" ") .append(platform.getApi().getVersion()); if (this.showPlugins) { final List<PluginContainer> containers = Lists.newArrayList(game.getPluginManager().getPlugins()); containers.remove(platform.getApi()); containers.remove(platform.getImplementation()); containers.remove(game.getMinecraftPlugin()); char delim = ':'; for (PluginContainer plugin : containers) { plugins.append(delim).append(' ').append(plugin.getName()); delim = ';'; } } final QueryServerEvent.Full event = SpongeEventFactory.createQueryServerEventFull( Cause.source(ctx.channel().remoteAddress()).build(), (InetSocketAddress) ctx.channel().localAddress(), Maps.newHashMap(), "MINECRAFT", "SMP", this.getWorldName(), game.getServer().getMotd().toPlain(), game.getServer().getOnlinePlayers() .stream().map(CommandSource::getName).collect(Collectors.toList()), plugins.toString(), game.getMinecraftPlugin().getVersion().orElse("unknown"), game.getServer().getMaxPlayers(), Integer.MAX_VALUE, game.getServer().getOnlinePlayers().size(), 0); final InetSocketAddress address = event.getAddress(); final Map<String, Object> data = new LinkedHashMap<>(); data.put("hostname", event.getMotd()); data.put("gametype", event.getGameType()); data.put("game_id", event.getGameId()); data.put("version", event.getVersion()); data.put("plugins", event.getPlugins()); data.put("map", event.getMap()); data.put("numplayers", event.getPlayerCount()); data.put("maxplayers", event.getMaxPlayerCount()); data.put("hostport", address.getPort()); data.put("hostip", address.getHostString()); event.getCustomValuesMap().entrySet().stream().filter(entry -> !data.containsKey(entry.getKey())) .forEach(entry -> data.put(entry.getKey(), entry.getValue())); 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 (Player player : game.getServer().getOnlinePlayers()) { writeString(buf, player.getName()); } buf.writeByte(0); ctx.write(new DatagramPacket(buf, packet.sender())); } private String getWorldName() { final Collection<World> worlds = this.queryServer.getGame().getServer().getWorlds(); if (!worlds.isEmpty()) { return worlds.iterator().next().getName(); } return "none"; } private static void writeString(ByteBuf out, String str) { out.writeBytes(str.getBytes(StandardCharsets.UTF_8)).writeByte(0); } }