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..55d4dd9 --- /dev/null +++ b/src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java @@ -0,0 +1,66 @@ +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/PlayerController.java b/src/main/java/cc/fascinated/controller/PlayerController.java index 2dd5dbf..52bc8ec 100644 --- a/src/main/java/cc/fascinated/controller/PlayerController.java +++ b/src/main/java/cc/fascinated/controller/PlayerController.java @@ -1,11 +1,11 @@ package cc.fascinated.controller; +import cc.fascinated.common.PlayerUtils; import cc.fascinated.model.player.Player; import cc.fascinated.model.player.Skin; import cc.fascinated.model.response.impl.InvalidPartResponse; import cc.fascinated.model.response.impl.PlayerNotFoundResponse; import cc.fascinated.service.PlayerService; -import cc.fascinated.common.PlayerUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.CacheControl; import org.springframework.http.MediaType; 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/mojang/MojangProfile.java b/src/main/java/cc/fascinated/model/mojang/MojangProfile.java index 2c487c8..d10f97d 100644 --- a/src/main/java/cc/fascinated/model/mojang/MojangProfile.java +++ b/src/main/java/cc/fascinated/model/mojang/MojangProfile.java @@ -1,10 +1,10 @@ package cc.fascinated.model.mojang; import cc.fascinated.Main; -import cc.fascinated.model.player.Cape; -import cc.fascinated.model.player.Skin; import cc.fascinated.common.Tuple; import cc.fascinated.common.UUIDUtils; +import cc.fascinated.model.player.Cape; +import cc.fascinated.model.player.Skin; import com.google.gson.JsonObject; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/cc/fascinated/model/player/Player.java b/src/main/java/cc/fascinated/model/player/Player.java index 4660ae5..021560e 100644 --- a/src/main/java/cc/fascinated/model/player/Player.java +++ b/src/main/java/cc/fascinated/model/player/Player.java @@ -1,8 +1,8 @@ package cc.fascinated.model.player; -import cc.fascinated.model.mojang.MojangProfile; import cc.fascinated.common.Tuple; import cc.fascinated.common.UUIDUtils; +import cc.fascinated.model.mojang.MojangProfile; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; diff --git a/src/main/java/cc/fascinated/model/player/Skin.java b/src/main/java/cc/fascinated/model/player/Skin.java index bb27d6c..80206c4 100644 --- a/src/main/java/cc/fascinated/model/player/Skin.java +++ b/src/main/java/cc/fascinated/model/player/Skin.java @@ -1,7 +1,7 @@ package cc.fascinated.model.player; -import cc.fascinated.config.Config; import cc.fascinated.common.PlayerUtils; +import cc.fascinated.config.Config; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.google.gson.JsonObject; 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/MojangAPIService.java b/src/main/java/cc/fascinated/service/MojangAPIService.java index 56982c4..54c6330 100644 --- a/src/main/java/cc/fascinated/service/MojangAPIService.java +++ b/src/main/java/cc/fascinated/service/MojangAPIService.java @@ -1,8 +1,8 @@ package cc.fascinated.service; +import cc.fascinated.common.WebRequest; import cc.fascinated.model.mojang.MojangProfile; import cc.fascinated.model.mojang.MojangUsernameToUuid; -import cc.fascinated.common.WebRequest; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/src/main/java/cc/fascinated/service/PlayerService.java b/src/main/java/cc/fascinated/service/PlayerService.java index 2ec5909..e24fb69 100644 --- a/src/main/java/cc/fascinated/service/PlayerService.java +++ b/src/main/java/cc/fascinated/service/PlayerService.java @@ -1,9 +1,9 @@ package cc.fascinated.service; +import cc.fascinated.common.UUIDUtils; import cc.fascinated.model.mojang.MojangProfile; import cc.fascinated.model.mojang.MojangUsernameToUuid; import cc.fascinated.model.player.Player; -import cc.fascinated.common.UUIDUtils; import lombok.extern.log4j.Log4j2; import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpiringMap; 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