This commit is contained in:
2023-08-01 22:40:05 +02:00
parent 6e693d74c7
commit 5a686c8bdc
8 changed files with 573 additions and 0 deletions

View File

@@ -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();
}
}

View File

@@ -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<Message> 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<String, Color> codeMap = new HashMap<>();
public static Color fromCode(String code) {
return codeMap.get(code);
}
static {
for (Color color : values()) {
codeMap.put(color.code, color);
}
}
}

View File

@@ -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<String> 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();
}
}

View File

@@ -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<Message> 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;
}
}

View File

@@ -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<Channel>() {
@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();
}
}
}

View File

@@ -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> 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();
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}