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 daa4531..f8b6029 100644 Binary files a/src/main/resources/public/favicon.ico and b/src/main/resources/public/favicon.ico differ 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 daa4531..f8b6029 100644 Binary files a/target/classes/public/favicon.ico and b/target/classes/public/favicon.ico differ