From 5a686c8bdcb7d09b502a3a896bdca9c932aa2756 Mon Sep 17 00:00:00 2001 From: Finn Date: Tue, 1 Aug 2023 22:40:05 +0200 Subject: [PATCH] Bug fix --- .../BasePacketHandler.java | 97 +++++++++ .../fakeminecraftserver/ChatConverter.java | 195 ++++++++++++++++++ .../FakeMinecraftServer.java | 66 ++++++ .../fakeminecraftserver/Message.java | 34 +++ .../fakeminecraftserver/SLPServer.java | 41 ++++ .../data/StatusResponse.java | 62 ++++++ .../streams/MojewInputStream.java | 37 ++++ .../streams/MojewOutputStream.java | 41 ++++ 8 files changed, 573 insertions(+) create mode 100644 src/main/java/me/unlegitdqrk/fakeminecraftserver/BasePacketHandler.java create mode 100644 src/main/java/me/unlegitdqrk/fakeminecraftserver/ChatConverter.java create mode 100644 src/main/java/me/unlegitdqrk/fakeminecraftserver/FakeMinecraftServer.java create mode 100644 src/main/java/me/unlegitdqrk/fakeminecraftserver/Message.java create mode 100644 src/main/java/me/unlegitdqrk/fakeminecraftserver/SLPServer.java create mode 100644 src/main/java/me/unlegitdqrk/fakeminecraftserver/data/StatusResponse.java create mode 100644 src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewInputStream.java create mode 100644 src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewOutputStream.java diff --git a/src/main/java/me/unlegitdqrk/fakeminecraftserver/BasePacketHandler.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/BasePacketHandler.java new file mode 100644 index 0000000..7b242b3 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/BasePacketHandler.java @@ -0,0 +1,97 @@ +package me.unlegitdqrk.fakeminecraftserver; + +import com.google.gson.Gson; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import me.unlegitdqrk.fakeminecraftserver.streams.MojewInputStream; +import me.unlegitdqrk.fakeminecraftserver.streams.MojewOutputStream; + + +/** + * @author UnlegitDqrk + */ + +public class BasePacketHandler extends ChannelInboundHandlerAdapter { + private final Gson gson = new Gson(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + MojewInputStream inputStream = new MojewInputStream((ByteBuf) msg); + + // handshake + int length = inputStream.readInt(); + int id = inputStream.readInt(); + + if (id == 0) { + // status request + try { + int version = inputStream.readInt(); + String address = inputStream.readUTF(); + int port = inputStream.readUnsignedShort(); + int state = inputStream.readInt(); + System.out.println("Received request: " + length + ", " + id + ", " + version + ", " + address + ", " + port + ", " + state); + } catch (Exception ignored) { + // status request packet is sent inconsistently, so we ignore it + } + + inputStream.close(); + + // status response + String response = gson.toJson(FakeMinecraftServer.response).replace(ChatConverter.ESCAPE + "", "\\u00A7"); // Mojew's parser needs this escaped (classic) + + if (FakeMinecraftServer.response.favicon == null) + System.out.println("Sending response: " + response); + else + System.out.println("Sent response with image data."); + + MojewOutputStream outputStream = new MojewOutputStream(Unpooled.buffer()); + MojewOutputStream dataOutputStream = new MojewOutputStream(Unpooled.buffer()); + dataOutputStream.writeInt(0); + dataOutputStream.writeUTF(response); + dataOutputStream.close(); + outputStream.writeInt(dataOutputStream.writtenBytes()); + outputStream.write(dataOutputStream.getData()); + outputStream.close(); + ctx.writeAndFlush(outputStream.buffer()); + } else if (id == 1) { + // ping request + long time = inputStream.readLong(); + System.out.println("Received ping packet: " + length + ", " + id + ", " + time); + + // ping response + MojewOutputStream outputStream = new MojewOutputStream(Unpooled.buffer()); + MojewOutputStream dataOutputStream = new MojewOutputStream(Unpooled.buffer()); + dataOutputStream.writeInt(1); + dataOutputStream.writeLong(time); + dataOutputStream.close(); + outputStream.writeInt(dataOutputStream.writtenBytes()); + outputStream.write(dataOutputStream.getData()); + outputStream.close(); + ctx.writeAndFlush(outputStream.buffer()); + } else if (id == 2) { + // TODO: Fixing kick message + + // login attempt + System.out.println("Received login packet: " + length + ", " + id); + + // login response + MojewOutputStream outputStream = new MojewOutputStream(Unpooled.buffer()); + MojewOutputStream dataOutputStream = new MojewOutputStream(Unpooled.buffer()); + dataOutputStream.writeInt(2); + dataOutputStream.writeUTF("{text:"+ ChatConverter.replaceColors(FakeMinecraftServer.KICK_MESSAGE) +", color: white}"); + dataOutputStream.close(); + outputStream.writeInt(dataOutputStream.writtenBytes()); + outputStream.write(dataOutputStream.getData()); + outputStream.close(); + ctx.writeAndFlush(outputStream.buffer()); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/src/main/java/me/unlegitdqrk/fakeminecraftserver/ChatConverter.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/ChatConverter.java new file mode 100644 index 0000000..39c2e2e --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/ChatConverter.java @@ -0,0 +1,195 @@ +/* +Copyright (c) 2012, md_5. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +The name of the author may not be used to endorse or promote products derived +from this software without specific prior written permission. + +You may not use the software for commercial software hosting services without +written permission from the author. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. +*/ + +package me.unlegitdqrk.fakeminecraftserver; + +import com.google.gson.annotations.SerializedName; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author UnlegitDqrk + */ + +public class ChatConverter { + private static final char COLOR_CHAR = '\u00A7'; + private static final Pattern url = Pattern.compile("^(?:(https?)://)?([-\\w_\\.]{2,}\\.[a-z]{2,4})(/\\S*)?$"); + public static final char ESCAPE = '\u00A7'; + + public static String replaceColors(String text) { + char[] chrarray = text.toCharArray(); + for (int index = 0; index < chrarray.length; index++) { + char chr = chrarray[index]; + if (chr != '&') + continue; + if ((index + 1) == chrarray.length) + break; + char forward = chrarray[index + 1]; + if ((forward >= '0' && forward <= '9') || (forward >= 'a' && forward <= 'f') || (forward >= 'k' && forward <= 'r')) + chrarray[index] = ESCAPE; + } + return new String(chrarray); + } + + public static Message toJSONChat(String txt) { + Message msg = new Message(); + ArrayList parts = new ArrayList<>(); + StringBuilder buf = new StringBuilder(); + Matcher matcher = url.matcher(txt); + for (int i = 0; i < txt.length(); i++) { + char c = txt.charAt(i); + if (c != COLOR_CHAR) { + int pos = txt.indexOf(' ', i); + if (pos == -1) pos = txt.length(); + if (matcher.region(i, pos).find()) { //Web link handling + msg.text = buf.toString(); + buf = new StringBuilder(); + parts.add(msg); + Message old = msg; + msg = new Message(old); + msg.underlined = true; + msg.clickEvent = new ClickEvent(); + msg.clickEvent.action = "open_url"; + String urlString = txt.substring(i, pos); + msg.text = msg.clickEvent.value = urlString; + parts.add(msg); + i += pos - i - 1; + msg = new Message(old); + continue; + } + buf.append(c); + continue; + } + i++; + c = txt.charAt(i); + if (c >= 'A' && c <= 'Z') { + c += 32; + } + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || c == 'r') { + msg.text = buf.toString(); + buf = new StringBuilder(); + parts.add(msg); + msg = new Message(); + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { + msg.color = Color.fromCode(Character.toString(c)); + } + continue; + } + switch (c) { + case 'k': + msg.obfuscated = true; + break; + case 'l': + msg.bold = true; + break; + case 'm': + msg.strikethrough = true; + break; + case 'n': + msg.underlined = true; + break; + case 'o': + msg.italic = true; + break; + } + } + msg.text = buf.toString(); + parts.add(msg); + if (parts.get(0).text.equals("")) + parts.remove(0); // occasionally empty text? + Message base = parts.remove(0); + if (parts.size() != 0) + base.extra = new ArrayList<>(parts); + return base; + } +} + +class ClickEvent { + public String action; + public String value; +} + +enum Color { + @SerializedName("black") + BLACK("0"), + @SerializedName("dark_blue") + DARK_BLUE("1"), + @SerializedName("dark_green") + DARK_GREEN("2"), + @SerializedName("dark_aqua") + DARK_AQUA("3"), + @SerializedName("dark_red") + DARK_RED("4"), + @SerializedName("purple") + DARK_PURPLE("5"), + @SerializedName("gold") + GOLD("6"), + @SerializedName("gray") + GRAY("7"), + @SerializedName("dark_gray") + DARK_GRAY("8"), + @SerializedName("blue") + BLUE("9"), + @SerializedName("green") + GREEN("a"), + @SerializedName("aqua") + AQUA("b"), + @SerializedName("red") + RED("c"), + @SerializedName("light_purple") + LIGHT_PURPLE("d"), + @SerializedName("yellow") + YELLOW("e"), + @SerializedName("white") + WHITE("f"); + + public String code; + + Color(String code) { + this.code = code; + } + + + private static HashMap codeMap = new HashMap<>(); + + public static Color fromCode(String code) { + return codeMap.get(code); + } + + static { + for (Color color : values()) { + codeMap.put(color.code, color); + } + } +} diff --git a/src/main/java/me/unlegitdqrk/fakeminecraftserver/FakeMinecraftServer.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/FakeMinecraftServer.java new file mode 100644 index 0000000..57141ac --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/FakeMinecraftServer.java @@ -0,0 +1,66 @@ +package me.unlegitdqrk.fakeminecraftserver; + +import me.unlegitdqrk.fakeminecraftserver.data.StatusResponse; + +import javax.xml.bind.DatatypeConverter; +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.*; + +import static javax.imageio.ImageIO.read; +import static javax.imageio.ImageIO.write; + +/** + * @author UnlegitDqrk + */ + +public class FakeMinecraftServer { + public static StatusResponse response = null; + + public static final int START_PORT = 25565; + + public static final String ENGINE = "classic"; + + public static final int MAX_PLAYERS = 100; + public static final int ONLINE_PLAYERS = 10; + + public static final int PROTOCOL_VERSION = 47; + public static final String PROTOCOL_TEXT = "1.8.8"; + + public static final String MOTD_LINE1 = "&4Ein fake Minecraftserver"; + public static final String MOTD_LINE2 = "&cDeveloped by UnlegitDqrk"; + + public static final String KICK_MESSAGE = "&4Just a Fakeserver!\n&aDeveloped by UnlegitDqrk"; + public static final List SAMPLES = Arrays.asList("&4Fakeserver", "&5by", "&aUnlegitDqrk"); + + public static final File SERVER_ICON = new File("server-icon.png"); + + public static void start() throws Exception { + String serverIconAsString = null; + + if (SERVER_ICON.exists()) { + BufferedImage image = read(SERVER_ICON); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + write(image, "png", baos); + serverIconAsString = "data:image/png;base64," + DatatypeConverter.printBase64Binary(baos.toByteArray()); + } else System.err.println("Starting fake server without icon!"); + + String motd = MOTD_LINE1 + "\n" + MOTD_LINE2; + + Message description; + + if (ENGINE.equalsIgnoreCase("json")) + description = ChatConverter.toJSONChat(ChatConverter.replaceColors(motd).replace("\\n", "\n")); + else { + description = new Message(); + description.text = ChatConverter.replaceColors(motd).replace("\\n", "\n"); + } + + response = new StatusResponse(PROTOCOL_TEXT, PROTOCOL_VERSION, + MAX_PLAYERS, ONLINE_PLAYERS, + description, serverIconAsString); + + new SLPServer(START_PORT).run(); + } + +} diff --git a/src/main/java/me/unlegitdqrk/fakeminecraftserver/Message.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/Message.java new file mode 100644 index 0000000..9418419 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/Message.java @@ -0,0 +1,34 @@ +package me.unlegitdqrk.fakeminecraftserver; + +import java.util.List; + +/** + * @author UnlegitDqrk + */ + +public class Message { + public String text; + + public boolean bold; + public boolean italic; + public boolean underlined; + public boolean strikethrough; + public boolean obfuscated; + public List extra; + + public Color color; + + public ClickEvent clickEvent; + + public Message() { + + } + + public Message(Message old) { + this.bold = old.bold; + this.italic = old.italic; + this.underlined = old.underlined; + this.strikethrough = old.strikethrough; + this.color = old.color; + } +} diff --git a/src/main/java/me/unlegitdqrk/fakeminecraftserver/SLPServer.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/SLPServer.java new file mode 100644 index 0000000..e416629 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/SLPServer.java @@ -0,0 +1,41 @@ +package me.unlegitdqrk.fakeminecraftserver; + +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.*; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; + +/** + * @author UnlegitDqrk + */ + +public class SLPServer { + private int port; + + public SLPServer(int port) { + this.port = port; + } + + public void run() throws Exception { + EventLoopGroup bossGroup = new NioEventLoopGroup(); + EventLoopGroup workerGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap serverBootstrap = new ServerBootstrap(); + serverBootstrap.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(Channel channel) { + channel.pipeline().addLast(new BasePacketHandler()); + } + }).option(ChannelOption.SO_BACKLOG, 128) + .childOption(ChannelOption.SO_KEEPALIVE, true); + ChannelFuture channelFuture = serverBootstrap.bind(port).sync(); + channelFuture.channel().closeFuture().sync(); + } finally { + workerGroup.shutdownGracefully(); + bossGroup.shutdownGracefully(); + } + } +} diff --git a/src/main/java/me/unlegitdqrk/fakeminecraftserver/data/StatusResponse.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/data/StatusResponse.java new file mode 100644 index 0000000..1632b11 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/data/StatusResponse.java @@ -0,0 +1,62 @@ +package me.unlegitdqrk.fakeminecraftserver.data; + +import me.unlegitdqrk.fakeminecraftserver.ChatConverter; +import me.unlegitdqrk.fakeminecraftserver.FakeMinecraftServer; +import me.unlegitdqrk.fakeminecraftserver.Message; + +import java.util.*; + +/** + * @author UnlegitDqrk + */ + +public class StatusResponse { + Version version; + Players players; + Message description; + + public String favicon; + + public StatusResponse(String name, int protocol, int max, int online, Message description,String favicon) { + this.version = new Version(name, protocol); + this.players = new Players(max, online); + this.description = description; + this.favicon = favicon; + } + + public class Version { + String name; + int protocol; + + public Version(String name, int protocol) { + this.name = name; + this.protocol = protocol; + } + } + + class Players { + int max; + int online; + List sample; + + Players(int max, int online) { + this.max = max; + this.online = online; + this.sample = new ArrayList<>(); + + for (String sample : FakeMinecraftServer.SAMPLES) { + this.sample.add(new Sample(ChatConverter.replaceColors(sample))); + } + } + + class Sample { + String name; + String id; + + Sample(String name) { + this.name = name; + this.id = UUID.randomUUID().toString(); + } + } + } +} diff --git a/src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewInputStream.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewInputStream.java new file mode 100644 index 0000000..f278eaa --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewInputStream.java @@ -0,0 +1,37 @@ +package me.unlegitdqrk.fakeminecraftserver.streams; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * @author UnlegitDqrk + */ + +public class MojewInputStream extends ByteBufInputStream { + public MojewInputStream(ByteBuf buffer) { + super(buffer); + } + + @Override + public String readUTF() throws IOException { + byte[] input = new byte[readInt()]; + readFully(input); + return new String(input, StandardCharsets.UTF_8); + } + + @Override + public int readInt() throws IOException { + int i = 0; + int j = 0; + while (true) { + int k = readByte(); + i |= (k & 0x7F) << j++ * 7; + if (j > 5) throw new RuntimeException("VarInt too big"); + if ((k & 0x80) != 128) break; + } + return i; + } +} diff --git a/src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewOutputStream.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewOutputStream.java new file mode 100644 index 0000000..3d5f4ed --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewOutputStream.java @@ -0,0 +1,41 @@ +package me.unlegitdqrk.fakeminecraftserver.streams; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; + +/** + * @author UnlegitDqrk + */ + +public class MojewOutputStream extends ByteBufOutputStream { + public MojewOutputStream(ByteBuf buffer) { + super(buffer); + } + + @Override + public void writeUTF(String s) throws IOException { + byte[] data = s.getBytes(StandardCharsets.UTF_8); + writeInt(s.length()); + write(data); + } + + @Override + public void writeInt(int paramInt) throws IOException { + while (true) { + if ((paramInt & 0xFFFFFF80) == 0) { + writeByte(paramInt); + return; + } + writeByte(paramInt & 0x7F | 0x80); + paramInt >>>= 7; + } + } + + public byte[] getData() { + return Arrays.copyOfRange(buffer().array(), 0, writtenBytes()); + } +}