diff --git a/src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java b/src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java new file mode 100644 index 0000000..76b95a3 --- /dev/null +++ b/src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java @@ -0,0 +1,89 @@ +/* + * MIT License + * + * Copyright (c) 2024 Braydon (Rainnny). + * + * 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 cc.fascinated.common.packet; + +import lombok.NonNull; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * Represents a packet in the + * Minecraft Java protocol. + * + * @author Braydon + * @see Protocol Docs + */ +public abstract class MinecraftJavaPacket { + /** + * Process this packet. + * + * @param inputStream the input stream to read from + * @param outputStream the output stream to write to + * @throws IOException if an I/O error occurs + */ + public abstract void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException; + + /** + * Write a variable integer to the output stream. + * + * @param outputStream the output stream to write to + * @param paramInt the integer to write + * @throws IOException if an I/O error occurs + */ + protected final void writeVarInt(DataOutputStream outputStream, int paramInt) throws IOException { + while (true) { + if ((paramInt & 0xFFFFFF80) == 0) { + outputStream.writeByte(paramInt); + return; + } + outputStream.writeByte(paramInt & 0x7F | 0x80); + paramInt >>>= 7; + } + } + + /** + * Read a variable integer from the input stream. + * + * @param inputStream the input stream to read from + * @return the integer that was read + * @throws IOException if an I/O error occurs + */ + protected final int readVarInt(@NonNull DataInputStream inputStream) throws IOException { + int i = 0; + int j = 0; + while (true) { + int k = inputStream.readByte(); + i |= (k & 0x7F) << j++ * 7; + if (j > 5) { + throw new RuntimeException("VarInt too big"); + } + if ((k & 0x80) != 128) { + break; + } + } + return i; + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java b/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java new file mode 100644 index 0000000..d589ad2 --- /dev/null +++ b/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java @@ -0,0 +1,64 @@ +package cc.fascinated.common.packet.impl.java; + +import cc.fascinated.common.packet.MinecraftJavaPacket; +import lombok.AllArgsConstructor; +import lombok.NonNull; +import lombok.ToString; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * This packet is sent by the client to the server to set + * the hostname, port, and protocol version of the client. + * + * @author Braydon + * @see Protocol Docs + */ +@AllArgsConstructor @ToString +public final class JavaPacketHandshakingInSetProtocol extends MinecraftJavaPacket { + private static final byte ID = 0x00; // The ID of the packet + private static final int STATUS_HANDSHAKE = 1; // The status handshake ID + + /** + * The hostname of the server. + */ + @NonNull private final String hostname; + + /** + * The port of the server. + */ + private final int port; + + /** + * The protocol version of the server. + */ + private final int protocolVersion; + + /** + * Process this packet. + * + * @param inputStream the input stream to read from + * @param outputStream the output stream to write to + * @throws IOException if an I/O error occurs + */ + @Override + public void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException { + try (ByteArrayOutputStream handshakeBytes = new ByteArrayOutputStream(); + DataOutputStream handshake = new DataOutputStream(handshakeBytes) + ) { + handshake.writeByte(ID); // Write the ID of the packet + writeVarInt(handshake, protocolVersion); // Write the protocol version + writeVarInt(handshake, hostname.length()); // Write the length of the hostname + handshake.writeBytes(hostname); // Write the hostname + handshake.writeShort(port); // Write the port + writeVarInt(handshake, STATUS_HANDSHAKE); // Write the status handshake ID + + // Write the handshake bytes to the output stream + writeVarInt(outputStream, handshakeBytes.size()); + outputStream.write(handshakeBytes.toByteArray()); + } + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketStatusInStart.java b/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketStatusInStart.java new file mode 100644 index 0000000..bf3a540 --- /dev/null +++ b/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketStatusInStart.java @@ -0,0 +1,62 @@ +package cc.fascinated.common.packet.impl.java; + +import cc.fascinated.common.packet.MinecraftJavaPacket; +import lombok.Getter; +import lombok.NonNull; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * This packet is sent by the client to the server to request the + * status of the server. The server will respond with a json object + * containing the server's status. + * + * @author Braydon + * @see Protocol Docs + */ +@Getter +public final class JavaPacketStatusInStart extends MinecraftJavaPacket { + private static final byte ID = 0x00; // The ID of the packet + + /** + * The response json from the server, null if none. + */ + private String response; + + /** + * Process this packet. + * + * @param inputStream the input stream to read from + * @param outputStream the output stream to write to + * @throws IOException if an I/O error occurs + */ + @Override + public void process(@NonNull DataInputStream inputStream, @NonNull DataOutputStream outputStream) throws IOException { + // Send the status request + outputStream.writeByte(0x01); // Size of packet + outputStream.writeByte(ID); + + // Read the status response + readVarInt(inputStream); // Size of the response + int id = readVarInt(inputStream); + if (id == -1) { // The stream was prematurely ended + throw new IOException("Server prematurely ended stream."); + } else if (id != ID) { // Invalid packet ID + throw new IOException("Server returned invalid packet ID."); + } + + int length = readVarInt(inputStream); // Length of the response + if (length == -1) { // The stream was prematurely ended + throw new IOException("Server prematurely ended stream."); + } else if (length == 0) { + throw new IOException("Server returned unexpected value."); + } + + // Get the json response + byte[] data = new byte[length]; + inputStream.readFully(data); + response = new String(data); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/controller/ServerController.java b/src/main/java/cc/fascinated/controller/ServerController.java new file mode 100644 index 0000000..7adbd57 --- /dev/null +++ b/src/main/java/cc/fascinated/controller/ServerController.java @@ -0,0 +1,18 @@ +package cc.fascinated.controller; + +import cc.fascinated.model.server.MinecraftServer; +import cc.fascinated.service.pinger.impl.JavaMinecraftServerPinger; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping(value = "/server/") +public class ServerController { + + @ResponseBody + @GetMapping(value = "/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getServer(@PathVariable String hostname) { + return ResponseEntity.ok(JavaMinecraftServerPinger.INSTANCE.ping(hostname, 25565)); + } +} diff --git a/src/main/java/cc/fascinated/model/mojang/JavaServerStatusToken.java b/src/main/java/cc/fascinated/model/mojang/JavaServerStatusToken.java new file mode 100644 index 0000000..ce98ab4 --- /dev/null +++ b/src/main/java/cc/fascinated/model/mojang/JavaServerStatusToken.java @@ -0,0 +1,13 @@ +package cc.fascinated.model.mojang; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter @ToString +public final class JavaServerStatusToken { + private final String description; +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/model/server/JavaMinecraftServer.java b/src/main/java/cc/fascinated/model/server/JavaMinecraftServer.java new file mode 100644 index 0000000..21948d3 --- /dev/null +++ b/src/main/java/cc/fascinated/model/server/JavaMinecraftServer.java @@ -0,0 +1,10 @@ +package cc.fascinated.model.server; + +/** + * @author Braydon + */ +public final class JavaMinecraftServer extends MinecraftServer { + public JavaMinecraftServer(String hostname, int port, String motd) { + super(hostname, port, motd); + } +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/model/server/MinecraftServer.java b/src/main/java/cc/fascinated/model/server/MinecraftServer.java new file mode 100644 index 0000000..6028a00 --- /dev/null +++ b/src/main/java/cc/fascinated/model/server/MinecraftServer.java @@ -0,0 +1,15 @@ +package cc.fascinated.model.server; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.ToString; + +/** + * @author Braydon + */ +@AllArgsConstructor @Getter @ToString +public class MinecraftServer { + private final String hostname; + private final int port; + private final String motd; +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/service/pinger/MinecraftServerPinger.java b/src/main/java/cc/fascinated/service/pinger/MinecraftServerPinger.java new file mode 100644 index 0000000..f223b1d --- /dev/null +++ b/src/main/java/cc/fascinated/service/pinger/MinecraftServerPinger.java @@ -0,0 +1,9 @@ +package cc.fascinated.service.pinger; + +/** + * @author Braydon + * @param the type of server to ping + */ +public interface MinecraftServerPinger { + T ping(String hostname, int port); +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/service/pinger/impl/JavaMinecraftServerPinger.java b/src/main/java/cc/fascinated/service/pinger/impl/JavaMinecraftServerPinger.java new file mode 100644 index 0000000..d4bd2f5 --- /dev/null +++ b/src/main/java/cc/fascinated/service/pinger/impl/JavaMinecraftServerPinger.java @@ -0,0 +1,53 @@ +package cc.fascinated.service.pinger.impl; + +import cc.fascinated.Main; +import cc.fascinated.common.packet.impl.java.JavaPacketHandshakingInSetProtocol; +import cc.fascinated.common.packet.impl.java.JavaPacketStatusInStart; +import cc.fascinated.model.mojang.JavaServerStatusToken; +import cc.fascinated.model.server.JavaMinecraftServer; +import cc.fascinated.service.pinger.MinecraftServerPinger; +import lombok.extern.log4j.Log4j2; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; + +/** + * @author Braydon + */ +@Log4j2(topic = "Java Pinger") +public final class JavaMinecraftServerPinger implements MinecraftServerPinger { + public static final JavaMinecraftServerPinger INSTANCE = new JavaMinecraftServerPinger(); + + private static final int TIMEOUT = 3000; // The timeout for the socket + + @Override + public JavaMinecraftServer ping(String hostname, int port) { + log.info("Pinging {}:{}...", hostname, port); + + // Open a socket connection to the server + try (Socket socket = new Socket()) { + socket.setTcpNoDelay(true); + socket.connect(new InetSocketAddress(hostname, port), TIMEOUT); + + // Open data streams to begin packet transaction + try (DataInputStream inputStream = new DataInputStream(socket.getInputStream()); + DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) { + // Begin handshaking with the server + new JavaPacketHandshakingInSetProtocol(hostname, port, 47).process(inputStream, outputStream); + + // Send the status request to the server, and await back the response + JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart(); + packetStatusInStart.process(inputStream, outputStream); + System.out.println("packetStatusInStart.getResponse() = " + packetStatusInStart.getResponse()); + JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class); + return new JavaMinecraftServer(hostname, port, token.getDescription()); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + return null; + } +} \ No newline at end of file