From 25c69e11e15497b5c8ccbc9bd1bb543af2a80fe7 Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 10 Apr 2024 07:43:13 +0100 Subject: [PATCH] add server pinger --- .gitignore | 1 + pom.xml | 38 +++++ src/main/java/cc/fascinated/Main.java | 35 ---- .../java/cc/fascinated/common/IPUtils.java | 42 ----- .../cc/fascinated/common/PlayerUtils.java | 72 -------- src/main/java/cc/fascinated/common/Tuple.java | 18 -- .../java/cc/fascinated/common/UUIDUtils.java | 22 --- .../java/cc/fascinated/common/WebRequest.java | 41 ----- .../common/packet/MinecraftJavaPacket.java | 66 -------- .../JavaPacketHandshakingInSetProtocol.java | 64 -------- .../impl/java/JavaPacketStatusInStart.java | 62 ------- .../java/cc/fascinated/config/Config.java | 20 --- .../fascinated/controller/HomeController.java | 23 --- .../controller/PlayerController.java | 65 -------- .../controller/ServerController.java | 18 -- .../exception/ExceptionControllerAdvice.java | 33 ---- .../cc/fascinated/log/TransactionLogger.java | 78 --------- .../model/mojang/JavaServerStatusToken.java | 13 -- .../model/mojang/MojangProfile.java | 111 ------------- .../model/mojang/MojangUsernameToUuid.java | 27 --- .../java/cc/fascinated/model/player/Cape.java | 27 --- .../cc/fascinated/model/player/Player.java | 48 ------ .../java/cc/fascinated/model/player/Skin.java | 154 ------------------ .../fascinated/model/response/Response.java | 29 ---- .../response/impl/InvalidPartResponse.java | 11 -- .../response/impl/PlayerNotFoundResponse.java | 11 -- .../model/server/JavaMinecraftServer.java | 10 -- .../model/server/MinecraftServer.java | 15 -- .../fascinated/service/MojangAPIService.java | 40 ----- .../cc/fascinated/service/PlayerService.java | 81 --------- .../service/pinger/MinecraftServerPinger.java | 9 - .../impl/JavaMinecraftServerPinger.java | 53 ------ src/main/resources/application.yml | 10 ++ src/main/resources/public/favicon.ico | Bin 67646 -> 4286 bytes .../cc/fascinated/PlayerControllerTests.java | 2 +- .../java/cc/fascinated/TestRedisConfig.java | 44 +++++ target/classes/application.yml | 10 ++ target/classes/public/favicon.ico | Bin 67646 -> 4286 bytes 38 files changed, 104 insertions(+), 1299 deletions(-) delete mode 100644 src/main/java/cc/fascinated/Main.java delete mode 100644 src/main/java/cc/fascinated/common/IPUtils.java delete mode 100644 src/main/java/cc/fascinated/common/PlayerUtils.java delete mode 100644 src/main/java/cc/fascinated/common/Tuple.java delete mode 100644 src/main/java/cc/fascinated/common/UUIDUtils.java delete mode 100644 src/main/java/cc/fascinated/common/WebRequest.java delete mode 100644 src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java delete mode 100644 src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java delete mode 100644 src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketStatusInStart.java delete mode 100644 src/main/java/cc/fascinated/config/Config.java delete mode 100644 src/main/java/cc/fascinated/controller/HomeController.java delete mode 100644 src/main/java/cc/fascinated/controller/PlayerController.java delete mode 100644 src/main/java/cc/fascinated/controller/ServerController.java delete mode 100644 src/main/java/cc/fascinated/exception/ExceptionControllerAdvice.java delete mode 100644 src/main/java/cc/fascinated/log/TransactionLogger.java delete mode 100644 src/main/java/cc/fascinated/model/mojang/JavaServerStatusToken.java delete mode 100644 src/main/java/cc/fascinated/model/mojang/MojangProfile.java delete mode 100644 src/main/java/cc/fascinated/model/mojang/MojangUsernameToUuid.java delete mode 100644 src/main/java/cc/fascinated/model/player/Cape.java delete mode 100644 src/main/java/cc/fascinated/model/player/Player.java delete mode 100644 src/main/java/cc/fascinated/model/player/Skin.java delete mode 100644 src/main/java/cc/fascinated/model/response/Response.java delete mode 100644 src/main/java/cc/fascinated/model/response/impl/InvalidPartResponse.java delete mode 100644 src/main/java/cc/fascinated/model/response/impl/PlayerNotFoundResponse.java delete mode 100644 src/main/java/cc/fascinated/model/server/JavaMinecraftServer.java delete mode 100644 src/main/java/cc/fascinated/model/server/MinecraftServer.java delete mode 100644 src/main/java/cc/fascinated/service/MojangAPIService.java delete mode 100644 src/main/java/cc/fascinated/service/PlayerService.java delete mode 100644 src/main/java/cc/fascinated/service/pinger/MinecraftServerPinger.java delete mode 100644 src/main/java/cc/fascinated/service/pinger/impl/JavaMinecraftServerPinger.java create mode 100644 src/test/java/cc/fascinated/TestRedisConfig.java diff --git a/.gitignore b/.gitignore index 1cfa6f5..77440ae 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ fabric.properties git.properties pom.xml.versionsBackup application.yml +target/ diff --git a/pom.xml b/pom.xml index e38df86..b6e24d7 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,14 @@ + + + + jitpack.io + https://jitpack.io + + + org.projectlombok @@ -84,6 +92,30 @@ compile + + + com.github.dnsjava + dnsjava + v3.5.2 + compile + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + io.lettuce + lettuce-core + + + + + redis.clients + jedis + + org.junit.jupiter @@ -108,6 +140,12 @@ spring-boot-starter-test test + + com.github.codemonstur + embedded-redis + 1.4.3 + test + \ No newline at end of file diff --git a/src/main/java/cc/fascinated/Main.java b/src/main/java/cc/fascinated/Main.java deleted file mode 100644 index 5383feb..0000000 --- a/src/main/java/cc/fascinated/Main.java +++ /dev/null @@ -1,35 +0,0 @@ -package cc.fascinated; - -import com.google.gson.Gson; -import lombok.SneakyThrows; -import lombok.extern.log4j.Log4j2; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -import java.io.File; -import java.net.http.HttpClient; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.util.Objects; - -@SpringBootApplication @Log4j2 -public class Main { - - public static final Gson GSON = new Gson(); - public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); - - @SneakyThrows - public static void main(String[] args) { - File config = new File("application.yml"); - if (!config.exists()) { // Saving the default config if it doesn't exist locally - Files.copy(Objects.requireNonNull(Main.class.getResourceAsStream("/application.yml")), config.toPath(), StandardCopyOption.REPLACE_EXISTING); - log.info("Saved the default configuration to '{}', please re-launch the application", // Log the default config being saved - config.getAbsolutePath() - ); - return; - } - log.info("Found configuration at '{}'", config.getAbsolutePath()); // Log the found config - - SpringApplication.run(Main.class, args); - } -} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/common/IPUtils.java b/src/main/java/cc/fascinated/common/IPUtils.java deleted file mode 100644 index eed9631..0000000 --- a/src/main/java/cc/fascinated/common/IPUtils.java +++ /dev/null @@ -1,42 +0,0 @@ -package cc.fascinated.common; - -import jakarta.servlet.http.HttpServletRequest; -import lombok.experimental.UtilityClass; - -@UtilityClass -public class IPUtils { - /** - * The headers that contain the IP. - */ - private static final String[] IP_HEADERS = new String[] { - "CF-Connecting-IP", - "X-Forwarded-For" - }; - - /** - * Get the real IP from the given request. - * - * @param request the request - * @return the real IP - */ - public static String getRealIp(HttpServletRequest request) { - String ip = request.getRemoteAddr(); - for (String headerName : IP_HEADERS) { - String header = request.getHeader(headerName); - if (header == null) { - continue; - } - if (!header.contains(",")) { // Handle single IP - ip = header; - break; - } - // Handle multiple IPs - String[] ips = header.split(","); - for (String ipHeader : ips) { - ip = ipHeader; - break; - } - } - return ip; - } -} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/common/PlayerUtils.java b/src/main/java/cc/fascinated/common/PlayerUtils.java deleted file mode 100644 index 246ed67..0000000 --- a/src/main/java/cc/fascinated/common/PlayerUtils.java +++ /dev/null @@ -1,72 +0,0 @@ -package cc.fascinated.common; - -import cc.fascinated.Main; -import cc.fascinated.model.player.Skin; -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.SneakyThrows; -import lombok.experimental.UtilityClass; -import lombok.extern.log4j.Log4j2; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.net.URI; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; - -@UtilityClass @Log4j2 -public class PlayerUtils { - - /** - * Gets the skin data from the URL. - * - * @return the skin data - */ - @SneakyThrows - @JsonIgnore - public static BufferedImage getSkinImage(String url) { - HttpResponse response = Main.HTTP_CLIENT.send(HttpRequest.newBuilder(URI.create(url)).build(), - HttpResponse.BodyHandlers.ofByteArray()); - byte[] body = response.body(); - if (body == null) { - return null; - } - return ImageIO.read(new ByteArrayInputStream(body)); - } - - /** - * Gets the part data from the skin. - * - * @return the part data - */ - public static byte[] getSkinPartBytes(Skin skin, Skin.Parts part, int size) { - if (size == -1) { - size = part.getDefaultSize(); - } - - try { - BufferedImage image = skin.getSkinImage(); - if (image == null) { - return null; - } - // Get the part of the image (e.g. the head) - BufferedImage partImage = image.getSubimage(part.getX(), part.getY(), part.getWidth(), part.getHeight()); - - // Scale the image - BufferedImage scaledImage = new BufferedImage(size, size, partImage.getType()); - Graphics2D graphics2D = scaledImage.createGraphics(); - graphics2D.drawImage(partImage, 0, 0, size, size, null); - graphics2D.dispose(); - partImage = scaledImage; - - ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); - ImageIO.write(partImage, "png", byteArrayOutputStream); - return byteArrayOutputStream.toByteArray(); - } catch (Exception ex) { - log.error("Failed to get {} part bytes for {}", part.name(), skin.getUrl(), ex); - return null; - } - } -} diff --git a/src/main/java/cc/fascinated/common/Tuple.java b/src/main/java/cc/fascinated/common/Tuple.java deleted file mode 100644 index d51de5d..0000000 --- a/src/main/java/cc/fascinated/common/Tuple.java +++ /dev/null @@ -1,18 +0,0 @@ -package cc.fascinated.common; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter @AllArgsConstructor -public class Tuple { - - /** - * The left value of the tuple. - */ - private final L left; - - /** - * The right value of the tuple. - */ - private final R right; -} diff --git a/src/main/java/cc/fascinated/common/UUIDUtils.java b/src/main/java/cc/fascinated/common/UUIDUtils.java deleted file mode 100644 index 204f5cd..0000000 --- a/src/main/java/cc/fascinated/common/UUIDUtils.java +++ /dev/null @@ -1,22 +0,0 @@ -package cc.fascinated.common; - -import lombok.experimental.UtilityClass; - -@UtilityClass -public class UUIDUtils { - - /** - * Add dashes to a UUID. - * - * @param idNoDashes the UUID without dashes - * @return the UUID with dashes - */ - public static String addUuidDashes(String idNoDashes) { - StringBuilder idBuff = new StringBuilder(idNoDashes); - idBuff.insert(20, '-'); - idBuff.insert(16, '-'); - idBuff.insert(12, '-'); - idBuff.insert(8, '-'); - return idBuff.toString(); - } -} diff --git a/src/main/java/cc/fascinated/common/WebRequest.java b/src/main/java/cc/fascinated/common/WebRequest.java deleted file mode 100644 index dc5ef00..0000000 --- a/src/main/java/cc/fascinated/common/WebRequest.java +++ /dev/null @@ -1,41 +0,0 @@ -package cc.fascinated.common; - -import lombok.experimental.UtilityClass; -import org.springframework.http.ResponseEntity; -import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestClient; - -@UtilityClass -public class WebRequest { - - /** - * The web client. - */ - private static final RestClient CLIENT = RestClient.builder() - .requestFactory(new HttpComponentsClientHttpRequestFactory()) - .build(); - - /** - * Gets a response from the given URL. - * - * @param url the url - * @return the response - * @param the type of the response - */ - public static T getAsEntity(String url, Class clazz) { - try { - ResponseEntity profile = CLIENT.get() - .uri(url) - .retrieve() - .toEntity(clazz); - - if (profile.getStatusCode().isError()) { - return null; - } - return profile.getBody(); - } catch (HttpClientErrorException ex) { - return null; - } - } -} diff --git a/src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java b/src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java deleted file mode 100644 index 55d4dd9..0000000 --- a/src/main/java/cc/fascinated/common/packet/MinecraftJavaPacket.java +++ /dev/null @@ -1,66 +0,0 @@ -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 deleted file mode 100644 index d589ad2..0000000 --- a/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketHandshakingInSetProtocol.java +++ /dev/null @@ -1,64 +0,0 @@ -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 deleted file mode 100644 index bf3a540..0000000 --- a/src/main/java/cc/fascinated/common/packet/impl/java/JavaPacketStatusInStart.java +++ /dev/null @@ -1,62 +0,0 @@ -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/config/Config.java b/src/main/java/cc/fascinated/config/Config.java deleted file mode 100644 index 2aac78f..0000000 --- a/src/main/java/cc/fascinated/config/Config.java +++ /dev/null @@ -1,20 +0,0 @@ -package cc.fascinated.config; - -import jakarta.annotation.PostConstruct; -import lombok.Getter; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Configuration; - -@Configuration -@Getter -public class Config { - public static Config INSTANCE; - - @Value("${public-url}") - private String webPublicUrl; - - @PostConstruct - public void onInitialize() { - INSTANCE = this; - } -} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/controller/HomeController.java b/src/main/java/cc/fascinated/controller/HomeController.java deleted file mode 100644 index 854c1f6..0000000 --- a/src/main/java/cc/fascinated/controller/HomeController.java +++ /dev/null @@ -1,23 +0,0 @@ -package cc.fascinated.controller; - -import cc.fascinated.config.Config; -import org.springframework.stereotype.Controller; -import org.springframework.ui.Model; -import org.springframework.web.bind.annotation.RequestMapping; - -@Controller -@RequestMapping(value = "/") -public class HomeController { - - /** - * The example UUID. - */ - @SuppressWarnings("FieldCanBeLocal") - private final String exampleUuid = "eeab5f8a-18dd-4d58-af78-2b3c4543da48"; - - @RequestMapping(value = "/") - public String home(Model model) { - model.addAttribute("player_example_url", Config.INSTANCE.getWebPublicUrl() + "/player/" + exampleUuid); - return "index"; - } -} diff --git a/src/main/java/cc/fascinated/controller/PlayerController.java b/src/main/java/cc/fascinated/controller/PlayerController.java deleted file mode 100644 index 52bc8ec..0000000 --- a/src/main/java/cc/fascinated/controller/PlayerController.java +++ /dev/null @@ -1,65 +0,0 @@ -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 org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.CacheControl; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.concurrent.TimeUnit; - -@RestController -@RequestMapping(value = "/player/") -public class PlayerController { - - private final CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS).cachePublic(); - private final PlayerService playerManagerService; - - @Autowired - public PlayerController(PlayerService playerManagerService) { - this.playerManagerService = playerManagerService; - } - - @ResponseBody - @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getPlayer(@PathVariable String id) { - Player player = playerManagerService.getPlayer(id); - if (player == null) { // No player with that id was found - return new PlayerNotFoundResponse().toResponseEntity(); - } - // Return the player - return ResponseEntity.ok() - .cacheControl(cacheControl) - .body(player); - } - - @GetMapping(value = "/{part}/{id}") - public ResponseEntity getPlayerHead(@PathVariable String part, - @PathVariable String id, - @RequestParam(required = false, defaultValue = "256") int size) { - Player player = playerManagerService.getPlayer(id); - byte[] partBytes = new byte[0]; - if (player != null) { // The player exists - Skin skin = player.getSkin(); - Skin.Parts skinPart = Skin.Parts.fromName(part); - if (skinPart == null) { // Unknown part name - return new InvalidPartResponse().toResponseEntity(); - } - partBytes = PlayerUtils.getSkinPartBytes(skin, skinPart, size); - } - if (partBytes == null) { // Fallback to the default head - partBytes = PlayerUtils.getSkinPartBytes(Skin.DEFAULT_SKIN, Skin.Parts.HEAD, size); - } - // Return the part image - return ResponseEntity.ok() - .cacheControl(cacheControl) - .contentType(MediaType.IMAGE_PNG) - .body(partBytes); - } -} diff --git a/src/main/java/cc/fascinated/controller/ServerController.java b/src/main/java/cc/fascinated/controller/ServerController.java deleted file mode 100644 index 7adbd57..0000000 --- a/src/main/java/cc/fascinated/controller/ServerController.java +++ /dev/null @@ -1,18 +0,0 @@ -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/exception/ExceptionControllerAdvice.java b/src/main/java/cc/fascinated/exception/ExceptionControllerAdvice.java deleted file mode 100644 index 423c43b..0000000 --- a/src/main/java/cc/fascinated/exception/ExceptionControllerAdvice.java +++ /dev/null @@ -1,33 +0,0 @@ -package cc.fascinated.exception; - -import cc.fascinated.model.response.Response; -import io.micrometer.common.lang.NonNull; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ControllerAdvice -public final class ExceptionControllerAdvice { - - /** - * Handle a raised exception. - * - * @param ex the raised exception - * @return the error response - */ - @ExceptionHandler(Exception.class) - public ResponseEntity handleException(@NonNull Exception ex) { - HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; // Get the HTTP status - if (ex.getClass().isAnnotationPresent(ResponseStatus.class)) { // Get from the @ResponseStatus annotation - status = ex.getClass().getAnnotation(ResponseStatus.class).value(); - } - String message = ex.getLocalizedMessage(); // Get the error message - if (message == null) { // Fallback - message = "An internal error has occurred."; - } - ex.printStackTrace(); // Print the stack trace - return new Response(status, message).toResponseEntity(); // Return the error response - } -} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/log/TransactionLogger.java b/src/main/java/cc/fascinated/log/TransactionLogger.java deleted file mode 100644 index ab9de46..0000000 --- a/src/main/java/cc/fascinated/log/TransactionLogger.java +++ /dev/null @@ -1,78 +0,0 @@ -package cc.fascinated.log; - -import cc.fascinated.common.IPUtils; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import org.springframework.core.MethodParameter; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.http.server.ServletServerHttpResponse; -import org.springframework.web.bind.annotation.ControllerAdvice; -import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; - -import java.util.Arrays; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -@ControllerAdvice -@Slf4j(topic = "Req/Res Transaction") -public class TransactionLogger implements ResponseBodyAdvice { - @Override - public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, - @NonNull Class> selectedConverterType, @NonNull ServerHttpRequest rawRequest, - @NonNull ServerHttpResponse rawResponse) { - HttpServletRequest request = ((ServletServerHttpRequest) rawRequest).getServletRequest(); - HttpServletResponse response = ((ServletServerHttpResponse) rawResponse).getServletResponse(); - - // Get the request ip ip - String ip = IPUtils.getRealIp(request); - - // Getting params - Map params = new HashMap<>(); - for (Entry entry : request.getParameterMap().entrySet()) { - params.put(entry.getKey(), Arrays.toString(entry.getValue())); - } - - // Getting headers - Map headers = new HashMap<>(); - Enumeration headerNames = request.getHeaderNames(); - while (headerNames.hasMoreElements()) { - String headerName = headerNames.nextElement(); - headers.put(headerName, request.getHeader(headerName)); - } - - // Log the request - log.info(String.format("[Req] %s | %s | '%s', params=%s, headers=%s", - request.getMethod(), - ip, - request.getRequestURI(), - params, - headers - )); - - // Getting response headers - headers = new HashMap<>(); - for (String headerName : response.getHeaderNames()) { - headers.put(headerName, response.getHeader(headerName)); - } - - // Log the response - log.info(String.format("[Res] %s, headers=%s", - response.getStatus(), - headers - )); - return body; - } - - @Override - public boolean supports(@NonNull MethodParameter returnType, @NonNull Class> converterType) { - return true; - } -} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/model/mojang/JavaServerStatusToken.java b/src/main/java/cc/fascinated/model/mojang/JavaServerStatusToken.java deleted file mode 100644 index ce98ab4..0000000 --- a/src/main/java/cc/fascinated/model/mojang/JavaServerStatusToken.java +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index d10f97d..0000000 --- a/src/main/java/cc/fascinated/model/mojang/MojangProfile.java +++ /dev/null @@ -1,111 +0,0 @@ -package cc.fascinated.model.mojang; - -import cc.fascinated.Main; -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; -import lombok.NoArgsConstructor; - -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; - -@Getter @NoArgsConstructor -public class MojangProfile { - - /** - * The UUID of the player. - */ - private String id; - - /** - * The name of the player. - */ - private String name; - - /** - * The properties of the player. - */ - private final List properties = new ArrayList<>(); - - /** - * Get the skin and cape of the player. - * - * @return the skin and cape of the player - */ - public Tuple getSkinAndCape() { - ProfileProperty textureProperty = getProfileProperty("textures"); - if (textureProperty == null) { - return null; - } - - JsonObject json = Main.GSON.fromJson(textureProperty.getDecodedValue(), JsonObject.class); // Decode the texture property - JsonObject texturesJson = json.getAsJsonObject("textures"); // Parse the decoded JSON and get the textures object - - return new Tuple<>(Skin.fromJson(texturesJson.getAsJsonObject("SKIN")).populatePartUrls(this.getFormattedUuid()), - Cape.fromJson(texturesJson.getAsJsonObject("CAPE"))); - } - - /** - * Gets the formatted UUID of the player. - * - * @return the formatted UUID - */ - public String getFormattedUuid() { - return id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id; - } - - /** - * Get a profile property for the player - * - * @return the profile property - */ - public ProfileProperty getProfileProperty(String name) { - for (ProfileProperty property : properties) { - if (property.getName().equals(name)) { - return property; - } - } - return null; - } - - @Getter @AllArgsConstructor - public static class ProfileProperty { - /** - * The name of the property. - */ - private String name; - - /** - * The base64 value of the property. - */ - private String value; - - /** - * The signature of the property. - */ - private String signature; - - /** - * Decodes the value for this property. - * - * @return the decoded value - */ - public String getDecodedValue() { - return new String(Base64.getDecoder().decode(this.value)); - } - - /** - * Check if the property is signed. - * - * @return true if the property is signed, false otherwise - */ - public boolean isSigned() { - return signature != null; - } - } -} diff --git a/src/main/java/cc/fascinated/model/mojang/MojangUsernameToUuid.java b/src/main/java/cc/fascinated/model/mojang/MojangUsernameToUuid.java deleted file mode 100644 index a8a2c0d..0000000 --- a/src/main/java/cc/fascinated/model/mojang/MojangUsernameToUuid.java +++ /dev/null @@ -1,27 +0,0 @@ -package cc.fascinated.model.mojang; - -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter @NoArgsConstructor -public class MojangUsernameToUuid { - - /** - * The UUID of the player. - */ - private String id; - - /** - * The name of the player. - */ - private String name; - - /** - * Check if the profile is valid. - * - * @return if the profile is valid - */ - public boolean isValid() { - return id != null && name != null; - } -} diff --git a/src/main/java/cc/fascinated/model/player/Cape.java b/src/main/java/cc/fascinated/model/player/Cape.java deleted file mode 100644 index cb26d0a..0000000 --- a/src/main/java/cc/fascinated/model/player/Cape.java +++ /dev/null @@ -1,27 +0,0 @@ -package cc.fascinated.model.player; - -import com.google.gson.JsonObject; -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter @AllArgsConstructor -public class Cape { - - /** - * The URL of the cape - */ - private final String url; - - /** - * Gets the cape from a {@link JsonObject}. - * - * @param json the JSON object - * @return the cape - */ - public static Cape fromJson(JsonObject json) { - if (json == null) { - return null; - } - return new Cape(json.get("url").getAsString()); - } -} diff --git a/src/main/java/cc/fascinated/model/player/Player.java b/src/main/java/cc/fascinated/model/player/Player.java deleted file mode 100644 index 021560e..0000000 --- a/src/main/java/cc/fascinated/model/player/Player.java +++ /dev/null @@ -1,48 +0,0 @@ -package cc.fascinated.model.player; - -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; - -import java.util.UUID; - -@Getter -public class Player { - - /** - * The UUID of the player - */ - private final UUID uuid; - - /** - * The username of the player - */ - @JsonProperty("username") - private final String name; - - /** - * The skin of the player, null if the - * player does not have a skin - */ - private Skin skin; - - /** - * The cape of the player, null if the - * player does not have a cape - */ - private Cape cape; - - public Player(MojangProfile profile) { - this.uuid = UUID.fromString(UUIDUtils.addUuidDashes(profile.getId())); - this.name = profile.getName(); - - // Get the skin and cape - Tuple skinAndCape = profile.getSkinAndCape(); - if (skinAndCape != null) { - this.skin = skinAndCape.getLeft(); - this.cape = skinAndCape.getRight(); - } - } -} diff --git a/src/main/java/cc/fascinated/model/player/Skin.java b/src/main/java/cc/fascinated/model/player/Skin.java deleted file mode 100644 index 80206c4..0000000 --- a/src/main/java/cc/fascinated/model/player/Skin.java +++ /dev/null @@ -1,154 +0,0 @@ -package cc.fascinated.model.player; - -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; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.extern.log4j.Log4j2; - -import java.awt.image.BufferedImage; -import java.util.HashMap; -import java.util.Map; - -@Getter @Log4j2 -public class Skin { - /** - * The default skin, usually used when the skin is not found. - */ - public static final Skin DEFAULT_SKIN = new Skin("http://textures.minecraft.net/texture/60a5bd016b3c9a1b9272e4929e30827a67be4ebb219017adbbc4a4d22ebd5b1", - Model.DEFAULT); - - /** - * The URL for the skin - */ - private final String url; - - /** - * The model for the skin - */ - private final Model model; - - /** - * The skin image for the skin - */ - @JsonIgnore - private final BufferedImage skinImage; - - /** - * The part URLs of the skin - */ - @JsonProperty("parts") - private final Map partUrls = new HashMap<>(); - - public Skin(String url, Model model) { - this.url = url; - this.model = model; - - this.skinImage = PlayerUtils.getSkinImage(url); - } - - /** - * Gets the skin from a {@link JsonObject}. - * - * @param json the JSON object - * @return the skin - */ - public static Skin fromJson(JsonObject json) { - if (json == null) { - return null; - } - String url = json.get("url").getAsString(); - JsonObject metadata = json.getAsJsonObject("metadata"); - Model model = Model.fromName(metadata == null ? "slim" : // Fall back to slim if the model is not found - metadata.get("model").getAsString()); - return new Skin(url, model); - } - - /** - * Populates the part URLs for the skin. - * - * @param playerUuid the player's UUID - */ - public Skin populatePartUrls(String playerUuid) { - for (Parts part : Parts.values()) { - String partName = part.name().toLowerCase(); - this.partUrls.put(partName, Config.INSTANCE.getWebPublicUrl() + "/player/" + partName + "/" + playerUuid + "?size=" + part.getDefaultSize()); - } - return this; - } - - /** - * The skin part enum that contains the - * information about the part. - */ - @Getter @AllArgsConstructor - public enum Parts { - - HEAD(8, 8, 8, 8, 256); - - /** - * The x and y position of the part. - */ - private final int x, y; - - /** - * The width and height of the part. - */ - private final int width, height; - - /** - * The scale of the part. - */ - private final int defaultSize; - - /** - * Gets the name of the part. - * - * @return the name of the part - */ - public String getName() { - return this.name().toLowerCase(); - } - - /** - * Gets the skin part from its name. - * - * @param name the name of the part - * @return the skin part - */ - public static Parts fromName(String name) { - for (Parts part : values()) { - if (part.name().equalsIgnoreCase(name)) { - return part; - } - } - return null; - } - } - - /** - * The model of the skin. - */ - public enum Model { - DEFAULT, - SLIM; - - /** - * Gets the model from its name. - * - * @param name the name of the model - * @return the model - */ - public static Model fromName(String name) { - for (Model model : values()) { - if (model.name().equalsIgnoreCase(name)) { - return model; - } - } - return null; - } - } -} diff --git a/src/main/java/cc/fascinated/model/response/Response.java b/src/main/java/cc/fascinated/model/response/Response.java deleted file mode 100644 index d44ea2c..0000000 --- a/src/main/java/cc/fascinated/model/response/Response.java +++ /dev/null @@ -1,29 +0,0 @@ -package cc.fascinated.model.response; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; - -@Getter @AllArgsConstructor -public class Response { - - /** - * The status code of this error. - */ - private HttpStatus status; - - /** - * The message of this error. - */ - private String message; - - /** - * Gets this response as a {@link ResponseEntity}. - * - * @return the response entity - */ - public ResponseEntity toResponseEntity() { - return new ResponseEntity<>(this, status); - } -} diff --git a/src/main/java/cc/fascinated/model/response/impl/InvalidPartResponse.java b/src/main/java/cc/fascinated/model/response/impl/InvalidPartResponse.java deleted file mode 100644 index 132e643..0000000 --- a/src/main/java/cc/fascinated/model/response/impl/InvalidPartResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package cc.fascinated.model.response.impl; - -import cc.fascinated.model.response.Response; -import org.springframework.http.HttpStatus; - -public class InvalidPartResponse extends Response { - - public InvalidPartResponse() { - super(HttpStatus.NOT_FOUND, "Invalid part name."); - } -} diff --git a/src/main/java/cc/fascinated/model/response/impl/PlayerNotFoundResponse.java b/src/main/java/cc/fascinated/model/response/impl/PlayerNotFoundResponse.java deleted file mode 100644 index aee048c..0000000 --- a/src/main/java/cc/fascinated/model/response/impl/PlayerNotFoundResponse.java +++ /dev/null @@ -1,11 +0,0 @@ -package cc.fascinated.model.response.impl; - -import cc.fascinated.model.response.Response; -import org.springframework.http.HttpStatus; - -public class PlayerNotFoundResponse extends Response { - - public PlayerNotFoundResponse() { - super(HttpStatus.NOT_FOUND, "Player not found."); - } -} diff --git a/src/main/java/cc/fascinated/model/server/JavaMinecraftServer.java b/src/main/java/cc/fascinated/model/server/JavaMinecraftServer.java deleted file mode 100644 index 21948d3..0000000 --- a/src/main/java/cc/fascinated/model/server/JavaMinecraftServer.java +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 6028a00..0000000 --- a/src/main/java/cc/fascinated/model/server/MinecraftServer.java +++ /dev/null @@ -1,15 +0,0 @@ -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 deleted file mode 100644 index 54c6330..0000000 --- a/src/main/java/cc/fascinated/service/MojangAPIService.java +++ /dev/null @@ -1,40 +0,0 @@ -package cc.fascinated.service; - -import cc.fascinated.common.WebRequest; -import cc.fascinated.model.mojang.MojangProfile; -import cc.fascinated.model.mojang.MojangUsernameToUuid; -import lombok.extern.log4j.Log4j2; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; - -@Service @Log4j2 -public class MojangAPIService { - - @Value("${mojang.session-server}") - private String mojangSessionServerUrl; - - @Value("${mojang.api}") - private String mojangApiUrl; - - /** - * Gets the Session Server profile of the - * player with the given UUID. - * - * @param id the uuid or name of the player - * @return the profile - */ - public MojangProfile getProfile(String id) { - return WebRequest.getAsEntity(mojangSessionServerUrl + "/session/minecraft/profile/" + id, MojangProfile.class); - } - - /** - * Gets the UUID of the player using - * the name of the player. - * - * @param id the name of the player - * @return the profile - */ - public MojangUsernameToUuid getUuidFromUsername(String id) { - return WebRequest.getAsEntity(mojangApiUrl + "/users/profiles/minecraft/" + id, MojangUsernameToUuid.class); - } -} diff --git a/src/main/java/cc/fascinated/service/PlayerService.java b/src/main/java/cc/fascinated/service/PlayerService.java deleted file mode 100644 index e24fb69..0000000 --- a/src/main/java/cc/fascinated/service/PlayerService.java +++ /dev/null @@ -1,81 +0,0 @@ -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 lombok.extern.log4j.Log4j2; -import net.jodah.expiringmap.ExpirationPolicy; -import net.jodah.expiringmap.ExpiringMap; -import org.springframework.stereotype.Service; - -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.TimeUnit; - -@Service @Log4j2 -public class PlayerService { - - /** - * The cache of players. - */ - private final Map players = ExpiringMap.builder() - .expiration(1, TimeUnit.HOURS) - .expirationPolicy(ExpirationPolicy.CREATED) - .build(); - - /** - * The cache of player names to UUIDs. - */ - private final Map playerNameToUUIDCache = ExpiringMap.builder() - .expiration(1, TimeUnit.DAYS) - .expirationPolicy(ExpirationPolicy.CREATED) - .build(); - - private final MojangAPIService mojangAPIService; - - public PlayerService(MojangAPIService mojangAPIService) { - this.mojangAPIService = mojangAPIService; - } - - /** - * Gets a player by their UUID. - * - * @param id the uuid or name of the player - * @return the player or null if the player does not exist - */ - public Player getPlayer(String id) { - UUID uuid = null; - if (id.length() == 32 || id.length() == 36) { // Check if the id is a UUID - try { - uuid = UUID.fromString(id.length() == 32 ? UUIDUtils.addUuidDashes(id) : id); - } catch (Exception ignored) {} - } else { // Check if the id is a name - uuid = playerNameToUUIDCache.get(id.toUpperCase()); - } - - // Check if the player is cached - if (uuid != null && players.containsKey(uuid)) { - return players.get(uuid); - } - - MojangProfile profile = uuid == null ? null : mojangAPIService.getProfile(uuid.toString()); - if (profile == null) { // The player cannot be found using their UUID - MojangUsernameToUuid apiProfile = mojangAPIService.getUuidFromUsername(id); // Get the UUID of the player using their name - if (apiProfile == null || !apiProfile.isValid()) { - return null; - } - // Get the profile of the player using their UUID - profile = mojangAPIService.getProfile(apiProfile.getId().length() == 32 ? - UUIDUtils.addUuidDashes(apiProfile.getId()) : apiProfile.getId()); - } - if (profile == null) { // The player cannot be found using their name or UUID - log.info("Player with id {} could not be found", id); - return null; - } - Player player = new Player(profile); - players.put(player.getUuid(), player); - playerNameToUUIDCache.put(player.getName().toUpperCase(), player.getUuid()); - return player; - } -} diff --git a/src/main/java/cc/fascinated/service/pinger/MinecraftServerPinger.java b/src/main/java/cc/fascinated/service/pinger/MinecraftServerPinger.java deleted file mode 100644 index f223b1d..0000000 --- a/src/main/java/cc/fascinated/service/pinger/MinecraftServerPinger.java +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index d4bd2f5..0000000 --- a/src/main/java/cc/fascinated/service/pinger/impl/JavaMinecraftServerPinger.java +++ /dev/null @@ -1,53 +0,0 @@ -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 diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e52f767..646f5d8 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -5,6 +5,16 @@ server: whitelabel: enabled: false +# Spring Configuration +spring: + data: + # Redis - This is used for caching + redis: + host: "localhost" + port: 6379 + database: 0 + auth: "" # Leave blank for no auth + public-url: http://localhost:80 mojang: diff --git a/src/main/resources/public/favicon.ico b/src/main/resources/public/favicon.ico index daa4531ab24116340ef9987f1943b41d87be6a12..f8b6029c95459777e5701c5c8ab482e5bf5fdadb 100644 GIT binary patch literal 4286 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 ee&D!O$wJRJSb2uDTSkOd`yqz{3I3|9C1cU^H z1cU^H1cU^H1cU^H1cU^H1eTQq>h@pl?wN|XdnR5eYl(c%*&OL|IHupXtYokZU$&y- z$PJ#JspD8)_x4UPPtPPPYl-s3)g1YWvnjIY*0lrIEkn^1^((LHKJf`p@8lQ2`z_+G zEL#2=J@~b=C45Key68uWS}4ol%UZ&15S=KbTSR|&PS-nmOv&lev0kX%2!BNnXpOk=QfYH|XGKNF zH5L)WvU?25>A>_JFignl7UG<)EnEJIF2E1v6dF0UK7aoGVV&wpWsq7uUWuZEq5vE4YHQoQq_OIoyY(ocLcQr+h-P9Pn!ECwN z{eD!Z!+4A-Ih|Gc$Mqocma8dxPf2t57Q1s>>htqDUC-pVV28SOIwMStoSRSkSNJPy z!GEC#-*ByswAzZJ!H4BJU1;hk za{1SC!7`hBT7kc!2ed{l=?7&^;T?{;x~mO}*RrQqZXUnI8=6>Boh}=;nOtKv{)#Rj zSNu8VA{_jvd*DNs1-rU0*#=`!K2oMRXMsvfjN{)5=? zeCgW37OnUTwXN3adM961WNN3i-S{hdKx^p1_p$Uj90OP7n?Kd*ygiejhy1c0d$od| z*qOhg3+Ow(;%W|$-t60XeXjMu9hxk~GH=G%tKcuqJ61R@bqziEJ?5p{iTTSnkaw~r*g&L9hx{`mh7!?pQ7a796llV$LIL#>6zefOjCsYn;Wx} zeYY9=s1peL*L=rE*uOCw*E?I-zp#Hjd$QXx5&!k9J@uHde_{W2vvIxW751NM<9aQ^ z{)PSP*^}Ll3H#Tx_S9p-{)PS9&BpbfSJ;25jq9}t`xo}FXHRxJChT9&+Eb4S`xo|a zHyhV`USa>KHm=tq>|fZwo;}&^n6Q66Yfn8U>|fZw-E3U%d4>I_+PGeeuzz9ydiG?u zW5WLRtUdLZuzz9ycC&H4=N0y!YU6q>!v2N*>)DgtjtTqMv-Z?u!v2N*+s(%Ho>$m^ zs*UTl6s!H?`_uS7eFT38h!*SHxZX30@Bd`*-9P-@C-MuPo(YEE$;q5+2l?V^3N!o; z=L~hgn))L6r{NFgp3nqycb{hN&^Z6y-`>7)Ryn+dm9_P5Oab zkg7A1@34m7SQh+=rOcDoi^X`(y%XHG9%tUzVOHIFlH0zwcbrv^ZDQ4jHnPgSo0+p^ z8)vG-0piNpYJEa;l&$RTV`ZDRvC@X^TxZCCSVaeloj>t*caJl7*GViVVAJCZ^5^H_ zSaruK<{3E3)|@%Of)__vVB!GUa27>Xpof(<0HA-_@D zkXhWwhkg_jf9~U9r;3lSiJoG?C%{qWSuOLI&XZyS*Mq)E*6<*>KNIJ!9 z{<6O(f1vn&?*9$gKd&>Of1&0QIjA?-oi_3k!Sv4dW#F-rAjJD_BJINZ)@0EPF%)h$x1or{n*s%ouT9%wY@_OFC?}eVE@&DRu7UZwm zxvUS1>?iY2TTcRi?l1OjX64=8=r{KBIHUA6&B_gnjlXx#7^~_$%6wx7S;NDycR7zw zUI$JdV3nJXGSA=Ip%qKas#6$F=;+Hw?0}_2^@wZ$a@vswZVvPp}$);!gD%s>8Uu zk;kD1n=$_kIcoL5$ac>7%Q2&(ze}nqQ!b#jf0e)7*X-ENeZuP47Uo^FwS3&536sJ>J6?<+FFBT|8$Mr?LQrVPxlmZwF7EjZq41` z-+r9y293)!o?p0jXC$^~a;|yte>)#H#@}#bJpYsVRw4&hp4ZSNKz%ue$pL3+9vmUb+6OWny5P`u`eKjpK?|6|MvW6RWca8jR;;zx#kz=HEf+~+xe*Ep;F z^q9(6>;K9A6ZqdX$|~EBF#pjpDBkF~Mt+ZV6%F342N5`1a=Akmj+rbB7(4c#j5>WqRdxJ z9V03Pe~Q_>kB|DhZ}bprcnI}d&6qp4HOl|x{4d%5&ElEqeHxw^X0@|17MO|hweP+i z%zYYl8#DAks}m+{b3Z`+L>>b$9|`e*#|6wwp>3sIi4S?_PjfOtn3HiBb21(WcX@0r znT4MHC!d>({oqr3Sp9`v3EXSvVoY7;W6b}RLFSq2LY#&_pv44!OTCAdKh+5-ON_@P<=_7hsf*9$fo!fNj7Lv0rQ zvjWA2m0Oh>!T>94-4U-7a^D0!IIxMWBpqp@v1RlZa>T#xp>TKYUAwQ;e`yr+XAZ^Z zWEka7^RF>Cui=q+|Ib|RyV;t#o!}mUug{KqvaXQ+)STVM+{aDI6%E7{$|uX(L#QKR zym;NVB>qZ%iQIEnn>2pZJdl&W|I&D2_&1yrjmZxyb22a|YtM_RKE>zDkz&7LD06w4MKG}J?IVo}ex%vA) zh@<~d_9b*2Iw@YIJCXDS+PDgz>vh!SW*XjV@rX1lx zYvqb@{<*RL1pd1)Cj))C8uan&@25E#sq<{L>}i|k-SN41N&Uj)^D@~toYQ4#?vdn?DJgl?!x7YmG~}7$(nxVYHR-{7_Z2dbGqwP{wDZsqwE`>G(V>^i+l2Y3Ysfo zetJb@16vsjo=nFv+x9~6&qq#|%+Jhy-~ENDE8_bsf6JD0y6f`AKc3U&HkUV(v+li= zT=5R7E9NIh!1vkynT}(&?YZWU`^)v&Jm>Oh;<+xuQ;+^ifa+NDunZK0NnOmPtCto|Y zQ?BSy#-Nv2SF{>`p3^0bx!Z|N-kvofSImA4+A92uKBvo7d{F9&pG95qknui4q4_Vz zoGw>BBAZ;XQ2dpg&WiQfT=B`*eJNM;o$bdMbo-)n5%SHyk&nGD?_54#dO)AESgx2a z{>k$w^C^R6|5-Y@VlMfMoK7=mG*|hbk^3rj#m{2CqMR${f`8-1J@2aXD5+k9`P0kZ z290pa!?}TTn6F4<&_~+d&9=^;?mGW$Y!Erf^H}~NxLZ^A19Q>82IgbW`Der1(s5jB zd&SeS8v?W89sb$q*P#o47rIbbT>yi3z~Wh~`+QIBynd;Cja*+j-}_$PUE$@e`-7f5IQ zGtrB9pFay-(ANdn%qKh*Le3w{c}U>9W!-T7ZY2f??O5c zzp$D79{Imw)dlGJn}~xC)yxik(lOooexq_)4(Hd+?7tc^+5p>l4D$Mo-E{%_{ENVB zrudqD}nU;1Vu z>mb($A>UtGO&4J2Kf`i1Ff-WTn0CBx!FLdi=12fGRXZPPg1o;Rn2o(&C|!UazT%&Y z-cvIdt;36%{+&B>gjM8sfvx$@hlAvA!Q<7O>cVX73i8yK@w+AN_RWWD1#^q(7IeXT zE?f^rXUQL%q6@RpE6}SS`{yEOYG$G}7JFmyIO!z794p8#lYa)kpBm8x=+6I8Z+{6j z>^t3$$K2^YLbTgQ0_3lu2cHMqmlx6na(6`?Pl^Qe~ev6HjVX0^Yf>kNi2s za?}}K#4?F_)~k%(N1QJtAS56pAS56pAS56pAS56pAS56pU?&N%{QO*YCVtKv*Cg(r zS&^`R#o?INHe7Z%oZ9_M4#!p6eUi9l|2^7ojW_(2_HT^uU!wi^4e)97qaMen(T`JU zc!Tdc)JHxe?JJKgU#>nf|1>&9zQHBwboCRJUHKFF!%A(f^oi=_@*|&#pHGjhei}7T zGbTT*bdr;?H>54;e>}3-*9b(&L+FD>9mu4YY9E)9?zg0U7S5^- dG`sW&>fNXvk1(x0&Ms;0Mh7lC@n1fE{tty7$}#`| diff --git a/src/test/java/cc/fascinated/PlayerControllerTests.java b/src/test/java/cc/fascinated/PlayerControllerTests.java index b57f3d2..a49a2df 100644 --- a/src/test/java/cc/fascinated/PlayerControllerTests.java +++ b/src/test/java/cc/fascinated/PlayerControllerTests.java @@ -13,7 +13,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @AutoConfigureMockMvc -@SpringBootTest +@SpringBootTest(classes = TestRedisConfig.class) class PlayerControllerTests { @Autowired diff --git a/src/test/java/cc/fascinated/TestRedisConfig.java b/src/test/java/cc/fascinated/TestRedisConfig.java new file mode 100644 index 0000000..d7d21e6 --- /dev/null +++ b/src/test/java/cc/fascinated/TestRedisConfig.java @@ -0,0 +1,44 @@ +package cc.fascinated; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import lombok.NonNull; +import org.springframework.boot.test.context.TestConfiguration; +import redis.embedded.RedisServer; + +import java.io.IOException; + +/** + * Test configuration for + * a mock Redis server. + * + * @author Braydon + */ +@TestConfiguration +public class TestRedisConfig { + @NonNull private final RedisServer server; + + public TestRedisConfig() throws IOException { + server = new RedisServer(); // Construct the mock server + } + + /** + * Start up the mock Redis server. + * + * @throws IOException if there was an issue starting the server + */ + @PostConstruct + public void onInitialize() throws IOException { + server.start(); + } + + /** + * Shutdown the running mock Redis server. + * + * @throws IOException if there was an issue stopping the server + */ + @PreDestroy + public void housekeeping() throws IOException { + server.stop(); + } +} \ No newline at end of file diff --git a/target/classes/application.yml b/target/classes/application.yml index e52f767..646f5d8 100644 --- a/target/classes/application.yml +++ b/target/classes/application.yml @@ -5,6 +5,16 @@ server: whitelabel: enabled: false +# Spring Configuration +spring: + data: + # Redis - This is used for caching + redis: + host: "localhost" + port: 6379 + database: 0 + auth: "" # Leave blank for no auth + public-url: http://localhost:80 mojang: diff --git a/target/classes/public/favicon.ico b/target/classes/public/favicon.ico index daa4531ab24116340ef9987f1943b41d87be6a12..f8b6029c95459777e5701c5c8ab482e5bf5fdadb 100644 GIT binary patch literal 4286 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 ee&D!O$wJRJSb2uDTSkOd`yqz{3I3|9C1cU^H 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