forked from MinecraftUtilities/Backend
add server pinger
This commit is contained in:
parent
ed3b7e3064
commit
25c69e11e1
1
.gitignore
vendored
1
.gitignore
vendored
@ -28,3 +28,4 @@ fabric.properties
|
|||||||
git.properties
|
git.properties
|
||||||
pom.xml.versionsBackup
|
pom.xml.versionsBackup
|
||||||
application.yml
|
application.yml
|
||||||
|
target/
|
||||||
|
38
pom.xml
38
pom.xml
@ -30,6 +30,14 @@
|
|||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<!-- Jitpack - Used for dnsjava -->
|
||||||
|
<repository>
|
||||||
|
<id>jitpack.io</id>
|
||||||
|
<url>https://jitpack.io</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
@ -84,6 +92,30 @@
|
|||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- DNS Lookup -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.dnsjava</groupId>
|
||||||
|
<artifactId>dnsjava</artifactId>
|
||||||
|
<version>v3.5.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Redis for caching -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>io.lettuce</groupId>
|
||||||
|
<artifactId>lettuce-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>redis.clients</groupId>
|
||||||
|
<artifactId>jedis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Unit Tests -->
|
<!-- Unit Tests -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
@ -108,6 +140,12 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.codemonstur</groupId>
|
||||||
|
<artifactId>embedded-redis</artifactId>
|
||||||
|
<version>1.4.3</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<byte[]> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
package cc.fascinated.common;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter @AllArgsConstructor
|
|
||||||
public class Tuple<L, R> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The left value of the tuple.
|
|
||||||
*/
|
|
||||||
private final L left;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The right value of the tuple.
|
|
||||||
*/
|
|
||||||
private final R right;
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 <T> the type of the response
|
|
||||||
*/
|
|
||||||
public static <T> T getAsEntity(String url, Class<T> clazz) {
|
|
||||||
try {
|
|
||||||
ResponseEntity<T> profile = CLIENT.get()
|
|
||||||
.uri(url)
|
|
||||||
.retrieve()
|
|
||||||
.toEntity(clazz);
|
|
||||||
|
|
||||||
if (profile.getStatusCode().isError()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return profile.getBody();
|
|
||||||
} catch (HttpClientErrorException ex) {
|
|
||||||
return 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 <a href="https://wiki.vg/Protocol">Protocol Docs</a>
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 <a href="https://wiki.vg/Protocol#Handshake">Protocol Docs</a>
|
|
||||||
*/
|
|
||||||
@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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 <a href="https://wiki.vg/Protocol#Status_Request">Protocol Docs</a>
|
|
||||||
*/
|
|
||||||
@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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<MinecraftServer> getServer(@PathVariable String hostname) {
|
|
||||||
return ResponseEntity.ok(JavaMinecraftServerPinger.INSTANCE.ping(hostname, 25565));
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Object> {
|
|
||||||
@Override
|
|
||||||
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType,
|
|
||||||
@NonNull Class<? extends HttpMessageConverter<?>> 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<String, String> params = new HashMap<>();
|
|
||||||
for (Entry<String, String[]> entry : request.getParameterMap().entrySet()) {
|
|
||||||
params.put(entry.getKey(), Arrays.toString(entry.getValue()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getting headers
|
|
||||||
Map<String, String> headers = new HashMap<>();
|
|
||||||
Enumeration<String> 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<? extends HttpMessageConverter<?>> converterType) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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<ProfileProperty> properties = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the skin and cape of the player.
|
|
||||||
*
|
|
||||||
* @return the skin and cape of the player
|
|
||||||
*/
|
|
||||||
public Tuple<Skin, Cape> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -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());
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<Skin, Cape> skinAndCape = profile.getSkinAndCape();
|
|
||||||
if (skinAndCape != null) {
|
|
||||||
this.skin = skinAndCape.getLeft();
|
|
||||||
this.cape = skinAndCape.getRight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<String, String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
@ -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.");
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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;
|
|
||||||
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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<UUID, Player> players = ExpiringMap.builder()
|
|
||||||
.expiration(1, TimeUnit.HOURS)
|
|
||||||
.expirationPolicy(ExpirationPolicy.CREATED)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The cache of player names to UUIDs.
|
|
||||||
*/
|
|
||||||
private final Map<String, UUID> 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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package cc.fascinated.service.pinger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Braydon
|
|
||||||
* @param <T> the type of server to ping
|
|
||||||
*/
|
|
||||||
public interface MinecraftServerPinger<T> {
|
|
||||||
T ping(String hostname, int port);
|
|
||||||
}
|
|
@ -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<JavaMinecraftServer> {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,6 +5,16 @@ server:
|
|||||||
whitelabel:
|
whitelabel:
|
||||||
enabled: false
|
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
|
public-url: http://localhost:80
|
||||||
|
|
||||||
mojang:
|
mojang:
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 4.2 KiB |
@ -13,7 +13,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
|
|||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
@AutoConfigureMockMvc
|
@AutoConfigureMockMvc
|
||||||
@SpringBootTest
|
@SpringBootTest(classes = TestRedisConfig.class)
|
||||||
class PlayerControllerTests {
|
class PlayerControllerTests {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
44
src/test/java/cc/fascinated/TestRedisConfig.java
Normal file
44
src/test/java/cc/fascinated/TestRedisConfig.java
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,16 @@ server:
|
|||||||
whitelabel:
|
whitelabel:
|
||||||
enabled: false
|
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
|
public-url: http://localhost:80
|
||||||
|
|
||||||
mojang:
|
mojang:
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 4.2 KiB |
Loading…
Reference in New Issue
Block a user