From 7dad243735f7ab98b3cb03b4e633922d68ceed90 Mon Sep 17 00:00:00 2001 From: egg82 Date: Fri, 16 Aug 2019 18:04:19 -0600 Subject: [PATCH 1/2] Added PlayerProfile support --- src/main/java/io/papermc/lib/PaperLib.java | 31 +++ .../papermc/lib/environments/Environment.java | 20 ++ .../playerprofile/BukkitPlayerInfo.java | 185 ++++++++++++++++++ .../playerprofile/PaperPlayerInfo.java | 75 +++++++ .../features/playerprofile/PlayerInfo.java | 20 ++ 5 files changed, 331 insertions(+) create mode 100644 src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java create mode 100644 src/main/java/io/papermc/lib/features/playerprofile/PaperPlayerInfo.java create mode 100644 src/main/java/io/papermc/lib/features/playerprofile/PlayerInfo.java diff --git a/src/main/java/io/papermc/lib/PaperLib.java b/src/main/java/io/papermc/lib/PaperLib.java index 53e4265..ecc0357 100644 --- a/src/main/java/io/papermc/lib/PaperLib.java +++ b/src/main/java/io/papermc/lib/PaperLib.java @@ -5,6 +5,9 @@ import io.papermc.lib.environments.PaperEnvironment; import io.papermc.lib.environments.SpigotEnvironment; import io.papermc.lib.features.blockstatesnapshot.BlockStateSnapshotResult; +import io.papermc.lib.features.playerprofile.PlayerInfo; +import java.io.IOException; +import java.util.UUID; import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.World; @@ -163,6 +166,34 @@ public static BlockStateSnapshotResult getBlockState(@Nonnull Block block, boole return ENVIRONMENT.getBlockState(block, useSnapshot); } + /** + * Gets information about a potentially offline player, such as their current name or UUID. + * Note: Calling this method may contact Mojang's API and will block the current thread with the web request if it does. + * Additionally, it's possible that the name or UUID returned is null and/or an IOException is thrown. + * + * @param playerName The real name of the player to get + * @return A PlayerInfo object containing information about a player + * @throws IOException Thrown when an IOException is encountered while querying the Mojang API. + */ + @Nonnull + public static PlayerInfo getPlayerInfo(@Nonnull String playerName) throws IOException { + return ENVIRONMENT.getPlayerInfo(playerName); + } + + /** + * Gets information about a potentially offline player, such as their current name or UUID. + * Note: Calling this method may contact Mojang's API and will block the current thread with the web request if it does. + * Additionally, it's possible that the name or UUID returned is null and/or an IOException is thrown. + * + * @param playerUUID The UUID of the player to get + * @return A PlayerInfo object containing information about a player + * @throws IOException Thrown when an IOException is encountered while querying the Mojang API. + */ + @Nonnull + public static PlayerInfo getPlayerInfo(@Nonnull UUID playerUUID) throws IOException { + return ENVIRONMENT.getPlayerInfo(playerUUID); + } + /** * Detects if the current MC version is at least the following version. * diff --git a/src/main/java/io/papermc/lib/environments/Environment.java b/src/main/java/io/papermc/lib/environments/Environment.java index b498571..c10d361 100644 --- a/src/main/java/io/papermc/lib/environments/Environment.java +++ b/src/main/java/io/papermc/lib/environments/Environment.java @@ -11,6 +11,11 @@ import io.papermc.lib.features.chunkisgenerated.ChunkIsGenerated; import io.papermc.lib.features.chunkisgenerated.ChunkIsGeneratedApiExists; import io.papermc.lib.features.chunkisgenerated.ChunkIsGeneratedUnknown; +import io.papermc.lib.features.playerprofile.BukkitPlayerInfo; +import io.papermc.lib.features.playerprofile.PaperPlayerInfo; +import io.papermc.lib.features.playerprofile.PlayerInfo; +import java.io.IOException; +import java.util.UUID; import org.bukkit.Bukkit; import org.bukkit.Chunk; import org.bukkit.Location; @@ -34,6 +39,7 @@ public abstract class Environment { protected AsyncTeleport asyncTeleportHandler = new AsyncTeleportSync(); protected ChunkIsGenerated isGeneratedHandler = new ChunkIsGeneratedUnknown(); protected BlockStateSnapshot blockStateSnapshotHandler; + protected boolean hasPlayerProfile = true; public Environment() { Pattern versionPattern = Pattern.compile("\\(MC: (\\d)\\.(\\d+)\\.?(\\d+?)?\\)"); @@ -67,6 +73,12 @@ public Environment() { } else { blockStateSnapshotHandler = new BlockStateSnapshotNoOption(); } + + try { + Class.forName("com.destroystokyo.paper.profile.PlayerProfile"); + } catch (ClassNotFoundException ignored) { + hasPlayerProfile = false; + } } public abstract String getName(); @@ -87,6 +99,14 @@ public BlockStateSnapshotResult getBlockState(Block block, boolean useSnapshot) return blockStateSnapshotHandler.getBlockState(block, useSnapshot); } + public PlayerInfo getPlayerInfo(String playerName) throws IOException { + return (hasPlayerProfile) ? new PaperPlayerInfo(playerName) : new BukkitPlayerInfo(playerName); + } + + public PlayerInfo getPlayerInfo(UUID playerUUID) throws IOException { + return (hasPlayerProfile) ? new PaperPlayerInfo(playerUUID) : new BukkitPlayerInfo(playerUUID); + } + public boolean isVersion(int minor) { return isVersion(minor, 0); } diff --git a/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java b/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java new file mode 100644 index 0000000..2028aa7 --- /dev/null +++ b/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java @@ -0,0 +1,185 @@ +package io.papermc.lib.features.playerprofile; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +public class BukkitPlayerInfo implements PlayerInfo { + private UUID uuid; + private String name; + + private static ConcurrentMap uuidCache = new ConcurrentHashMap<>(); + private static ConcurrentMap nameCache = new ConcurrentHashMap<>(); + + private static final Object uuidCacheLock = new Object(); + private static final Object nameCacheLock = new Object(); + + private static final JSONParser uuidParser = new JSONParser(); // Thread-safe access via synchronized lock + private static final JSONParser nameParser = new JSONParser(); // Thread-safe access via synchronized lock + + public BukkitPlayerInfo(UUID uuid) throws IOException { + this.uuid = uuid; + + Optional name = Optional.ofNullable(uuidCache.getOrDefault(uuid, null)); + if (!name.isPresent()) { + synchronized (uuidCacheLock) { // Synchronize for thread-safe JSONParser access + defeating potential race conditions causing multiple lookups + name = Optional.ofNullable(uuidCache.getOrDefault(uuid, null)); + if (!name.isPresent()) { + name = Optional.ofNullable(nameExpensive(uuid)); + name.ifPresent(v -> uuidCache.put(uuid, v)); + } + } + } + + this.name = name.orElse(null); + } + + public BukkitPlayerInfo(String name) throws IOException { + this.name = name; + + Optional uuid = Optional.ofNullable(nameCache.getOrDefault(name, null)); + if (!uuid.isPresent()) { + synchronized (nameCacheLock) { // Synchronize for thread-safe JSONParser access + defeating potential race conditions causing multiple lookups + uuid = Optional.ofNullable(nameCache.getOrDefault(name, null)); + if (!uuid.isPresent()) { + uuid = Optional.ofNullable(uuidExpensive(name)); + uuid.ifPresent(v -> nameCache.put(name, v)); + } + } + } + + this.uuid = uuid.orElse(null); + } + + public UUID getUUID() { return uuid; } + + public String getName() { return name; } + + private static String nameExpensive(UUID uuid) throws IOException { + // Currently-online lookup + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + return player.getName(); + } + + // Network lookup + HttpURLConnection conn = getConnection(new URL("https://api.mojang.com/user/profiles/" + uuid.toString().replace("-", "") + "/names")); + + int code = conn.getResponseCode(); + try ( + InputStream in = (code == 200) ? conn.getInputStream() : conn.getErrorStream(); // Ensure we always get some text + InputStreamReader reader = new InputStreamReader(in); + BufferedReader buffer = new BufferedReader(reader) + ) { + StringBuilder builder = new StringBuilder(); + String line; + while ((line = buffer.readLine()) != null) { + builder.append(line); + } + + if (code == 200) { + JSONArray json = (JSONArray) nameParser.parse(builder.toString()); + JSONObject last = (JSONObject) json.get(json.size() - 1); + String name = (String) last.get("name"); + + nameCache.put(name, uuid); + } else if (code == 204) { + // No data exists + return null; + } + } catch (ParseException ex) { + throw new IOException(ex.getMessage(), ex); + } + + throw new IOException("Could not load player data from Mojang (rate-limited?)"); + } + + private static UUID uuidExpensive(String name) throws IOException { + // Currently-online lookup + Player player = Bukkit.getPlayer(name); + if (player != null) { + return player.getUniqueId(); + } + + // Network lookup + HttpURLConnection conn = getConnection(new URL("https://api.mojang.com/users/profiles/minecraft/" + name)); + + int code = conn.getResponseCode(); + try ( + InputStream in = (code == 200) ? conn.getInputStream() : conn.getErrorStream(); // Ensure we always get some text + InputStreamReader reader = new InputStreamReader(in); + BufferedReader buffer = new BufferedReader(reader) + ) { + StringBuilder builder = new StringBuilder(); + String line; + while ((line = buffer.readLine()) != null) { + builder.append(line); + } + + if (code == 200) { + JSONObject json = (JSONObject) uuidParser.parse(builder.toString()); + UUID uuid = UUID.fromString(((String) json.get("id")).replaceFirst("(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)", "$1-$2-$3-$4-$5")); // Normalize UUID for fromString (expects dashes, Mojang returns non-dashed result) + name = (String) json.get("name"); + + uuidCache.put(uuid, name); + } else if (code == 204) { + // No data exists + return null; + } + } catch (ParseException ex) { + throw new IOException(ex.getMessage(), ex); + } + + throw new IOException("Could not load player data from Mojang (rate-limited?)"); + } + + private static HttpURLConnection getConnection(URL url) throws IOException { + HttpURLConnection conn = getBaseConnection(url); + conn.setInstanceFollowRedirects(true); + + int status; + boolean redirect; + + do { + status = conn.getResponseCode(); + redirect = status == HttpURLConnection.HTTP_MOVED_TEMP || status == HttpURLConnection.HTTP_MOVED_PERM || status == HttpURLConnection.HTTP_SEE_OTHER; // Follow redirects + + if (redirect) { + // Set cookies on redirect and follow redirect URL + String newUrl = conn.getHeaderField("Location"); + String cookies = conn.getHeaderField("Set-Cookie"); + + conn = getBaseConnection(new URL(newUrl)); + conn.setRequestProperty("Cookie", cookies); + conn.addRequestProperty("Accept-Language", "en-US,en;q=0.8"); + } + } while (redirect); + + return conn; + } + + private static HttpURLConnection getBaseConnection(URL url) throws IOException { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + + // Set standard headers + conn.setRequestProperty("Accept", "application/json"); + conn.setRequestProperty("Connection", "close"); + conn.setRequestProperty("User-Agent", "PaperMC/PaperLib"); + conn.setRequestMethod("GET"); + + return conn; + } +} diff --git a/src/main/java/io/papermc/lib/features/playerprofile/PaperPlayerInfo.java b/src/main/java/io/papermc/lib/features/playerprofile/PaperPlayerInfo.java new file mode 100644 index 0000000..f684c98 --- /dev/null +++ b/src/main/java/io/papermc/lib/features/playerprofile/PaperPlayerInfo.java @@ -0,0 +1,75 @@ +package io.papermc.lib.features.playerprofile; + +import com.destroystokyo.paper.profile.PlayerProfile; +import java.io.IOException; +import java.util.Optional; +import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +public class PaperPlayerInfo implements PlayerInfo { + private UUID uuid; + private String name; + + public PaperPlayerInfo(UUID uuid) throws IOException { + this.uuid = uuid; + + Optional name = Optional.ofNullable(nameExpensive(uuid)); + this.name = name.orElse(null); + } + + public PaperPlayerInfo(String name) throws IOException { + this.name = name; + + Optional uuid = Optional.ofNullable(uuidExpensive(name)); + this.uuid = uuid.orElse(null); + } + + public UUID getUUID() { return uuid; } + + public String getName() { return name; } + + private static String nameExpensive(UUID uuid) throws IOException { + // Currently-online lookup + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + return player.getName(); + } + + // Cached profile lookup + PlayerProfile profile = Bukkit.createProfile(uuid); + if ((profile.isComplete() || profile.completeFromCache()) && profile.getName() != null && profile.getId() != null) { + return profile.getName(); + } + + // Network lookup + if (profile.complete(false) && profile.getName() != null && profile.getId() != null) { + return profile.getName(); + } + + // Sorry, nada + throw new IOException("Could not load player data from Mojang (rate-limited?)"); + } + + private static UUID uuidExpensive(String name) throws IOException { + // Currently-online lookup + Player player = Bukkit.getPlayer(name); + if (player != null) { + return player.getUniqueId(); + } + + // Cached profile lookup + PlayerProfile profile = Bukkit.createProfile(name); + if ((profile.isComplete() || profile.completeFromCache()) && profile.getName() != null && profile.getId() != null) { + return profile.getId(); + } + + // Network lookup + if (profile.complete(false) && profile.getName() != null && profile.getId() != null) { + return profile.getId(); + } + + // Sorry, nada + throw new IOException("Could not load player data from Mojang (rate-limited?)"); + } +} diff --git a/src/main/java/io/papermc/lib/features/playerprofile/PlayerInfo.java b/src/main/java/io/papermc/lib/features/playerprofile/PlayerInfo.java new file mode 100644 index 0000000..4a02a4a --- /dev/null +++ b/src/main/java/io/papermc/lib/features/playerprofile/PlayerInfo.java @@ -0,0 +1,20 @@ +package io.papermc.lib.features.playerprofile; + +import java.util.UUID; +import javax.annotation.Nullable; + +public interface PlayerInfo { + /** + * The name of the player. + * + * @return The name of the payer. + */ + @Nullable String getName(); + + /** + * The UUID of the player. + * + * @return The UUID of the player. + */ + @Nullable UUID getUUID(); +} From fbe40a01be93c2f6977ddf7607578ada2be72760 Mon Sep 17 00:00:00 2001 From: egg82 Date: Fri, 16 Aug 2019 18:37:55 -0600 Subject: [PATCH 2/2] Fixed name/UUID caching --- .../io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java b/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java index 2028aa7..204fcb6 100644 --- a/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java +++ b/src/main/java/io/papermc/lib/features/playerprofile/BukkitPlayerInfo.java @@ -72,6 +72,7 @@ private static String nameExpensive(UUID uuid) throws IOException { // Currently-online lookup Player player = Bukkit.getPlayer(uuid); if (player != null) { + nameCache.put(player.getName(), uuid); return player.getName(); } @@ -111,6 +112,7 @@ private static UUID uuidExpensive(String name) throws IOException { // Currently-online lookup Player player = Bukkit.getPlayer(name); if (player != null) { + uuidCache.put(player.getUniqueId(), name); return player.getUniqueId(); }