3224 lines
122 KiB
Diff
3224 lines
122 KiB
Diff
From 809c29fa77ca09648e0035f1b1861a13dace5442 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.8 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..34c0703 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;
|
|
@@ -871,7 +873,7 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo
|
|
}
|
|
|
|
public String getVersion() {
|
|
- return "1.7.5";
|
|
+ return "1.7.8";
|
|
}
|
|
|
|
public int C() {
|
|
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
|
|
|