commit b2215884b43d8c2a160600521748265daeb39d1a Author: Finn Date: Tue Aug 1 21:32:27 2023 +0200 Init diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9b3b07a --- /dev/null +++ b/pom.xml @@ -0,0 +1,30 @@ + + + 4.0.0 + + me.unlegitdqrk.fakeminecraftserver + FakeMinecraftServer + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + + + + com.google.code.gson + gson + 2.10.1 + + + io.netty + netty-all + 4.1.96.Final + + + + \ No newline at end of file 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..d28e230 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/BasePacketHandler.java @@ -0,0 +1,85 @@ +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(StartFakeServer.response).replace(ColorConverter.ESCAPE + "", "\\u00A7"); // Mojew's parser needs this escaped (classic) + + if (StartFakeServer.response.getServerIconAsString() == 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()); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} diff --git a/src/main/java/me/unlegitdqrk/fakeminecraftserver/ColorConverter.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/ColorConverter.java new file mode 100644 index 0000000..356bab0 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/ColorConverter.java @@ -0,0 +1,120 @@ +/* +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.HashMap; +import java.util.regex.Pattern; + +public class ColorConverter { + 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[] charArray = text.toCharArray(); + + for (int index = 0; index < charArray.length; index++) { + char chr = charArray[index]; + if (chr != '&') continue; + if ((index + 1) == charArray.length) break; + + char forward = charArray[index + 1]; + + if ((forward >= '0' && forward <= '9') || (forward >= 'a' && forward <= 'f') + || (forward >= 'k' && forward <= 'r')) charArray[index] = ESCAPE; + } + return new String(charArray); + } + + public class ClickEvent { + public String action; + public String value; + } + + public 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/Message.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/Message.java new file mode 100644 index 0000000..1b638db --- /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 ColorConverter.Color color; + + public ColorConverter.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..1c6ce14 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/SLPServer.java @@ -0,0 +1,45 @@ +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 int getPort() { + return 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/StartFakeServer.java b/src/main/java/me/unlegitdqrk/fakeminecraftserver/StartFakeServer.java new file mode 100644 index 0000000..07da860 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/StartFakeServer.java @@ -0,0 +1,53 @@ +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.Arrays; +import java.util.List; + +import static javax.imageio.ImageIO.read; +import static javax.imageio.ImageIO.write; + +/** + * @author UnlegitDqrk + */ + +public class StartFakeServer { + public static StatusResponse response = null; + + public static final int START_PORT = 25565; + + 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 List SAMPLES = Arrays.asList("&cSample 1", "&bSample 2", "&aSample 3"); + 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()); + } + + Message description = new Message(); + String motd = MOTD_LINE1 + "\n" + MOTD_LINE2; + description.text = ColorConverter.replaceColors(motd).replace("\\n", "\n"); + + response = new StatusResponse(ColorConverter.replaceColors(PROTOCOL_TEXT), PROTOCOL_VERSION, + MAX_PLAYERS, ONLINE_PLAYERS, + description, serverIconAsString); + + new SLPServer(START_PORT).run(); + } + +} 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..5cf269a --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/data/StatusResponse.java @@ -0,0 +1,73 @@ +package me.unlegitdqrk.fakeminecraftserver.data; + +import me.unlegitdqrk.fakeminecraftserver.Message; + +/** + * @author UnlegitDqrk + */ + +public class StatusResponse { + private final Version version; + private final Players players; + private final Message description; + private final String serverIconAsString; + + public Message getDescription() { + return description; + } + + public Players getPlayers() { + return players; + } + + public String getServerIconAsString() { + return serverIconAsString; + } + + public Version getVersion() { + return version; + } + + public StatusResponse(String name, int protocol, int max, int online, Message description, String serverIconAsString) { + this.version = new Version(name, protocol); + this.players = new Players(max, online); + this.description = description; + this.serverIconAsString = serverIconAsString; + } + + public class Version { + private final String name; + private final int protocol; + + public String getName() { + return name; + } + + public int getProtocol() { + return protocol; + } + + public Version(String name, int protocol) { + this.name = name; + this.protocol = protocol; + } + } + + private class Players { + private final int maxPlayers; + private final int onlinePlayers; + + public int getMaxPlayers() { + return maxPlayers; + } + + public int getOnlinePlayers() { + return onlinePlayers; + } + + private Players(int maxPlayers, int onlinePlayers) { + this.maxPlayers = maxPlayers; + this.onlinePlayers = onlinePlayers; + } + } +} 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..c9e1172 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewInputStream.java @@ -0,0 +1,41 @@ +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..bf6d727 --- /dev/null +++ b/src/main/java/me/unlegitdqrk/fakeminecraftserver/streams/MojewOutputStream.java @@ -0,0 +1,42 @@ +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()); + } +}