fix mojang status endpoint

This commit is contained in:
Lee 2024-07-30 20:49:36 +01:00
parent ee5b1f12d8
commit 2b017f9ef7
8 changed files with 126 additions and 42 deletions

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

@ -22,7 +22,7 @@ public final class AppConfig {
private static boolean isRunningTest = true;
static {
try {
Class.forName("org.junit.Test");
Class.forName("org.junit.jupiter.engine.JupiterTestEngine");
} catch (ClassNotFoundException e) {
isRunningTest = false;
}

@ -0,0 +1,85 @@
package xyz.mcutils.backend.common;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.ToString;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.concurrent.TimeUnit;
/**
* @author Fascinated (fascinated7)
*/
@AllArgsConstructor
@Getter
@ToString
public enum MojangServer {
SESSION("Session Server", "https://sessionserver.mojang.com"),
API("Mojang API", "https://api.mojang.com"),
TEXTURES("Textures Server", "https://textures.minecraft.net"),
ASSETS("Assets Server", "https://assets.mojang.com"),
LIBRARIES("Libraries Server", "https://libraries.minecraft.net"),
SERVICES("Minecraft Services", "https://api.minecraftservices.com");
private static final long STATUS_TIMEOUT = TimeUnit.SECONDS.toMillis(4);
/**
* The name of this server.
*/
@NonNull private final String name;
/**
* The endpoint of this service.
*/
@NonNull private final String endpoint;
/**
* Ping this service and get the status of it.
*
* @return the service status
*/
@NonNull
public Status getStatus() {
try {
InetAddress address = InetAddress.getByName(endpoint.substring(8));
long before = System.currentTimeMillis();
if (address.isReachable((int) STATUS_TIMEOUT)) {
// The time it took to reach the host is 75% of
// the timeout, consider it to be degraded.
if ((System.currentTimeMillis() - before) > STATUS_TIMEOUT * 0.75D) {
return Status.DEGRADED;
}
return Status.ONLINE;
}
} catch (UnknownHostException ex) {
ex.printStackTrace();
} catch (IOException ignored) {
// We can safely ignore any errors, we're simply checking
// if the host is reachable, if it's not, it's offline.
}
return Status.OFFLINE;
}
/**
* The status of a service.
*/
public enum Status {
/**
* The service is online and accessible.
*/
ONLINE,
/**
* The service is online, but is experiencing degraded performance.
*/
DEGRADED,
/**
* The service is offline and inaccessible.
*/
OFFLINE
}
}

@ -24,9 +24,11 @@ public class MojangController {
@ResponseBody
@GetMapping(value = "/status")
public ResponseEntity<CachedEndpointStatus> getStatus() {
public ResponseEntity<?> getStatus() {
CachedEndpointStatus status = mojangService.getMojangApiStatus();
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic())
.body(status);

@ -1,6 +1,7 @@
package xyz.mcutils.backend.exception;
import io.micrometer.common.lang.NonNull;
import io.sentry.Sentry;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
@ -39,6 +40,7 @@ public final class ExceptionControllerAdvice {
}
if (status == null) { // Fallback to 500
status = HttpStatus.INTERNAL_SERVER_ERROR;
Sentry.captureException(ex); // Capture the exception with Sentry
}
return new ResponseEntity<>(new ErrorResponse(status, message), status);
}

@ -8,10 +8,13 @@ import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import xyz.mcutils.backend.common.CachedResponse;
import xyz.mcutils.backend.model.mojang.EndpointStatus;
import xyz.mcutils.backend.common.MojangServer;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Setter @Getter @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "mojangEndpointStatus", timeToLive = 60L) // 1 minute (in seconds)
@ -26,11 +29,21 @@ public class CachedEndpointStatus extends CachedResponse implements Serializable
/**
* The endpoint cache.
*/
private final List<EndpointStatus> endpoints;
private final List<Map<String, Object>> endpoints;
public CachedEndpointStatus(@NonNull String id, List<EndpointStatus> endpoints) {
public CachedEndpointStatus(@NonNull String id, Map<MojangServer, MojangServer.Status> mojangServers) {
super(Cache.defaultCache());
this.id = id;
this.endpoints = endpoints;
this.endpoints = new ArrayList<>();
for (Map.Entry<MojangServer, MojangServer.Status> entry : mojangServers.entrySet()) {
MojangServer server = entry.getKey();
Map<String, Object> serverStatus = new HashMap<>();
serverStatus.put("name", server.getName());
serverStatus.put("endpoint", server.getEndpoint());
serverStatus.put("status", entry.getValue().name());
endpoints.add(serverStatus);
}
}
}

@ -3,6 +3,7 @@ package xyz.mcutils.backend.service;
import com.maxmind.geoip2.DatabaseReader;
import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.CityResponse;
import io.sentry.Sentry;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver;
@ -68,6 +69,7 @@ public class MaxMindService {
return database.city(InetAddress.getByName(ip));
} catch (IOException | GeoIp2Exception e) {
log.error("Failed to lookup the GeoIP information for '{}'", ip, e);
Sentry.captureException(e);
return null;
}
}

@ -13,16 +13,15 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import xyz.mcutils.backend.common.AppConfig;
import xyz.mcutils.backend.common.ExpiringSet;
import xyz.mcutils.backend.common.MojangServer;
import xyz.mcutils.backend.common.WebRequest;
import xyz.mcutils.backend.model.cache.CachedEndpointStatus;
import xyz.mcutils.backend.model.mojang.EndpointStatus;
import xyz.mcutils.backend.model.token.MojangProfileToken;
import xyz.mcutils.backend.model.token.MojangUsernameToUuidToken;
import xyz.mcutils.backend.repository.redis.EndpointStatusRepository;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
@ -49,18 +48,6 @@ public class MojangService {
*/
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("Minecraft Textures", "textures.minecraft.net"),
new EndpointStatus("Minecraft Libraries", "libraries.minecraft.net"),
new EndpointStatus("Minecraft Services", "api.minecraftservices.com"),
new EndpointStatus("Mojang Assets", "assets.mojang.com"),
new EndpointStatus("Mojang API", "api.mojang.com"),
new EndpointStatus("Mojang Session Server", "sessionserver.mojang.com"));
@Autowired
private EndpointStatusRepository mojangEndpointStatusRepository;
@ -107,6 +94,8 @@ public class MojangService {
}
bannedServerHashes = Collections.synchronizedList(hashes);
log.info("Fetched {} banned server hashes", bannedServerHashes.size());
} catch (IOException e) {
log.error("Failed to fetch blocked servers from Mojang", e);
}
}
@ -184,33 +173,24 @@ public class MojangService {
*/
public CachedEndpointStatus getMojangApiStatus() {
log.info("Getting Mojang API status");
Optional<CachedEndpointStatus> endpointStatus = mojangEndpointStatusRepository.findById(MOJANG_ENDPOINT_STATUS_KEY);
Optional<CachedEndpointStatus> endpointStatus = mojangEndpointStatusRepository.findById("mojang-servers-status");
if (endpointStatus.isPresent() && AppConfig.isProduction()) {
log.info("Got cached Mojang API status");
return endpointStatus.get();
}
MOJANG_ENDPOINTS.parallelStream().forEach(endpoint -> {
try {
long start = System.currentTimeMillis();
InetAddress address = InetAddress.getByName(endpoint.getHostname());
if (address.isReachable((int) TimeUnit.SECONDS.toMillis(4))) { // Check if the endpoint is reachable
endpoint.setStatus(EndpointStatus.Status.ONLINE);
return;
}
// Check if the endpoint took too long to respond
if (System.currentTimeMillis() - start > TimeUnit.SECONDS.toMillis(2)) {
endpoint.setStatus(EndpointStatus.Status.DEGRADED);
}
} catch (IOException e) {
endpoint.setStatus(EndpointStatus.Status.OFFLINE);
}
Map<MojangServer, MojangServer.Status> mojangServers = new HashMap<>();
Arrays.stream(MojangServer.values()).parallel().forEach(server -> {
log.info("Pinging {}...", server.getEndpoint());
MojangServer.Status status = server.getStatus(); // Retrieve the server status
log.info("Retrieved status of {}: {}", server.getEndpoint(), status.name());
mojangServers.put(server, status); // Cache the server status
});
log.info("Fetched Mojang API status for {} endpoints", MOJANG_ENDPOINTS.size());
log.info("Fetched Mojang API status for {} endpoints", mojangServers.size());
CachedEndpointStatus status = new CachedEndpointStatus(
MOJANG_ENDPOINT_STATUS_KEY,
MOJANG_ENDPOINTS
"mojang-servers-status",
mojangServers
);
mojangEndpointStatusRepository.save(status);
status.getCache().setCached(false);