add mojang api status endpoint
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 27s
Some checks failed
Deploy App / docker (ubuntu-latest, 2.44.0, 17, 3.8.5) (push) Failing after 27s
This commit is contained in:
parent
0c8f769ee7
commit
811ea348cf
@ -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();
|
||||||
|
22
src/main/java/cc/fascinated/common/EndpointStatus.java
Normal file
22
src/main/java/cc/fascinated/common/EndpointStatus.java
Normal file
@ -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";
|
||||||
}
|
}
|
||||||
|
23
src/main/java/cc/fascinated/controller/MojangController.java
Normal file
23
src/main/java/cc/fascinated/controller/MojangController.java
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
31
src/main/java/cc/fascinated/model/cache/CachedEndpointStatus.java
vendored
Normal file
31
src/main/java/cc/fascinated/model/cache/CachedEndpointStatus.java
vendored
Normal file
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user