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

@ -22,7 +22,7 @@ public final class AppConfig {
private static boolean isRunningTest = true; private static boolean isRunningTest = true;
static { static {
try { try {
Class.forName("org.junit.Test"); Class.forName("org.junit.jupiter.engine.JupiterTestEngine");
} catch (ClassNotFoundException e) { } catch (ClassNotFoundException e) {
isRunningTest = false; 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 @ResponseBody
@GetMapping(value = "/status") @GetMapping(value = "/status")
public ResponseEntity<CachedEndpointStatus> getStatus() { public ResponseEntity<?> getStatus() {
CachedEndpointStatus status = mojangService.getMojangApiStatus(); CachedEndpointStatus status = mojangService.getMojangApiStatus();
return ResponseEntity.ok() return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic()) .cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic())
.body(status); .body(status);

@ -1,6 +1,7 @@
package xyz.mcutils.backend.exception; package xyz.mcutils.backend.exception;
import io.micrometer.common.lang.NonNull; import io.micrometer.common.lang.NonNull;
import io.sentry.Sentry;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
@ -39,6 +40,7 @@ public final class ExceptionControllerAdvice {
} }
if (status == null) { // Fallback to 500 if (status == null) { // Fallback to 500
status = HttpStatus.INTERNAL_SERVER_ERROR; status = HttpStatus.INTERNAL_SERVER_ERROR;
Sentry.captureException(ex); // Capture the exception with Sentry
} }
return new ResponseEntity<>(new ErrorResponse(status, message), status); 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.annotation.Id;
import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.RedisHash;
import xyz.mcutils.backend.common.CachedResponse; 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.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
@Setter @Getter @EqualsAndHashCode(callSuper = false) @Setter @Getter @EqualsAndHashCode(callSuper = false)
@RedisHash(value = "mojangEndpointStatus", timeToLive = 60L) // 1 minute (in seconds) @RedisHash(value = "mojangEndpointStatus", timeToLive = 60L) // 1 minute (in seconds)
@ -26,11 +29,21 @@ public class CachedEndpointStatus extends CachedResponse implements Serializable
/** /**
* The endpoint cache. * 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()); super(Cache.defaultCache());
this.id = id; 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.DatabaseReader;
import com.maxmind.geoip2.exception.GeoIp2Exception; import com.maxmind.geoip2.exception.GeoIp2Exception;
import com.maxmind.geoip2.model.CityResponse; import com.maxmind.geoip2.model.CityResponse;
import io.sentry.Sentry;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver; import org.codehaus.plexus.archiver.tar.TarGZipUnArchiver;
@ -68,6 +69,7 @@ public class MaxMindService {
return database.city(InetAddress.getByName(ip)); return database.city(InetAddress.getByName(ip));
} catch (IOException | GeoIp2Exception e) { } catch (IOException | GeoIp2Exception e) {
log.error("Failed to lookup the GeoIP information for '{}'", ip, e); log.error("Failed to lookup the GeoIP information for '{}'", ip, e);
Sentry.captureException(e);
return null; return null;
} }
} }

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