├── src
├── main
│ ├── resources
│ │ ├── public
│ │ │ ├── robots.txt
│ │ │ └── favicon.ico
│ │ ├── icons
│ │ │ ├── ping.png
│ │ │ └── server_background.png
│ │ ├── fonts
│ │ │ └── minecraft-font.ttf
│ │ ├── application.yml
│ │ └── templates
│ │ │ └── index.html
│ └── java
│ │ └── xyz
│ │ └── mcutils
│ │ └── backend
│ │ ├── exception
│ │ ├── impl
│ │ │ ├── MojangAPIRateLimitException.java
│ │ │ ├── ResourceNotFoundException.java
│ │ │ ├── BadRequestException.java
│ │ │ ├── RateLimitException.java
│ │ │ └── InternalServerErrorException.java
│ │ └── ExceptionControllerAdvice.java
│ │ ├── service
│ │ ├── metric
│ │ │ ├── metrics
│ │ │ │ ├── TotalRequestsMetric.java
│ │ │ │ ├── RequestsPerRouteMetric.java
│ │ │ │ ├── ConnectedSocketsMetric.java
│ │ │ │ ├── process
│ │ │ │ │ ├── MemoryMetric.java
│ │ │ │ │ └── CpuUsageMetric.java
│ │ │ │ ├── UniquePlayerLookupsMetric.java
│ │ │ │ └── UniqueServerLookupsMetric.java
│ │ │ ├── Metric.java
│ │ │ └── impl
│ │ │ │ ├── DoubleMetric.java
│ │ │ │ ├── IntegerMetric.java
│ │ │ │ └── MapMetric.java
│ │ ├── pinger
│ │ │ ├── MinecraftServerPinger.java
│ │ │ └── impl
│ │ │ │ ├── BedrockMinecraftServerPinger.java
│ │ │ │ └── JavaMinecraftServerPinger.java
│ │ ├── MaxMindService.java
│ │ ├── MetricService.java
│ │ └── PlayerService.java
│ │ ├── model
│ │ ├── metric
│ │ │ └── WebsocketMetrics.java
│ │ ├── dns
│ │ │ ├── DNSRecord.java
│ │ │ └── impl
│ │ │ │ ├── ARecord.java
│ │ │ │ └── SRVRecord.java
│ │ ├── cache
│ │ │ ├── CachedPlayerSkinPart.java
│ │ │ ├── CachedServerPreview.java
│ │ │ ├── CachedPlayerName.java
│ │ │ ├── CachedPlayer.java
│ │ │ ├── CachedMinecraftServer.java
│ │ │ └── CachedEndpointStatus.java
│ │ ├── player
│ │ │ ├── Cape.java
│ │ │ └── Player.java
│ │ ├── token
│ │ │ ├── MojangUsernameToUuidToken.java
│ │ │ ├── JavaServerStatusToken.java
│ │ │ └── MojangProfileToken.java
│ │ ├── response
│ │ │ └── ErrorResponse.java
│ │ ├── mojang
│ │ │ └── EndpointStatus.java
│ │ ├── skin
│ │ │ ├── Skin.java
│ │ │ └── ISkinPart.java
│ │ └── server
│ │ │ ├── BedrockMinecraftServer.java
│ │ │ └── MinecraftServer.java
│ │ ├── repository
│ │ ├── mongo
│ │ │ └── MetricsRepository.java
│ │ └── redis
│ │ │ ├── ServerPreviewCacheRepository.java
│ │ │ ├── PlayerCacheRepository.java
│ │ │ ├── MinecraftServerCacheRepository.java
│ │ │ ├── PlayerSkinPartCacheRepository.java
│ │ │ └── PlayerNameCacheRepository.java
│ │ ├── common
│ │ ├── Tuple.java
│ │ ├── ServerUtils.java
│ │ ├── renderer
│ │ │ ├── Renderer.java
│ │ │ ├── IsometricSkinRenderer.java
│ │ │ ├── impl
│ │ │ │ ├── skin
│ │ │ │ │ ├── SquareRenderer.java
│ │ │ │ │ ├── BodyRenderer.java
│ │ │ │ │ └── IsometricHeadRenderer.java
│ │ │ │ └── server
│ │ │ │ │ └── ServerPreviewRenderer.java
│ │ │ └── SkinRenderer.java
│ │ ├── Endpoint.java
│ │ ├── Timer.java
│ │ ├── packet
│ │ │ ├── MinecraftBedrockPacket.java
│ │ │ ├── impl
│ │ │ │ ├── bedrock
│ │ │ │ │ ├── BedrockPacketUnconnectedPing.java
│ │ │ │ │ └── BedrockPacketUnconnectedPong.java
│ │ │ │ └── java
│ │ │ │ │ ├── JavaPacketStatusInStart.java
│ │ │ │ │ └── JavaPacketHandshakingInSetProtocol.java
│ │ │ └── MinecraftJavaPacket.java
│ │ ├── EnumUtils.java
│ │ ├── AppConfig.java
│ │ ├── Fonts.java
│ │ ├── UUIDUtils.java
│ │ ├── IPUtils.java
│ │ ├── PlayerUtils.java
│ │ ├── CachedResponse.java
│ │ ├── DNSUtils.java
│ │ ├── MojangServer.java
│ │ ├── WebRequest.java
│ │ ├── ImageUtils.java
│ │ ├── ColorUtils.java
│ │ ├── ExpiringSet.java
│ │ └── JavaMinecraftVersion.java
│ │ ├── controller
│ │ ├── HealthController.java
│ │ ├── HomeController.java
│ │ ├── MojangController.java
│ │ ├── PlayerController.java
│ │ └── ServerController.java
│ │ ├── config
│ │ ├── MongoConfig.java
│ │ ├── OpenAPIConfiguration.java
│ │ ├── Config.java
│ │ └── RedisConfig.java
│ │ ├── Main.java
│ │ ├── websocket
│ │ ├── WebSocketManager.java
│ │ ├── impl
│ │ │ └── MetricsWebSocket.java
│ │ └── WebSocket.java
│ │ └── log
│ │ └── TransactionLogger.java
└── test
│ └── java
│ └── xyz
│ └── mcutils
│ └── backend
│ └── test
│ ├── tests
│ ├── MojangControllerTests.java
│ ├── ServerControllerTests.java
│ └── PlayerControllerTests.java
│ └── config
│ └── TestRedisConfig.java
├── renovate.json
├── README.md
├── influx-commands.md
├── .gitignore
├── Dockerfile
├── LICENSE
├── .gitea
└── workflows
│ └── ci.yml
└── pom.xml
/src/main/resources/public/robots.txt:
--------------------------------------------------------------------------------
1 | # Allow all robots
2 | User-agent: *
3 | Allow: /
--------------------------------------------------------------------------------
/src/main/resources/icons/ping.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RealFascinated/MinecraftUtilities/HEAD/src/main/resources/icons/ping.png
--------------------------------------------------------------------------------
/src/main/resources/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RealFascinated/MinecraftUtilities/HEAD/src/main/resources/public/favicon.ico
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/src/main/resources/fonts/minecraft-font.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RealFascinated/MinecraftUtilities/HEAD/src/main/resources/fonts/minecraft-font.ttf
--------------------------------------------------------------------------------
/src/main/resources/icons/server_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RealFascinated/MinecraftUtilities/HEAD/src/main/resources/icons/server_background.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Minecraft Utilities - Backend
2 |
3 | See [The Website](https://mcutils.xyz) or [Minecraft Utilities Documentation](https://mcutils.xyz/docs) for more information.
--------------------------------------------------------------------------------
/influx-commands.md:
--------------------------------------------------------------------------------
1 | # Useful InfluxDB commands
2 |
3 | ## Delete data from bucket
4 |
5 | ```bash
6 | influx delete --bucket mcutils --start 2024-01-01T00:00:00Z --stop 2025-01-05T00:00:00Z --org mcutils --token setme --predicate '_measurement="requests_per_route"
7 | ```
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/MojangAPIRateLimitException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 |
4 | public class MojangAPIRateLimitException extends RateLimitException {
5 |
6 | public MojangAPIRateLimitException() {
7 | super("Mojang API rate limit exceeded. Please try again later.");
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/metrics/TotalRequestsMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.metrics;
2 |
3 | import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
4 |
5 | public class TotalRequestsMetric extends IntegerMetric {
6 |
7 | public TotalRequestsMetric() {
8 | super("total_requests");
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/metric/WebsocketMetrics.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.metric;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 |
6 | import java.util.Map;
7 |
8 | @AllArgsConstructor
9 | @Getter
10 | public class WebsocketMetrics {
11 | /**
12 | * The metrics to send to the client.
13 | */
14 | private final Map metrics;
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/repository/mongo/MetricsRepository.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.repository.mongo;
2 |
3 | import org.springframework.data.mongodb.repository.MongoRepository;
4 | import xyz.mcutils.backend.service.metric.Metric;
5 |
6 | /**
7 | * A repository for {@link Metric}s.
8 | *
9 | * @author Braydon
10 | */
11 | public interface MetricsRepository extends MongoRepository, String> { }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/repository/redis/ServerPreviewCacheRepository.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.repository.redis;
2 |
3 | import org.springframework.data.repository.CrudRepository;
4 | import xyz.mcutils.backend.model.cache.CachedServerPreview;
5 |
6 | /**
7 | * A cache repository for server previews.
8 | */
9 | public interface ServerPreviewCacheRepository extends CrudRepository { }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/Tuple.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 |
6 | @Getter @AllArgsConstructor
7 | public class Tuple {
8 |
9 | /**
10 | * The left value of the tuple.
11 | */
12 | private final L left;
13 |
14 | /**
15 | * The right value of the tuple.
16 | */
17 | private final R right;
18 | }
19 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/ResourceNotFoundException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 | import lombok.experimental.StandardException;
4 | import org.springframework.http.HttpStatus;
5 | import org.springframework.web.bind.annotation.ResponseStatus;
6 |
7 | @StandardException
8 | @ResponseStatus(HttpStatus.NOT_FOUND)
9 | public class ResourceNotFoundException extends RuntimeException { }
10 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/BadRequestException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(HttpStatus.BAD_REQUEST)
7 | public class BadRequestException extends RuntimeException {
8 |
9 | public BadRequestException(String message) {
10 | super(message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/RateLimitException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(HttpStatus.TOO_MANY_REQUESTS)
7 | public class RateLimitException extends RuntimeException {
8 |
9 | public RateLimitException(String message) {
10 | super(message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/ServerUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.experimental.UtilityClass;
4 |
5 | @UtilityClass
6 | public class ServerUtils {
7 |
8 | /**
9 | * Gets the address of the server.
10 | *
11 | * @return the address of the server
12 | */
13 | public static String getAddress(String ip, int port) {
14 | return ip + (port == 25565 ? "" : ":" + port);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/renderer/Renderer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.renderer;
2 |
3 | import java.awt.image.BufferedImage;
4 |
5 | public abstract class Renderer {
6 |
7 | /**
8 | * Renders the object to the specified size.
9 | *
10 | * @param input The object to render.
11 | * @param size The size to render the object to.
12 | */
13 | public abstract BufferedImage render(T input, int size);
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/repository/redis/PlayerCacheRepository.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.repository.redis;
2 |
3 | import org.springframework.data.repository.CrudRepository;
4 | import xyz.mcutils.backend.model.cache.CachedPlayer;
5 |
6 | import java.util.UUID;
7 |
8 | /**
9 | * A cache repository for {@link CachedPlayer}'s.
10 | *
11 | * @author Braydon
12 | */
13 | public interface PlayerCacheRepository extends CrudRepository { }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/pinger/MinecraftServerPinger.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.pinger;
2 |
3 | import xyz.mcutils.backend.model.dns.DNSRecord;
4 | import xyz.mcutils.backend.model.server.MinecraftServer;
5 |
6 | /**
7 | * @author Braydon
8 | * @param the type of server to ping
9 | */
10 | public interface MinecraftServerPinger {
11 | T ping(String hostname, String ip, int port, DNSRecord[] records);
12 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/repository/redis/MinecraftServerCacheRepository.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.repository.redis;
2 |
3 | import org.springframework.data.repository.CrudRepository;
4 | import xyz.mcutils.backend.model.cache.CachedMinecraftServer;
5 |
6 | /**
7 | * A cache repository for {@link CachedMinecraftServer}'s.
8 | *
9 | * @author Braydon
10 | */
11 | public interface MinecraftServerCacheRepository extends CrudRepository { }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/exception/impl/InternalServerErrorException.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.exception.impl;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.web.bind.annotation.ResponseStatus;
5 |
6 | @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
7 | public class InternalServerErrorException extends RuntimeException {
8 |
9 | public InternalServerErrorException(String message) {
10 | super(message);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/repository/redis/PlayerSkinPartCacheRepository.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.repository.redis;
2 |
3 | import org.springframework.data.repository.CrudRepository;
4 | import xyz.mcutils.backend.model.cache.CachedPlayerSkinPart;
5 |
6 | /**
7 | * A cache repository for player skin parts.
8 | *
9 | * This will allow us to easily lookup a
10 | * player skin part by it's id.
11 | *
12 | */
13 | public interface PlayerSkinPartCacheRepository extends CrudRepository { }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/repository/redis/PlayerNameCacheRepository.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.repository.redis;
2 |
3 | import org.springframework.data.repository.CrudRepository;
4 | import xyz.mcutils.backend.model.cache.CachedPlayerName;
5 |
6 | /**
7 | * A cache repository for player usernames.
8 | *
9 | * This will allow us to easily lookup a
10 | * player's username and get their uuid.
11 | *
12 | *
13 | * @author Braydon
14 | */
15 | public interface PlayerNameCacheRepository extends CrudRepository { }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/Endpoint.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Getter;
5 | import org.springframework.http.HttpStatusCode;
6 |
7 | import java.util.List;
8 |
9 | @AllArgsConstructor @Getter
10 | public class Endpoint {
11 |
12 | /**
13 | * The endpoint.
14 | */
15 | private final String endpoint;
16 |
17 | /**
18 | * The statuses that indicate that the endpoint is online.
19 | */
20 | private final List allowedStatuses;
21 | }
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### ME template
2 | *.class
3 | *.log
4 | *.ctxt
5 | .mtj.tmp/
6 | *.jar
7 | *.war
8 | *.nar
9 | *.ear
10 | *.zip
11 | *.tar.gz
12 | *.rar
13 | hs_err_pid*
14 | replay_pid*
15 | .idea
16 | cmake-build-*/
17 | .idea/**/mongoSettings.xml
18 | *.iws
19 | out/
20 | build/
21 | work/
22 | .idea_modules/
23 | atlassian-ide-plugin.xml
24 | com_crashlytics_export_strings.xml
25 | crashlytics.properties
26 | crashlytics-build.properties
27 | fabric.properties
28 | git.properties
29 | pom.xml.versionsBackup
30 | application.yml
31 | target/
32 |
33 | ### MaxMind GeoIP2
34 | data/
35 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/dns/DNSRecord.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.dns;
2 |
3 | import io.micrometer.common.lang.NonNull;
4 | import lombok.*;
5 |
6 | @NoArgsConstructor @AllArgsConstructor
7 | @Setter @Getter @EqualsAndHashCode
8 | public abstract class DNSRecord {
9 | /**
10 | * The type of this record.
11 | */
12 | @NonNull
13 | private Type type;
14 |
15 | /**
16 | * The TTL (Time To Live) of this record.
17 | */
18 | private long ttl;
19 |
20 | /**
21 | * Types of a record.
22 | */
23 | public enum Type {
24 | A, SRV
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/Timer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | public class Timer {
4 |
5 | /**
6 | * Schedules a task to run after a delay.
7 | *
8 | * @param runnable the task to run
9 | * @param delay the delay before the task runs
10 | */
11 | public static void scheduleRepeating(Runnable runnable, long delay, long period) {
12 | new java.util.Timer().scheduleAtFixedRate(new java.util.TimerTask() {
13 | @Override
14 | public void run() {
15 | runnable.run();
16 | }
17 | }, delay, period);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/cache/CachedPlayerSkinPart.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.cache;
2 |
3 | import lombok.*;
4 | import org.springframework.data.annotation.Id;
5 | import org.springframework.data.redis.core.RedisHash;
6 |
7 | @AllArgsConstructor
8 | @Setter @Getter @EqualsAndHashCode
9 | @RedisHash(value = "playerSkinPart", timeToLive = 60L * 60L) // 1 hour (in seconds)
10 | public class CachedPlayerSkinPart {
11 |
12 | /**
13 | * The ID of the skin part
14 | */
15 | @Id @NonNull private String id;
16 |
17 | /**
18 | * The skin part bytes
19 | */
20 | private byte[] bytes;
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/cache/CachedServerPreview.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.cache;
2 |
3 | import lombok.*;
4 | import org.springframework.data.annotation.Id;
5 | import org.springframework.data.redis.core.RedisHash;
6 |
7 | @AllArgsConstructor
8 | @Setter @Getter @EqualsAndHashCode
9 | @RedisHash(value = "serverPreview", timeToLive = 60L * 5) // 5 minutes (in seconds)
10 | public class CachedServerPreview {
11 |
12 | /**
13 | * The ID of the server preview
14 | */
15 | @Id @NonNull private String id;
16 |
17 | /**
18 | * The server preview bytes
19 | */
20 | private byte[] bytes;
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/metrics/RequestsPerRouteMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.metrics;
2 |
3 | import xyz.mcutils.backend.service.metric.impl.MapMetric;
4 |
5 | public class RequestsPerRouteMetric extends MapMetric {
6 |
7 | public RequestsPerRouteMetric() {
8 | super("requests_per_route");
9 | }
10 |
11 | /**
12 | * Increment the value for this route.
13 | *
14 | * @param route the route to increment
15 | */
16 | public void increment(String route) {
17 | getValue().put(route, getValue().getOrDefault(route, 0) + 1);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/metrics/ConnectedSocketsMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.metrics;
2 |
3 | import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
4 | import xyz.mcutils.backend.websocket.WebSocketManager;
5 |
6 | public class ConnectedSocketsMetric extends IntegerMetric {
7 |
8 | public ConnectedSocketsMetric() {
9 | super("connected_sockets");
10 | }
11 |
12 | @Override
13 | public boolean isCollector() {
14 | return true;
15 | }
16 |
17 | @Override
18 | public void collect() {
19 | setValue(WebSocketManager.getTotalConnections());
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/controller/HealthController.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.controller;
2 |
3 | import org.springframework.http.ResponseEntity;
4 | import org.springframework.stereotype.Controller;
5 | import org.springframework.web.bind.annotation.GetMapping;
6 | import org.springframework.web.bind.annotation.RequestMapping;
7 |
8 | import java.util.Map;
9 |
10 | @Controller
11 | @RequestMapping(value = "/")
12 | public class HealthController {
13 |
14 | @GetMapping(value = "/health")
15 | public ResponseEntity> home() {
16 | return ResponseEntity.ok(Map.of(
17 | "status", "OK"
18 | ));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/config/MongoConfig.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.config;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.context.annotation.Configuration;
5 | import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
6 | import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
7 |
8 | @Configuration
9 | @EnableMongoRepositories(basePackages = "xyz.mcutils.backend.repository.mongo")
10 | public class MongoConfig {
11 | @Autowired
12 | void setMapKeyDotReplacement(MappingMongoConverter mappingMongoConverter) {
13 | mappingMongoConverter.setMapKeyDotReplacement("-DOT");
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/packet/MinecraftBedrockPacket.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.packet;
2 |
3 | import lombok.NonNull;
4 |
5 | import java.io.IOException;
6 | import java.net.DatagramSocket;
7 |
8 | /**
9 | * Represents a packet in the
10 | * Minecraft Bedrock protocol.
11 | *
12 | * @author Braydon
13 | * @see Protocol Docs
14 | */
15 | public interface MinecraftBedrockPacket {
16 | /**
17 | * Process this packet.
18 | *
19 | * @param socket the socket to process the packet for
20 | * @throws IOException if an I/O error occurs
21 | */
22 | void process(@NonNull DatagramSocket socket) throws IOException;
23 | }
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM maven:3.9.8-eclipse-temurin-17-alpine
2 |
3 | RUN apk --update --upgrade --no-cache add fontconfig ttf-freefont font-noto terminus-font \
4 | && fc-cache -f \
5 | && fc-list | sort
6 |
7 | # Set the working directory
8 | WORKDIR /home/container
9 |
10 | # Copy the current directory contents into the container at /home/container
11 | COPY . .
12 |
13 | # Build the jar
14 | RUN mvn package -q -Dmaven.test.skip -DskipTests -T2C
15 |
16 | # Make port 80 available to the world outside this container
17 | EXPOSE 80
18 | ENV PORT=80
19 |
20 | # Indicate that we're running in production
21 | ENV ENVIRONMENT=production
22 |
23 | # Run the jar file
24 | CMD java -jar target/Minecraft-Utilities.jar -Djava.awt.headless=true
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/player/Cape.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.player;
2 |
3 | import com.google.gson.JsonObject;
4 | import lombok.AllArgsConstructor;
5 | import lombok.EqualsAndHashCode;
6 | import lombok.Getter;
7 |
8 | @AllArgsConstructor
9 | @Getter @EqualsAndHashCode
10 | public class Cape {
11 |
12 | /**
13 | * The URL of the cape
14 | */
15 | private final String url;
16 |
17 | /**
18 | * Gets the cape from a {@link JsonObject}.
19 | *
20 | * @param json the JSON object
21 | * @return the cape
22 | */
23 | public static Cape fromJson(JsonObject json) {
24 | if (json == null) {
25 | return null;
26 | }
27 | return new Cape(json.get("url").getAsString());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/metrics/process/MemoryMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.metrics.process;
2 |
3 | import xyz.mcutils.backend.service.metric.impl.MapMetric;
4 |
5 | public class MemoryMetric extends MapMetric {
6 |
7 | public MemoryMetric() {
8 | super("memory");
9 | }
10 |
11 | @Override
12 | public boolean isCollector() {
13 | return true;
14 | }
15 |
16 | @Override
17 | public void collect() {
18 | Runtime runtime = Runtime.getRuntime();
19 |
20 | this.getValue().put("total", runtime.maxMemory());
21 | this.getValue().put("allocated", runtime.totalMemory());
22 | this.getValue().put("used", runtime.totalMemory() - runtime.freeMemory());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/token/MojangUsernameToUuidToken.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.token;
2 |
3 | import com.fasterxml.jackson.annotation.JsonProperty;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 |
7 | @Getter @NoArgsConstructor
8 | public class MojangUsernameToUuidToken {
9 |
10 | /**
11 | * The UUID of the player.
12 | */
13 | @JsonProperty("id")
14 | private String uuid;
15 |
16 | /**
17 | * The name of the player.
18 | */
19 | @JsonProperty("name")
20 | private String username;
21 |
22 | /**
23 | * Check if the profile is valid.
24 | *
25 | * @return if the profile is valid
26 | */
27 | public boolean isValid() {
28 | return uuid != null && username != null;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/dns/impl/ARecord.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.dns.impl;
2 |
3 | import io.micrometer.common.lang.NonNull;
4 | import lombok.Getter;
5 | import lombok.NoArgsConstructor;
6 | import lombok.Setter;
7 | import xyz.mcutils.backend.model.dns.DNSRecord;
8 |
9 | import java.net.InetAddress;
10 |
11 | @Setter @Getter
12 | @NoArgsConstructor
13 | public final class ARecord extends DNSRecord {
14 | /**
15 | * The address of this record, null if unresolved.
16 | */
17 | private String address;
18 |
19 | public ARecord(@NonNull org.xbill.DNS.ARecord bootstrap) {
20 | super(Type.A, bootstrap.getTTL());
21 | InetAddress address = bootstrap.getAddress();
22 | this.address = address == null ? null : address.getHostAddress();
23 | }
24 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/EnumUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.NonNull;
4 | import lombok.experimental.UtilityClass;
5 |
6 | /**
7 | * @author Braydon
8 | */
9 | @UtilityClass
10 | public final class EnumUtils {
11 | /**
12 | * Get the enum constant of the specified enum type with the specified name.
13 | *
14 | * @param enumType the enum type
15 | * @param name the name of the constant to return
16 | * @param the type of the enum
17 | * @return the enum constant of the specified enum type with the specified name
18 | */
19 | public > T getEnumConstant(@NonNull Class enumType, @NonNull String name) {
20 | try {
21 | return Enum.valueOf(enumType, name);
22 | } catch (IllegalArgumentException ex) {
23 | return null;
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/metrics/process/CpuUsageMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.metrics.process;
2 |
3 | import com.sun.management.OperatingSystemMXBean;
4 | import xyz.mcutils.backend.service.metric.impl.DoubleMetric;
5 |
6 | import java.lang.management.ManagementFactory;
7 |
8 | public class CpuUsageMetric extends DoubleMetric {
9 | /**
10 | * The OperatingSystemMXBean instance to get the CPU load from.
11 | */
12 | private static final OperatingSystemMXBean OS_BEAN = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
13 |
14 | public CpuUsageMetric() {
15 | super("cpu_usage");
16 | }
17 |
18 | @Override
19 | public boolean isCollector() {
20 | return true;
21 | }
22 |
23 | @Override
24 | public void collect() {
25 | this.setValue(OS_BEAN.getProcessCpuLoad() * 100);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/AppConfig.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.Getter;
4 | import lombok.experimental.UtilityClass;
5 |
6 | @UtilityClass
7 | public final class AppConfig {
8 | /**
9 | * Is the app running in a production environment?
10 | */
11 | @Getter
12 | private static final boolean production;
13 | static { // Are we running on production?
14 | String env = System.getenv("ENVIRONMENT");
15 | production = env != null && (env.equals("production"));
16 | }
17 |
18 | /**
19 | * Is the app running in a test environment?
20 | */
21 | @Getter
22 | private static boolean isRunningTest = true;
23 | static {
24 | try {
25 | Class.forName("org.junit.jupiter.engine.JupiterTestEngine");
26 | } catch (ClassNotFoundException e) {
27 | isRunningTest = false;
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/Fonts.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import lombok.extern.log4j.Log4j2;
4 | import xyz.mcutils.backend.Main;
5 |
6 | import java.awt.*;
7 | import java.io.IOException;
8 | import java.io.InputStream;
9 |
10 | @Log4j2(topic = "Fonts")
11 | public class Fonts {
12 |
13 | public static final Font MINECRAFT;
14 | public static final Font MINECRAFT_BOLD;
15 | public static final Font MINECRAFT_ITALIC;
16 |
17 | static {
18 | InputStream stream = Main.class.getResourceAsStream("/fonts/minecraft-font.ttf");
19 | try {
20 | MINECRAFT = Font.createFont(Font.TRUETYPE_FONT, stream).deriveFont(18f);
21 | MINECRAFT_BOLD = MINECRAFT.deriveFont(Font.BOLD);
22 | MINECRAFT_ITALIC = MINECRAFT.deriveFont(Font.ITALIC);
23 | } catch (FontFormatException | IOException e) {
24 | log.error("Failed to load Minecraft font", e);
25 | throw new RuntimeException("Failed to load Minecraft font", e);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/UUIDUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import io.micrometer.common.lang.NonNull;
4 | import lombok.experimental.UtilityClass;
5 |
6 | import java.util.UUID;
7 |
8 | @UtilityClass
9 | public class UUIDUtils {
10 |
11 | /**
12 | * Add dashes to a UUID.
13 | *
14 | * @param trimmed the UUID without dashes
15 | * @return the UUID with dashes
16 | */
17 | @NonNull
18 | public static UUID addDashes(@NonNull String trimmed) {
19 | StringBuilder builder = new StringBuilder(trimmed);
20 | for (int i = 0, pos = 20; i < 4; i++, pos -= 4) {
21 | builder.insert(pos, "-");
22 | }
23 | return UUID.fromString(builder.toString());
24 | }
25 |
26 | /**
27 | * Remove dashes from a UUID.
28 | *
29 | * @param dashed the UUID with dashes
30 | * @return the UUID without dashes
31 | */
32 | @NonNull
33 | public static String removeDashes(@NonNull UUID dashed) {
34 | return dashed.toString().replace("-", "");
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/metrics/UniquePlayerLookupsMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.metrics;
2 |
3 | import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 | import java.util.UUID;
8 |
9 | public class UniquePlayerLookupsMetric extends IntegerMetric {
10 | private List uniqueLookups = new ArrayList<>();
11 |
12 | public UniquePlayerLookupsMetric() {
13 | super("unique_player_lookups");
14 | }
15 |
16 | @Override
17 | public boolean isCollector() {
18 | return true;
19 | }
20 |
21 | /**
22 | * Adds a lookup to the list of unique lookups.
23 | *
24 | * @param uuid the query that was used to look up a player
25 | */
26 | public void addLookup(UUID uuid) {
27 | if (!uniqueLookups.contains(uuid.toString())) {
28 | uniqueLookups.add(uuid.toString());
29 | }
30 | }
31 |
32 | @Override
33 | public void collect() {
34 | setValue(uniqueLookups.size());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/metrics/UniqueServerLookupsMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.metrics;
2 |
3 | import xyz.mcutils.backend.service.metric.impl.IntegerMetric;
4 |
5 | import java.util.ArrayList;
6 | import java.util.List;
7 |
8 | public class UniqueServerLookupsMetric extends IntegerMetric {
9 | private List uniqueLookups = new ArrayList<>();
10 |
11 | public UniqueServerLookupsMetric() {
12 | super("unique_server_lookups");
13 | }
14 |
15 | @Override
16 | public boolean isCollector() {
17 | return true;
18 | }
19 |
20 | /**
21 | * Adds a lookup to the list of unique lookups.
22 | *
23 | * @param hostname the query that was used to look up a player
24 | */
25 | public void addLookup(String hostname) {
26 | hostname = hostname.toLowerCase();
27 | if (!uniqueLookups.contains(hostname)) {
28 | uniqueLookups.add(hostname);
29 | }
30 | }
31 |
32 | @Override
33 | public void collect() {
34 | setValue(uniqueLookups.size());
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/response/ErrorResponse.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.response;
2 |
3 | import io.micrometer.common.lang.NonNull;
4 | import lombok.EqualsAndHashCode;
5 | import lombok.Getter;
6 | import lombok.ToString;
7 | import org.springframework.http.HttpStatus;
8 |
9 | import java.util.Date;
10 |
11 | @Getter @ToString @EqualsAndHashCode
12 | public class ErrorResponse {
13 | /**
14 | * The status code of this error.
15 | */
16 | @NonNull
17 | private final HttpStatus status;
18 |
19 | /**
20 | * The HTTP code of this error.
21 | */
22 | private final int code;
23 |
24 | /**
25 | * The message of this error.
26 | */
27 | @NonNull private final String message;
28 |
29 | /**
30 | * The timestamp this error occurred.
31 | */
32 | @NonNull private final Date timestamp;
33 |
34 | public ErrorResponse(@NonNull HttpStatus status, @NonNull String message) {
35 | this.status = status;
36 | code = status.value();
37 | this.message = message;
38 | timestamp = new Date();
39 | }
40 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024, Fascinated
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/renderer/IsometricSkinRenderer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common.renderer;
2 |
3 | import xyz.mcutils.backend.model.skin.ISkinPart;
4 |
5 | import java.awt.*;
6 | import java.awt.geom.AffineTransform;
7 | import java.awt.image.BufferedImage;
8 |
9 | public abstract class IsometricSkinRenderer extends SkinRenderer {
10 |
11 | /**
12 | * Draw a part onto the texture.
13 | *
14 | * @param graphics the graphics to draw to
15 | * @param partImage the part image to draw
16 | * @param transform the transform to apply
17 | * @param x the x position to draw at
18 | * @param y the y position to draw at
19 | * @param width the part image width
20 | * @param height the part image height
21 | */
22 | protected final void drawPart(Graphics2D graphics, BufferedImage partImage, AffineTransform transform,
23 | double x, double y, int width, int height) {
24 | graphics.setTransform(transform);
25 | graphics.drawImage(partImage, (int) x, (int) y, width, height, null);
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/mojang/EndpointStatus.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.mojang;
2 |
3 | import lombok.EqualsAndHashCode;
4 | import lombok.Getter;
5 | import lombok.RequiredArgsConstructor;
6 | import lombok.Setter;
7 |
8 | @RequiredArgsConstructor
9 | @Getter @Setter @EqualsAndHashCode
10 | public class EndpointStatus {
11 |
12 | /**
13 | * The name of the service.
14 | */
15 | private final String name;
16 |
17 | /**
18 | * The hostname of the service.
19 | */
20 | private final String hostname;
21 |
22 | /**
23 | * The status of the service.
24 | */
25 | private Status status;
26 |
27 | /**
28 | * Statuses for the endpoint.
29 | */
30 | public enum Status {
31 | /**
32 | * The service is online and operational.
33 | */
34 | ONLINE,
35 |
36 | /**
37 | * The service is online, but may be experiencing issues.
38 | * This could be due to high load or other issues.
39 | */
40 | DEGRADED,
41 |
42 | /**
43 | * The service is offline and not operational.
44 | */
45 | OFFLINE
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/test/java/xyz/mcutils/backend/test/tests/MojangControllerTests.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.test.tests;
2 |
3 | import org.junit.jupiter.api.Test;
4 | import org.springframework.beans.factory.annotation.Autowired;
5 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
6 | import org.springframework.boot.test.context.SpringBootTest;
7 | import org.springframework.http.MediaType;
8 | import org.springframework.test.web.servlet.MockMvc;
9 | import xyz.mcutils.backend.test.config.TestRedisConfig;
10 |
11 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
12 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
13 |
14 | @SpringBootTest(classes = { TestRedisConfig.class })
15 | @AutoConfigureMockMvc
16 | class MojangControllerTests {
17 |
18 | @Autowired
19 | private MockMvc mockMvc;
20 |
21 | @Test
22 | public void ensureEndpointStatusLookupSuccess() throws Exception {
23 | mockMvc.perform(get("/mojang/status")
24 | .accept(MediaType.APPLICATION_JSON)
25 | .contentType(MediaType.APPLICATION_JSON))
26 | .andExpect(status().isOk());
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/Metric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 | import com.influxdb.client.write.Point;
5 | import lombok.AllArgsConstructor;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 | import lombok.ToString;
9 | import org.springframework.data.annotation.Id;
10 | import org.springframework.data.annotation.Transient;
11 | import org.springframework.data.mongodb.core.mapping.Document;
12 |
13 | @AllArgsConstructor
14 | @Getter @Setter @ToString
15 | @Document("metrics")
16 | public abstract class Metric {
17 | /**
18 | * The id of the metric.
19 | */
20 | @Id private String id;
21 |
22 | /**
23 | * The value of the metric.
24 | */
25 | private T value;
26 |
27 | /**
28 | * Should this metric be collected
29 | * before pushing to Influx?
30 | */
31 | @Transient @JsonIgnore
32 | private boolean collector;
33 |
34 | /**
35 | * Collects the metric.
36 | */
37 | public void collect() {}
38 |
39 | /**
40 | * Gets this point as a {@link Point}.
41 | *
42 | * @return the point
43 | */
44 | public abstract Point toPoint();
45 | }
46 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/cache/CachedPlayerName.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.cache;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 | import lombok.EqualsAndHashCode;
5 | import lombok.Getter;
6 | import lombok.Setter;
7 | import org.springframework.data.annotation.Id;
8 | import org.springframework.data.redis.core.RedisHash;
9 | import xyz.mcutils.backend.common.CachedResponse;
10 |
11 | import java.util.UUID;
12 |
13 | /**
14 | * @author Braydon
15 | */
16 | @Setter
17 | @Getter @EqualsAndHashCode(callSuper = false)
18 | @RedisHash(value = "playerName", timeToLive = 60L * 60L * 6) // 6 hours (in seconds)
19 | public class CachedPlayerName extends CachedResponse {
20 | /**
21 | * The id of the player.
22 | */
23 | @JsonIgnore
24 | @Id private final String id;
25 |
26 | /**
27 | * The username of the player.
28 | */
29 | private final String username;
30 |
31 | /**
32 | * The unique id of the player.
33 | */
34 | private final UUID uniqueId;
35 |
36 | public CachedPlayerName(String id, String username, UUID uniqueId) {
37 | super(Cache.defaultCache());
38 | this.id = id;
39 | this.username = username;
40 | this.uniqueId = uniqueId;
41 | }
42 | }
--------------------------------------------------------------------------------
/src/test/java/xyz/mcutils/backend/test/config/TestRedisConfig.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.test.config;
2 |
3 | import jakarta.annotation.PostConstruct;
4 | import jakarta.annotation.PreDestroy;
5 | import lombok.NonNull;
6 | import org.springframework.boot.test.context.TestConfiguration;
7 | import redis.embedded.RedisServer;
8 |
9 | import java.io.IOException;
10 |
11 | /**
12 | * Test configuration for
13 | * a mock Redis server.
14 | *
15 | * @author Braydon
16 | */
17 | @TestConfiguration
18 | public class TestRedisConfig {
19 | @NonNull
20 | private final RedisServer server;
21 |
22 | public TestRedisConfig() throws IOException {
23 | server = new RedisServer(); // Construct the mock server
24 | }
25 |
26 | /**
27 | * Start up the mock Redis server.
28 | *
29 | * @throws IOException if there was an issue starting the server
30 | */
31 | @PostConstruct
32 | public void onInitialize() throws IOException {
33 | server.start();
34 | }
35 |
36 | /**
37 | * Shutdown the running mock Redis server.
38 | *
39 | * @throws IOException if there was an issue stopping the server
40 | */
41 | @PreDestroy
42 | public void housekeeping() throws IOException {
43 | server.stop();
44 | }
45 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/cache/CachedPlayer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.cache;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 | import com.fasterxml.jackson.annotation.JsonUnwrapped;
5 | import lombok.EqualsAndHashCode;
6 | import lombok.Getter;
7 | import lombok.Setter;
8 | import org.springframework.data.annotation.Id;
9 | import org.springframework.data.redis.core.RedisHash;
10 | import xyz.mcutils.backend.common.CachedResponse;
11 | import xyz.mcutils.backend.model.player.Player;
12 |
13 | import java.io.Serializable;
14 | import java.util.UUID;
15 |
16 | /**
17 | * A cacheable {@link Player}.
18 | *
19 | * @author Braydon
20 | */
21 | @Setter @Getter @EqualsAndHashCode(callSuper = false)
22 | @RedisHash(value = "player", timeToLive = 60L * 60L) // 1 hour (in seconds)
23 | public class CachedPlayer extends CachedResponse implements Serializable {
24 | /**
25 | * The unique id of the player.
26 | */
27 | @JsonIgnore
28 | @Id private UUID uniqueId;
29 |
30 | /**
31 | * The player to cache.
32 | */
33 | @JsonUnwrapped
34 | private Player player;
35 |
36 | public CachedPlayer(UUID uniqueId, Player player) {
37 | super(Cache.defaultCache());
38 | this.uniqueId = uniqueId;
39 | this.player = player;
40 | }
41 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/impl/DoubleMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.impl;
2 |
3 | import com.influxdb.client.write.Point;
4 | import xyz.mcutils.backend.service.metric.Metric;
5 |
6 | public class DoubleMetric extends Metric {
7 |
8 | public DoubleMetric(String id) {
9 | super(id, 0D, false);
10 | }
11 |
12 | /**
13 | * Increment the value of this metric.
14 | *
15 | * @param amount the amount to increment by
16 | */
17 | public void increment(double amount) {
18 | setValue(getValue() + amount);
19 | }
20 |
21 | /**
22 | * Increment the value of this metric by 1.
23 | */
24 | public void increment() {
25 | increment(1);
26 | }
27 |
28 | /**
29 | * Decrement the value of this metric.
30 | *
31 | * @param amount the amount to decrement by
32 | */
33 | public void decrement(double amount) {
34 | setValue(getValue() - amount);
35 | }
36 |
37 | /**
38 | * Decrement the value of this metric by 1.
39 | */
40 | public void decrement() {
41 | decrement(1D);
42 | }
43 |
44 | @Override
45 | public Point toPoint() {
46 | return Point.measurement(getId())
47 | .addField("value", getValue());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/impl/IntegerMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.impl;
2 |
3 | import com.influxdb.client.write.Point;
4 | import xyz.mcutils.backend.service.metric.Metric;
5 |
6 | public class IntegerMetric extends Metric {
7 |
8 | public IntegerMetric(String id) {
9 | super(id, 0, false);
10 | }
11 |
12 | /**
13 | * Increment the value of this metric.
14 | *
15 | * @param amount the amount to increment by
16 | */
17 | public void increment(int amount) {
18 | setValue(getValue() + amount);
19 | }
20 |
21 | /**
22 | * Increment the value of this metric by 1.
23 | */
24 | public void increment() {
25 | increment(1);
26 | }
27 |
28 | /**
29 | * Decrement the value of this metric.
30 | *
31 | * @param amount the amount to decrement by
32 | */
33 | public void decrement(int amount) {
34 | setValue(getValue() - amount);
35 | }
36 |
37 | /**
38 | * Decrement the value of this metric by 1.
39 | */
40 | public void decrement() {
41 | decrement(1);
42 | }
43 |
44 | @Override
45 | public Point toPoint() {
46 | return Point.measurement(getId())
47 | .addField("value", getValue());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/common/IPUtils.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.common;
2 |
3 | import jakarta.servlet.http.HttpServletRequest;
4 | import lombok.experimental.UtilityClass;
5 |
6 | @UtilityClass
7 | public class IPUtils {
8 | /**
9 | * The headers that contain the IP.
10 | */
11 | private static final String[] IP_HEADERS = new String[] {
12 | "CF-Connecting-IP",
13 | "X-Forwarded-For"
14 | };
15 |
16 | /**
17 | * Get the real IP from the given request.
18 | *
19 | * @param request the request
20 | * @return the real IP
21 | */
22 | public static String getRealIp(HttpServletRequest request) {
23 | String ip = request.getRemoteAddr();
24 | for (String headerName : IP_HEADERS) {
25 | String header = request.getHeader(headerName);
26 | if (header == null) {
27 | continue;
28 | }
29 | if (!header.contains(",")) { // Handle single IP
30 | ip = header;
31 | break;
32 | }
33 | // Handle multiple IPs
34 | String[] ips = header.split(",");
35 | for (String ipHeader : ips) {
36 | ip = ipHeader;
37 | break;
38 | }
39 | }
40 | return ip;
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/cache/CachedMinecraftServer.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.cache;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 | import com.fasterxml.jackson.annotation.JsonUnwrapped;
5 | import lombok.EqualsAndHashCode;
6 | import lombok.Getter;
7 | import lombok.NonNull;
8 | import lombok.Setter;
9 | import org.springframework.data.annotation.Id;
10 | import org.springframework.data.redis.core.RedisHash;
11 | import xyz.mcutils.backend.common.CachedResponse;
12 | import xyz.mcutils.backend.model.server.MinecraftServer;
13 |
14 | import java.io.Serializable;
15 |
16 | /**
17 | * @author Braydon
18 | */
19 | @Setter @Getter @EqualsAndHashCode(callSuper = false)
20 | @RedisHash(value = "server", timeToLive = 60L) // 1 minute (in seconds)
21 | public class CachedMinecraftServer extends CachedResponse implements Serializable {
22 | /**
23 | * The id of this cached server.
24 | */
25 | @Id @NonNull @JsonIgnore
26 | private String id;
27 |
28 | /**
29 | * The cached server.
30 | */
31 | @NonNull @JsonUnwrapped
32 | private MinecraftServer server;
33 |
34 | public CachedMinecraftServer(@NonNull String id, @NonNull MinecraftServer server) {
35 | super(CachedResponse.Cache.defaultCache());
36 | this.id = id;
37 | this.server = server;
38 | }
39 | }
--------------------------------------------------------------------------------
/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | server:
2 | address: 0.0.0.0
3 | port: 80
4 | servlet:
5 | context-path: /
6 |
7 | # Spring Configuration
8 | spring:
9 | # Don't include null properties in JSON
10 | jackson:
11 | default-property-inclusion: non_null
12 | data:
13 | # Redis - This is used for caching
14 | redis:
15 | host: "127.0.0.1"
16 | port: 6379
17 | database: 1
18 | auth: "" # Leave blank for no auth
19 |
20 | # MongoDB - This is used for general data storage
21 | mongodb:
22 | uri: mongodb://localhost:27017
23 | database: test
24 | port: 27017
25 |
26 | # Sentry Configuration
27 | sentry:
28 | dsn: ""
29 |
30 | # The URL of the API
31 | public-url: http://localhost
32 |
33 | # MaxMind Configuration
34 | # This is used for IP Geolocation
35 | maxmind:
36 | license: ""
37 |
38 | # InfluxDB Configuration
39 | influx:
40 | url: http://localhost
41 | token: token
42 | org: org
43 | bucket: bucket
44 |
45 | management:
46 | # Disable all actuator endpoints
47 | endpoints:
48 | web:
49 | exposure:
50 | exclude:
51 | - "*"
52 | # Disable default metrics
53 | influx:
54 | metrics:
55 | export:
56 | enabled: false
57 |
58 | # Set the embedded MongoDB version
59 | de:
60 | flapdoodle:
61 | mongodb:
62 | embedded:
63 | version: 7.0.8
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/controller/HomeController.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.controller;
2 |
3 | import org.springframework.stereotype.Controller;
4 | import org.springframework.ui.Model;
5 | import org.springframework.web.bind.annotation.GetMapping;
6 | import org.springframework.web.bind.annotation.RequestMapping;
7 | import xyz.mcutils.backend.config.Config;
8 |
9 | @Controller
10 | @RequestMapping(value = "/")
11 | public class HomeController {
12 | private final String examplePlayer = "Notch";
13 | private final String exampleJavaServer = "aetheria.cc";
14 | private final String exampleBedrockServer = "geo.hivebedrock.network";
15 |
16 | @GetMapping(value = "/")
17 | public String home(Model model) {
18 | String publicUrl = Config.INSTANCE.getWebPublicUrl();
19 |
20 | model.addAttribute("public_url", publicUrl);
21 | model.addAttribute("player_example_url", publicUrl + "/player/" + examplePlayer);
22 | model.addAttribute("java_server_example_url", publicUrl + "/server/java/" + exampleJavaServer);
23 | model.addAttribute("bedrock_server_example_url", publicUrl + "/server/bedrock/" + exampleBedrockServer);
24 | model.addAttribute("mojang_endpoint_status_url", publicUrl + "/mojang/status");
25 | model.addAttribute("swagger_url", publicUrl + "/swagger-ui.html");
26 | return "index";
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.gitea/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Deploy App
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 | paths-ignore:
7 | - .gitignore
8 | - README.md
9 | - LICENSE
10 | - docker-compose.yml
11 |
12 | jobs:
13 | docker:
14 | strategy:
15 | matrix:
16 | arch: ["ubuntu-latest"]
17 | git-version: ["2.44.0"]
18 | java-version: ["17"]
19 | maven-version: ["3.8.5"]
20 | runs-on: ${{ matrix.arch }}
21 |
22 | # Steps to run
23 | steps:
24 | # Checkout the repo
25 | - name: Checkout
26 | uses: actions/checkout@v4
27 |
28 | # Setup Java and Maven
29 | - name: Set up JDK and Maven
30 | uses: s4u/setup-maven-action@v1.14.0
31 | with:
32 | java-version: ${{ matrix.java-version }}
33 | distribution: "zulu"
34 | maven-version: ${{ matrix.maven-version }}
35 |
36 | # Run JUnit Tests
37 | - name: Run Tests
38 | run: mvn --batch-mode test -q
39 |
40 | # Re-checkout to reset the FS before deploying to Dokku
41 | - name: Checkout - Reset FS
42 | uses: actions/checkout@v4
43 | with:
44 | fetch-depth: 0
45 |
46 | # Deploy to Dokku
47 | - name: Push to dokku
48 | uses: dokku/github-action@master
49 | with:
50 | git_remote_url: "ssh://dokku@10.0.50.137:22/minecraft-helper"
51 | ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }}
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/service/metric/impl/MapMetric.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.service.metric.impl;
2 |
3 | import com.influxdb.client.write.Point;
4 | import xyz.mcutils.backend.service.metric.Metric;
5 |
6 | import java.util.HashMap;
7 | import java.util.Map;
8 |
9 | public class MapMetric extends Metric
40 | *
41 | * @return the default cache information object
42 | */
43 | public static Cache defaultCache() {
44 | return new Cache(true, System.currentTimeMillis());
45 | }
46 |
47 | /**
48 | * Sets if this request is cached.
49 | *
50 | * @param cached the new value of if this request is cached
51 | */
52 | public void setCached(boolean cached) {
53 | this.cached = cached;
54 | if (!cached) {
55 | cachedTime = -1;
56 | }
57 | }
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/xyz/mcutils/backend/model/cache/CachedEndpointStatus.java:
--------------------------------------------------------------------------------
1 | package xyz.mcutils.backend.model.cache;
2 |
3 | import com.fasterxml.jackson.annotation.JsonIgnore;
4 | import lombok.EqualsAndHashCode;
5 | import lombok.Getter;
6 | import lombok.NonNull;
7 | import lombok.Setter;
8 | import org.springframework.data.annotation.Id;
9 | import org.springframework.data.redis.core.RedisHash;
10 | import xyz.mcutils.backend.common.CachedResponse;
11 | import xyz.mcutils.backend.common.MojangServer;
12 |
13 | import java.io.Serializable;
14 | import java.util.ArrayList;
15 | import java.util.HashMap;
16 | import java.util.List;
17 | import java.util.Map;
18 |
19 | @Setter @Getter @EqualsAndHashCode(callSuper = false)
20 | @RedisHash(value = "mojangEndpointStatus", timeToLive = 60L) // 1 minute (in seconds)
21 | public class CachedEndpointStatus extends CachedResponse implements Serializable {
22 |
23 | /**
24 | * The id for this endpoint cache.
25 | */
26 | @Id @NonNull @JsonIgnore
27 | private final String id;
28 |
29 | /**
30 | * The endpoint cache.
31 | */
32 | private final List