From 1e1ba0f873e3e3319f1497f25244750b6e19f16e Mon Sep 17 00:00:00 2001 From: Thinkofdeath Date: Thu, 10 Apr 2014 12:56:45 +0100 Subject: [PATCH] Fix the console issue QueueLogAppender doesn't like being added twice --- .../0128-1.7.6-pre1-support.patch | 2778 ++++++++++++++++- 1 file changed, 2759 insertions(+), 19 deletions(-) diff --git a/CraftBukkit-Patches/0128-1.7.6-pre1-support.patch b/CraftBukkit-Patches/0128-1.7.6-pre1-support.patch index be6a2e03c5..a9a0092745 100644 --- a/CraftBukkit-Patches/0128-1.7.6-pre1-support.patch +++ b/CraftBukkit-Patches/0128-1.7.6-pre1-support.patch @@ -1,11 +1,11 @@ -From af89e32891c1b2b4be699afe3bdae190e1c4babb Mon Sep 17 00:00:00 2001 +From 53269d6cfc3f178e66c9bb1ecbaca96341e700d2 Mon Sep 17 00:00:00 2001 From: Thinkofdeath Date: Thu, 3 Apr 2014 17:04:18 +0100 Subject: [PATCH] 1.7.6-pre1 support diff --git a/pom.xml b/pom.xml -index c8285e0..21876f0 100644 +index c8285e0..24d101c 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,10 @@ @@ -34,9 +34,9 @@ index c8285e0..21876f0 100644 + 2.4 + + -+ com.mojang -+ authlib -+ 1.5.5 ++ commons-codec ++ commons-codec ++ 1.6 + @@ -84,30 +84,37 @@ index 42539b4..490123f 100644 this.b.handle(new PacketLoginOutDisconnect(chatcomponenttext), new GenericFutureListener[0]); this.b.close(chatcomponenttext); diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 8ce9dd7..b95123c 100644 +index 8ce9dd7..96b904c 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -17,6 +17,7 @@ import java.util.UUID; +@@ -10,13 +10,13 @@ import java.util.ArrayList; + import java.util.Arrays; + import java.util.Collections; + import java.util.Date; +-import java.util.Iterator; + import java.util.List; + import java.util.Random; + import java.util.UUID; import java.util.concurrent.Callable; import javax.imageio.ImageIO; -+import com.mojang.authlib.yggdrasil.YggdrasilMinecraftSessionService; ++import org.spigotmc.authlib.yggdrasil.YggdrasilMinecraftSessionService; import net.minecraft.util.com.google.common.base.Charsets; import net.minecraft.util.com.mojang.authlib.GameProfile; import net.minecraft.util.com.mojang.authlib.minecraft.MinecraftSessionService; -@@ -107,6 +108,7 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo +@@ -107,6 +107,7 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo private static final int TICK_TIME = 1000000000 / TPS; private static final int SAMPLE_INTERVAL = 100; public final double[] recentTps = new double[ 3 ]; -+ public final com.mojang.authlib.minecraft.MinecraftSessionService newSessionService; ++ public final org.spigotmc.authlib.minecraft.MinecraftSessionService newSessionService; // Spigot end public MinecraftServer(OptionSet options, Proxy proxy) { // CraftBukkit - signature file -> OptionSet -@@ -117,6 +119,7 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo +@@ -117,6 +118,7 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo this.n = new CommandDispatcher(); // this.convertable = new WorldLoaderServer(file1); // CraftBukkit - moved to DedicatedServer.init this.S = (new YggdrasilAuthenticationService(proxy, UUID.randomUUID().toString())).createMinecraftSessionService(); -+ newSessionService = new com.mojang.authlib.yggdrasil.YggdrasilAuthenticationService(proxy, UUID.randomUUID().toString()).createMinecraftSessionService(); ++ newSessionService = new org.spigotmc.authlib.yggdrasil.YggdrasilAuthenticationService(proxy, UUID.randomUUID().toString()).createMinecraftSessionService(); // CraftBukkit start this.options = options; @@ -257,14 +264,14 @@ index 0000000..3aa93cd + } +} diff --git a/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java b/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java -index 8bab528..01630be 100644 +index 8bab528..0884047 100644 --- a/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java +++ b/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java @@ -2,6 +2,7 @@ package net.minecraft.server; import java.util.List; -+import com.mojang.authlib.properties.Property; ++import org.spigotmc.authlib.properties.Property; import net.minecraft.util.com.mojang.authlib.GameProfile; import java.io.IOException; // CraftBukkit @@ -282,7 +289,7 @@ index 8bab528..01630be 100644 + + if ( this.b instanceof ThreadPlayerLookupUUID.NewGameProfileWrapper ) + { -+ com.mojang.authlib.GameProfile newProfile = ((ThreadPlayerLookupUUID.NewGameProfileWrapper) b).newProfile; ++ org.spigotmc.authlib.GameProfile newProfile = ((ThreadPlayerLookupUUID.NewGameProfileWrapper) b).newProfile; + packetdataserializer.b( newProfile.getProperties().size() ); + for ( String key : newProfile.getProperties().keys() ) + { @@ -439,7 +446,7 @@ index f9da452..fa493ca 100644 + // Spigot end } diff --git a/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java b/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java -index fe4502a..0649ccf 100644 +index fe4502a..63101fb 100644 --- a/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java +++ b/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java @@ -33,7 +33,9 @@ class ThreadPlayerLookupUUID extends Thread { @@ -448,7 +455,7 @@ index fe4502a..0649ccf 100644 String s = (new BigInteger(MinecraftEncryption.a(LoginListener.a(this.a), LoginListener.b(this.a).J().getPublic(), LoginListener.c(this.a)))).toString(16); - LoginListener.a(this.a, LoginListener.b(this.a).at().hasJoinedServer(new GameProfile((String) null, LoginListener.d(this.a).getName()), s)); + //LoginListener.a(this.a, LoginListener.b(this.a).at().hasJoinedServer(new GameProfile((String) null, LoginListener.d(this.a).getName()), s)); -+ com.mojang.authlib.GameProfile profile = LoginListener.b(this.a).newSessionService.hasJoinedServer( new com.mojang.authlib.GameProfile( null, LoginListener.d(this.a).getName() ), s ); ++ org.spigotmc.authlib.GameProfile profile = LoginListener.b(this.a).newSessionService.hasJoinedServer( new org.spigotmc.authlib.GameProfile( null, LoginListener.d(this.a).getName() ), s ); + LoginListener.a(this.a, new NewGameProfileWrapper( profile ) ); if (LoginListener.d(this.a) != null) { // Spigot Start @@ -460,15 +467,2748 @@ index fe4502a..0649ccf 100644 + + public static class NewGameProfileWrapper extends GameProfile { + -+ public com.mojang.authlib.GameProfile newProfile; ++ public org.spigotmc.authlib.GameProfile newProfile; + -+ public NewGameProfileWrapper(com.mojang.authlib.GameProfile newProfile) ++ public NewGameProfileWrapper(org.spigotmc.authlib.GameProfile newProfile) + { + super( newProfile.getId().toString().replaceAll( "-", "" ), newProfile.getName() ); + this.newProfile = newProfile; + } + } } +diff --git a/src/main/java/org/spigotmc/authlib/Agent.java b/src/main/java/org/spigotmc/authlib/Agent.java +new file mode 100644 +index 0000000..873743d +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/Agent.java +@@ -0,0 +1,30 @@ ++package org.spigotmc.authlib; ++ ++public class Agent { ++ public static final Agent MINECRAFT = new Agent("Minecraft", 1); ++ public static final Agent SCROLLS = new Agent("Scrolls", 1); ++ ++ private final String name; ++ private final int version; ++ ++ public Agent(String name, int version) { ++ this.name = name; ++ this.version = version; ++ } ++ ++ public String getName() { ++ return name; ++ } ++ ++ public int getVersion() { ++ return version; ++ } ++ ++ @Override ++ public String toString() { ++ return "Agent{" + ++ "name='" + name + '\'' + ++ ", version=" + version + ++ '}'; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/AuthenticationService.java b/src/main/java/org/spigotmc/authlib/AuthenticationService.java +new file mode 100644 +index 0000000..4110e53 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/AuthenticationService.java +@@ -0,0 +1,33 @@ ++package org.spigotmc.authlib; ++ ++import org.spigotmc.authlib.minecraft.MinecraftSessionService; ++ ++public interface AuthenticationService { ++ /** ++ * Creates a relevant {@link org.spigotmc.authlib.UserAuthentication} designed for this authentication service. ++ *

++ * Certain Authentication Services may have restrictions as to which {@link Agent}s are supported. ++ * Please consult their javadoc for more information. ++ * ++ * @param agent Game agent to authenticate for ++ * @throws java.lang.IllegalArgumentException Agent is null or not allowed for this AuthenticationService ++ * @return New user authenticator ++ */ ++ public UserAuthentication createUserAuthentication(Agent agent); ++ ++ /** ++ * Creates a relevant {@link org.spigotmc.authlib.minecraft.MinecraftSessionService} designed for this authentication service. ++ *

++ * This is a Minecraft specific service and is not relevant to any other game agent. ++ * ++ * @return New minecraft session service ++ */ ++ public MinecraftSessionService createMinecraftSessionService(); ++ ++ /** ++ * Creates a relevant {@link org.spigotmc.authlib.GameProfileRepository} designed for this authentication service. ++ * ++ * @return New profile repository ++ */ ++ public GameProfileRepository createProfileRepository(); ++} +diff --git a/src/main/java/org/spigotmc/authlib/BaseAuthenticationService.java b/src/main/java/org/spigotmc/authlib/BaseAuthenticationService.java +new file mode 100644 +index 0000000..b3cb3bb +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/BaseAuthenticationService.java +@@ -0,0 +1,4 @@ ++package org.spigotmc.authlib; ++ ++public abstract class BaseAuthenticationService implements AuthenticationService { ++} +diff --git a/src/main/java/org/spigotmc/authlib/BaseUserAuthentication.java b/src/main/java/org/spigotmc/authlib/BaseUserAuthentication.java +new file mode 100644 +index 0000000..3bdcea2 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/BaseUserAuthentication.java +@@ -0,0 +1,268 @@ ++package org.spigotmc.authlib; ++ ++import org.spigotmc.authlib.properties.Property; ++import org.spigotmc.authlib.properties.PropertyMap; ++import org.spigotmc.authlib.util.UUIDTypeAdapter; ++import org.apache.commons.lang3.StringUtils; ++import org.apache.commons.lang3.Validate; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++ ++import java.util.ArrayList; ++import java.util.HashMap; ++import java.util.List; ++import java.util.Map; ++ ++public abstract class BaseUserAuthentication implements UserAuthentication { ++ private static final Logger LOGGER = LogManager.getLogger(); ++ ++ protected static final String STORAGE_KEY_PROFILE_NAME = "displayName"; ++ protected static final String STORAGE_KEY_PROFILE_ID = "uuid"; ++ protected static final String STORAGE_KEY_PROFILE_PROPERTIES = "profileProperties"; ++ protected static final String STORAGE_KEY_USER_NAME = "username"; ++ protected static final String STORAGE_KEY_USER_ID = "userid"; ++ protected static final String STORAGE_KEY_USER_PROPERTIES = "userProperties"; ++ ++ private final AuthenticationService authenticationService; ++ private final PropertyMap userProperties = new PropertyMap(); ++ private String userid; ++ private String username; ++ private String password; ++ private GameProfile selectedProfile; ++ private UserType userType; ++ ++ protected BaseUserAuthentication(AuthenticationService authenticationService) { ++ Validate.notNull(authenticationService); ++ this.authenticationService = authenticationService; ++ } ++ ++ @Override ++ public boolean canLogIn() { ++ return !canPlayOnline() && StringUtils.isNotBlank(getUsername()) && StringUtils.isNotBlank(getPassword()); ++ } ++ ++ @Override ++ public void logOut() { ++ password = null; ++ userid = null; ++ setSelectedProfile(null); ++ getModifiableUserProperties().clear(); ++ setUserType(null); ++ } ++ ++ @Override ++ public boolean isLoggedIn() { ++ return getSelectedProfile() != null; ++ } ++ ++ @Override ++ public void setUsername(String username) { ++ if (isLoggedIn() && canPlayOnline()) { ++ throw new IllegalStateException("Cannot change username whilst logged in & online"); ++ } ++ ++ this.username = username; ++ } ++ ++ @Override ++ public void setPassword(String password) { ++ if (isLoggedIn() && canPlayOnline() && StringUtils.isNotBlank(password)) { ++ throw new IllegalStateException("Cannot set password whilst logged in & online"); ++ } ++ ++ this.password = password; ++ } ++ ++ protected String getUsername() { ++ return username; ++ } ++ ++ protected String getPassword() { ++ return password; ++ } ++ ++ @SuppressWarnings("unchecked") ++ @Override ++ public void loadFromStorage(Map credentials) { ++ logOut(); ++ ++ setUsername(String.valueOf(credentials.get(STORAGE_KEY_USER_NAME))); ++ ++ if (credentials.containsKey(STORAGE_KEY_USER_ID)) { ++ userid = String.valueOf(credentials.get(STORAGE_KEY_USER_ID)); ++ } else { ++ userid = username; ++ } ++ ++ if (credentials.containsKey(STORAGE_KEY_USER_PROPERTIES)) { ++ try { ++ List> list = (List>) credentials.get(STORAGE_KEY_USER_PROPERTIES); ++ ++ for (Map propertyMap : list) { ++ String name = propertyMap.get("name"); ++ String value = propertyMap.get("value"); ++ String signature = propertyMap.get("signature"); ++ ++ if (signature == null) { ++ getModifiableUserProperties().put(name, new Property(name, value)); ++ } else { ++ getModifiableUserProperties().put(name, new Property(name, value, signature)); ++ } ++ } ++ } catch (Throwable t) { ++ LOGGER.warn("Couldn't deserialize user properties", t); ++ } ++ } ++ ++ if (credentials.containsKey(STORAGE_KEY_PROFILE_NAME) && credentials.containsKey(STORAGE_KEY_PROFILE_ID)) { ++ GameProfile profile = new GameProfile(UUIDTypeAdapter.fromString(String.valueOf(credentials.get(STORAGE_KEY_PROFILE_ID))), String.valueOf(credentials.get(STORAGE_KEY_PROFILE_NAME))); ++ if (credentials.containsKey(STORAGE_KEY_PROFILE_PROPERTIES)) { ++ try { ++ List> list = (List>) credentials.get(STORAGE_KEY_PROFILE_PROPERTIES); ++ for (Map propertyMap : list) { ++ String name = propertyMap.get("name"); ++ String value = propertyMap.get("value"); ++ String signature = propertyMap.get("signature"); ++ ++ if (signature == null) { ++ profile.getProperties().put(name, new Property(name, value)); ++ } else { ++ profile.getProperties().put(name, new Property(name, value, signature)); ++ } ++ } ++ } catch (Throwable t) { ++ LOGGER.warn("Couldn't deserialize profile properties", t); ++ } ++ } ++ setSelectedProfile(profile); ++ } ++ } ++ ++ @Override ++ public Map saveForStorage() { ++ Map result = new HashMap(); ++ ++ if (getUsername() != null) { ++ result.put(STORAGE_KEY_USER_NAME, getUsername()); ++ } ++ if (getUserID() != null) { ++ result.put(STORAGE_KEY_USER_ID, getUserID()); ++ } else if (getUsername() != null) { ++ result.put(STORAGE_KEY_USER_NAME, getUsername()); ++ } ++ ++ if (!getUserProperties().isEmpty()) { ++ List> properties = new ArrayList>(); ++ for (Property userProperty : getUserProperties().values()) { ++ Map property = new HashMap(); ++ property.put("name", userProperty.getName()); ++ property.put("value", userProperty.getValue()); ++ property.put("signature", userProperty.getSignature()); ++ properties.add(property); ++ } ++ result.put(STORAGE_KEY_USER_PROPERTIES, properties); ++ } ++ ++ GameProfile selectedProfile = getSelectedProfile(); ++ if (selectedProfile != null) { ++ result.put(STORAGE_KEY_PROFILE_NAME, selectedProfile.getName()); ++ result.put(STORAGE_KEY_PROFILE_ID, selectedProfile.getId()); ++ ++ List> properties = new ArrayList>(); ++ for (Property profileProperty : selectedProfile.getProperties().values()) { ++ Map property = new HashMap(); ++ property.put("name", profileProperty.getName()); ++ property.put("value", profileProperty.getValue()); ++ property.put("signature", profileProperty.getSignature()); ++ properties.add(property); ++ } ++ ++ if (!properties.isEmpty()) { ++ result.put(STORAGE_KEY_PROFILE_PROPERTIES, properties); ++ } ++ } ++ ++ return result; ++ } ++ ++ protected void setSelectedProfile(GameProfile selectedProfile) { ++ this.selectedProfile = selectedProfile; ++ } ++ ++ @Override ++ public GameProfile getSelectedProfile() { ++ return selectedProfile; ++ } ++ ++ @Override ++ public String toString() { ++ StringBuilder result = new StringBuilder(); ++ ++ result.append(getClass().getSimpleName()); ++ result.append("{"); ++ ++ if (isLoggedIn()) { ++ result.append("Logged in as "); ++ result.append(getUsername()); ++ ++ if (getSelectedProfile() != null) { ++ result.append(" / "); ++ result.append(getSelectedProfile()); ++ result.append(" - "); ++ ++ if (canPlayOnline()) { ++ result.append("Online"); ++ } else { ++ result.append("Offline"); ++ } ++ } ++ } else { ++ result.append("Not logged in"); ++ } ++ ++ result.append("}"); ++ ++ return result.toString(); ++ } ++ ++ public AuthenticationService getAuthenticationService() { ++ return authenticationService; ++ } ++ ++ @Override ++ public String getUserID() { ++ return userid; ++ } ++ ++ @Override ++ public PropertyMap getUserProperties() { ++ if (isLoggedIn()) { ++ PropertyMap result = new PropertyMap(); ++ result.putAll(getModifiableUserProperties()); ++ return result; ++ } else { ++ return new PropertyMap(); ++ } ++ } ++ ++ protected PropertyMap getModifiableUserProperties() { ++ return userProperties; ++ } ++ ++ @Override ++ public UserType getUserType() { ++ if (isLoggedIn()) { ++ return userType == null ? UserType.LEGACY : userType; ++ } else { ++ return null; ++ } ++ } ++ ++ protected void setUserType(UserType userType) { ++ this.userType = userType; ++ } ++ ++ protected void setUserid(String userid) { ++ this.userid = userid; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/GameProfile.java b/src/main/java/org/spigotmc/authlib/GameProfile.java +new file mode 100644 +index 0000000..7e2d997 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/GameProfile.java +@@ -0,0 +1,106 @@ ++package org.spigotmc.authlib; ++ ++import org.spigotmc.authlib.properties.PropertyMap; ++import org.apache.commons.lang3.StringUtils; ++import org.apache.commons.lang3.builder.ToStringBuilder; ++ ++import java.util.UUID; ++ ++public class GameProfile { ++ private final UUID id; ++ private final String name; ++ private final PropertyMap properties = new PropertyMap(); ++ private boolean legacy; ++ ++ /** ++ * Constructs a new Game Profile with the specified ID and name. ++ *

++ * Either ID or name may be null/empty, but at least one must be filled. ++ * ++ * @param id Unique ID of the profile ++ * @param name Display name of the profile ++ * @throws java.lang.IllegalArgumentException Both ID and name are either null or empty ++ */ ++ public GameProfile(UUID id, String name) { ++ if (id == null && StringUtils.isBlank(name)) throw new IllegalArgumentException("Name and ID cannot both be blank"); ++ ++ this.id = id; ++ this.name = name; ++ } ++ ++ /** ++ * Gets the unique ID of this game profile. ++ *

++ * This may be null for partial profile data if constructed manually. ++ * ++ * @return ID of the profile ++ */ ++ public UUID getId() { ++ return id; ++ } ++ ++ /** ++ * Gets the display name of this game profile. ++ *

++ * This may be null for partial profile data if constructed manually. ++ * ++ * @return Name of the profile ++ */ ++ public String getName() { ++ return name; ++ } ++ ++ /** ++ * Returns any known properties about this game profile. ++ * ++ * @return Modifiable map of profile properties. ++ */ ++ public PropertyMap getProperties() { ++ return properties; ++ } ++ ++ /** ++ * Checks if this profile is complete. ++ *

++ * A complete profile has no empty fields. Partial profiles may be constructed manually and used as input to methods. ++ * ++ * @return True if this profile is complete (as opposed to partial) ++ */ ++ public boolean isComplete() { ++ return id != null && StringUtils.isNotBlank(getName()); ++ } ++ ++ @Override ++ public boolean equals(Object o) { ++ if (this == o) return true; ++ if (o == null || getClass() != o.getClass()) return false; ++ ++ GameProfile that = (GameProfile) o; ++ ++ if (id != null ? !id.equals(that.id) : that.id != null) return false; ++ if (name != null ? !name.equals(that.name) : that.name != null) return false; ++ ++ return true; ++ } ++ ++ @Override ++ public int hashCode() { ++ int result = id != null ? id.hashCode() : 0; ++ result = 31 * result + (name != null ? name.hashCode() : 0); ++ return result; ++ } ++ ++ @Override ++ public String toString() { ++ return new ToStringBuilder(this) ++ .append("id", id) ++ .append("name", name) ++ .append("properties", properties) ++ .append("legacy", legacy) ++ .toString(); ++ } ++ ++ public boolean isLegacy() { ++ return legacy; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/GameProfileRepository.java b/src/main/java/org/spigotmc/authlib/GameProfileRepository.java +new file mode 100644 +index 0000000..83864b5 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/GameProfileRepository.java +@@ -0,0 +1,5 @@ ++package org.spigotmc.authlib; ++ ++public interface GameProfileRepository { ++ public void findProfilesByNames(String[] names, Agent agent, ProfileLookupCallback callback); ++} +diff --git a/src/main/java/org/spigotmc/authlib/HttpAuthenticationService.java b/src/main/java/org/spigotmc/authlib/HttpAuthenticationService.java +new file mode 100644 +index 0000000..fb639d0 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/HttpAuthenticationService.java +@@ -0,0 +1,218 @@ ++package org.spigotmc.authlib; ++ ++import org.apache.commons.io.Charsets; ++import org.apache.commons.io.IOUtils; ++import org.apache.commons.lang3.Validate; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++ ++import java.io.IOException; ++import java.io.InputStream; ++import java.io.OutputStream; ++import java.io.UnsupportedEncodingException; ++import java.net.*; ++import java.util.Map; ++ ++public abstract class HttpAuthenticationService extends BaseAuthenticationService { ++ private static final Logger LOGGER = LogManager.getLogger(); ++ ++ private final Proxy proxy; ++ ++ protected HttpAuthenticationService(Proxy proxy) { ++ Validate.notNull(proxy); ++ this.proxy = proxy; ++ } ++ ++ /** ++ * Gets the proxy to be used with every HTTP(S) request. ++ * ++ * @return Proxy to be used. ++ */ ++ public Proxy getProxy() { ++ return proxy; ++ } ++ ++ protected HttpURLConnection createUrlConnection(URL url) throws IOException { ++ Validate.notNull(url); ++ LOGGER.debug("Opening connection to " + url); ++ HttpURLConnection connection = (HttpURLConnection) url.openConnection(proxy); ++ connection.setConnectTimeout(15000); ++ connection.setReadTimeout(15000); ++ connection.setUseCaches(false); ++ return connection; ++ } ++ ++ /** ++ * Performs a POST request to the specified URL and returns the result. ++ *

++ * The POST data will be encoded in UTF-8 as the specified contentType. The response will be parsed as UTF-8. ++ * If the server returns an error but still provides a body, the body will be returned as normal. ++ * If the server returns an error without any body, a relevant {@link java.io.IOException} will be thrown. ++ * ++ * @param url URL to submit the POST request to ++ * @param post POST data in the correct format to be submitted ++ * @param contentType Content type of the POST data ++ * @return Raw text response from the server ++ * @throws IOException The request was not successful ++ */ ++ public String performPostRequest(URL url, String post, String contentType) throws IOException { ++ Validate.notNull(url); ++ Validate.notNull(post); ++ Validate.notNull(contentType); ++ HttpURLConnection connection = createUrlConnection(url); ++ byte[] postAsBytes = post.getBytes(Charsets.UTF_8); ++ ++ connection.setRequestProperty("Content-Type", contentType + "; charset=utf-8"); ++ connection.setRequestProperty("Content-Length", "" + postAsBytes.length); ++ connection.setDoOutput(true); ++ ++ LOGGER.debug("Writing POST data to " + url + ": " + post); ++ ++ OutputStream outputStream = null; ++ try { ++ outputStream = connection.getOutputStream(); ++ IOUtils.write(postAsBytes, outputStream); ++ } finally { ++ IOUtils.closeQuietly(outputStream); ++ } ++ ++ LOGGER.debug("Reading data from " + url); ++ ++ InputStream inputStream = null; ++ try { ++ inputStream = connection.getInputStream(); ++ String result = IOUtils.toString(inputStream, Charsets.UTF_8); ++ LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); ++ LOGGER.debug("Response: " + result); ++ return result; ++ } catch (IOException e) { ++ IOUtils.closeQuietly(inputStream); ++ inputStream = connection.getErrorStream(); ++ ++ if (inputStream != null) { ++ LOGGER.debug("Reading error page from " + url); ++ String result = IOUtils.toString(inputStream, Charsets.UTF_8); ++ LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); ++ LOGGER.debug("Response: " + result); ++ return result; ++ } else { ++ LOGGER.debug("Request failed", e); ++ throw e; ++ } ++ } finally { ++ IOUtils.closeQuietly(inputStream); ++ } ++ } ++ ++ /** ++ * Performs a GET request to the specified URL and returns the result. ++ *

++ * The response will be parsed as UTF-8. ++ * If the server returns an error but still provides a body, the body will be returned as normal. ++ * If the server returns an error without any body, a relevant {@link java.io.IOException} will be thrown. ++ * ++ * @param url URL to submit the GET request to ++ * @return Raw text response from the server ++ * @throws IOException The request was not successful ++ */ ++ public String performGetRequest(URL url) throws IOException { ++ Validate.notNull(url); ++ HttpURLConnection connection = createUrlConnection(url); ++ ++ LOGGER.debug("Reading data from " + url); ++ ++ InputStream inputStream = null; ++ try { ++ inputStream = connection.getInputStream(); ++ String result = IOUtils.toString(inputStream, Charsets.UTF_8); ++ LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); ++ LOGGER.debug("Response: " + result); ++ return result; ++ } catch (IOException e) { ++ IOUtils.closeQuietly(inputStream); ++ inputStream = connection.getErrorStream(); ++ ++ if (inputStream != null) { ++ LOGGER.debug("Reading error page from " + url); ++ String result = IOUtils.toString(inputStream, Charsets.UTF_8); ++ LOGGER.debug("Successful read, server response was " + connection.getResponseCode()); ++ LOGGER.debug("Response: " + result); ++ return result; ++ } else { ++ LOGGER.debug("Request failed", e); ++ throw e; ++ } ++ } finally { ++ IOUtils.closeQuietly(inputStream); ++ } ++ } ++ ++ /** ++ * Creates a {@link URL} with the specified string, throwing an {@link java.lang.Error} if the URL was malformed. ++ *

++ * This is just a wrapper to allow URLs to be created in constants, where you know the URL is valid. ++ * ++ * @param url URL to construct ++ * @return URL constructed ++ */ ++ public static URL constantURL(String url) { ++ try { ++ return new URL(url); ++ } catch (MalformedURLException ex) { ++ throw new Error("Couldn't create constant for " + url, ex); ++ } ++ } ++ ++ /** ++ * Turns the specified Map into an encoded & escaped query ++ * ++ * @param query Map to convert into a text based query ++ * @return Resulting query. ++ */ ++ public static String buildQuery(Map query) { ++ if (query == null) return ""; ++ StringBuilder builder = new StringBuilder(); ++ ++ for (Map.Entry entry : query.entrySet()) { ++ if (builder.length() > 0) { ++ builder.append('&'); ++ } ++ ++ try { ++ builder.append(URLEncoder.encode(entry.getKey(), "UTF-8")); ++ } catch (UnsupportedEncodingException e) { ++ LOGGER.error("Unexpected exception building query", e); ++ } ++ ++ if (entry.getValue() != null) { ++ builder.append('='); ++ try { ++ builder.append(URLEncoder.encode(entry.getValue().toString(), "UTF-8")); ++ } catch (UnsupportedEncodingException e) { ++ LOGGER.error("Unexpected exception building query", e); ++ } ++ } ++ } ++ ++ return builder.toString(); ++ } ++ ++ /** ++ * Concatenates the given {@link java.net.URL} and query. ++ * ++ * @param url URL to base off ++ * @param query Query to append to URL ++ * @return URL constructed ++ */ ++ public static URL concatenateURL(URL url, String query) { ++ try { ++ if (url.getQuery() != null && url.getQuery().length() > 0) { ++ return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile() + "&" + query); ++ } else { ++ return new URL(url.getProtocol(), url.getHost(), url.getPort(), url.getFile() + "?" + query); ++ } ++ } catch (MalformedURLException ex) { ++ throw new IllegalArgumentException("Could not concatenate given URL with GET arguments!", ex); ++ } ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/HttpUserAuthentication.java b/src/main/java/org/spigotmc/authlib/HttpUserAuthentication.java +new file mode 100644 +index 0000000..1020391 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/HttpUserAuthentication.java +@@ -0,0 +1,12 @@ ++package org.spigotmc.authlib; ++ ++public abstract class HttpUserAuthentication extends BaseUserAuthentication { ++ protected HttpUserAuthentication(HttpAuthenticationService authenticationService) { ++ super(authenticationService); ++ } ++ ++ @Override ++ public HttpAuthenticationService getAuthenticationService() { ++ return (HttpAuthenticationService) super.getAuthenticationService(); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/ProfileLookupCallback.java b/src/main/java/org/spigotmc/authlib/ProfileLookupCallback.java +new file mode 100644 +index 0000000..5ec92d1 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/ProfileLookupCallback.java +@@ -0,0 +1,7 @@ ++package org.spigotmc.authlib; ++ ++public interface ProfileLookupCallback { ++ public void onProfileLookupSucceeded(GameProfile profile); ++ ++ public void onProfileLookupFailed(GameProfile profile, Exception exception); ++} +diff --git a/src/main/java/org/spigotmc/authlib/UserAuthentication.java b/src/main/java/org/spigotmc/authlib/UserAuthentication.java +new file mode 100644 +index 0000000..0f65242 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/UserAuthentication.java +@@ -0,0 +1,170 @@ ++package org.spigotmc.authlib; ++ ++import com.google.common.collect.Multimap; ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++import org.spigotmc.authlib.properties.Property; ++import org.spigotmc.authlib.properties.PropertyMap; ++ ++import java.util.Map; ++ ++public interface UserAuthentication { ++ /** ++ * Checks if enough details are provided to attempt authentication. ++ *

++ * The exact details required may depend on the service, but generally Username & Password should suffice. ++ * Attempting to call {@link #logIn()} when this method returns false will guarantee a failure. You may use ++ * this method to check if you can attempt a log in without altering the current state of the authentication. ++ * ++ * @return True if authentication may be attempted in this state ++ */ ++ boolean canLogIn(); ++ ++ /** ++ * Attempts authentication with the currently set details. ++ *

++ * If {@link #canLogIn()} returned false, this method is guaranteed to fail. However, an appropriate exception ++ * will be raised informing you as to why it failed. The exact required credentials to authenticate varies on ++ * the service being used, but generally {@link #setUsername(String) username} and {@link #setPassword(String) password} are a safe ++ * bet to log a user in. ++ *

++ * If the user is {@link #isLoggedIn() already logged in} this method will not fail early and will continue ++ * to reauthenticate the user. If the user is attempting to log in with a legacy username ("Steve") ++ * and that username is valid but migrated to a Mojang account ("steve@minecraft.net"), a {@link org.spigotmc.authlib.exceptions.UserMigratedException} ++ * will be thrown. ++ * ++ * @throws org.spigotmc.authlib.exceptions.AuthenticationUnavailableException Thrown when the servers return a malformed response, or are otherwise unavailable ++ * @throws org.spigotmc.authlib.exceptions.InvalidCredentialsException Thrown when the specified credentials are invalid ++ * @throws org.spigotmc.authlib.exceptions.UserMigratedException Thrown when attempting to authenticate with a {@link #setUsername(String) username} that has been migrated to an email address ++ * @throws org.spigotmc.authlib.exceptions.AuthenticationException Generic exception indicating that we could not authenticate the user ++ */ ++ void logIn() throws AuthenticationException; ++ ++ /** ++ * Logs this user out, clearing any local credentials. ++ */ ++ void logOut(); ++ ++ /** ++ * Checks if the user is currently logged in. ++ * ++ * @return True if the user is logged in ++ */ ++ boolean isLoggedIn(); ++ ++ /** ++ * Checks if the user {@link #isLoggedIn() is logged in}, has a valid {@link #getSelectedProfile() game profile} and has validated ++ * their session online. ++ * ++ * @return True if the user is allowed to play online ++ */ ++ boolean canPlayOnline(); ++ ++ /** ++ * Gets a list of valid {@link GameProfile GameProfiles} for this user. ++ *

++ * Calling this method whilst the user is not {@link #isLoggedIn() logged in} will always return null. ++ * If the result of this method is an empty array or null and the user is logged in, the user is considered to not have purchased the game but ++ * may be allowed to play demo mode. ++ * ++ * @return An array of available game profiles, or null. ++ */ ++ GameProfile[] getAvailableProfiles(); ++ ++ /** ++ * Gets the currently selected {@link GameProfile} for this user. ++ *

++ * Calling this method whilst the user is not {@link #isLoggedIn() logged in} or has no {@link #getAvailableProfiles() available profiles} will always return null. ++ * ++ * @return Users currently selected Game Profile ++ */ ++ GameProfile getSelectedProfile(); ++ ++ /** ++ * Attempts to select the specified {@link GameProfile}. ++ *

++ * The user must be {@link #isLoggedIn() logged in}, have no {@link #getSelectedProfile() currently selected game profile} and the specified profile must ++ * be retrieved from {@link #getAvailableProfiles()}. ++ * ++ * @param profile The game profile to select. ++ * @throws java.lang.IllegalArgumentException Profile is null or did not come from {@link #getAvailableProfiles()} ++ * @throws org.spigotmc.authlib.exceptions.AuthenticationException User is not currently {@link #isLoggedIn() logged in}, ++ * or already has a {@link #getSelectedProfile() selected profile}, ++ * or the authentication service did not allow the profile change ++ * @throws org.spigotmc.authlib.exceptions.AuthenticationUnavailableException Thrown when the servers return a malformed response, or are otherwise unavailable ++ */ ++ void selectGameProfile(GameProfile profile) throws AuthenticationException; ++ ++ /** ++ * Tries to load any stored details that may be used for authentication from a given Map. ++ *

++ * This may be used to load an approximation of the current state from a past {@link org.spigotmc.authlib.UserAuthentication} with {@link #saveForStorage()}. ++ * ++ * @param credentials Map to load credentials or state from ++ */ ++ void loadFromStorage(Map credentials); ++ ++ /** ++ * Saves any known credentials to a Map and returns the result. ++ *

++ * This may be used to save an approximation of the current state for a future {@link org.spigotmc.authlib.UserAuthentication} with {@link #loadFromStorage(java.util.Map)}. ++ * ++ * @return Map containing any saved credentials and state for storage ++ */ ++ Map saveForStorage(); ++ ++ /** ++ * Sets the username to authenticate with for the next {@link #logIn()} call. ++ *

++ * You may not call this method whilst the user is {@link #isLoggedIn() logged in}. ++ * ++ * @param username Username to authenticate with ++ * @throws java.lang.IllegalStateException User is already logged in ++ */ ++ void setUsername(String username); ++ ++ /** ++ * Sets the password to authenticate with for the next {@link #logIn()} call. ++ *

++ * You may not call this method with a non-null and non-empty string whilst the user is {@link #isLoggedIn() logged in}. ++ * ++ * @param password Password to authenticate with ++ * @throws java.lang.IllegalStateException User is already logged in and the password is non-null & non-empty ++ */ ++ void setPassword(String password); ++ ++ /** ++ * Gets an authenticated token for use in authenticated API calls. ++ * ++ * @return Authenticated token for the current user, or null if not logged in. ++ */ ++ public String getAuthenticatedToken(); ++ ++ /** ++ * Gets the unique ID of the currently logged in user. ++ *

++ * This method will return null if the user is not logged in. ++ * ++ * @return Unique ID of the currently logged in user, or null if not logged in ++ */ ++ public String getUserID(); ++ ++ /** ++ * Gets a Multimap of properties bound to the currently logged in user. ++ *

++ * This method will return an empty Multimap if the user is not logged in. ++ *

++ * The returned Multimap will ignore any changes. ++ * ++ * @return Multimap of user properties. ++ */ ++ public PropertyMap getUserProperties(); ++ ++ /** ++ * Gets the type of the currently logged in user. ++ *

++ * This method will return null if the user is not logged in. ++ * ++ * @return Type of current logged in user, or null. ++ */ ++ public UserType getUserType(); ++} +diff --git a/src/main/java/org/spigotmc/authlib/UserType.java b/src/main/java/org/spigotmc/authlib/UserType.java +new file mode 100644 +index 0000000..6ca7eff +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/UserType.java +@@ -0,0 +1,30 @@ ++package org.spigotmc.authlib; ++ ++import java.util.HashMap; ++import java.util.Map; ++ ++public enum UserType { ++ LEGACY("legacy"), ++ MOJANG("mojang"); ++ ++ private static final Map BY_NAME = new HashMap(); ++ private final String name; ++ ++ private UserType(String name) { ++ this.name = name; ++ } ++ ++ public static UserType byName(String name) { ++ return BY_NAME.get(name.toLowerCase()); ++ } ++ ++ public String getName() { ++ return name; ++ } ++ ++ static { ++ for (UserType type : UserType.values()) { ++ BY_NAME.put(type.name, type); ++ } ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/exceptions/AuthenticationException.java b/src/main/java/org/spigotmc/authlib/exceptions/AuthenticationException.java +new file mode 100644 +index 0000000..5366bbf +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/exceptions/AuthenticationException.java +@@ -0,0 +1,18 @@ ++package org.spigotmc.authlib.exceptions; ++ ++public class AuthenticationException extends Exception { ++ public AuthenticationException() { ++ } ++ ++ public AuthenticationException(String message) { ++ super(message); ++ } ++ ++ public AuthenticationException(String message, Throwable cause) { ++ super(message, cause); ++ } ++ ++ public AuthenticationException(Throwable cause) { ++ super(cause); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/exceptions/AuthenticationUnavailableException.java b/src/main/java/org/spigotmc/authlib/exceptions/AuthenticationUnavailableException.java +new file mode 100644 +index 0000000..f953f2c +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/exceptions/AuthenticationUnavailableException.java +@@ -0,0 +1,21 @@ ++package org.spigotmc.authlib.exceptions; ++ ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++ ++public class AuthenticationUnavailableException extends AuthenticationException ++{ ++ public AuthenticationUnavailableException() { ++ } ++ ++ public AuthenticationUnavailableException(String message) { ++ super(message); ++ } ++ ++ public AuthenticationUnavailableException(String message, Throwable cause) { ++ super(message, cause); ++ } ++ ++ public AuthenticationUnavailableException(Throwable cause) { ++ super(cause); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/exceptions/InvalidCredentialsException.java b/src/main/java/org/spigotmc/authlib/exceptions/InvalidCredentialsException.java +new file mode 100644 +index 0000000..edf8074 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/exceptions/InvalidCredentialsException.java +@@ -0,0 +1,21 @@ ++package org.spigotmc.authlib.exceptions; ++ ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++ ++public class InvalidCredentialsException extends AuthenticationException ++{ ++ public InvalidCredentialsException() { ++ } ++ ++ public InvalidCredentialsException(String message) { ++ super(message); ++ } ++ ++ public InvalidCredentialsException(String message, Throwable cause) { ++ super(message, cause); ++ } ++ ++ public InvalidCredentialsException(Throwable cause) { ++ super(cause); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/exceptions/UserMigratedException.java b/src/main/java/org/spigotmc/authlib/exceptions/UserMigratedException.java +new file mode 100644 +index 0000000..1df195f +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/exceptions/UserMigratedException.java +@@ -0,0 +1,18 @@ ++package org.spigotmc.authlib.exceptions; ++ ++public class UserMigratedException extends InvalidCredentialsException { ++ public UserMigratedException() { ++ } ++ ++ public UserMigratedException(String message) { ++ super(message); ++ } ++ ++ public UserMigratedException(String message, Throwable cause) { ++ super(message, cause); ++ } ++ ++ public UserMigratedException(Throwable cause) { ++ super(cause); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/legacy/LegacyAuthenticationService.java b/src/main/java/org/spigotmc/authlib/legacy/LegacyAuthenticationService.java +new file mode 100644 +index 0000000..1be0c80 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/legacy/LegacyAuthenticationService.java +@@ -0,0 +1,49 @@ ++package org.spigotmc.authlib.legacy; ++ ++import org.spigotmc.authlib.Agent; ++import org.spigotmc.authlib.GameProfileRepository; ++import org.spigotmc.authlib.HttpAuthenticationService; ++import org.apache.commons.lang3.Validate; ++import org.spigotmc.authlib.legacy.LegacyUserAuthentication; ++ ++import java.net.Proxy; ++ ++public class LegacyAuthenticationService extends HttpAuthenticationService { ++ /** ++ * Constructs a new AuthenticationService using the legacy service. ++ *

++ * The legacy authentication service only supports the Minecraft {@link Agent}. ++ * ++ * @param proxy Proxy to route all HTTP(s) requests through. ++ * @throws java.lang.IllegalArgumentException Proxy is null ++ */ ++ protected LegacyAuthenticationService(Proxy proxy) { ++ super(proxy); ++ } ++ ++ /** ++ * Creates a relevant {@link org.spigotmc.authlib.UserAuthentication} using the legacy servers. ++ *

++ * The legacy authentication service only supports the Minecraft {@link Agent}. ++ * ++ * @param agent Game agent to authenticate for ++ * @throws java.lang.IllegalArgumentException Agent is null or not allowed for this AuthenticationService ++ * @return New user authenticator ++ */ ++ @Override ++ public LegacyUserAuthentication createUserAuthentication(Agent agent) { ++ Validate.notNull(agent); ++ if (agent != Agent.MINECRAFT) throw new IllegalArgumentException("Legacy authentication cannot handle anything but Minecraft"); ++ return new LegacyUserAuthentication(this); ++ } ++ ++ @Override ++ public LegacyMinecraftSessionService createMinecraftSessionService() { ++ return new LegacyMinecraftSessionService(this); ++ } ++ ++ @Override ++ public GameProfileRepository createProfileRepository() { ++ throw new UnsupportedOperationException("Legacy authentication service has no profile repository"); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/legacy/LegacyMinecraftSessionService.java b/src/main/java/org/spigotmc/authlib/legacy/LegacyMinecraftSessionService.java +new file mode 100644 +index 0000000..6ed1afe +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/legacy/LegacyMinecraftSessionService.java +@@ -0,0 +1,79 @@ ++package org.spigotmc.authlib.legacy; ++ ++import org.spigotmc.authlib.GameProfile; ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++import org.spigotmc.authlib.exceptions.AuthenticationUnavailableException; ++import org.spigotmc.authlib.minecraft.HttpMinecraftSessionService; ++import org.spigotmc.authlib.minecraft.MinecraftProfileTexture; ++import org.spigotmc.authlib.legacy.LegacyAuthenticationService; ++ ++import java.io.IOException; ++import java.net.URL; ++import java.util.HashMap; ++import java.util.Map; ++ ++import static org.spigotmc.authlib.HttpAuthenticationService.*; ++ ++public class LegacyMinecraftSessionService extends HttpMinecraftSessionService { ++ private static final String BASE_URL = "http://session.minecraft.net/game/"; ++ private static final URL JOIN_URL = constantURL(BASE_URL + "joinserver.jsp"); ++ private static final URL CHECK_URL = constantURL(BASE_URL + "checkserver.jsp"); ++ ++ protected LegacyMinecraftSessionService(LegacyAuthenticationService authenticationService) { ++ super(authenticationService); ++ } ++ ++ @Override ++ public void joinServer(GameProfile profile, String authenticationToken, String serverId) throws AuthenticationException { ++ Map arguments = new HashMap(); ++ ++ arguments.put("user", profile.getName()); ++ arguments.put("sessionId", authenticationToken); ++ arguments.put("serverId", serverId); ++ ++ URL url = concatenateURL(JOIN_URL, buildQuery(arguments)); ++ ++ try { ++ String response = getAuthenticationService().performGetRequest(url); ++ ++ if (!response.equals("OK")) { ++ throw new AuthenticationException(response); ++ } ++ } catch (IOException e) { ++ throw new AuthenticationUnavailableException(e); ++ } ++ } ++ ++ @Override ++ public GameProfile hasJoinedServer(GameProfile user, String serverId) throws AuthenticationUnavailableException { ++ Map arguments = new HashMap(); ++ ++ arguments.put("user", user.getName()); ++ arguments.put("serverId", serverId); ++ ++ URL url = concatenateURL(CHECK_URL, buildQuery(arguments)); ++ ++ try { ++ String response = getAuthenticationService().performGetRequest(url); ++ ++ return response.equals("YES") ? user : null; ++ } catch (IOException e) { ++ throw new AuthenticationUnavailableException(e); ++ } ++ } ++ ++ @Override ++ public Map getTextures(GameProfile profile, boolean requireSecure) { ++ return new HashMap(); ++ } ++ ++ @Override ++ public GameProfile fillProfileProperties(GameProfile profile) { ++ return profile; ++ } ++ ++ @Override ++ public LegacyAuthenticationService getAuthenticationService() { ++ return (LegacyAuthenticationService) super.getAuthenticationService(); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/legacy/LegacyUserAuthentication.java b/src/main/java/org/spigotmc/authlib/legacy/LegacyUserAuthentication.java +new file mode 100644 +index 0000000..0dc670a +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/legacy/LegacyUserAuthentication.java +@@ -0,0 +1,117 @@ ++package org.spigotmc.authlib.legacy; ++ ++import org.spigotmc.authlib.GameProfile; ++import org.spigotmc.authlib.HttpAuthenticationService; ++import org.spigotmc.authlib.HttpUserAuthentication; ++import org.spigotmc.authlib.UserType; ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++import org.spigotmc.authlib.exceptions.InvalidCredentialsException; ++import org.spigotmc.authlib.util.UUIDTypeAdapter; ++import org.apache.commons.lang3.StringUtils; ++ ++import java.io.IOException; ++import java.net.URL; ++import java.util.HashMap; ++import java.util.Map; ++ ++public class LegacyUserAuthentication extends HttpUserAuthentication { ++ private static final URL AUTHENTICATION_URL = HttpAuthenticationService.constantURL("https://login.minecraft.net"); ++ private static final int AUTHENTICATION_VERSION = 14; ++ ++ // 0 1 2 3 4 ++ // deprecated,deprecated,profile name,session id,profile id ++ private static final int RESPONSE_PART_PROFILE_NAME = 2; ++ private static final int RESPONSE_PART_SESSION_TOKEN = 3; ++ private static final int RESPONSE_PART_PROFILE_ID = 4; ++ ++ private String sessionToken; ++ ++ protected LegacyUserAuthentication(LegacyAuthenticationService authenticationService) { ++ super(authenticationService); ++ } ++ ++ @Override ++ public void logIn() throws AuthenticationException { ++ if (StringUtils.isBlank(getUsername())) { ++ throw new InvalidCredentialsException("Invalid username"); ++ } ++ if (StringUtils.isBlank(getPassword())) { ++ throw new InvalidCredentialsException("Invalid password"); ++ } ++ ++ Map args = new HashMap(); ++ args.put("user", getUsername()); ++ args.put("password", getPassword()); ++ args.put("version", AUTHENTICATION_VERSION); ++ String response; ++ ++ try { ++ response = getAuthenticationService().performPostRequest(AUTHENTICATION_URL, HttpAuthenticationService.buildQuery(args), "application/x-www-form-urlencoded").trim(); ++ } catch (IOException e) { ++ throw new AuthenticationException("Authentication server is not responding", e); ++ } ++ ++ String[] split = response.split(":"); ++ ++ if (split.length == 5) { ++ String profileId = split[RESPONSE_PART_PROFILE_ID]; ++ String profileName = split[RESPONSE_PART_PROFILE_NAME]; ++ String sessionToken = split[RESPONSE_PART_SESSION_TOKEN]; ++ ++ if (StringUtils.isBlank(profileId) || StringUtils.isBlank(profileName) || StringUtils.isBlank(sessionToken)) { ++ throw new AuthenticationException("Unknown response from authentication server: " + response); ++ } ++ ++ setSelectedProfile(new GameProfile(UUIDTypeAdapter.fromString(profileId), profileName)); ++ this.sessionToken = sessionToken; ++ setUserType(UserType.LEGACY); ++ } else { ++ throw new InvalidCredentialsException(response); ++ } ++ } ++ ++ @Override ++ public void logOut() { ++ super.logOut(); ++ sessionToken = null; ++ } ++ ++ @Override ++ public boolean canPlayOnline() { ++ return isLoggedIn() && getSelectedProfile() != null && getAuthenticatedToken() != null; ++ } ++ ++ @Override ++ public GameProfile[] getAvailableProfiles() { ++ if (getSelectedProfile() != null) { ++ return new GameProfile[] {getSelectedProfile()}; ++ } else { ++ return new GameProfile[0]; ++ } ++ } ++ ++ /** ++ * This method is not supported in the Legacy authentication service. ++ *

++ * Attempts to call this method will fail. ++ */ ++ @Override ++ public void selectGameProfile(GameProfile profile) throws AuthenticationException { ++ throw new UnsupportedOperationException("Game profiles cannot be changed in the legacy authentication service"); ++ } ++ ++ @Override ++ public String getAuthenticatedToken() { ++ return sessionToken; ++ } ++ ++ @Override ++ public String getUserID() { ++ return getUsername(); ++ } ++ ++ @Override ++ public LegacyAuthenticationService getAuthenticationService() { ++ return (LegacyAuthenticationService) super.getAuthenticationService(); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/minecraft/BaseMinecraftSessionService.java b/src/main/java/org/spigotmc/authlib/minecraft/BaseMinecraftSessionService.java +new file mode 100644 +index 0000000..000ce45 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/minecraft/BaseMinecraftSessionService.java +@@ -0,0 +1,17 @@ ++package org.spigotmc.authlib.minecraft; ++ ++import org.spigotmc.authlib.AuthenticationService; ++import org.spigotmc.authlib.minecraft.MinecraftSessionService; ++ ++public abstract class BaseMinecraftSessionService implements MinecraftSessionService ++{ ++ private final AuthenticationService authenticationService; ++ ++ protected BaseMinecraftSessionService(AuthenticationService authenticationService) { ++ this.authenticationService = authenticationService; ++ } ++ ++ public AuthenticationService getAuthenticationService() { ++ return authenticationService; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/minecraft/HttpMinecraftSessionService.java b/src/main/java/org/spigotmc/authlib/minecraft/HttpMinecraftSessionService.java +new file mode 100644 +index 0000000..a3dc46b +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/minecraft/HttpMinecraftSessionService.java +@@ -0,0 +1,16 @@ ++package org.spigotmc.authlib.minecraft; ++ ++import org.spigotmc.authlib.HttpAuthenticationService; ++import org.spigotmc.authlib.minecraft.BaseMinecraftSessionService; ++ ++public abstract class HttpMinecraftSessionService extends BaseMinecraftSessionService ++{ ++ protected HttpMinecraftSessionService(HttpAuthenticationService authenticationService) { ++ super(authenticationService); ++ } ++ ++ @Override ++ public HttpAuthenticationService getAuthenticationService() { ++ return (HttpAuthenticationService) super.getAuthenticationService(); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/minecraft/MinecraftProfileTexture.java b/src/main/java/org/spigotmc/authlib/minecraft/MinecraftProfileTexture.java +new file mode 100644 +index 0000000..110f826 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/minecraft/MinecraftProfileTexture.java +@@ -0,0 +1,34 @@ ++package org.spigotmc.authlib.minecraft; ++ ++import org.apache.commons.io.FilenameUtils; ++import org.apache.commons.lang3.builder.ToStringBuilder; ++ ++public class MinecraftProfileTexture { ++ public enum Type { ++ SKIN, ++ CAPE, ++ ; ++ } ++ ++ private final String url; ++ ++ public MinecraftProfileTexture(String url) { ++ this.url = url; ++ } ++ ++ public String getUrl() { ++ return url; ++ } ++ ++ public String getHash() { ++ return FilenameUtils.getBaseName(url); ++ } ++ ++ @Override ++ public String toString() { ++ return new ToStringBuilder(this) ++ .append("url", url) ++ .append("hash", getHash()) ++ .toString(); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/minecraft/MinecraftSessionService.java b/src/main/java/org/spigotmc/authlib/minecraft/MinecraftSessionService.java +new file mode 100644 +index 0000000..0166693 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/minecraft/MinecraftSessionService.java +@@ -0,0 +1,60 @@ ++package org.spigotmc.authlib.minecraft; ++ ++import org.spigotmc.authlib.GameProfile; ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++import org.spigotmc.authlib.exceptions.AuthenticationUnavailableException; ++import org.spigotmc.authlib.minecraft.MinecraftProfileTexture; ++ ++import java.util.Map; ++ ++public interface MinecraftSessionService { ++ /** ++ * Attempts to join the specified Minecraft server. ++ *

++ * The {@link org.spigotmc.authlib.GameProfile} used to join with may be partial, but the exact requirements will vary on ++ * authentication service. If this method returns without throwing an exception, the join was successful and a subsequent call to ++ * {@link #hasJoinedServer(org.spigotmc.authlib.GameProfile, String)} will return true. ++ * ++ * @param profile Partial {@link org.spigotmc.authlib.GameProfile} to join as ++ * @param authenticationToken The {@link org.spigotmc.authlib.UserAuthentication#getAuthenticatedToken() authenticated token} of the user ++ * @param serverId The random ID of the server to join ++ * @throws org.spigotmc.authlib.exceptions.AuthenticationUnavailableException Thrown when the servers return a malformed response, or are otherwise unavailable ++ * @throws org.spigotmc.authlib.exceptions.InvalidCredentialsException Thrown when the specified authenticationToken is invalid ++ * @throws org.spigotmc.authlib.exceptions.AuthenticationException Generic exception indicating that we could not authenticate the user ++ */ ++ public void joinServer(GameProfile profile, String authenticationToken, String serverId) throws AuthenticationException; ++ ++ /** ++ * Checks if the specified user has joined a Minecraft server. ++ *

++ * The {@link org.spigotmc.authlib.GameProfile} used to join with may be partial, but the exact requirements will vary on ++ * authentication service. ++ * ++ * @param user Partial {@link org.spigotmc.authlib.GameProfile} to check for ++ * @param serverId The random ID of the server to check for ++ * @throws org.spigotmc.authlib.exceptions.AuthenticationUnavailableException Thrown when the servers return a malformed response, or are otherwise unavailable ++ * @return Full game profile if the user had joined, otherwise null ++ */ ++ public GameProfile hasJoinedServer(GameProfile user, String serverId) throws AuthenticationUnavailableException; ++ ++ /** ++ * Gets a map of all known textures from a {@link org.spigotmc.authlib.GameProfile}. ++ *

++ * If a profile contains invalid textures, they will not be returned. If a profile contains no textures, an empty map will be returned. ++ * ++ * @param profile Game profile to return textures from. ++ * @param requireSecure If true, requires the payload to be recent and securely fetched. ++ * @return Map of texture types to textures. ++ */ ++ public Map getTextures(GameProfile profile, boolean requireSecure); ++ ++ /** ++ * Fills a profile with all known properties from the session service. ++ *

++ * The profile must have an ID. If no information is found, nothing will be done. ++ * ++ * @param profile Game profile to fill with properties. ++ * @return Filled profile for the previous user. ++ */ ++ public GameProfile fillProfileProperties(GameProfile profile); ++} +diff --git a/src/main/java/org/spigotmc/authlib/properties/Property.java b/src/main/java/org/spigotmc/authlib/properties/Property.java +new file mode 100644 +index 0000000..6b8609b +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/properties/Property.java +@@ -0,0 +1,53 @@ ++package org.spigotmc.authlib.properties; ++ ++import org.apache.commons.codec.binary.Base64; ++ ++import java.security.*; ++ ++public class Property { ++ private final String name; ++ private final String value; ++ private final String signature; ++ ++ public Property(String value, String name) { ++ this(value, name, null); ++ } ++ ++ public Property(String name, String value, String signature) { ++ this.name = name; ++ this.value = value; ++ this.signature = signature; ++ } ++ ++ public String getName() { ++ return name; ++ } ++ ++ public String getValue() { ++ return value; ++ } ++ ++ public String getSignature() { ++ return signature; ++ } ++ ++ public boolean hasSignature() { ++ return signature != null; ++ } ++ ++ public boolean isSignatureValid(PublicKey publicKey) { ++ try { ++ Signature signature = Signature.getInstance("SHA1withRSA"); ++ signature.initVerify(publicKey); ++ signature.update(value.getBytes()); ++ return signature.verify(Base64.decodeBase64(this.signature)); ++ } catch (NoSuchAlgorithmException e) { ++ e.printStackTrace(); ++ } catch (InvalidKeyException e) { ++ e.printStackTrace(); ++ } catch (SignatureException e) { ++ e.printStackTrace(); ++ } ++ return false; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/properties/PropertyMap.java b/src/main/java/org/spigotmc/authlib/properties/PropertyMap.java +new file mode 100644 +index 0000000..ef27ad0 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/properties/PropertyMap.java +@@ -0,0 +1,73 @@ ++package org.spigotmc.authlib.properties; ++ ++import com.google.common.collect.ForwardingMultimap; ++import com.google.common.collect.LinkedHashMultimap; ++import com.google.common.collect.Multimap; ++import com.google.gson.*; ++ ++import java.lang.reflect.Type; ++import java.util.Map; ++ ++public class PropertyMap extends ForwardingMultimap { ++ private final Multimap properties = LinkedHashMultimap.create(); ++ ++ @Override ++ protected Multimap delegate() { ++ return properties; ++ } ++ ++ public static class Serializer implements JsonSerializer, JsonDeserializer { ++ @Override ++ public PropertyMap deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { ++ final PropertyMap result = new PropertyMap(); ++ ++ if (json instanceof JsonObject) { ++ JsonObject object = (JsonObject) json; ++ ++ for (Map.Entry entry : object.entrySet()) { ++ if (entry.getValue() instanceof JsonArray) { ++ for (JsonElement element : ((JsonArray) entry.getValue())) { ++ result.put(entry.getKey(), new Property(entry.getKey(), element.getAsString())); ++ } ++ } ++ } ++ } else if (json instanceof JsonArray) { ++ for (JsonElement element : (JsonArray) json) { ++ if (element instanceof JsonObject) { ++ JsonObject object = (JsonObject) element; ++ String name = object.getAsJsonPrimitive("name").getAsString(); ++ String value = object.getAsJsonPrimitive("value").getAsString(); ++ ++ if (object.has("signature")) { ++ result.put(name, new Property(name, value, object.getAsJsonPrimitive("signature").getAsString())); ++ } else { ++ result.put(name, new Property(name, value)); ++ } ++ } ++ } ++ } ++ ++ return result; ++ } ++ ++ @Override ++ public JsonElement serialize(PropertyMap src, Type typeOfSrc, JsonSerializationContext context) { ++ JsonArray result = new JsonArray(); ++ ++ for (Property property : src.values()) { ++ JsonObject object = new JsonObject(); ++ ++ object.addProperty("name", property.getName()); ++ object.addProperty("value", property.getValue()); ++ ++ if (property.hasSignature()) { ++ object.addProperty("signature", property.getSignature()); ++ } ++ ++ result.add(object); ++ } ++ ++ return result; ++ } ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/util/UUIDTypeAdapter.java b/src/main/java/org/spigotmc/authlib/util/UUIDTypeAdapter.java +new file mode 100644 +index 0000000..8c3516d +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/util/UUIDTypeAdapter.java +@@ -0,0 +1,28 @@ ++package org.spigotmc.authlib.util; ++ ++import com.google.gson.TypeAdapter; ++import com.google.gson.stream.JsonReader; ++import com.google.gson.stream.JsonWriter; ++ ++import java.io.IOException; ++import java.util.UUID; ++ ++public class UUIDTypeAdapter extends TypeAdapter { ++ @Override ++ public void write(JsonWriter out, UUID value) throws IOException { ++ out.value(fromUUID(value)); ++ } ++ ++ @Override ++ public UUID read(JsonReader in) throws IOException { ++ return fromString(in.nextString()); ++ } ++ ++ public static String fromUUID(UUID value) { ++ return value.toString().replace("-", ""); ++ } ++ ++ public static UUID fromString(String input) { ++ return UUID.fromString(input.replaceFirst("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})", "$1-$2-$3-$4-$5")); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/ProfileIncompleteException.java b/src/main/java/org/spigotmc/authlib/yggdrasil/ProfileIncompleteException.java +new file mode 100644 +index 0000000..125916a +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/ProfileIncompleteException.java +@@ -0,0 +1,18 @@ ++package org.spigotmc.authlib.yggdrasil; ++ ++public class ProfileIncompleteException extends RuntimeException { ++ public ProfileIncompleteException() { ++ } ++ ++ public ProfileIncompleteException(String message) { ++ super(message); ++ } ++ ++ public ProfileIncompleteException(String message, Throwable cause) { ++ super(message, cause); ++ } ++ ++ public ProfileIncompleteException(Throwable cause) { ++ super(cause); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/ProfileNotFoundException.java b/src/main/java/org/spigotmc/authlib/yggdrasil/ProfileNotFoundException.java +new file mode 100644 +index 0000000..66ba35e +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/ProfileNotFoundException.java +@@ -0,0 +1,18 @@ ++package org.spigotmc.authlib.yggdrasil; ++ ++public class ProfileNotFoundException extends RuntimeException { ++ public ProfileNotFoundException() { ++ } ++ ++ public ProfileNotFoundException(String message) { ++ super(message); ++ } ++ ++ public ProfileNotFoundException(String message, Throwable cause) { ++ super(message, cause); ++ } ++ ++ public ProfileNotFoundException(Throwable cause) { ++ super(cause); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilAuthenticationService.java b/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilAuthenticationService.java +new file mode 100644 +index 0000000..b4c1a6b +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilAuthenticationService.java +@@ -0,0 +1,99 @@ ++package org.spigotmc.authlib.yggdrasil; ++ ++import com.google.gson.*; ++import org.spigotmc.authlib.*; ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++import org.spigotmc.authlib.exceptions.AuthenticationUnavailableException; ++import org.spigotmc.authlib.exceptions.InvalidCredentialsException; ++import org.spigotmc.authlib.exceptions.UserMigratedException; ++import org.spigotmc.authlib.minecraft.MinecraftSessionService; ++import org.spigotmc.authlib.properties.PropertyMap; ++import org.spigotmc.authlib.yggdrasil.YggdrasilUserAuthentication; ++import org.spigotmc.authlib.yggdrasil.response.Response; ++import org.spigotmc.authlib.util.UUIDTypeAdapter; ++import org.apache.commons.lang3.StringUtils; ++ ++import java.io.IOException; ++import java.lang.reflect.Type; ++import java.net.Proxy; ++import java.net.URL; ++import java.util.UUID; ++ ++public class YggdrasilAuthenticationService extends HttpAuthenticationService { ++ private final String clientToken; ++ private final Gson gson; ++ ++ public YggdrasilAuthenticationService(Proxy proxy, String clientToken) { ++ super(proxy); ++ this.clientToken = clientToken; ++ GsonBuilder builder = new GsonBuilder(); ++ builder.registerTypeAdapter(GameProfile.class, new GameProfileSerializer()); ++ builder.registerTypeAdapter(PropertyMap.class, new PropertyMap.Serializer()); ++ builder.registerTypeAdapter(UUID.class, new UUIDTypeAdapter()); ++ gson = builder.create(); ++ } ++ ++ @Override ++ public UserAuthentication createUserAuthentication(Agent agent) { ++ return new YggdrasilUserAuthentication(this, agent); ++ } ++ ++ @Override ++ public MinecraftSessionService createMinecraftSessionService() { ++ return new YggdrasilMinecraftSessionService(this); ++ } ++ ++ @Override ++ public GameProfileRepository createProfileRepository() { ++ return new YggdrasilGameProfileRepository(this); ++ } ++ ++ protected T makeRequest(URL url, Object input, Class classOfT) throws AuthenticationException { ++ try { ++ String jsonResult = input == null ? performGetRequest(url) : performPostRequest(url, gson.toJson(input), "application/json"); ++ T result = gson.fromJson(jsonResult, classOfT); ++ ++ if (result == null) return null; ++ ++ if (StringUtils.isNotBlank(result.getError())) { ++ if ("UserMigratedException".equals(result.getCause())) { ++ throw new UserMigratedException(result.getErrorMessage()); ++ } else if (result.getError().equals("ForbiddenOperationException")) { ++ throw new InvalidCredentialsException(result.getErrorMessage()); ++ } else { ++ throw new AuthenticationException(result.getErrorMessage()); ++ } ++ } ++ ++ return result; ++ } catch (IOException e) { ++ throw new AuthenticationUnavailableException("Cannot contact authentication server", e); ++ } catch (IllegalStateException e) { ++ throw new AuthenticationUnavailableException("Cannot contact authentication server", e); ++ } catch (JsonParseException e) { ++ throw new AuthenticationUnavailableException("Cannot contact authentication server", e); ++ } ++ } ++ ++ public String getClientToken() { ++ return clientToken; ++ } ++ ++ private static class GameProfileSerializer implements JsonSerializer, JsonDeserializer { ++ @Override ++ public GameProfile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { ++ JsonObject object = (JsonObject) json; ++ UUID id = object.has("id") ? context.deserialize(object.get("id"), UUID.class) : null; ++ String name = object.has("name") ? object.getAsJsonPrimitive("name").getAsString() : null; ++ return new GameProfile(id, name); ++ } ++ ++ @Override ++ public JsonElement serialize(GameProfile src, Type typeOfSrc, JsonSerializationContext context) { ++ JsonObject result = new JsonObject(); ++ if (src.getId() != null) result.add("id", context.serialize(src.getId())); ++ if (src.getName() != null) result.addProperty("name", src.getName()); ++ return result; ++ } ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilGameProfileRepository.java b/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilGameProfileRepository.java +new file mode 100644 +index 0000000..0fc52cc +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilGameProfileRepository.java +@@ -0,0 +1,136 @@ ++package org.spigotmc.authlib.yggdrasil; ++ ++import com.google.common.base.Strings; ++import com.google.common.collect.Sets; ++import org.spigotmc.authlib.*; ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++import org.spigotmc.authlib.yggdrasil.ProfileNotFoundException; ++import org.spigotmc.authlib.yggdrasil.YggdrasilAuthenticationService; ++import org.spigotmc.authlib.yggdrasil.response.ProfileSearchResultsResponse; ++import org.apache.commons.lang3.builder.ToStringBuilder; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++ ++import java.util.Set; ++ ++public class YggdrasilGameProfileRepository implements GameProfileRepository { ++ private static final Logger LOGGER = LogManager.getLogger(); ++ private static final String BASE_URL = "https://api.mojang.com/"; ++ private static final String SEARCH_PAGE_URL = BASE_URL + "profiles/page/"; ++ private static final int MAX_FAIL_COUNT = 3; ++ private static final int DELAY_BETWEEN_PAGES = 100; ++ private static final int DELAY_BETWEEN_FAILURES = 750; ++ ++ private final YggdrasilAuthenticationService authenticationService; ++ ++ public YggdrasilGameProfileRepository(YggdrasilAuthenticationService authenticationService) { ++ this.authenticationService = authenticationService; ++ } ++ ++ @Override ++ public void findProfilesByNames(String[] names, Agent agent, ProfileLookupCallback callback) { ++ Set criteria = Sets.newHashSet(); ++ ++ for (String name : names) { ++ if (!Strings.isNullOrEmpty(name)) { ++ criteria.add(new ProfileCriteria(name, agent)); ++ } ++ } ++ ++ Exception exception = null; ++ Set request = Sets.newHashSet(criteria); ++ int page = 1; ++ int failCount = 0; ++ ++ while (!criteria.isEmpty()) { ++ try { ++ ProfileSearchResultsResponse response = authenticationService.makeRequest(HttpAuthenticationService.constantURL(SEARCH_PAGE_URL + page), request, ProfileSearchResultsResponse.class); ++ failCount = 0; ++ exception = null; ++ ++ if (response.getSize() == 0 || response.getProfiles().length == 0) { ++ LOGGER.debug("Page {} returned empty, aborting search", page); ++ break; ++ } else { ++ LOGGER.debug("Page {} returned {} results of {}, parsing", page, response.getProfiles().length, response.getSize()); ++ ++ for (GameProfile profile : response.getProfiles()) { ++ LOGGER.debug("Successfully looked up profile {}", profile); ++ criteria.remove(new ProfileCriteria(profile.getName(), agent)); ++ callback.onProfileLookupSucceeded(profile); ++ } ++ ++ LOGGER.debug("Page {} successfully parsed", page); ++ page++; ++ ++ try { ++ Thread.sleep(DELAY_BETWEEN_PAGES); ++ } catch (InterruptedException ignored) {} ++ } ++ } catch (AuthenticationException e) { ++ exception = e; ++ failCount++; ++ ++ if (failCount == MAX_FAIL_COUNT) { ++ break; ++ } else { ++ try { ++ Thread.sleep(DELAY_BETWEEN_FAILURES); ++ } catch (InterruptedException ignored) {} ++ } ++ } ++ } ++ ++ if (criteria.isEmpty()) { ++ LOGGER.debug("Successfully found every profile requested"); ++ } else { ++ LOGGER.debug("{} profiles were missing from search results", criteria.size()); ++ if (exception == null) { ++ exception = new ProfileNotFoundException("Server did not find the requested profile"); ++ } ++ for (ProfileCriteria profileCriteria : criteria) { ++ callback.onProfileLookupFailed(new GameProfile(null, profileCriteria.getName()), exception); ++ } ++ } ++ } ++ ++ private class ProfileCriteria { ++ private final String name; ++ private final String agent; ++ ++ private ProfileCriteria(String name, Agent agent) { ++ this.name = name; ++ this.agent = agent.getName(); ++ } ++ ++ public String getName() { ++ return name; ++ } ++ ++ public String getAgent() { ++ return agent; ++ } ++ ++ @Override ++ public boolean equals(Object o) { ++ if (this == o) return true; ++ if (o == null || getClass() != o.getClass()) return false; ++ ProfileCriteria that = (ProfileCriteria) o; ++ return agent.equals(that.agent) && name.toLowerCase().equals(that.name.toLowerCase()); ++ } ++ ++ @Override ++ public int hashCode() { ++ return 31 * name.toLowerCase().hashCode() + agent.hashCode(); ++ } ++ ++ @Override ++ public String toString() { ++ return new ToStringBuilder(this) ++ .append("agent", agent) ++ .append("name", name) ++ .toString(); ++ } ++ } ++ ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilMinecraftSessionService.java b/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilMinecraftSessionService.java +new file mode 100644 +index 0000000..dc2a6f8 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilMinecraftSessionService.java +@@ -0,0 +1,177 @@ ++package org.spigotmc.authlib.yggdrasil; ++ ++import com.google.common.collect.Iterables; ++import com.google.gson.Gson; ++import com.google.gson.GsonBuilder; ++import com.google.gson.JsonParseException; ++import net.minecraft.util.org.apache.commons.io.Charsets; ++import org.spigotmc.authlib.GameProfile; ++import org.spigotmc.authlib.HttpAuthenticationService; ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++import org.spigotmc.authlib.exceptions.AuthenticationUnavailableException; ++import org.spigotmc.authlib.minecraft.HttpMinecraftSessionService; ++import org.spigotmc.authlib.minecraft.MinecraftProfileTexture; ++import org.spigotmc.authlib.properties.Property; ++import org.spigotmc.authlib.yggdrasil.YggdrasilAuthenticationService; ++import org.spigotmc.authlib.yggdrasil.request.JoinMinecraftServerRequest; ++import org.spigotmc.authlib.yggdrasil.response.HasJoinedMinecraftServerResponse; ++import org.spigotmc.authlib.yggdrasil.response.MinecraftProfilePropertiesResponse; ++import org.spigotmc.authlib.yggdrasil.response.MinecraftTexturesPayload; ++import org.spigotmc.authlib.yggdrasil.response.Response; ++import org.spigotmc.authlib.util.UUIDTypeAdapter; ++import org.apache.commons.codec.binary.Base64; ++import org.apache.commons.io.IOUtils; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++ ++import java.net.URL; ++import java.security.KeyFactory; ++import java.security.PublicKey; ++import java.security.spec.X509EncodedKeySpec; ++import java.util.*; ++ ++public class YggdrasilMinecraftSessionService extends HttpMinecraftSessionService { ++ private static final Logger LOGGER = LogManager.getLogger(); ++ private static final String BASE_URL = "https://sessionserver.mojang.com/session/minecraft/"; ++ private static final URL JOIN_URL = HttpAuthenticationService.constantURL(BASE_URL + "join"); ++ private static final URL CHECK_URL = HttpAuthenticationService.constantURL(BASE_URL + "hasJoined"); ++ ++ private final PublicKey publicKey; ++ private final Gson gson = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create(); ++ ++ protected YggdrasilMinecraftSessionService(YggdrasilAuthenticationService authenticationService) { ++ super(authenticationService); ++ ++ try { ++ X509EncodedKeySpec spec = new X509EncodedKeySpec(IOUtils.toByteArray(YggdrasilMinecraftSessionService.class.getResourceAsStream("/yggdrasil_session_pubkey.der"))); ++ KeyFactory keyFactory = KeyFactory.getInstance("RSA"); ++ publicKey = keyFactory.generatePublic(spec); ++ } catch (Exception e) { ++ throw new Error("Missing/invalid yggdrasil public key!"); ++ } ++ } ++ ++ @Override ++ public void joinServer(GameProfile profile, String authenticationToken, String serverId) throws AuthenticationException { ++ JoinMinecraftServerRequest request = new JoinMinecraftServerRequest(); ++ request.accessToken = authenticationToken; ++ request.selectedProfile = profile.getId(); ++ request.serverId = serverId; ++ ++ getAuthenticationService().makeRequest(JOIN_URL, request, Response.class); ++ } ++ ++ @Override ++ public GameProfile hasJoinedServer(GameProfile user, String serverId) throws AuthenticationUnavailableException { ++ Map arguments = new HashMap(); ++ ++ arguments.put("username", user.getName()); ++ arguments.put("serverId", serverId); ++ ++ URL url = HttpAuthenticationService.concatenateURL(CHECK_URL, HttpAuthenticationService.buildQuery(arguments)); ++ ++ try { ++ HasJoinedMinecraftServerResponse response = getAuthenticationService().makeRequest(url, null, HasJoinedMinecraftServerResponse.class); ++ ++ if (response != null && response.getId() != null) { ++ GameProfile result = new GameProfile(response.getId(), user.getName()); ++ ++ if (response.getProperties() != null) { ++ result.getProperties().putAll(response.getProperties()); ++ } ++ ++ return result; ++ } else { ++ return null; ++ } ++ } catch (AuthenticationUnavailableException e) { ++ throw e; ++ } catch (AuthenticationException e) { ++ return null; ++ } ++ } ++ ++ @Override ++ public Map getTextures(GameProfile profile, boolean requireSecure) { ++ Property textureProperty = Iterables.getFirst(profile.getProperties().get("textures"), null); ++ if (textureProperty == null) return new HashMap(); ++ ++ if (!textureProperty.hasSignature()) { ++ LOGGER.error("Signature is missing from textures payload"); ++ return new HashMap(); ++ } ++ ++ if (!textureProperty.isSignatureValid(publicKey)) { ++ LOGGER.error("Textures payload has been tampered with (signature invalid)"); ++ return new HashMap(); ++ } ++ ++ MinecraftTexturesPayload result; ++ try { ++ String json = new String(Base64.decodeBase64(textureProperty.getValue()), Charsets.UTF_8); ++ result = gson.fromJson(json, MinecraftTexturesPayload.class); ++ } catch (JsonParseException e) { ++ LOGGER.error("Could not decode textures payload", e); ++ return new HashMap(); ++ } ++ ++ if (result.getProfileId() == null || !result.getProfileId().equals(profile.getId())) { ++ LOGGER.error("Decrypted textures payload was for another user (expected id {} but was for {})", profile.getId(), result.getProfileId()); ++ return new HashMap(); ++ } ++ ++ if (result.getProfileName() == null || !result.getProfileName().equals(profile.getName())) { ++ LOGGER.error("Decrypted textures payload was for another user (expected name {} but was for {})", profile.getName(), result.getProfileName()); ++ return new HashMap(); ++ } ++ ++ if (requireSecure) { ++ if (result.isPublic()) { ++ LOGGER.error("Decrypted textures payload was public but we require secure data"); ++ return new HashMap(); ++ } ++ ++ Calendar limit = Calendar.getInstance(); ++ limit.add(Calendar.DATE, -1); ++ Date validFrom = new Date(result.getTimestamp()); ++ ++ if (validFrom.before(limit.getTime())) { ++ LOGGER.error("Decrypted textures payload is too old ({0}, but we need it to be at least {1})", validFrom, limit); ++ return new HashMap(); ++ } ++ } ++ ++ return result.getTextures() == null ? new HashMap() : result.getTextures(); ++ } ++ ++ @Override ++ public GameProfile fillProfileProperties(GameProfile profile) { ++ if (profile.getId() == null) { ++ return profile; ++ } ++ ++ try { ++ URL url = HttpAuthenticationService.constantURL(BASE_URL + "profile/" + UUIDTypeAdapter.fromUUID(profile.getId())); ++ MinecraftProfilePropertiesResponse response = getAuthenticationService().makeRequest(url, null, MinecraftProfilePropertiesResponse.class); ++ ++ if (response == null) { ++ LOGGER.debug("Couldn't fetch profile properties for " + profile + " as the profile does not exist"); ++ return profile; ++ } else { ++ LOGGER.debug("Successfully fetched profile properties for " + profile); ++ GameProfile result = new GameProfile(response.getId(), response.getName()); ++ result.getProperties().putAll(response.getProperties()); ++ profile.getProperties().putAll(response.getProperties()); ++ return result; ++ } ++ } catch (AuthenticationException e) { ++ LOGGER.warn("Couldn't look up profile properties for " + profile, e); ++ return profile; ++ } ++ } ++ ++ @Override ++ public YggdrasilAuthenticationService getAuthenticationService() { ++ return (YggdrasilAuthenticationService) super.getAuthenticationService(); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilUserAuthentication.java b/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilUserAuthentication.java +new file mode 100644 +index 0000000..d1b3183 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/YggdrasilUserAuthentication.java +@@ -0,0 +1,257 @@ ++package org.spigotmc.authlib.yggdrasil; ++ ++import org.spigotmc.authlib.*; ++import org.spigotmc.authlib.exceptions.AuthenticationException; ++import org.spigotmc.authlib.exceptions.InvalidCredentialsException; ++import org.spigotmc.authlib.GameProfile; ++import org.spigotmc.authlib.yggdrasil.request.AuthenticationRequest; ++import org.spigotmc.authlib.yggdrasil.request.RefreshRequest; ++import org.spigotmc.authlib.yggdrasil.response.AuthenticationResponse; ++import org.spigotmc.authlib.yggdrasil.response.RefreshResponse; ++import org.spigotmc.authlib.yggdrasil.response.User; ++import org.apache.commons.lang3.ArrayUtils; ++import org.apache.commons.lang3.StringUtils; ++import org.apache.logging.log4j.LogManager; ++import org.apache.logging.log4j.Logger; ++ ++import java.net.URL; ++import java.util.*; ++ ++public class YggdrasilUserAuthentication extends HttpUserAuthentication { ++ private static final Logger LOGGER = LogManager.getLogger(); ++ private static final String BASE_URL = "https://authserver.mojang.com/"; ++ private static final URL ROUTE_AUTHENTICATE = HttpAuthenticationService.constantURL(BASE_URL + "authenticate"); ++ private static final URL ROUTE_REFRESH = HttpAuthenticationService.constantURL(BASE_URL + "refresh"); ++ private static final URL ROUTE_VALIDATE = HttpAuthenticationService.constantURL(BASE_URL + "validate"); ++ private static final URL ROUTE_INVALIDATE = HttpAuthenticationService.constantURL(BASE_URL + "invalidate"); ++ private static final URL ROUTE_SIGNOUT = HttpAuthenticationService.constantURL(BASE_URL + "signout"); ++ ++ private static final String STORAGE_KEY_ACCESS_TOKEN = "accessToken"; ++ ++ private final Agent agent; ++ private GameProfile[] profiles; ++ private String accessToken; ++ private boolean isOnline; ++ ++ public YggdrasilUserAuthentication(YggdrasilAuthenticationService authenticationService, Agent agent) { ++ super(authenticationService); ++ this.agent = agent; ++ } ++ ++ @Override ++ public boolean canLogIn() { ++ return !canPlayOnline() && StringUtils.isNotBlank(getUsername()) && (StringUtils.isNotBlank(getPassword()) || StringUtils.isNotBlank(getAuthenticatedToken())); ++ } ++ ++ @Override ++ public void logIn() throws AuthenticationException { ++ if (StringUtils.isBlank(getUsername())) { ++ throw new InvalidCredentialsException("Invalid username"); ++ } ++ ++ if (StringUtils.isNotBlank(getAuthenticatedToken())) { ++ logInWithToken(); ++ } else if (StringUtils.isNotBlank(getPassword())) { ++ logInWithPassword(); ++ } else { ++ throw new InvalidCredentialsException("Invalid password"); ++ } ++ } ++ ++ protected void logInWithPassword() throws AuthenticationException { ++ if (StringUtils.isBlank(getUsername())) { ++ throw new InvalidCredentialsException("Invalid username"); ++ } ++ if (StringUtils.isBlank(getPassword())) { ++ throw new InvalidCredentialsException("Invalid password"); ++ } ++ ++ LOGGER.info("Logging in with username & password"); ++ ++ AuthenticationRequest request = new AuthenticationRequest(this, getUsername(), getPassword()); ++ AuthenticationResponse response = getAuthenticationService().makeRequest(ROUTE_AUTHENTICATE, request, AuthenticationResponse.class); ++ ++ if (!response.getClientToken().equals(getAuthenticationService().getClientToken())) { ++ throw new AuthenticationException("Server requested we change our client token. Don't know how to handle this!"); ++ } ++ ++ if (response.getSelectedProfile() != null) { ++ setUserType(response.getSelectedProfile().isLegacy() ? UserType.LEGACY : UserType.MOJANG); ++ } else if (ArrayUtils.isNotEmpty(response.getAvailableProfiles())) { ++ setUserType(response.getAvailableProfiles()[0].isLegacy() ? UserType.LEGACY : UserType.MOJANG); ++ } ++ ++ User user = response.getUser(); ++ ++ if (user != null && user.getId() != null) { ++ setUserid(user.getId()); ++ } else { ++ setUserid(getUsername()); ++ } ++ ++ isOnline = true; ++ accessToken = response.getAccessToken(); ++ profiles = response.getAvailableProfiles(); ++ setSelectedProfile(response.getSelectedProfile()); ++ getModifiableUserProperties().clear(); ++ ++ updateUserProperties(user); ++ } ++ ++ protected void updateUserProperties(User user) { ++ if (user == null) return; ++ ++ if (user.getProperties() != null) { ++ getModifiableUserProperties().putAll(user.getProperties()); ++ } ++ } ++ ++ protected void logInWithToken() throws AuthenticationException { ++ if (StringUtils.isBlank(getUserID())) { ++ if (StringUtils.isBlank(getUsername())) { ++ setUserid(getUsername()); ++ } else { ++ throw new InvalidCredentialsException("Invalid uuid & username"); ++ } ++ } ++ if (StringUtils.isBlank(getAuthenticatedToken())) { ++ throw new InvalidCredentialsException("Invalid access token"); ++ } ++ ++ LOGGER.info("Logging in with access token"); ++ ++ RefreshRequest request = new RefreshRequest(this); ++ RefreshResponse response = getAuthenticationService().makeRequest(ROUTE_REFRESH, request, RefreshResponse.class); ++ ++ if (!response.getClientToken().equals(getAuthenticationService().getClientToken())) { ++ throw new AuthenticationException("Server requested we change our client token. Don't know how to handle this!"); ++ } ++ ++ if (response.getSelectedProfile() != null) { ++ setUserType(response.getSelectedProfile().isLegacy() ? UserType.LEGACY : UserType.MOJANG); ++ } else if (ArrayUtils.isNotEmpty(response.getAvailableProfiles())) { ++ setUserType(response.getAvailableProfiles()[0].isLegacy() ? UserType.LEGACY : UserType.MOJANG); ++ } ++ ++ if (response.getUser() != null && response.getUser().getId() != null) { ++ setUserid(response.getUser().getId()); ++ } else { ++ setUserid(getUsername()); ++ } ++ ++ isOnline = true; ++ accessToken = response.getAccessToken(); ++ profiles = response.getAvailableProfiles(); ++ setSelectedProfile(response.getSelectedProfile()); ++ getModifiableUserProperties().clear(); ++ ++ updateUserProperties(response.getUser()); ++ } ++ ++ @Override ++ public void logOut() { ++ super.logOut(); ++ ++ accessToken = null; ++ profiles = null; ++ isOnline = false; ++ } ++ ++ @Override ++ public GameProfile[] getAvailableProfiles() { ++ return profiles; ++ } ++ ++ @Override ++ public boolean isLoggedIn() { ++ return StringUtils.isNotBlank(accessToken); ++ } ++ ++ @Override ++ public boolean canPlayOnline() { ++ return isLoggedIn() && getSelectedProfile() != null && isOnline; ++ } ++ ++ @Override ++ public void selectGameProfile(GameProfile profile) throws AuthenticationException { ++ if (!isLoggedIn()) { ++ throw new AuthenticationException("Cannot change game profile whilst not logged in"); ++ } ++ if (getSelectedProfile() != null) { ++ throw new AuthenticationException("Cannot change game profile. You must log out and back in."); ++ } ++ if (profile == null || !ArrayUtils.contains(profiles, profile)) { ++ throw new IllegalArgumentException("Invalid profile '" + profile + "'"); ++ } ++ ++ RefreshRequest request = new RefreshRequest(this, profile); ++ RefreshResponse response = getAuthenticationService().makeRequest(ROUTE_REFRESH, request, RefreshResponse.class); ++ ++ if (!response.getClientToken().equals(getAuthenticationService().getClientToken())) { ++ throw new AuthenticationException("Server requested we change our client token. Don't know how to handle this!"); ++ } ++ ++ isOnline = true; ++ accessToken = response.getAccessToken(); ++ setSelectedProfile(response.getSelectedProfile()); ++ } ++ ++ @Override ++ public void loadFromStorage(Map credentials) { ++ super.loadFromStorage(credentials); ++ ++ accessToken = String.valueOf(credentials.get(STORAGE_KEY_ACCESS_TOKEN)); ++ } ++ ++ @Override ++ public Map saveForStorage() { ++ Map result = super.saveForStorage(); ++ ++ if (StringUtils.isNotBlank(getAuthenticatedToken())) { ++ result.put(STORAGE_KEY_ACCESS_TOKEN, getAuthenticatedToken()); ++ } ++ ++ return result; ++ } ++ ++ /** ++ * @deprecated ++ */ ++ @Deprecated ++ public String getSessionToken() { ++ if (isLoggedIn() && getSelectedProfile() != null && canPlayOnline()) { ++ return String.format("token:%s:%s", getAuthenticatedToken(), getSelectedProfile().getId()); ++ } else { ++ return null; ++ } ++ } ++ ++ @Override ++ public String getAuthenticatedToken() { ++ return accessToken; ++ } ++ ++ public Agent getAgent() { ++ return agent; ++ } ++ ++ @Override ++ public String toString() { ++ return "YggdrasilAuthenticationService{" + ++ "agent=" + agent + ++ ", profiles=" + Arrays.toString(profiles) + ++ ", selectedProfile=" + getSelectedProfile() + ++ ", username='" + getUsername() + '\''+ ++ ", isLoggedIn=" + isLoggedIn() + ++ ", userType=" + getUserType() + ++ ", canPlayOnline=" + canPlayOnline() + ++ ", accessToken='" + accessToken + '\'' + ++ ", clientToken='" + getAuthenticationService().getClientToken() + '\'' + ++ '}'; ++ } ++ ++ @Override ++ public YggdrasilAuthenticationService getAuthenticationService() { ++ return (YggdrasilAuthenticationService) super.getAuthenticationService(); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/request/AuthenticationRequest.java b/src/main/java/org/spigotmc/authlib/yggdrasil/request/AuthenticationRequest.java +new file mode 100644 +index 0000000..f0457a4 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/request/AuthenticationRequest.java +@@ -0,0 +1,19 @@ ++package org.spigotmc.authlib.yggdrasil.request; ++ ++import org.spigotmc.authlib.Agent; ++import org.spigotmc.authlib.yggdrasil.YggdrasilUserAuthentication; ++ ++public class AuthenticationRequest { ++ private Agent agent; ++ private String username; ++ private String password; ++ private String clientToken; ++ private boolean requestUser = true; ++ ++ public AuthenticationRequest(YggdrasilUserAuthentication authenticationService, String username, String password) { ++ this.agent = authenticationService.getAgent(); ++ this.username = username; ++ this.clientToken = authenticationService.getAuthenticationService().getClientToken(); ++ this.password = password; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/request/InvalidateRequest.java b/src/main/java/org/spigotmc/authlib/yggdrasil/request/InvalidateRequest.java +new file mode 100644 +index 0000000..75ee0f3 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/request/InvalidateRequest.java +@@ -0,0 +1,13 @@ ++package org.spigotmc.authlib.yggdrasil.request; ++ ++import org.spigotmc.authlib.yggdrasil.YggdrasilUserAuthentication; ++ ++public class InvalidateRequest { ++ private String accessToken; ++ private String clientToken; ++ ++ public InvalidateRequest(YggdrasilUserAuthentication authenticationService) { ++ this.accessToken = authenticationService.getAuthenticatedToken(); ++ this.clientToken = authenticationService.getAuthenticationService().getClientToken(); ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/request/JoinMinecraftServerRequest.java b/src/main/java/org/spigotmc/authlib/yggdrasil/request/JoinMinecraftServerRequest.java +new file mode 100644 +index 0000000..a9ff35c +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/request/JoinMinecraftServerRequest.java +@@ -0,0 +1,9 @@ ++package org.spigotmc.authlib.yggdrasil.request; ++ ++import java.util.UUID; ++ ++public class JoinMinecraftServerRequest { ++ public String accessToken; ++ public UUID selectedProfile; ++ public String serverId; ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/request/RefreshRequest.java b/src/main/java/org/spigotmc/authlib/yggdrasil/request/RefreshRequest.java +new file mode 100644 +index 0000000..4f52290 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/request/RefreshRequest.java +@@ -0,0 +1,21 @@ ++package org.spigotmc.authlib.yggdrasil.request; ++ ++import org.spigotmc.authlib.GameProfile; ++import org.spigotmc.authlib.yggdrasil.YggdrasilUserAuthentication; ++ ++public class RefreshRequest { ++ private String clientToken; ++ private String accessToken; ++ private GameProfile selectedProfile; ++ private boolean requestUser = true; ++ ++ public RefreshRequest(YggdrasilUserAuthentication authenticationService) { ++ this(authenticationService, null); ++ } ++ ++ public RefreshRequest(YggdrasilUserAuthentication authenticationService, GameProfile profile) { ++ this.clientToken = authenticationService.getAuthenticationService().getClientToken(); ++ this.accessToken = authenticationService.getAuthenticatedToken(); ++ this.selectedProfile = profile; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/response/AuthenticationResponse.java b/src/main/java/org/spigotmc/authlib/yggdrasil/response/AuthenticationResponse.java +new file mode 100644 +index 0000000..0a03b1b +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/response/AuthenticationResponse.java +@@ -0,0 +1,35 @@ ++package org.spigotmc.authlib.yggdrasil.response; ++ ++import org.spigotmc.authlib.GameProfile; ++import org.spigotmc.authlib.yggdrasil.response.Response; ++import org.spigotmc.authlib.yggdrasil.response.User; ++ ++public class AuthenticationResponse extends Response ++{ ++ private String accessToken; ++ private String clientToken; ++ private GameProfile selectedProfile; ++ private GameProfile[] availableProfiles; ++ private User user; ++ ++ public String getAccessToken() { ++ return accessToken; ++ } ++ ++ public String getClientToken() { ++ return clientToken; ++ } ++ ++ public GameProfile[] getAvailableProfiles() { ++ return availableProfiles; ++ } ++ ++ public GameProfile getSelectedProfile() { ++ return selectedProfile; ++ } ++ ++ public User getUser() { ++ return user; ++ } ++ ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse.java b/src/main/java/org/spigotmc/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse.java +new file mode 100644 +index 0000000..f93acf0 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/response/HasJoinedMinecraftServerResponse.java +@@ -0,0 +1,20 @@ ++package org.spigotmc.authlib.yggdrasil.response; ++ ++import org.spigotmc.authlib.properties.PropertyMap; ++import org.spigotmc.authlib.yggdrasil.response.Response; ++ ++import java.util.UUID; ++ ++public class HasJoinedMinecraftServerResponse extends Response ++{ ++ private UUID id; ++ private PropertyMap properties; ++ ++ public UUID getId() { ++ return id; ++ } ++ ++ public PropertyMap getProperties() { ++ return properties; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/response/MinecraftProfilePropertiesResponse.java b/src/main/java/org/spigotmc/authlib/yggdrasil/response/MinecraftProfilePropertiesResponse.java +new file mode 100644 +index 0000000..4cf76e5 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/response/MinecraftProfilePropertiesResponse.java +@@ -0,0 +1,25 @@ ++package org.spigotmc.authlib.yggdrasil.response; ++ ++import org.spigotmc.authlib.properties.PropertyMap; ++import org.spigotmc.authlib.yggdrasil.response.Response; ++ ++import java.util.UUID; ++ ++public class MinecraftProfilePropertiesResponse extends Response ++{ ++ private UUID id; ++ private String name; ++ private PropertyMap properties; ++ ++ public UUID getId() { ++ return id; ++ } ++ ++ public String getName() { ++ return name; ++ } ++ ++ public PropertyMap getProperties() { ++ return properties; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/response/MinecraftTexturesPayload.java b/src/main/java/org/spigotmc/authlib/yggdrasil/response/MinecraftTexturesPayload.java +new file mode 100644 +index 0000000..72293ce +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/response/MinecraftTexturesPayload.java +@@ -0,0 +1,34 @@ ++package org.spigotmc.authlib.yggdrasil.response; ++ ++import org.spigotmc.authlib.minecraft.MinecraftProfileTexture; ++ ++import java.util.Map; ++import java.util.UUID; ++ ++public class MinecraftTexturesPayload { ++ private long timestamp; ++ private UUID profileId; ++ private String profileName; ++ private boolean isPublic; ++ private Map textures; ++ ++ public long getTimestamp() { ++ return timestamp; ++ } ++ ++ public UUID getProfileId() { ++ return profileId; ++ } ++ ++ public String getProfileName() { ++ return profileName; ++ } ++ ++ public boolean isPublic() { ++ return isPublic; ++ } ++ ++ public Map getTextures() { ++ return textures; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/response/ProfileSearchResultsResponse.java b/src/main/java/org/spigotmc/authlib/yggdrasil/response/ProfileSearchResultsResponse.java +new file mode 100644 +index 0000000..2c10758 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/response/ProfileSearchResultsResponse.java +@@ -0,0 +1,18 @@ ++package org.spigotmc.authlib.yggdrasil.response; ++ ++import org.spigotmc.authlib.GameProfile; ++import org.spigotmc.authlib.yggdrasil.response.Response; ++ ++public class ProfileSearchResultsResponse extends Response ++{ ++ private GameProfile[] profiles; ++ private int size; ++ ++ public GameProfile[] getProfiles() { ++ return profiles; ++ } ++ ++ public int getSize() { ++ return size; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/response/RefreshResponse.java b/src/main/java/org/spigotmc/authlib/yggdrasil/response/RefreshResponse.java +new file mode 100644 +index 0000000..d64667b +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/response/RefreshResponse.java +@@ -0,0 +1,34 @@ ++package org.spigotmc.authlib.yggdrasil.response; ++ ++import org.spigotmc.authlib.GameProfile; ++import org.spigotmc.authlib.yggdrasil.response.Response; ++import org.spigotmc.authlib.yggdrasil.response.User; ++ ++public class RefreshResponse extends Response ++{ ++ private String accessToken; ++ private String clientToken; ++ private GameProfile selectedProfile; ++ private GameProfile[] availableProfiles; ++ private User user; ++ ++ public String getAccessToken() { ++ return accessToken; ++ } ++ ++ public String getClientToken() { ++ return clientToken; ++ } ++ ++ public GameProfile[] getAvailableProfiles() { ++ return availableProfiles; ++ } ++ ++ public GameProfile getSelectedProfile() { ++ return selectedProfile; ++ } ++ ++ public User getUser() { ++ return user; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/response/Response.java b/src/main/java/org/spigotmc/authlib/yggdrasil/response/Response.java +new file mode 100644 +index 0000000..73c9da8 +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/response/Response.java +@@ -0,0 +1,19 @@ ++package org.spigotmc.authlib.yggdrasil.response; ++ ++public class Response { ++ private String error; ++ private String errorMessage; ++ private String cause; ++ ++ public String getError() { ++ return error; ++ } ++ ++ public String getCause() { ++ return cause; ++ } ++ ++ public String getErrorMessage() { ++ return errorMessage; ++ } ++} +diff --git a/src/main/java/org/spigotmc/authlib/yggdrasil/response/User.java b/src/main/java/org/spigotmc/authlib/yggdrasil/response/User.java +new file mode 100644 +index 0000000..e4893fd +--- /dev/null ++++ b/src/main/java/org/spigotmc/authlib/yggdrasil/response/User.java +@@ -0,0 +1,16 @@ ++package org.spigotmc.authlib.yggdrasil.response; ++ ++import org.spigotmc.authlib.properties.PropertyMap; ++ ++public class User { ++ private String id; ++ private PropertyMap properties; ++ ++ public String getId() { ++ return id; ++ } ++ ++ public PropertyMap getProperties() { ++ return properties; ++ } ++} +diff --git a/src/main/resources/yggdrasil_session_pubkey.der b/src/main/resources/yggdrasil_session_pubkey.der +new file mode 100644 +index 0000000000000000000000000000000000000000..9c79a3aa4771da1f15af37a2af0898f878ad816f +GIT binary patch +literal 550 +zcmV+>0@?jAf&wBi4F(A+hDe6@4FLfG1potr0uKN%f&vNxf&u{m%20R*skxUv>`)yD7%qARQ>bP36CtP}x@~Z9*VkPYUS6pWrZtT#q)8GCbf0*5+4Pyw;#Tq!Aq{bDO|Iw<43LnG*#4;?Z&LM71zXE@J`@_dMl`?qQ1 +z@3qR;mm=XG_-{G#4vhRxmZiQslrTtB_&7&0ECnD9)gZ}j`e&Je+s