add mojang api status endpoint

This commit is contained in:
Lee 2024-04-13 15:34:19 +01:00
parent 0c8f769ee7
commit 811ea348cf
10 changed files with 205 additions and 21 deletions

@ -16,7 +16,6 @@ import java.util.Objects;
@Log4j2 @Log4j2
@SpringBootApplication @SpringBootApplication
public class Main { public class Main {
public static final Gson GSON = new GsonBuilder() public static final Gson GSON = new GsonBuilder()
.setDateFormat("MM-dd-yyyy HH:mm:ss") .setDateFormat("MM-dd-yyyy HH:mm:ss")
.create(); .create();

@ -0,0 +1,22 @@
package cc.fascinated.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import java.util.List;
@AllArgsConstructor @Getter
public class EndpointStatus {
/**
* The endpoint.
*/
private final String endpoint;
/**
* The statuses that indicate that the endpoint is online.
*/
private final List<HttpStatusCode> allowedStatuses;
}

@ -3,9 +3,9 @@ package cc.fascinated.common;
import cc.fascinated.exception.impl.RateLimitException; import cc.fascinated.exception.impl.RateLimitException;
import lombok.experimental.UtilityClass; import lombok.experimental.UtilityClass;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
@UtilityClass @UtilityClass
@ -14,9 +14,15 @@ public class WebRequest {
/** /**
* The web client. * The web client.
*/ */
private static final RestClient CLIENT = RestClient.builder() private static final RestClient CLIENT;
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.build(); static {
HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
requestFactory.setConnectTimeout(5000); // 5 seconds
CLIENT = RestClient.builder()
.requestFactory(requestFactory)
.build();
}
/** /**
* Gets a response from the given URL. * Gets a response from the given URL.
@ -26,21 +32,31 @@ public class WebRequest {
* @param <T> the type of the response * @param <T> the type of the response
*/ */
public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException { public static <T> T getAsEntity(String url, Class<T> clazz) throws RateLimitException {
try { ResponseEntity<T> profile = CLIENT.get()
ResponseEntity<T> profile = CLIENT.get() .uri(url)
.uri(url) .retrieve()
.retrieve() .toEntity(clazz);
.toEntity(clazz);
if (profile.getStatusCode().isError()) { if (profile.getStatusCode().isError()) {
return null;
}
if (profile.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
throw new RateLimitException("Rate limit reached");
}
return profile.getBody();
} catch (HttpClientErrorException ex) {
return null; return null;
} }
if (profile.getStatusCode().isSameCodeAs(HttpStatus.TOO_MANY_REQUESTS)) {
throw new RateLimitException("Rate limit reached");
}
return profile.getBody();
}
/**
* Gets a response from the given URL.
*
* @param url the url
* @return the response
*/
public static ResponseEntity<?> getAndIgnoreErrors(String url) {
return CLIENT.get()
.uri(url)
.retrieve()
.onStatus(HttpStatusCode::isError, (request, response) -> {}) // Don't throw exceptions on error
.toEntity(String.class);
} }
} }

@ -19,6 +19,7 @@ public class HomeController {
public String home(Model model) { public String home(Model model) {
model.addAttribute("player_example_url", Config.INSTANCE.getWebPublicUrl() + "/player/" + exampleUuid); model.addAttribute("player_example_url", Config.INSTANCE.getWebPublicUrl() + "/player/" + exampleUuid);
model.addAttribute("java_server_example_url", Config.INSTANCE.getWebPublicUrl() + "/server/java/" + exampleServer); model.addAttribute("java_server_example_url", Config.INSTANCE.getWebPublicUrl() + "/server/java/" + exampleServer);
model.addAttribute("mojang_endpoint_status_url", Config.INSTANCE.getWebPublicUrl() + "/mojang/status");
model.addAttribute("swagger_url", Config.INSTANCE.getWebPublicUrl() + "/swagger-ui.html"); model.addAttribute("swagger_url", Config.INSTANCE.getWebPublicUrl() + "/swagger-ui.html");
return "index"; return "index";
} }

@ -0,0 +1,23 @@
package cc.fascinated.controller;
import cc.fascinated.model.cache.CachedEndpointStatus;
import cc.fascinated.service.MojangService;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Tag(name = "Mojang Controller", description = "The Mojang Controller is used to get information about the Mojang APIs.")
@RequestMapping(value = "/mojang/", produces = MediaType.APPLICATION_JSON_VALUE)
public class MojangController {
@Autowired
private MojangService mojangService;
@RequestMapping(value = "/status")
public CachedEndpointStatus getStatus() {
return mojangService.getMojangApiStatus();
}
}

@ -0,0 +1,12 @@
package cc.fascinated.exception.impl;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class InternalServerErrorException extends RuntimeException {
public InternalServerErrorException(String message) {
super(message);
}
}

@ -0,0 +1,31 @@
package cc.fascinated.model.cache;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import java.io.Serializable;
import java.util.Map;
@AllArgsConstructor @Setter @Getter @ToString
@RedisHash(value = "mojangEndpointStatus", timeToLive = 60L) // 1 minute (in seconds)
public final class CachedEndpointStatus implements Serializable {
/**
* The id for this endpoint cache.
*/
@Id @NonNull @JsonIgnore
private final String id;
/**
* The list of endpoints and their status.
*/
private final Map<String, Boolean> endpoints;
/**
* The unix timestamp of when this
* server was cached, -1 if not cached.
*/
private long cached;
}

@ -0,0 +1,11 @@
package cc.fascinated.repository;
import cc.fascinated.model.cache.CachedEndpointStatus;
import org.springframework.data.repository.CrudRepository;
/**
* A cache repository for the Mojang endpoint status.
*
* @author Braydon
*/
public interface EndpointStatusRepository extends CrudRepository<CachedEndpointStatus, String> { }

@ -1,17 +1,25 @@
package cc.fascinated.service; package cc.fascinated.service;
import cc.fascinated.common.EndpointStatus;
import cc.fascinated.common.ExpiringSet; import cc.fascinated.common.ExpiringSet;
import cc.fascinated.common.WebRequest; import cc.fascinated.common.WebRequest;
import cc.fascinated.config.Config;
import cc.fascinated.model.cache.CachedEndpointStatus;
import cc.fascinated.model.mojang.MojangProfile; import cc.fascinated.model.mojang.MojangProfile;
import cc.fascinated.model.mojang.MojangUsernameToUuid; import cc.fascinated.model.mojang.MojangUsernameToUuid;
import cc.fascinated.repository.EndpointStatusRepository;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.base.Splitter; import com.google.common.base.Splitter;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.hash.Hashing; import com.google.common.hash.Hashing;
import io.micrometer.common.lang.NonNull; import io.micrometer.common.lang.NonNull;
import lombok.Getter;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import net.jodah.expiringmap.ExpirationPolicy; import net.jodah.expiringmap.ExpirationPolicy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.io.InputStream; import java.io.InputStream;
@ -20,16 +28,40 @@ import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@Service @Log4j2 @Service @Log4j2 @Getter
public class MojangService { public class MojangService {
/**
* The splitter and joiner for dots.
*/
private static final Splitter DOT_SPLITTER = Splitter.on('.');
private static final Joiner DOT_JOINER = Joiner.on('.');
/**
* The Mojang API endpoints.
*/
private static final String SESSION_SERVER_ENDPOINT = "https://sessionserver.mojang.com"; private static final String SESSION_SERVER_ENDPOINT = "https://sessionserver.mojang.com";
private static final String API_ENDPOINT = "https://api.mojang.com"; private static final String API_ENDPOINT = "https://api.mojang.com";
private static final String FETCH_BLOCKED_SERVERS = SESSION_SERVER_ENDPOINT + "/blockedservers"; private static final String FETCH_BLOCKED_SERVERS = SESSION_SERVER_ENDPOINT + "/blockedservers";
private static final Splitter DOT_SPLITTER = Splitter.on('.');
private static final Joiner DOT_JOINER = Joiner.on('.'); /**
* The interval to fetch the blocked servers from Mojang.
*/
private static final long FETCH_BLOCKED_SERVERS_INTERVAL = TimeUnit.HOURS.toMillis(1L); private static final long FETCH_BLOCKED_SERVERS_INTERVAL = TimeUnit.HOURS.toMillis(1L);
/**
* Information about the Mojang API endpoints.
*/
private static final String MOJANG_ENDPOINT_STATUS_KEY = "mojang";
private static final List<EndpointStatus> MOJANG_ENDPOINTS = List.of(
new EndpointStatus("https://textures.minecraft.net", List.of(HttpStatus.BAD_REQUEST)),
new EndpointStatus(API_ENDPOINT, List.of(HttpStatus.OK)),
new EndpointStatus(SESSION_SERVER_ENDPOINT, List.of(HttpStatus.FORBIDDEN))
);
@Autowired
private EndpointStatusRepository mojangEndpointStatusRepository;
/** /**
* A list of banned server hashes provided by Mojang. * A list of banned server hashes provided by Mojang.
* <p> * <p>
@ -62,6 +94,7 @@ public class MojangService {
*/ */
@SneakyThrows @SneakyThrows
private void fetchBlockedServers() { private void fetchBlockedServers() {
log.info("Fetching blocked servers from Mojang");
try ( try (
InputStream inputStream = new URL(FETCH_BLOCKED_SERVERS).openStream(); InputStream inputStream = new URL(FETCH_BLOCKED_SERVERS).openStream();
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8).useDelimiter("\n"); Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8).useDelimiter("\n");
@ -142,6 +175,41 @@ public class MojangService {
return blocked; return blocked;
} }
/**
* Gets the status of the Mojang API.
*
* @return the status of the Mojang API
*/
public CachedEndpointStatus getMojangApiStatus() {
log.info("Getting Mojang API status");
Optional<CachedEndpointStatus> endpointStatus = mojangEndpointStatusRepository.findById(MOJANG_ENDPOINT_STATUS_KEY);
if (endpointStatus.isPresent() && Config.INSTANCE.isProduction()) {
log.info("Got cached Mojang API status");
return endpointStatus.get();
}
// Fetch the status of the Mojang APIs
Map<String, Boolean> endpoints = new HashMap<>();
for (EndpointStatus endpoint : MOJANG_ENDPOINTS) {
boolean online = false;
ResponseEntity<?> response = WebRequest.getAndIgnoreErrors(endpoint.getEndpoint());
if (endpoint.getAllowedStatuses().contains(response.getStatusCode())) {
online = true;
}
endpoints.put(endpoint.getEndpoint(), online);
}
log.info("Fetched Mojang API status for {} endpoints", endpoints.size());
CachedEndpointStatus status = new CachedEndpointStatus(
MOJANG_ENDPOINT_STATUS_KEY,
endpoints,
System.currentTimeMillis()
);
mojangEndpointStatusRepository.save(status);
status.setCached(-1L); // Indicate that the status is not cached
return status;
}
/** /**
* Gets the Session Server profile of the * Gets the Session Server profile of the
* player with the given UUID. * player with the given UUID.

@ -22,6 +22,7 @@
<div class="flex flex-col mt-3"> <div class="flex flex-col mt-3">
<p>Player Data: <a class="text-blue-600" target=”_blank” th:href="${player_example_url}" th:text="${player_example_url}">???</a></p> <p>Player Data: <a class="text-blue-600" target=”_blank” th:href="${player_example_url}" th:text="${player_example_url}">???</a></p>
<p>Server Data: <a class="text-blue-600" target=”_blank” th:href="${java_server_example_url}" th:text="${java_server_example_url}">???</a></p> <p>Server Data: <a class="text-blue-600" target=”_blank” th:href="${java_server_example_url}" th:text="${java_server_example_url}">???</a></p>
<p>Mojang Endpoint Status: <a class="text-blue-600" target=”_blank” th:href="${mojang_endpoint_status_url}" th:text="${mojang_endpoint_status_url}">???</a></p>
<p>Swagger Docs: <a class="text-blue-600" target=”_blank” th:href="${swagger_url}" th:text="${swagger_url}">???</a></p> <p>Swagger Docs: <a class="text-blue-600" target=”_blank” th:href="${swagger_url}" th:text="${swagger_url}">???</a></p>
</div> </div>
</body> </body>