forked from MinecraftUtilities/Backend
add server banned status and blocked endpoint
This commit is contained in:
parent
23bcb1d76e
commit
5ad2f438d1
124
pom.xml
124
pom.xml
@ -55,65 +55,18 @@
|
|||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<!-- Spring -->
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
<version>1.18.32</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
|
||||||
<artifactId>log4j-api</artifactId>
|
|
||||||
<version>2.20.0</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.logging.log4j</groupId>
|
|
||||||
<artifactId>log4j-core</artifactId>
|
|
||||||
<version>2.20.0</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.yaml</groupId>
|
|
||||||
<artifactId>snakeyaml</artifactId>
|
|
||||||
<version>2.2</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.google.code.gson</groupId>
|
|
||||||
<artifactId>gson</artifactId>
|
|
||||||
<version>2.10.1</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-web</artifactId>
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework.boot</groupId>
|
|
||||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.jodah</groupId>
|
|
||||||
<artifactId>expiringmap</artifactId>
|
|
||||||
<version>0.5.11</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.httpcomponents.client5</groupId>
|
|
||||||
<artifactId>httpclient5</artifactId>
|
|
||||||
<version>5.3.1</version>
|
|
||||||
<scope>compile</scope>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- DNS Lookup -->
|
<!-- Exclude the default Jackson dependency -->
|
||||||
<dependency>
|
<exclusions>
|
||||||
<groupId>com.github.dnsjava</groupId>
|
<exclusion>
|
||||||
<artifactId>dnsjava</artifactId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<version>v3.5.2</version>
|
<artifactId>spring-boot-starter-json</artifactId>
|
||||||
<scope>compile</scope>
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Redis for caching -->
|
<!-- Redis for caching -->
|
||||||
@ -132,6 +85,47 @@
|
|||||||
<artifactId>jedis</artifactId>
|
<artifactId>jedis</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Libraries -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>1.18.32</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.google.code.gson</groupId>
|
||||||
|
<artifactId>gson</artifactId>
|
||||||
|
<version>2.10.1</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.jodah</groupId>
|
||||||
|
<artifactId>expiringmap</artifactId>
|
||||||
|
<version>0.5.11</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>net.md-5</groupId>
|
||||||
|
<artifactId>bungeecord-chat</artifactId>
|
||||||
|
<version>1.20-R0.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Web Templating -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- DNS Lookup -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.dnsjava</groupId>
|
||||||
|
<artifactId>dnsjava</artifactId>
|
||||||
|
<version>v3.5.2</version>
|
||||||
|
<scope>compile</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- SwaggerUI -->
|
<!-- SwaggerUI -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springdoc</groupId>
|
<groupId>org.springdoc</groupId>
|
||||||
@ -140,25 +134,7 @@
|
|||||||
<scope>compile</scope>
|
<scope>compile</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Unit Tests -->
|
<!-- Tests -->
|
||||||
<dependency>
|
|
||||||
<groupId>org.junit.jupiter</groupId>
|
|
||||||
<artifactId>junit-jupiter-engine</artifactId>
|
|
||||||
<version>5.10.2</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.junit.jupiter</groupId>
|
|
||||||
<artifactId>junit-jupiter-api</artifactId>
|
|
||||||
<version>5.10.2</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.springframework</groupId>
|
|
||||||
<artifactId>spring-test</artifactId>
|
|
||||||
<version>6.1.5</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
132
src/main/java/cc.fascinated/common/ExpiringSet.java
Normal file
132
src/main/java/cc.fascinated/common/ExpiringSet.java
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
package cc.fascinated.common;
|
||||||
|
|
||||||
|
import lombok.NonNull;
|
||||||
|
import net.jodah.expiringmap.ExpirationPolicy;
|
||||||
|
import net.jodah.expiringmap.ExpiringMap;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple set that expires elements after a certain
|
||||||
|
* amount of time, utilizing the {@link ExpiringMap} library.
|
||||||
|
*
|
||||||
|
* @param <T> The type of element to store within this set
|
||||||
|
* @author Braydon
|
||||||
|
*/
|
||||||
|
public final class ExpiringSet<T> implements Iterable<T> {
|
||||||
|
/**
|
||||||
|
* The internal cache for this set.
|
||||||
|
*/
|
||||||
|
@NonNull private final ExpiringMap<T, Long> cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The lifetime (in millis) of the elements in this set.
|
||||||
|
*/
|
||||||
|
private final long lifetime;
|
||||||
|
|
||||||
|
public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit) {
|
||||||
|
this(expirationPolicy, duration, timeUnit, ignored -> {});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExpiringSet(@NonNull ExpirationPolicy expirationPolicy, long duration, @NonNull TimeUnit timeUnit, @NonNull Consumer<T> onExpire) {
|
||||||
|
//noinspection unchecked
|
||||||
|
this.cache = ExpiringMap.builder()
|
||||||
|
.expirationPolicy(expirationPolicy)
|
||||||
|
.expiration(duration, timeUnit)
|
||||||
|
.expirationListener((key, ignored) -> onExpire.accept((T) key))
|
||||||
|
.build();
|
||||||
|
this.lifetime = timeUnit.toMillis(duration); // Get the lifetime in millis
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an element to this set.
|
||||||
|
*
|
||||||
|
* @param element the element
|
||||||
|
* @return whether the element was added
|
||||||
|
*/
|
||||||
|
public boolean add(@NonNull T element) {
|
||||||
|
boolean contains = contains(element); // Does this set already contain the element?
|
||||||
|
this.cache.put(element, System.currentTimeMillis() + this.lifetime);
|
||||||
|
return !contains;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the entry time of an element in this set.
|
||||||
|
*
|
||||||
|
* @param element the element
|
||||||
|
* @return the entry time, -1 if not contained
|
||||||
|
*/
|
||||||
|
public long getEntryTime(@NonNull T element) {
|
||||||
|
return contains(element) ? this.cache.get(element) - this.lifetime : -1L;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if an element is
|
||||||
|
* contained within this set.
|
||||||
|
*
|
||||||
|
* @param element the element
|
||||||
|
* @return whether the element is contained
|
||||||
|
*/
|
||||||
|
public boolean contains(@NonNull T element) {
|
||||||
|
Long timeout = this.cache.get(element); // Get the timeout for the element
|
||||||
|
return timeout != null && (timeout > System.currentTimeMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this set is empty.
|
||||||
|
*
|
||||||
|
* @return whether this set is empty
|
||||||
|
*/
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return this.cache.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of this set.
|
||||||
|
*
|
||||||
|
* @return the size
|
||||||
|
*/
|
||||||
|
public int size() {
|
||||||
|
return this.cache.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an element from this set.
|
||||||
|
*
|
||||||
|
* @param element the element
|
||||||
|
* @return whether the element was removed
|
||||||
|
*/
|
||||||
|
public boolean remove(@NonNull T element) {
|
||||||
|
return this.cache.remove(element) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear this set.
|
||||||
|
*/
|
||||||
|
public void clear() {
|
||||||
|
this.cache.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the elements in this set.
|
||||||
|
*
|
||||||
|
* @return the elements
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public Set<T> getElements() {
|
||||||
|
return this.cache.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an iterator over elements of type {@code T}.
|
||||||
|
*
|
||||||
|
* @return an Iterator.
|
||||||
|
*/
|
||||||
|
@Override @NonNull
|
||||||
|
public Iterator<T> iterator() {
|
||||||
|
return this.cache.keySet().iterator();
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,7 @@ package cc.fascinated.controller;
|
|||||||
import cc.fascinated.common.ServerUtils;
|
import cc.fascinated.common.ServerUtils;
|
||||||
import cc.fascinated.common.Tuple;
|
import cc.fascinated.common.Tuple;
|
||||||
import cc.fascinated.model.cache.CachedMinecraftServer;
|
import cc.fascinated.model.cache.CachedMinecraftServer;
|
||||||
|
import cc.fascinated.service.MojangService;
|
||||||
import cc.fascinated.service.ServerService;
|
import cc.fascinated.service.ServerService;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@ -12,13 +13,21 @@ import org.springframework.http.MediaType;
|
|||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@Tag(name = "Server Controller", description = "The Server Controller is used to get information about a server.")
|
@Tag(name = "Server Controller", description = "The Server Controller is used to get information about a server.")
|
||||||
@RequestMapping(value = "/server/")
|
@RequestMapping(value = "/server/")
|
||||||
public class ServerController {
|
public class ServerController {
|
||||||
|
|
||||||
|
private final ServerService serverService;
|
||||||
|
private final MojangService mojangService;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ServerService serverService;
|
public ServerController(ServerService serverService, MojangService mojangService) {
|
||||||
|
this.serverService = serverService;
|
||||||
|
this.mojangService = mojangService;
|
||||||
|
}
|
||||||
|
|
||||||
@ResponseBody
|
@ResponseBody
|
||||||
@GetMapping(value = "/{platform}/{hostnameAndPort}", produces = MediaType.APPLICATION_JSON_VALUE)
|
@GetMapping(value = "/{platform}/{hostnameAndPort}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
@ -44,4 +53,13 @@ public class ServerController {
|
|||||||
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(ServerUtils.getAddress(hostname, port)))
|
.header(HttpHeaders.CONTENT_DISPOSITION, dispositionHeader.formatted(ServerUtils.getAddress(hostname, port)))
|
||||||
.body(serverService.getServerFavicon(hostname, port));
|
.body(serverService.getServerFavicon(hostname, port));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
@GetMapping(value = "/blocked/{hostname}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
public ResponseEntity<?> getServerBlockedStatus(
|
||||||
|
@Parameter(description = "The hostname of the server", example = "play.hypixel.net") @PathVariable String hostname) {
|
||||||
|
return ResponseEntity.ok(Map.of(
|
||||||
|
"banned", mojangService.isServerBlocked(hostname)
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ public final class JavaServerStatusToken {
|
|||||||
/**
|
/**
|
||||||
* The motd of the server.
|
* The motd of the server.
|
||||||
*/
|
*/
|
||||||
private final String description;
|
private final Object description;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The favicon of the server.
|
* The favicon of the server.
|
||||||
|
@ -1,11 +1,18 @@
|
|||||||
package cc.fascinated.model.server;
|
package cc.fascinated.model.server;
|
||||||
|
|
||||||
|
import cc.fascinated.Main;
|
||||||
import cc.fascinated.common.JavaMinecraftVersion;
|
import cc.fascinated.common.JavaMinecraftVersion;
|
||||||
|
import cc.fascinated.common.ServerUtils;
|
||||||
import cc.fascinated.config.Config;
|
import cc.fascinated.config.Config;
|
||||||
|
import cc.fascinated.model.mojang.JavaServerStatusToken;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
import net.md_5.bungee.api.chat.TextComponent;
|
||||||
|
import net.md_5.bungee.chat.ComponentSerializer;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Braydon
|
* @author Braydon
|
||||||
@ -28,6 +35,11 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
|||||||
*/
|
*/
|
||||||
private Favicon favicon;
|
private Favicon favicon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The mojang banned status of the server.
|
||||||
|
*/
|
||||||
|
private boolean mojangBanned;
|
||||||
|
|
||||||
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, @NonNull Version version, Players players, Favicon favicon) {
|
public JavaMinecraftServer(String hostname, String ip, int port, MOTD motd, @NonNull Version version, Players players, Favicon favicon) {
|
||||||
super(hostname, ip, port, motd);
|
super(hostname, ip, port, motd);
|
||||||
this.version = version;
|
this.version = version;
|
||||||
@ -35,6 +47,32 @@ public final class JavaMinecraftServer extends MinecraftServer {
|
|||||||
this.favicon = favicon;
|
this.favicon = favicon;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new Java Minecraft server.
|
||||||
|
*
|
||||||
|
* @param hostname the hostname of the server
|
||||||
|
* @param ip the IP address of the server
|
||||||
|
* @param port the port of the server
|
||||||
|
* @param token the status token
|
||||||
|
* @return the Java Minecraft server
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static JavaMinecraftServer create(@NonNull String hostname, String ip, int port, @NonNull JavaServerStatusToken token) {
|
||||||
|
String motdString = token.getDescription() instanceof String ? (String) token.getDescription() : null;
|
||||||
|
if (motdString == null) { // Not a string motd, convert from Json
|
||||||
|
motdString = new TextComponent(ComponentSerializer.parse(Main.GSON.toJson(token.getDescription()))).toLegacyText();
|
||||||
|
}
|
||||||
|
return new JavaMinecraftServer(
|
||||||
|
hostname,
|
||||||
|
ip,
|
||||||
|
port,
|
||||||
|
MinecraftServer.MOTD.create(motdString),
|
||||||
|
token.getVersion().detailedCopy(),
|
||||||
|
token.getPlayers(),
|
||||||
|
JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@AllArgsConstructor @Getter
|
@AllArgsConstructor @Getter
|
||||||
public static class Version {
|
public static class Version {
|
||||||
/**
|
/**
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
package cc.fascinated.service;
|
|
||||||
|
|
||||||
import cc.fascinated.common.WebRequest;
|
|
||||||
import cc.fascinated.model.mojang.MojangProfile;
|
|
||||||
import cc.fascinated.model.mojang.MojangUsernameToUuid;
|
|
||||||
import lombok.extern.log4j.Log4j2;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
@Service @Log4j2
|
|
||||||
public class MojangAPIService {
|
|
||||||
|
|
||||||
@Value("${mojang.session-server}")
|
|
||||||
private String mojangSessionServerUrl;
|
|
||||||
|
|
||||||
@Value("${mojang.api}")
|
|
||||||
private String mojangApiUrl;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the Session Server profile of the
|
|
||||||
* player with the given UUID.
|
|
||||||
*
|
|
||||||
* @param id the uuid or name of the player
|
|
||||||
* @return the profile
|
|
||||||
*/
|
|
||||||
public MojangProfile getProfile(String id) {
|
|
||||||
return WebRequest.getAsEntity(mojangSessionServerUrl + "/session/minecraft/profile/" + id, MojangProfile.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the UUID of the player using
|
|
||||||
* the name of the player.
|
|
||||||
*
|
|
||||||
* @param id the name of the player
|
|
||||||
* @return the profile
|
|
||||||
*/
|
|
||||||
public MojangUsernameToUuid getUuidFromUsername(String id) {
|
|
||||||
return WebRequest.getAsEntity(mojangApiUrl + "/users/profiles/minecraft/" + id, MojangUsernameToUuid.class);
|
|
||||||
}
|
|
||||||
}
|
|
166
src/main/java/cc.fascinated/service/MojangService.java
Normal file
166
src/main/java/cc.fascinated/service/MojangService.java
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
package cc.fascinated.service;
|
||||||
|
|
||||||
|
import cc.fascinated.common.ExpiringSet;
|
||||||
|
import cc.fascinated.common.WebRequest;
|
||||||
|
import cc.fascinated.model.mojang.MojangProfile;
|
||||||
|
import cc.fascinated.model.mojang.MojangUsernameToUuid;
|
||||||
|
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.SneakyThrows;
|
||||||
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
import net.jodah.expiringmap.ExpirationPolicy;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Service @Log4j2
|
||||||
|
public class MojangService {
|
||||||
|
|
||||||
|
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('.');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of banned server hashes provided by Mojang.
|
||||||
|
* <p>
|
||||||
|
* This is periodically fetched from Mojang, see
|
||||||
|
* {@link #fetchBlockedServers()} for more info.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @see <a href="https://wiki.vg/Mojang_API#Blocked_Servers">Mojang API</a>
|
||||||
|
*/
|
||||||
|
private List<String> bannedServerHashes;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A cache of blocked server hostnames.
|
||||||
|
*
|
||||||
|
* @see #isServerHostnameBlocked(String) for more
|
||||||
|
*/
|
||||||
|
private final ExpiringSet<String> blockedServersCache = new ExpiringSet<>(ExpirationPolicy.CREATED, 10L, TimeUnit.MINUTES);
|
||||||
|
|
||||||
|
public MojangService() {
|
||||||
|
new Timer().scheduleAtFixedRate(new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
fetchBlockedServers();
|
||||||
|
}
|
||||||
|
}, 0L, 60L * 15L * 1000L);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a list of blocked servers from Mojang.
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
private void fetchBlockedServers() {
|
||||||
|
try (
|
||||||
|
InputStream inputStream = new URL(FETCH_BLOCKED_SERVERS).openStream();
|
||||||
|
Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8).useDelimiter("\n");
|
||||||
|
) {
|
||||||
|
List<String> hashes = new ArrayList<>();
|
||||||
|
while (scanner.hasNext()) {
|
||||||
|
hashes.add(scanner.next());
|
||||||
|
}
|
||||||
|
bannedServerHashes = Collections.synchronizedList(hashes);
|
||||||
|
log.info("Fetched {} banned server hashes", bannedServerHashes.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the server with the
|
||||||
|
* given hostname is blocked by Mojang.
|
||||||
|
*
|
||||||
|
* @param hostname the server hostname to check
|
||||||
|
* @return whether the hostname is blocked
|
||||||
|
*/
|
||||||
|
public boolean isServerBlocked(@NonNull String hostname) {
|
||||||
|
// Remove trailing dots
|
||||||
|
while (hostname.charAt(hostname.length() - 1) == '.') {
|
||||||
|
hostname = hostname.substring(0, hostname.length() - 1);
|
||||||
|
}
|
||||||
|
// Is the hostname banned?
|
||||||
|
if (isServerHostnameBlocked(hostname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
List<String> splitDots = Lists.newArrayList(DOT_SPLITTER.split(hostname)); // Split the hostname by dots
|
||||||
|
boolean isIp = splitDots.size() == 4; // Is it an IP address?
|
||||||
|
if (isIp) {
|
||||||
|
for (String element : splitDots) {
|
||||||
|
try {
|
||||||
|
int part = Integer.parseInt(element);
|
||||||
|
if (part >= 0 && part <= 255) { // Ensure the part is within the valid range
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
// Safely ignore, not a number
|
||||||
|
}
|
||||||
|
isIp = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check if the hostname is blocked
|
||||||
|
if (!isIp && isServerHostnameBlocked("*." + hostname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Additional checks for the hostname
|
||||||
|
while (splitDots.size() > 1) {
|
||||||
|
splitDots.remove(isIp ? splitDots.size() - 1 : 0);
|
||||||
|
String starredPart = isIp ? DOT_JOINER.join(splitDots) + ".*" : "*." + DOT_JOINER.join(splitDots);
|
||||||
|
if (isServerHostnameBlocked(starredPart)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the hash for the given
|
||||||
|
* hostname is in the blocked server list.
|
||||||
|
*
|
||||||
|
* @param hostname the hostname to check
|
||||||
|
* @return whether the hostname is blocked
|
||||||
|
*/
|
||||||
|
private boolean isServerHostnameBlocked(@NonNull String hostname) {
|
||||||
|
// Check the cache first for the hostname
|
||||||
|
if (blockedServersCache.contains(hostname)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
String hashed = Hashing.sha1().hashBytes(hostname.toLowerCase().getBytes(StandardCharsets.ISO_8859_1)).toString();
|
||||||
|
boolean blocked = bannedServerHashes.contains(hashed); // Is the hostname blocked?
|
||||||
|
if (blocked) { // Cache the blocked hostname
|
||||||
|
blockedServersCache.add(hostname);
|
||||||
|
}
|
||||||
|
return blocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the Session Server profile of the
|
||||||
|
* player with the given UUID.
|
||||||
|
*
|
||||||
|
* @param id the uuid or name of the player
|
||||||
|
* @return the profile
|
||||||
|
*/
|
||||||
|
public MojangProfile getProfile(String id) {
|
||||||
|
return WebRequest.getAsEntity(SESSION_SERVER_ENDPOINT + "/session/minecraft/profile/" + id, MojangProfile.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the UUID of the player using
|
||||||
|
* the name of the player.
|
||||||
|
*
|
||||||
|
* @param id the name of the player
|
||||||
|
* @return the profile
|
||||||
|
*/
|
||||||
|
public MojangUsernameToUuid getUuidFromUsername(String id) {
|
||||||
|
return WebRequest.getAsEntity(API_ENDPOINT + "/users/profiles/minecraft/" + id, MojangUsernameToUuid.class);
|
||||||
|
}
|
||||||
|
}
|
@ -24,12 +24,12 @@ import java.util.UUID;
|
|||||||
@Service @Log4j2
|
@Service @Log4j2
|
||||||
public class PlayerService {
|
public class PlayerService {
|
||||||
|
|
||||||
private final MojangAPIService mojangAPIService;
|
private final MojangService mojangAPIService;
|
||||||
private final PlayerCacheRepository playerCacheRepository;
|
private final PlayerCacheRepository playerCacheRepository;
|
||||||
private final PlayerNameCacheRepository playerNameCacheRepository;
|
private final PlayerNameCacheRepository playerNameCacheRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PlayerService(MojangAPIService mojangAPIService, PlayerCacheRepository playerCacheRepository, PlayerNameCacheRepository playerNameCacheRepository) {
|
public PlayerService(MojangService mojangAPIService, PlayerCacheRepository playerCacheRepository, PlayerNameCacheRepository playerNameCacheRepository) {
|
||||||
this.mojangAPIService = mojangAPIService;
|
this.mojangAPIService = mojangAPIService;
|
||||||
this.playerCacheRepository = playerCacheRepository;
|
this.playerCacheRepository = playerCacheRepository;
|
||||||
this.playerNameCacheRepository = playerNameCacheRepository;
|
this.playerNameCacheRepository = playerNameCacheRepository;
|
||||||
|
@ -20,10 +20,12 @@ import java.util.Optional;
|
|||||||
public class ServerService {
|
public class ServerService {
|
||||||
private static final String DEFAULT_SERVER_ICON = "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";
|
private static final String DEFAULT_SERVER_ICON = "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";
|
||||||
|
|
||||||
|
private final MojangService mojangService;
|
||||||
private final MinecraftServerCacheRepository serverCacheRepository;
|
private final MinecraftServerCacheRepository serverCacheRepository;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ServerService(MinecraftServerCacheRepository serverCacheRepository) {
|
public ServerService(MojangService mojangService, MinecraftServerCacheRepository serverCacheRepository) {
|
||||||
|
this.mojangService = mojangService;
|
||||||
this.serverCacheRepository = serverCacheRepository;
|
this.serverCacheRepository = serverCacheRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,6 +63,12 @@ public class ServerService {
|
|||||||
platform.getPinger().ping(hostname, port),
|
platform.getPinger().ping(hostname, port),
|
||||||
System.currentTimeMillis()
|
System.currentTimeMillis()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (platform == MinecraftServer.Platform.JAVA) { // Check if the server is blocked by Mojang
|
||||||
|
JavaMinecraftServer javaServer = (JavaMinecraftServer) server.getServer();
|
||||||
|
javaServer.setMojangBanned(mojangService.isServerBlocked(hostname));
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Found server: {}:{}", hostname, port);
|
log.info("Found server: {}:{}", hostname, port);
|
||||||
serverCacheRepository.save(server);
|
serverCacheRepository.save(server);
|
||||||
server.setCached(-1); // Indicate that the server is not cached
|
server.setCached(-1); // Indicate that the server is not cached
|
||||||
|
@ -51,15 +51,7 @@ public final class JavaMinecraftServerPinger implements MinecraftServerPinger<Ja
|
|||||||
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
|
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
|
||||||
packetStatusInStart.process(inputStream, outputStream);
|
packetStatusInStart.process(inputStream, outputStream);
|
||||||
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
|
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
|
||||||
return new JavaMinecraftServer(
|
return JavaMinecraftServer.create(hostname, ip, port, token);
|
||||||
hostname,
|
|
||||||
ip,
|
|
||||||
port,
|
|
||||||
MinecraftServer.MOTD.create(token.getDescription()),
|
|
||||||
token.getVersion().detailedCopy(),
|
|
||||||
token.getPlayers(),
|
|
||||||
JavaMinecraftServer.Favicon.create(token.getFavicon(), ServerUtils.getAddress(hostname, port))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
} catch (IOException ex) {
|
||||||
if (ex instanceof UnknownHostException) {
|
if (ex instanceof UnknownHostException) {
|
||||||
|
@ -17,8 +17,4 @@ spring:
|
|||||||
database: 0
|
database: 0
|
||||||
auth: "" # Leave blank for no auth
|
auth: "" # Leave blank for no auth
|
||||||
|
|
||||||
public-url: http://localhost:80
|
public-url: http://localhost:80
|
||||||
|
|
||||||
mojang:
|
|
||||||
session-server: https://sessionserver.mojang.com
|
|
||||||
api: https://api.mojang.com
|
|
@ -17,8 +17,4 @@ spring:
|
|||||||
database: 0
|
database: 0
|
||||||
auth: "" # Leave blank for no auth
|
auth: "" # Leave blank for no auth
|
||||||
|
|
||||||
public-url: http://localhost:80
|
public-url: http://localhost:80
|
||||||
|
|
||||||
mojang:
|
|
||||||
session-server: https://sessionserver.mojang.com
|
|
||||||
api: https://api.mojang.com
|
|
Loading…
Reference in New Issue
Block a user