diff --git a/src/main/java/xyz/mcutils/backend/Main.java b/src/main/java/xyz/mcutils/backend/Main.java index 2e98d64..9c8016e 100644 --- a/src/main/java/xyz/mcutils/backend/Main.java +++ b/src/main/java/xyz/mcutils/backend/Main.java @@ -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 diff --git a/src/main/java/xyz/mcutils/backend/common/AppConfig.java b/src/main/java/xyz/mcutils/backend/common/AppConfig.java index fbae4ff..0e0a974 100644 --- a/src/main/java/xyz/mcutils/backend/common/AppConfig.java +++ b/src/main/java/xyz/mcutils/backend/common/AppConfig.java @@ -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; } diff --git a/src/main/java/xyz/mcutils/backend/common/MojangServer.java b/src/main/java/xyz/mcutils/backend/common/MojangServer.java new file mode 100644 index 0000000..3950866 --- /dev/null +++ b/src/main/java/xyz/mcutils/backend/common/MojangServer.java @@ -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 + } +} \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/controller/MojangController.java b/src/main/java/xyz/mcutils/backend/controller/MojangController.java index e0cfd9f..3784d7a 100644 --- a/src/main/java/xyz/mcutils/backend/controller/MojangController.java +++ b/src/main/java/xyz/mcutils/backend/controller/MojangController.java @@ -24,9 +24,11 @@ public class MojangController { @ResponseBody @GetMapping(value = "/status") - public ResponseEntity getStatus() { + public ResponseEntity getStatus() { CachedEndpointStatus status = mojangService.getMojangApiStatus(); + + return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(1, TimeUnit.MINUTES).cachePublic()) .body(status); diff --git a/src/main/java/xyz/mcutils/backend/exception/ExceptionControllerAdvice.java b/src/main/java/xyz/mcutils/backend/exception/ExceptionControllerAdvice.java index d16fd46..c5af472 100644 --- a/src/main/java/xyz/mcutils/backend/exception/ExceptionControllerAdvice.java +++ b/src/main/java/xyz/mcutils/backend/exception/ExceptionControllerAdvice.java @@ -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); } diff --git a/src/main/java/xyz/mcutils/backend/model/cache/CachedEndpointStatus.java b/src/main/java/xyz/mcutils/backend/model/cache/CachedEndpointStatus.java index 1631055..d46a397 100644 --- a/src/main/java/xyz/mcutils/backend/model/cache/CachedEndpointStatus.java +++ b/src/main/java/xyz/mcutils/backend/model/cache/CachedEndpointStatus.java @@ -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 endpoints; + private final List> endpoints; - public CachedEndpointStatus(@NonNull String id, List endpoints) { + public CachedEndpointStatus(@NonNull String id, Map mojangServers) { super(Cache.defaultCache()); this.id = id; - this.endpoints = endpoints; + this.endpoints = new ArrayList<>(); + + for (Map.Entry entry : mojangServers.entrySet()) { + MojangServer server = entry.getKey(); + + Map serverStatus = new HashMap<>(); + serverStatus.put("name", server.getName()); + serverStatus.put("endpoint", server.getEndpoint()); + serverStatus.put("status", entry.getValue().name()); + endpoints.add(serverStatus); + } } } \ No newline at end of file diff --git a/src/main/java/xyz/mcutils/backend/service/MaxMindService.java b/src/main/java/xyz/mcutils/backend/service/MaxMindService.java index 1f20c35..977739a 100644 --- a/src/main/java/xyz/mcutils/backend/service/MaxMindService.java +++ b/src/main/java/xyz/mcutils/backend/service/MaxMindService.java @@ -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; } } diff --git a/src/main/java/xyz/mcutils/backend/service/MojangService.java b/src/main/java/xyz/mcutils/backend/service/MojangService.java index 2a46561..074ce36 100644 --- a/src/main/java/xyz/mcutils/backend/service/MojangService.java +++ b/src/main/java/xyz/mcutils/backend/service/MojangService.java @@ -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 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 endpointStatus = mojangEndpointStatusRepository.findById(MOJANG_ENDPOINT_STATUS_KEY); + Optional 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 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);