/* * 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. */ package org.lanternpowered.server.network.pipeline; import static org.lanternpowered.server.text.translation.TranslationHelper.t; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import org.lanternpowered.server.LanternServer; import org.lanternpowered.server.game.Lantern; import org.lanternpowered.server.game.version.LanternMinecraftVersion; import org.lanternpowered.server.network.NetworkSession; import org.lanternpowered.server.network.status.LanternStatusClient; import org.lanternpowered.server.network.status.LanternStatusHelper; import org.lanternpowered.server.network.status.LanternStatusResponse; import org.lanternpowered.server.text.LanternTexts; import org.spongepowered.api.MinecraftVersion; import org.spongepowered.api.Sponge; import org.spongepowered.api.event.SpongeEventFactory; import org.spongepowered.api.event.cause.Cause; import org.spongepowered.api.event.server.ClientPingServerEvent; import org.spongepowered.api.text.Text; import org.spongepowered.api.text.serializer.TextSerializers; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; @SuppressWarnings("deprecation") public final class LegacyProtocolHandler extends ChannelInboundHandlerAdapter { private static final int V1_3_2_PROTOCOL = 39; private static final int V1_5_2_PROTOCOL = 61; private final NetworkSession session; public LegacyProtocolHandler(NetworkSession session) { this.session = session; } @Override public void channelRead(ChannelHandlerContext ctx, Object object) throws Exception { final LanternServer server = this.session.getServer(); ByteBuf buf = (ByteBuf) object; buf.markReaderIndex(); // Whether it was a valid legacy message boolean legacy = false; try { int messageId = buf.readUnsignedByte(); // Old client's are not so smart, make sure that // they don't attempt to login if (messageId == 0x02) { int protocol = buf.readByte(); // Protocol version int value = buf.readShort(); // Check the length if (value < 0 || value > 16) { return; } buf.readBytes(value << 1); // Username value = buf.readShort(); // Check the length if (value < 0 || value > 255) { return; } buf.readBytes(value << 1); // Host address buf.readInt(); // Port if (buf.readableBytes() > 0) { return; } legacy = true; sendDisconnectMessage(ctx, LanternTexts.toPlain(t("handshake.outdated.client", Lantern.getGame().getPlatform().getMinecraftVersion().getName()))); final MinecraftVersion clientVersion = Lantern.getGame().getMinecraftVersionCache().getVersionOrUnknown(protocol, true); if (clientVersion == LanternMinecraftVersion.UNKNOWN_LEGACY) { Lantern.getLogger().debug("Client with unknown legacy protocol version {} attempted to join the server.", protocol); } else { Lantern.getLogger().debug("Client with legacy protocol version {} (mc-version {}) attempted to join the server.", protocol, clientVersion.getName()); } return; } // Check for the ping message id. if (messageId != 0xfe) { return; } int readable = buf.readableBytes(); boolean full = false; // The version used to ping the server int protocol = V1_3_2_PROTOCOL; // Versions 1.4 - 1.5.x + 1.6 - Can request full data. if (readable > 0) { // Is always 1 if (buf.readUnsignedByte() != 1) { return; } full = true; protocol = V1_5_2_PROTOCOL; } // The virtual address that was used to join the server InetSocketAddress virtualAddress = null; // Version 1.6 - Used extra data. if (readable > 1) { if (buf.readUnsignedByte() != 0xfa) { return; } byte[] bytes = new byte[buf.readShort() << 1]; buf.readBytes(bytes); if (!new String(bytes, StandardCharsets.UTF_16BE).equals("MC|PingHost")){ return; } // Not used buf.readShort(); // The protocol version is present protocol = buf.readUnsignedByte(); // There is extra host and port data if (protocol >= 73) { bytes = new byte[buf.readShort() << 1]; buf.readBytes(bytes); String host = new String(bytes, StandardCharsets.UTF_16BE); int port = buf.readInt(); virtualAddress = InetSocketAddress.createUnresolved(host, port); } readable = buf.readableBytes(); if (readable > 0) { Lantern.getLogger().warn("Trailing bytes on a legacy ping message: {}b", readable); } } final MinecraftVersion clientVersion = Lantern.getGame().getMinecraftVersionCache().getVersionOrUnknown(protocol, true); if (clientVersion == LanternMinecraftVersion.UNKNOWN) { Lantern.getLogger().debug("Client with unknown legacy protocol version {} pinged the server.", protocol); } // The message was successfully decoded as a legacy one legacy = true; MinecraftVersion serverVersion = Lantern.getGame().getPlatform().getMinecraftVersion(); Text description = server.getMotd(); InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress(); LanternStatusClient client = new LanternStatusClient(address, clientVersion, virtualAddress); ClientPingServerEvent.Response.Players players = LanternStatusHelper.createPlayers(server); LanternStatusResponse response = new LanternStatusResponse(serverVersion, server.getFavicon(), description, players); ClientPingServerEvent event = SpongeEventFactory.createClientPingServerEvent(Cause.source(client).build(), client, response); Sponge.getEventManager().post(event); // Cancelled, we are done here if (event.isCancelled()) { ctx.channel().close(); return; } description = response.getDescription(); int online = players.getOnline(); int max = players.getMax(); // The players should be hidden, this will replace the player count // with ??? if (!response.getPlayers().isPresent()) { online = -1; } StringBuilder dataBuilder = new StringBuilder(); if (full) { String description0 = getFirstLine(TextSerializers.LEGACY_FORMATTING_CODE.serialize(description)); dataBuilder .append('\u00A7') // This value is always 1. .append(1) .append('\u0000') // The protocol version, just use a value out of range // of the available ones. .append(127) .append('\u0000') // The version/name string of the server. .append(Lantern.getGame().getPlatform().getMinecraftVersion().getName()) .append('\u0000') // The motd of the server. In legacy format. .append(description0) .append('\u0000') .append(online) .append('\u0000') .append(max); } else { String description0 = getFirstLine(TextSerializers.PLAIN.serialize(description)); dataBuilder .append(description0) .append('\u00A7') .append(online) .append('\u00A7') .append(max); } sendDisconnectMessage(ctx, dataBuilder.toString()); } catch (Exception ignore) { } finally { if (legacy) { buf.release(); } else { buf.resetReaderIndex(); ctx.channel().pipeline().remove(this); ctx.fireChannelRead(buf); } } } /** * Sends a disconnect message to a legacy client and closes the connection. * * @param ctx The channel handler context * @param message The message */ private static void sendDisconnectMessage(ChannelHandlerContext ctx, String message) { byte[] data = message.getBytes(StandardCharsets.UTF_16BE); ByteBuf output = ctx.alloc().buffer(); output.writeByte(0xff); output.writeShort(data.length >> 1); output.writeBytes(data); ctx.channel().pipeline().firstContext().writeAndFlush(output).addListener(ChannelFutureListener.CLOSE); } private static String getFirstLine(String value) { int i = value.indexOf('\n'); return i == -1 ? value : value.substring(0, i); } }