diff --git a/src/main/java/cc/fascinated/Main.java b/src/main/java/cc/fascinated/Main.java index 1b01d49..71a2a08 100644 --- a/src/main/java/cc/fascinated/Main.java +++ b/src/main/java/cc/fascinated/Main.java @@ -16,7 +16,6 @@ import java.util.Objects; @Log4j2 @SpringBootApplication public class Main { - public static final Gson GSON = new GsonBuilder() .setDateFormat("MM-dd-yyyy HH:mm:ss") .create(); diff --git a/src/main/java/cc/fascinated/common/EndpointStatus.java b/src/main/java/cc/fascinated/common/EndpointStatus.java new file mode 100644 index 0000000..aff138f --- /dev/null +++ b/src/main/java/cc/fascinated/common/EndpointStatus.java @@ -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 allowedStatuses; +} diff --git a/src/main/java/cc/fascinated/common/WebRequest.java b/src/main/java/cc/fascinated/common/WebRequest.java index 24ff756..e524a72 100644 --- a/src/main/java/cc/fascinated/common/WebRequest.java +++ b/src/main/java/cc/fascinated/common/WebRequest.java @@ -3,9 +3,9 @@ package cc.fascinated.common; import cc.fascinated.exception.impl.RateLimitException; import lombok.experimental.UtilityClass; import org.springframework.http.HttpStatus; +import org.springframework.http.HttpStatusCode; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; -import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClient; @UtilityClass @@ -14,9 +14,15 @@ public class WebRequest { /** * The web client. */ - private static final RestClient CLIENT = RestClient.builder() - .requestFactory(new HttpComponentsClientHttpRequestFactory()) - .build(); + private static final RestClient CLIENT; + + static { + HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); + requestFactory.setConnectTimeout(5000); // 5 seconds + CLIENT = RestClient.builder() + .requestFactory(requestFactory) + .build(); + } /** * Gets a response from the given URL. @@ -26,21 +32,31 @@ public class WebRequest { * @param the type of the response */ public static T getAsEntity(String url, Class clazz) throws RateLimitException { - try { - ResponseEntity profile = CLIENT.get() - .uri(url) - .retrieve() - .toEntity(clazz); + ResponseEntity profile = CLIENT.get() + .uri(url) + .retrieve() + .toEntity(clazz); - 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) { + if (profile.getStatusCode().isError()) { 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); } } diff --git a/src/main/java/cc/fascinated/controller/HomeController.java b/src/main/java/cc/fascinated/controller/HomeController.java index a02fac2..bdeade8 100644 --- a/src/main/java/cc/fascinated/controller/HomeController.java +++ b/src/main/java/cc/fascinated/controller/HomeController.java @@ -19,6 +19,7 @@ public class HomeController { public String home(Model model) { model.addAttribute("player_example_url", Config.INSTANCE.getWebPublicUrl() + "/player/" + exampleUuid); 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"); return "index"; } diff --git a/src/main/java/cc/fascinated/controller/MojangController.java b/src/main/java/cc/fascinated/controller/MojangController.java new file mode 100644 index 0000000..cb6e658 --- /dev/null +++ b/src/main/java/cc/fascinated/controller/MojangController.java @@ -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(); + } +} diff --git a/src/main/java/cc/fascinated/exception/impl/InternalServerErrorException.java b/src/main/java/cc/fascinated/exception/impl/InternalServerErrorException.java new file mode 100644 index 0000000..0d439f2 --- /dev/null +++ b/src/main/java/cc/fascinated/exception/impl/InternalServerErrorException.java @@ -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); + } +} diff --git a/src/main/java/cc/fascinated/model/cache/CachedEndpointStatus.java b/src/main/java/cc/fascinated/model/cache/CachedEndpointStatus.java new file mode 100644 index 0000000..c4071a2 --- /dev/null +++ b/src/main/java/cc/fascinated/model/cache/CachedEndpointStatus.java @@ -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 endpoints; + + /** + * The unix timestamp of when this + * server was cached, -1 if not cached. + */ + private long cached; +} \ No newline at end of file diff --git a/src/main/java/cc/fascinated/repository/EndpointStatusRepository.java b/src/main/java/cc/fascinated/repository/EndpointStatusRepository.java new file mode 100644 index 0000000..49e8a3b --- /dev/null +++ b/src/main/java/cc/fascinated/repository/EndpointStatusRepository.java @@ -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 { } \ No newline at end of file diff --git a/src/main/java/cc/fascinated/service/MojangService.java b/src/main/java/cc/fascinated/service/MojangService.java index f9515f3..f2bc480 100644 --- a/src/main/java/cc/fascinated/service/MojangService.java +++ b/src/main/java/cc/fascinated/service/MojangService.java @@ -1,17 +1,25 @@ package cc.fascinated.service; +import cc.fascinated.common.EndpointStatus; import cc.fascinated.common.ExpiringSet; 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.MojangUsernameToUuid; +import cc.fascinated.repository.EndpointStatusRepository; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.Lists; import com.google.common.hash.Hashing; import io.micrometer.common.lang.NonNull; +import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; 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 java.io.InputStream; @@ -20,16 +28,40 @@ import java.nio.charset.StandardCharsets; import java.util.*; import java.util.concurrent.TimeUnit; -@Service @Log4j2 +@Service @Log4j2 @Getter 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 API_ENDPOINT = "https://api.mojang.com"; 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); + /** + * 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("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. *

@@ -62,6 +94,7 @@ public class MojangService { */ @SneakyThrows private void fetchBlockedServers() { + log.info("Fetching blocked servers from Mojang"); try ( InputStream inputStream = new URL(FETCH_BLOCKED_SERVERS).openStream(); Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8).useDelimiter("\n"); @@ -142,6 +175,41 @@ public class MojangService { 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 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 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 * player with the given UUID. diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index d20880a..b0bbbeb 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -22,6 +22,7 @@

Player Data: ???

Server Data: ???

+

Mojang Endpoint Status: ???

Swagger Docs: ???