add server pinger
Some checks failed
ci / deploy (push) Failing after 1m3s

This commit is contained in:
Lee
2024-04-10 07:43:38 +01:00
parent 25c69e11e1
commit fcb8ef0357
40 changed files with 1751 additions and 0 deletions

View File

@ -0,0 +1,40 @@
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);
}
}

View File

@ -0,0 +1,93 @@
package cc.fascinated.service;
import cc.fascinated.common.PlayerUtils;
import cc.fascinated.common.Tuple;
import cc.fascinated.common.UUIDUtils;
import cc.fascinated.exception.impl.BadRequestException;
import cc.fascinated.exception.impl.ResourceNotFoundException;
import cc.fascinated.model.cache.CachedPlayer;
import cc.fascinated.model.cache.CachedPlayerName;
import cc.fascinated.model.mojang.MojangProfile;
import cc.fascinated.model.mojang.MojangUsernameToUuid;
import cc.fascinated.model.player.Cape;
import cc.fascinated.model.player.Player;
import cc.fascinated.model.player.Skin;
import cc.fascinated.repository.PlayerCacheRepository;
import cc.fascinated.repository.PlayerNameCacheRepository;
import lombok.extern.log4j.Log4j2;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@Service @Log4j2
public class PlayerService {
private final MojangAPIService mojangAPIService;
private final PlayerCacheRepository playerCacheRepository;
private final PlayerNameCacheRepository playerNameCacheRepository;
@Autowired
public PlayerService(MojangAPIService mojangAPIService, PlayerCacheRepository playerCacheRepository, PlayerNameCacheRepository playerNameCacheRepository) {
this.mojangAPIService = mojangAPIService;
this.playerCacheRepository = playerCacheRepository;
this.playerNameCacheRepository = playerNameCacheRepository;
}
/**
* Get a player from the cache or
* from the Mojang API.
*
* @param id the id of the player
* @return the player
*/
public CachedPlayer getPlayer(String id) {
UUID uuid = PlayerUtils.getUuidFromString(id);
if (uuid == null) { // If the id is not a valid uuid, get the uuid from the username
uuid = usernameToUuid(id);
}
Optional<CachedPlayer> cachedPlayer = playerCacheRepository.findById(uuid);
if (cachedPlayer.isPresent()) { // Return the cached player if it exists
return cachedPlayer.get();
}
MojangProfile mojangProfile = mojangAPIService.getProfile(uuid.toString());
Tuple<Skin, Cape> skinAndCape = mojangProfile.getSkinAndCape();
CachedPlayer player = new CachedPlayer(
uuid,
mojangProfile.getName(),
skinAndCape.getLeft(), // Skin
skinAndCape.getRight(), // Cape
System.currentTimeMillis()
);
playerCacheRepository.save(player);
return player;
}
/**
* Gets the player's uuid from their username.
*
* @param username the username of the player
* @return the uuid of the player
*/
private UUID usernameToUuid(String username) {
Optional<CachedPlayerName> cachedPlayerName = playerNameCacheRepository.findById(username);
if (cachedPlayerName.isPresent()) {
return cachedPlayerName.get().getUniqueId();
}
MojangUsernameToUuid mojangUsernameToUuid = mojangAPIService.getUuidFromUsername(username);
if (mojangUsernameToUuid == null) {
throw new ResourceNotFoundException("Player with username '%s' not found".formatted(username));
}
UUID uuid = UUIDUtils.addDashes(mojangUsernameToUuid.getId());
playerNameCacheRepository.save(new CachedPlayerName(username, uuid));
return uuid;
}
}

View File

@ -0,0 +1,64 @@
package cc.fascinated.service;
import cc.fascinated.EnumUtils;
import cc.fascinated.common.DNSUtils;
import cc.fascinated.common.WebRequest;
import cc.fascinated.exception.impl.BadRequestException;
import cc.fascinated.model.cache.CachedMinecraftServer;
import cc.fascinated.model.mojang.MojangProfile;
import cc.fascinated.model.mojang.MojangUsernameToUuid;
import cc.fascinated.model.server.MinecraftServer;
import cc.fascinated.repository.MinecraftServerCacheRepository;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.net.InetSocketAddress;
import java.util.Optional;
@Service @Log4j2
public class ServerService {
private final MinecraftServerCacheRepository serverCacheRepository;
@Autowired
public ServerService(MinecraftServerCacheRepository serverCacheRepository) {
this.serverCacheRepository = serverCacheRepository;
}
/**
* Ping a server to get the server information.
*
* @param platformName the name of the platform
* @param hostname the hostname of the server
* @param port the port of the server
* @return the server
*/
public CachedMinecraftServer getServer(String platformName, String hostname, int port) {
MinecraftServer.Platform platform = EnumUtils.getEnumConstant(MinecraftServer.Platform.class, platformName.toUpperCase());
if (platform == null) {
throw new BadRequestException("Invalid platform: %s".formatted(platformName));
}
String key = "%s-%s:%s".formatted(platformName, hostname, port);
Optional<CachedMinecraftServer> cached = serverCacheRepository.findById(key);
if (cached.isPresent()) {
return cached.get();
}
InetSocketAddress address = platform == MinecraftServer.Platform.JAVA ? DNSUtils.resolveSRV(hostname) : null;
if (address != null) {
port = port != -1 ? port : platform.getDefaultPort(); // If the port is -1, set it to the default port
hostname = address.getHostName();
}
CachedMinecraftServer server = new CachedMinecraftServer(
key,
platform.getPinger().ping(hostname, port),
System.currentTimeMillis()
);
serverCacheRepository.save(server);
return server;
}
}

View File

@ -0,0 +1,11 @@
package cc.fascinated.service.pinger;
import cc.fascinated.model.server.MinecraftServer;
/**
* @author Braydon
* @param <T> the type of server to ping
*/
public interface MinecraftServerPinger<T extends MinecraftServer> {
T ping(String hostname, int port);
}

View File

@ -0,0 +1,64 @@
package cc.fascinated.service.pinger.impl;
import cc.fascinated.Main;
import cc.fascinated.common.DNSUtils;
import cc.fascinated.common.packet.impl.java.JavaPacketHandshakingInSetProtocol;
import cc.fascinated.common.packet.impl.java.JavaPacketStatusInStart;
import cc.fascinated.exception.impl.BadRequestException;
import cc.fascinated.exception.impl.ResourceNotFoundException;
import cc.fascinated.model.mojang.JavaServerStatusToken;
import cc.fascinated.model.server.JavaMinecraftServer;
import cc.fascinated.service.pinger.MinecraftServerPinger;
import lombok.extern.log4j.Log4j2;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.*;
/**
* @author Braydon
*/
@Log4j2(topic = "Java Pinger")
public final class JavaMinecraftServerPinger implements MinecraftServerPinger<JavaMinecraftServer> {
public static final JavaMinecraftServerPinger INSTANCE = new JavaMinecraftServerPinger();
private static final int TIMEOUT = 3000; // The timeout for the socket
@Override
public JavaMinecraftServer ping(String hostname, int port) {
InetAddress inetAddress = DNSUtils.resolveA(hostname); // Resolve the hostname to an IP address
String ip = inetAddress == null ? null : inetAddress.getHostAddress(); // Get the IP address
if (ip != null) { // Was the IP resolved?
log.info("Resolved hostname: {} -> {}", hostname, ip);
}
log.info("Pinging {}:{}...", hostname, port);
// Open a socket connection to the server
try (Socket socket = new Socket()) {
socket.setTcpNoDelay(true);
socket.connect(new InetSocketAddress(hostname, port), TIMEOUT);
// Open data streams to begin packet transaction
try (DataInputStream inputStream = new DataInputStream(socket.getInputStream());
DataOutputStream outputStream = new DataOutputStream(socket.getOutputStream())) {
// Begin handshaking with the server
new JavaPacketHandshakingInSetProtocol(hostname, port, 47).process(inputStream, outputStream);
// Send the status request to the server, and await back the response
JavaPacketStatusInStart packetStatusInStart = new JavaPacketStatusInStart();
packetStatusInStart.process(inputStream, outputStream);
JavaServerStatusToken token = Main.GSON.fromJson(packetStatusInStart.getResponse(), JavaServerStatusToken.class);
return new JavaMinecraftServer(hostname, ip, port, token.getDescription());
}
} catch (IOException ex) {
if (ex instanceof UnknownHostException) {
throw new BadRequestException("Unknown hostname: %s".formatted(hostname));
} else if (ex instanceof ConnectException || ex instanceof SocketTimeoutException) {
throw new ResourceNotFoundException(ex);
}
log.error("An error occurred pinging %s:%s:".formatted(hostname, port), ex);
}
return null;
}
}