Paper/CraftBukkit-Patches/0128-1.7.6-pre1-support.patch
Thinkofdeath 1e1ba0f873 Fix the console issue
QueueLogAppender doesn't like being added twice
2014-04-10 12:56:45 +01:00

3215 lines
122 KiB
Diff

From 53269d6cfc3f178e66c9bb1ecbaca96341e700d2 Mon Sep 17 00:00:00 2001
From: Thinkofdeath <thethinkofdeath@gmail.com>
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..24d101c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,6 +31,10 @@
<id>repobo-snap</id>
<url>http://repo.bukkit.org/content/groups/public</url>
</repository>
+ <repository>
+ <id>vanilla</id>
+ <url>https://libraries.minecraft.net/</url>
+ </repository>
</repositories>
<pluginRepositories>
@@ -114,6 +118,21 @@
<artifactId>trove4j</artifactId>
<version>3.0.3</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ <version>3.2.1</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.4</version>
+ </dependency>
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>1.6</version>
+ </dependency>
</dependencies>
<!-- This builds a completely 'ready to start' jar with all dependencies inside -->
diff --git a/src/main/java/net/minecraft/server/HandshakeListener.java b/src/main/java/net/minecraft/server/HandshakeListener.java
index 42539b4..490123f 100644
--- a/src/main/java/net/minecraft/server/HandshakeListener.java
+++ b/src/main/java/net/minecraft/server/HandshakeListener.java
@@ -1,5 +1,6 @@
package net.minecraft.server;
+import net.minecraft.util.io.netty.util.AttributeKey;
import net.minecraft.util.io.netty.util.concurrent.GenericFutureListener;
// CraftBukkit start
@@ -13,6 +14,7 @@ public class HandshakeListener implements PacketHandshakingInListener {
private static final HashMap<InetAddress, Long> throttleTracker = new HashMap<InetAddress, Long>();
private static int throttleCounter = 0;
// CraftBukkit end
+ public static final AttributeKey<Integer> protocolVersion = new AttributeKey<Integer>( "protocolVersion" ); // Spigot
private final MinecraftServer a;
private final NetworkManager b;
@@ -23,6 +25,12 @@ public class HandshakeListener implements PacketHandshakingInListener {
}
public void a(PacketHandshakingInSetProtocol packethandshakinginsetprotocol) {
+ // Spigot start
+ b.m.attr( protocolVersion ).set( 4 );
+ if (packethandshakinginsetprotocol.d() == 5) {
+ b.m.attr( protocolVersion ).set( 5 );
+ }
+ // Spigot end
switch (ProtocolOrdinalWrapper.a[packethandshakinginsetprotocol.c().ordinal()]) {
case 1:
this.b.a(EnumProtocol.LOGIN);
@@ -62,8 +70,7 @@ public class HandshakeListener implements PacketHandshakingInListener {
org.apache.logging.log4j.LogManager.getLogger().debug("Failed to check connection throttle", t);
}
// CraftBukkit end
-
- if (packethandshakinginsetprotocol.d() > 4) {
+ if (packethandshakinginsetprotocol.d() > 5) { // Spigot
chatcomponenttext = new ChatComponentText( org.spigotmc.SpigotConfig.outdatedServerMessage ); // Spigot
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..96b904c 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -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 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 +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 org.spigotmc.authlib.minecraft.MinecraftSessionService newSessionService;
// Spigot end
public MinecraftServer(OptionSet options, Proxy proxy) { // CraftBukkit - signature file -> OptionSet
@@ -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 org.spigotmc.authlib.yggdrasil.YggdrasilAuthenticationService(proxy, UUID.randomUUID().toString()).createMinecraftSessionService();
// CraftBukkit start
this.options = options;
diff --git a/src/main/java/net/minecraft/server/NetworkManager.java b/src/main/java/net/minecraft/server/NetworkManager.java
index f6cca80..56bfe34 100644
--- a/src/main/java/net/minecraft/server/NetworkManager.java
+++ b/src/main/java/net/minecraft/server/NetworkManager.java
@@ -35,7 +35,7 @@ public class NetworkManager extends SimpleChannelInboundHandler {
private final boolean j;
private final Queue k = Queues.newConcurrentLinkedQueue();
private final Queue l = Queues.newConcurrentLinkedQueue();
- private Channel m;
+ public Channel m; // Spigot
public SocketAddress n; // Spigot
public String spoofedUUID; // Spigot
private PacketListener o;
diff --git a/src/main/java/net/minecraft/server/Packet.java b/src/main/java/net/minecraft/server/Packet.java
index 592ffc5..190da32 100644
--- a/src/main/java/net/minecraft/server/Packet.java
+++ b/src/main/java/net/minecraft/server/Packet.java
@@ -47,6 +47,12 @@ public abstract class Packet {
public abstract void b(PacketDataSerializer packetdataserializer) throws IOException; // CraftBukkit - added throws
+ // Spigot start
+ public void writeSnapshot(PacketDataSerializer packetDataSerializer) throws IOException {
+ b( packetDataSerializer );
+ }
+ // Spigot end
+
public abstract void handle(PacketListener packetlistener);
public boolean a() {
diff --git a/src/main/java/net/minecraft/server/PacketEncoder.java b/src/main/java/net/minecraft/server/PacketEncoder.java
new file mode 100644
index 0000000..ab00152
--- /dev/null
+++ b/src/main/java/net/minecraft/server/PacketEncoder.java
@@ -0,0 +1,52 @@
+package net.minecraft.server;
+
+import java.io.IOException;
+
+import net.minecraft.util.com.google.common.collect.BiMap;
+import net.minecraft.util.io.netty.buffer.ByteBuf;
+import net.minecraft.util.io.netty.channel.ChannelHandlerContext;
+import net.minecraft.util.io.netty.handler.codec.MessageToByteEncoder;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.Marker;
+import org.apache.logging.log4j.MarkerManager;
+
+public class PacketEncoder extends MessageToByteEncoder {
+
+ private static final Logger a = LogManager.getLogger();
+ private static final Marker b = MarkerManager.getMarker("PACKET_SENT", NetworkManager.b);
+ private final NetworkStatistics c;
+
+ public PacketEncoder(NetworkStatistics networkstatistics) {
+ this.c = networkstatistics;
+ }
+
+ protected void a(ChannelHandlerContext channelhandlercontext, Packet packet, ByteBuf bytebuf) throws IOException
+ {
+ Integer integer = (Integer) ((BiMap) channelhandlercontext.channel().attr(NetworkManager.f).get()).inverse().get(packet.getClass());
+
+ if (a.isDebugEnabled()) {
+ a.debug(b, "OUT: [{}:{}] {}[{}]", new Object[] { channelhandlercontext.channel().attr(NetworkManager.d).get(), integer, packet.getClass().getName(), packet.b()});
+ }
+
+ if (integer == null) {
+ throw new IOException("Can\'t serialize unregistered packet");
+ } else {
+ PacketDataSerializer packetdataserializer = new PacketDataSerializer(bytebuf);
+
+ packetdataserializer.b(integer.intValue());
+ if ( channelhandlercontext.channel().attr( HandshakeListener.protocolVersion ).get() == 4)
+ {
+ packet.b( packetdataserializer );
+ } else {
+ packet.writeSnapshot( packetdataserializer );
+ }
+ this.c.b(integer.intValue(), (long) packetdataserializer.readableBytes());
+ }
+ }
+
+ protected void encode(ChannelHandlerContext channelhandlercontext, Object object, ByteBuf bytebuf) throws IOException
+ {
+ this.a(channelhandlercontext, (Packet) object, bytebuf);
+ }
+}
diff --git a/src/main/java/net/minecraft/server/PacketLoginOutSuccess.java b/src/main/java/net/minecraft/server/PacketLoginOutSuccess.java
new file mode 100644
index 0000000..3aa93cd
--- /dev/null
+++ b/src/main/java/net/minecraft/server/PacketLoginOutSuccess.java
@@ -0,0 +1,51 @@
+package net.minecraft.server;
+
+import net.minecraft.util.com.mojang.authlib.GameProfile;
+
+import java.io.IOException;
+
+public class PacketLoginOutSuccess extends Packet {
+
+ private GameProfile a;
+
+ public PacketLoginOutSuccess() {}
+
+ public PacketLoginOutSuccess(GameProfile gameprofile) {
+ this.a = gameprofile;
+ }
+
+ public void a(PacketDataSerializer packetdataserializer) throws IOException
+ {
+ String s = packetdataserializer.c(36);
+ String s1 = packetdataserializer.c(16);
+
+ this.a = new GameProfile(s, s1);
+ }
+
+ public void b(PacketDataSerializer packetdataserializer) throws IOException
+ {
+ packetdataserializer.a(this.a.getId());
+ packetdataserializer.a(this.a.getName());
+ }
+
+ // Spigot start
+ @Override
+ public void writeSnapshot(PacketDataSerializer packetdataserializer) throws IOException
+ {
+ packetdataserializer.a( EntityHuman.a( this.a ).toString() );
+ packetdataserializer.a( this.a.getName());
+ }
+ // Spigot end
+
+ public void a(PacketLoginOutListener packetloginoutlistener) {
+ packetloginoutlistener.a(this);
+ }
+
+ public boolean a() {
+ return true;
+ }
+
+ public void handle(PacketListener packetlistener) {
+ this.a((PacketLoginOutListener) packetlistener);
+ }
+}
diff --git a/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java b/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java
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 org.spigotmc.authlib.properties.Property;
import net.minecraft.util.com.mojang.authlib.GameProfile;
import java.io.IOException; // CraftBukkit
@@ -60,6 +61,41 @@ public class PacketPlayOutNamedEntitySpawn extends Packet {
this.i.a(packetdataserializer);
}
+ // Spigot start
+ @Override
+ public void writeSnapshot(PacketDataSerializer packetdataserializer) throws IOException
+ { // CraftBukkit - added throws
+ packetdataserializer.b( this.a );
+ packetdataserializer.a( EntityHuman.a( this.b ).toString() );
+ packetdataserializer.a( this.b.getName().length() > 16 ? this.b.getName().substring( 0, 16 ) : this.b.getName() ); // CraftBukkit - Limit name length to 16 characters
+
+ if ( this.b instanceof ThreadPlayerLookupUUID.NewGameProfileWrapper )
+ {
+ org.spigotmc.authlib.GameProfile newProfile = ((ThreadPlayerLookupUUID.NewGameProfileWrapper) b).newProfile;
+ packetdataserializer.b( newProfile.getProperties().size() );
+ for ( String key : newProfile.getProperties().keys() )
+ {
+ for ( Property prop : newProfile.getProperties().get( key ) )
+ {
+ packetdataserializer.a( prop.getName() );
+ packetdataserializer.a( prop.getValue() );
+ packetdataserializer.a( prop.getSignature() );
+ }
+ }
+ } else {
+ packetdataserializer.b( 0 );
+ }
+ packetdataserializer.writeInt( this.c );
+ packetdataserializer.writeInt( this.d );
+ packetdataserializer.writeInt( this.e );
+ packetdataserializer.writeByte( this.f );
+ packetdataserializer.writeByte( this.g );
+ packetdataserializer.writeShort( this.h );
+ this.i.a( packetdataserializer );
+ }
+
+ // Spigot end
+
public void a(PacketPlayOutListener packetplayoutlistener) {
packetplayoutlistener.a(this);
}
diff --git a/src/main/java/net/minecraft/server/PacketPlayOutTileEntityData.java b/src/main/java/net/minecraft/server/PacketPlayOutTileEntityData.java
new file mode 100644
index 0000000..005f1fe
--- /dev/null
+++ b/src/main/java/net/minecraft/server/PacketPlayOutTileEntityData.java
@@ -0,0 +1,61 @@
+package net.minecraft.server;
+
+public class PacketPlayOutTileEntityData extends Packet {
+
+ private int a;
+ private int b;
+ private int c;
+ private int d;
+ private NBTTagCompound e;
+
+ public PacketPlayOutTileEntityData() {}
+
+ public PacketPlayOutTileEntityData(int i, int j, int k, int l, NBTTagCompound nbttagcompound) {
+ this.a = i;
+ this.b = j;
+ this.c = k;
+ this.d = l;
+ this.e = nbttagcompound;
+ }
+
+ public void a(PacketDataSerializer packetdataserializer) {
+ this.a = packetdataserializer.readInt();
+ this.b = packetdataserializer.readShort();
+ this.c = packetdataserializer.readInt();
+ this.d = packetdataserializer.readUnsignedByte();
+ this.e = packetdataserializer.b();
+ }
+
+ public void b(PacketDataSerializer packetdataserializer) {
+ packetdataserializer.writeInt(this.a);
+ packetdataserializer.writeShort(this.b);
+ packetdataserializer.writeInt(this.c);
+ packetdataserializer.writeByte((byte) this.d);
+ packetdataserializer.a(this.e);
+ }
+
+ @Override
+ public void writeSnapshot(PacketDataSerializer packetdataserializer)
+ {
+ packetdataserializer.writeInt(this.a);
+ packetdataserializer.writeShort(this.b);
+ packetdataserializer.writeInt(this.c);
+ packetdataserializer.writeByte((byte) this.d);
+ if ( this.e.hasKey( "ExtraType" ) )
+ {
+ NBTTagCompound profile = new NBTTagCompound();
+ profile.setString( "Name", this.e.getString( "ExtraType" ) );
+ profile.setString( "Id", "" );
+ this.e.set( "Owner", profile );
+ }
+ packetdataserializer.a(this.e);
+ }
+
+ public void a(PacketPlayOutListener packetplayoutlistener) {
+ packetplayoutlistener.a(this);
+ }
+
+ public void handle(PacketListener packetlistener) {
+ this.a((PacketPlayOutListener) packetlistener);
+ }
+}
diff --git a/src/main/java/net/minecraft/server/PacketStatusListener.java b/src/main/java/net/minecraft/server/PacketStatusListener.java
index f9da452..fa493ca 100644
--- a/src/main/java/net/minecraft/server/PacketStatusListener.java
+++ b/src/main/java/net/minecraft/server/PacketStatusListener.java
@@ -4,6 +4,7 @@ import java.net.InetSocketAddress;
// CraftBukkit start
import java.util.Iterator;
+import java.util.UUID;
import org.bukkit.craftbukkit.util.CraftIconCache;
import org.bukkit.entity.Player;
@@ -117,13 +118,22 @@ public class PacketStatusListener implements PacketStatusInListener {
profiles = profiles.subList( 0, Math.min( profiles.size(), org.spigotmc.SpigotConfig.playerSample ) ); // Cap the sample to n (or less) displayed players, ie: Vanilla behaviour
}
// Spigot End
- playerSample.a(profiles.toArray(new GameProfile[profiles.size()]));
+ // Spigot start
+ GameProfile[] aProfiles = profiles.toArray( new GameProfile[ profiles.size() ] );
+ if ( networkManager.m.attr( HandshakeListener.protocolVersion ).get() == 5 )
+ {
+ for (int i = 0; i < aProfiles.length; i++) {
+ aProfiles[i] = new GameProfileWrapper( EntityHuman.a( aProfiles[i] ), aProfiles[i].getName() );
+ }
+ }
+ // Spigot end
+ playerSample.a(aProfiles);
ServerPing ping = new ServerPing();
ping.setFavicon(event.icon.value);
ping.setMOTD(new ChatComponentText(event.getMotd()));
ping.setPlayerSample(playerSample);
- ping.setServerInfo(new ServerPingServerData(minecraftServer.getServerModName() + " " + minecraftServer.getVersion(), 4)); // TODO: Update when protocol changes
+ ping.setServerInfo(new ServerPingServerData(minecraftServer.getServerModName() + " " + minecraftServer.getVersion(), networkManager.m.attr( HandshakeListener.protocolVersion ).get())); // Spigot // TODO: Update when protocol changes
this.networkManager.handle(new PacketStatusOutServerInfo(ping), new GenericFutureListener[0]);
// CraftBukkit end
@@ -132,4 +142,23 @@ public class PacketStatusListener implements PacketStatusInListener {
public void a(PacketStatusInPing packetstatusinping) {
this.networkManager.handle(new PacketStatusOutPong(packetstatusinping.c()), new GenericFutureListener[0]);
}
+
+
+ // Spigot start
+ private static class GameProfileWrapper extends GameProfile {
+
+ private final UUID uuid;
+
+ public GameProfileWrapper(UUID uuid, String name) {
+ super("", name);
+ this.uuid = uuid;
+ }
+
+ @Override
+ public String getId() {
+ return uuid.toString();
+ }
+ }
+
+ // Spigot end
}
diff --git a/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java b/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java
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 {
}
// Spigot End
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));
+ 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
fireLoginEvents();
@@ -95,4 +97,15 @@ class ThreadPlayerLookupUUID extends Thread {
}
// CraftBukkit end
}
+
+ public static class NewGameProfileWrapper extends GameProfile {
+
+ public org.spigotmc.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.
+ * <p />
+ * 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.
+ * </p>
+ * 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<String, Object> 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<Map<String, String>> list = (List<Map<String, String>>) credentials.get(STORAGE_KEY_USER_PROPERTIES);
+
+ for (Map<String, String> 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<Map<String, String>> list = (List<Map<String, String>>) credentials.get(STORAGE_KEY_PROFILE_PROPERTIES);
+ for (Map<String, String> 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<String, Object> saveForStorage() {
+ Map<String, Object> result = new HashMap<String, Object>();
+
+ 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<Map<String, String>> properties = new ArrayList<Map<String, String>>();
+ for (Property userProperty : getUserProperties().values()) {
+ Map<String, String> property = new HashMap<String, String>();
+ 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<Map<String, String>> properties = new ArrayList<Map<String, String>>();
+ for (Property profileProperty : selectedProfile.getProperties().values()) {
+ Map<String, String> property = new HashMap<String, String>();
+ 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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<String, Object> query) {
+ if (query == null) return "";
+ StringBuilder builder = new StringBuilder();
+
+ for (Map.Entry<String, Object> 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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.
+ * <p />
+ * If the user is {@link #isLoggedIn() already logged in} this method will <b>not</b> 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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}.
+ * <p />
+ * 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.
+ * <p />
+ * 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<String, Object> credentials);
+
+ /**
+ * Saves any known credentials to a Map and returns the result.
+ * <p />
+ * 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<String, Object> saveForStorage();
+
+ /**
+ * Sets the username to authenticate with for the next {@link #logIn()} call.
+ * <p />
+ * 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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.
+ * <p />
+ * This method will return an empty Multimap if the user is not logged in.
+ * <p />
+ * The returned Multimap will ignore any changes.
+ *
+ * @return Multimap of user properties.
+ */
+ public PropertyMap getUserProperties();
+
+ /**
+ * Gets the type of the currently logged in user.
+ * <p />
+ * 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<String, UserType> BY_NAME = new HashMap<String, UserType>();
+ 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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<String, Object> arguments = new HashMap<String, Object>();
+
+ 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<String, Object> arguments = new HashMap<String, Object>();
+
+ 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<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(GameProfile profile, boolean requireSecure) {
+ return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
+ }
+
+ @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<String, Object> args = new HashMap<String, Object>();
+ 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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.
+ * <p />
+ * 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}.
+ * <p />
+ * 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<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(GameProfile profile, boolean requireSecure);
+
+ /**
+ * Fills a profile with all known properties from the session service.
+ * <p />
+ * 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<String, Property> {
+ private final Multimap<String, Property> properties = LinkedHashMultimap.create();
+
+ @Override
+ protected Multimap<String, Property> delegate() {
+ return properties;
+ }
+
+ public static class Serializer implements JsonSerializer<PropertyMap>, JsonDeserializer<PropertyMap> {
+ @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<String, JsonElement> 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<UUID> {
+ @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 extends Response> T makeRequest(URL url, Object input, Class<T> 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<GameProfile>, JsonDeserializer<GameProfile> {
+ @Override
+ public GameProfile deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ JsonObject object = (JsonObject) json;
+ UUID id = object.has("id") ? context.<UUID>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<ProfileCriteria> criteria = Sets.newHashSet();
+
+ for (String name : names) {
+ if (!Strings.isNullOrEmpty(name)) {
+ criteria.add(new ProfileCriteria(name, agent));
+ }
+ }
+
+ Exception exception = null;
+ Set<ProfileCriteria> 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<String, Object> arguments = new HashMap<String, Object>();
+
+ 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<MinecraftProfileTexture.Type, MinecraftProfileTexture> getTextures(GameProfile profile, boolean requireSecure) {
+ Property textureProperty = Iterables.getFirst(profile.getProperties().get("textures"), null);
+ if (textureProperty == null) return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
+
+ if (!textureProperty.hasSignature()) {
+ LOGGER.error("Signature is missing from textures payload");
+ return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
+ }
+
+ if (!textureProperty.isSignatureValid(publicKey)) {
+ LOGGER.error("Textures payload has been tampered with (signature invalid)");
+ return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
+ }
+
+ 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<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
+ }
+
+ 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<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
+ }
+
+ 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<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
+ }
+
+ if (requireSecure) {
+ if (result.isPublic()) {
+ LOGGER.error("Decrypted textures payload was public but we require secure data");
+ return new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
+ }
+
+ 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<MinecraftProfileTexture.Type, MinecraftProfileTexture>();
+ }
+ }
+
+ return result.getTextures() == null ? new HashMap<MinecraftProfileTexture.Type, MinecraftProfileTexture>() : 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<String, Object> credentials) {
+ super.loadFromStorage(credentials);
+
+ accessToken = String.valueOf(credentials.get(STORAGE_KEY_ACCESS_TOKEN));
+ }
+
+ @Override
+ public Map<String, Object> saveForStorage() {
+ Map<String, Object> 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<MinecraftProfileTexture.Type, MinecraftProfileTexture> textures;
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public UUID getProfileId() {
+ return profileId;
+ }
+
+ public String getProfileName() {
+ return profileName;
+ }
+
+ public boolean isPublic() {
+ return isPublic;
+ }
+
+ public Map<MinecraftProfileTexture.Type, MinecraftProfileTexture> 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<HENE
z&!JuY`QK6(FP9q=N`7P@ul8tRwF`L+%Y|cy32_&oMNui=#uIG7_~G4{t&ki<09H2U
z?Bpa=a32Lg_GfU7t07#1?MI`kbhXzNT$DV85iI1EsAEC3y*v=!UovsLh*IDXk^wUt
zJqGc2vwy{g;qpAFl}(wKV?~(ZoS3Tm3P<JRX*5qh@N5mP79(r5&lSvgd1!S#<(w?l
zzay9gwae%tU<WIX#m5Qq8P|MYa$|vXY2x(rLUaFrgapyNMD78~8$o}Aa`4nTvem4O
z_mm>>`)yD7%qARQ>bP36CtP}x@~Z9*VkPYUS6pWrZtT#q)8GCbf<y%=Kl9}scMO*I
zu#RDr=*Pn=>0*5+4Pyw;#Tq!Aq{bDO|Iw<43LnG*#4;?Z&LM<E(on|TyupXcARxlP
zx(fh&7Wy-JstTf-Ou!%gP$P$tMJ9OC`?kdSwnSG-uxlMc5`)hqy<Z^+&P&YW)6Srd
zk&z12viIiGJvY-w^ojXZ0px9ncp@#3(di|9vVw@kXW97{c>71zXE@J`@_dMl`?qQ1
z@3qR;mm=XG_-{G#4vhRxmZiQslrTtB_&7&0ECnD9)gZ}j`e&Je+s<w&Z{mU+*|6l^
oy;TO;3<Dz5n;~AJ@er{Uvv!~{@;#%uk6Tk{6LMDH0s{d60Z%FkUH||9
literal 0
HcmV?d00001
--
1.8.5.2.msysgit.0