From 53269d6cfc3f178e66c9bb1ecbaca96341e700d2 Mon Sep 17 00:00:00 2001 From: Thinkofdeath Date: Thu, 3 Apr 2014 17:04:18 +0100 Subject: [PATCH] 1.7.6-pre1 support diff --git a/pom.xml b/pom.xml index c8285e0..24d101c 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,10 @@ repobo-snap http://repo.bukkit.org/content/groups/public + + vanilla + https://libraries.minecraft.net/ + @@ -114,6 +118,21 @@ trove4j 3.0.3 + + org.apache.commons + commons-lang3 + 3.2.1 + + + commons-io + commons-io + 2.4 + + + commons-codec + commons-codec + 1.6 + 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 throttleTracker = new HashMap(); private static int throttleCounter = 0; // CraftBukkit end + public static final AttributeKey protocolVersion = new AttributeKey( "protocolVersion" ); // Spigot private final MinecraftServer a; private final NetworkManager b; @@ -23,6 +25,12 @@ public class HandshakeListener implements PacketHandshakingInListener { } public void a(PacketHandshakingInSetProtocol packethandshakinginsetprotocol) { + // Spigot start + b.m.attr( protocolVersion ).set( 4 ); + if (packethandshakinginsetprotocol.d() == 5) { + b.m.attr( protocolVersion ).set( 5 ); + } + // Spigot end switch (ProtocolOrdinalWrapper.a[packethandshakinginsetprotocol.c().ordinal()]) { case 1: this.b.a(EnumProtocol.LOGIN); @@ -62,8 +70,7 @@ public class HandshakeListener implements PacketHandshakingInListener { org.apache.logging.log4j.LogManager.getLogger().debug("Failed to check connection throttle", t); } // CraftBukkit end - - if (packethandshakinginsetprotocol.d() > 4) { + if (packethandshakinginsetprotocol.d() > 5) { // Spigot chatcomponenttext = new ChatComponentText( org.spigotmc.SpigotConfig.outdatedServerMessage ); // Spigot this.b.handle(new PacketLoginOutDisconnect(chatcomponenttext), new GenericFutureListener[0]); this.b.close(chatcomponenttext); diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java index 8ce9dd7..96b904c 100644 --- a/src/main/java/net/minecraft/server/MinecraftServer.java +++ b/src/main/java/net/minecraft/server/MinecraftServer.java @@ -10,13 +10,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; -import java.util.Iterator; import java.util.List; import java.util.Random; import java.util.UUID; import java.util.concurrent.Callable; import javax.imageio.ImageIO; +import org.spigotmc.authlib.yggdrasil.YggdrasilMinecraftSessionService; import net.minecraft.util.com.google.common.base.Charsets; import net.minecraft.util.com.mojang.authlib.GameProfile; import net.minecraft.util.com.mojang.authlib.minecraft.MinecraftSessionService; @@ -107,6 +107,7 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo private static final int TICK_TIME = 1000000000 / TPS; private static final int SAMPLE_INTERVAL = 100; public final double[] recentTps = new double[ 3 ]; + public final org.spigotmc.authlib.minecraft.MinecraftSessionService newSessionService; // Spigot end public MinecraftServer(OptionSet options, Proxy proxy) { // CraftBukkit - signature file -> OptionSet @@ -117,6 +118,7 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo this.n = new CommandDispatcher(); // this.convertable = new WorldLoaderServer(file1); // CraftBukkit - moved to DedicatedServer.init this.S = (new YggdrasilAuthenticationService(proxy, UUID.randomUUID().toString())).createMinecraftSessionService(); + newSessionService = new org.spigotmc.authlib.yggdrasil.YggdrasilAuthenticationService(proxy, UUID.randomUUID().toString()).createMinecraftSessionService(); // CraftBukkit start this.options = options; diff --git a/src/main/java/net/minecraft/server/NetworkManager.java b/src/main/java/net/minecraft/server/NetworkManager.java index f6cca80..56bfe34 100644 --- a/src/main/java/net/minecraft/server/NetworkManager.java +++ b/src/main/java/net/minecraft/server/NetworkManager.java @@ -35,7 +35,7 @@ public class NetworkManager extends SimpleChannelInboundHandler { private final boolean j; private final Queue k = Queues.newConcurrentLinkedQueue(); private final Queue l = Queues.newConcurrentLinkedQueue(); - private Channel m; + public Channel m; // Spigot public SocketAddress n; // Spigot public String spoofedUUID; // Spigot private PacketListener o; diff --git a/src/main/java/net/minecraft/server/Packet.java b/src/main/java/net/minecraft/server/Packet.java index 592ffc5..190da32 100644 --- a/src/main/java/net/minecraft/server/Packet.java +++ b/src/main/java/net/minecraft/server/Packet.java @@ -47,6 +47,12 @@ public abstract class Packet { public abstract void b(PacketDataSerializer packetdataserializer) throws IOException; // CraftBukkit - added throws + // Spigot start + public void writeSnapshot(PacketDataSerializer packetDataSerializer) throws IOException { + b( packetDataSerializer ); + } + // Spigot end + public abstract void handle(PacketListener packetlistener); public boolean a() { diff --git a/src/main/java/net/minecraft/server/PacketEncoder.java b/src/main/java/net/minecraft/server/PacketEncoder.java new file mode 100644 index 0000000..ab00152 --- /dev/null +++ b/src/main/java/net/minecraft/server/PacketEncoder.java @@ -0,0 +1,52 @@ +package net.minecraft.server; + +import java.io.IOException; + +import net.minecraft.util.com.google.common.collect.BiMap; +import net.minecraft.util.io.netty.buffer.ByteBuf; +import net.minecraft.util.io.netty.channel.ChannelHandlerContext; +import net.minecraft.util.io.netty.handler.codec.MessageToByteEncoder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.MarkerManager; + +public class PacketEncoder extends MessageToByteEncoder { + + private static final Logger a = LogManager.getLogger(); + private static final Marker b = MarkerManager.getMarker("PACKET_SENT", NetworkManager.b); + private final NetworkStatistics c; + + public PacketEncoder(NetworkStatistics networkstatistics) { + this.c = networkstatistics; + } + + protected void a(ChannelHandlerContext channelhandlercontext, Packet packet, ByteBuf bytebuf) throws IOException + { + Integer integer = (Integer) ((BiMap) channelhandlercontext.channel().attr(NetworkManager.f).get()).inverse().get(packet.getClass()); + + if (a.isDebugEnabled()) { + a.debug(b, "OUT: [{}:{}] {}[{}]", new Object[] { channelhandlercontext.channel().attr(NetworkManager.d).get(), integer, packet.getClass().getName(), packet.b()}); + } + + if (integer == null) { + throw new IOException("Can\'t serialize unregistered packet"); + } else { + PacketDataSerializer packetdataserializer = new PacketDataSerializer(bytebuf); + + packetdataserializer.b(integer.intValue()); + if ( channelhandlercontext.channel().attr( HandshakeListener.protocolVersion ).get() == 4) + { + packet.b( packetdataserializer ); + } else { + packet.writeSnapshot( packetdataserializer ); + } + this.c.b(integer.intValue(), (long) packetdataserializer.readableBytes()); + } + } + + protected void encode(ChannelHandlerContext channelhandlercontext, Object object, ByteBuf bytebuf) throws IOException + { + this.a(channelhandlercontext, (Packet) object, bytebuf); + } +} diff --git a/src/main/java/net/minecraft/server/PacketLoginOutSuccess.java b/src/main/java/net/minecraft/server/PacketLoginOutSuccess.java new file mode 100644 index 0000000..3aa93cd --- /dev/null +++ b/src/main/java/net/minecraft/server/PacketLoginOutSuccess.java @@ -0,0 +1,51 @@ +package net.minecraft.server; + +import net.minecraft.util.com.mojang.authlib.GameProfile; + +import java.io.IOException; + +public class PacketLoginOutSuccess extends Packet { + + private GameProfile a; + + public PacketLoginOutSuccess() {} + + public PacketLoginOutSuccess(GameProfile gameprofile) { + this.a = gameprofile; + } + + public void a(PacketDataSerializer packetdataserializer) throws IOException + { + String s = packetdataserializer.c(36); + String s1 = packetdataserializer.c(16); + + this.a = new GameProfile(s, s1); + } + + public void b(PacketDataSerializer packetdataserializer) throws IOException + { + packetdataserializer.a(this.a.getId()); + packetdataserializer.a(this.a.getName()); + } + + // Spigot start + @Override + public void writeSnapshot(PacketDataSerializer packetdataserializer) throws IOException + { + packetdataserializer.a( EntityHuman.a( this.a ).toString() ); + packetdataserializer.a( this.a.getName()); + } + // Spigot end + + public void a(PacketLoginOutListener packetloginoutlistener) { + packetloginoutlistener.a(this); + } + + public boolean a() { + return true; + } + + public void handle(PacketListener packetlistener) { + this.a((PacketLoginOutListener) packetlistener); + } +} diff --git a/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java b/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java index 8bab528..0884047 100644 --- a/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java +++ b/src/main/java/net/minecraft/server/PacketPlayOutNamedEntitySpawn.java @@ -2,6 +2,7 @@ package net.minecraft.server; import java.util.List; +import org.spigotmc.authlib.properties.Property; import net.minecraft.util.com.mojang.authlib.GameProfile; import java.io.IOException; // CraftBukkit @@ -60,6 +61,41 @@ public class PacketPlayOutNamedEntitySpawn extends Packet { this.i.a(packetdataserializer); } + // Spigot start + @Override + public void writeSnapshot(PacketDataSerializer packetdataserializer) throws IOException + { // CraftBukkit - added throws + packetdataserializer.b( this.a ); + packetdataserializer.a( EntityHuman.a( this.b ).toString() ); + packetdataserializer.a( this.b.getName().length() > 16 ? this.b.getName().substring( 0, 16 ) : this.b.getName() ); // CraftBukkit - Limit name length to 16 characters + + if ( this.b instanceof ThreadPlayerLookupUUID.NewGameProfileWrapper ) + { + org.spigotmc.authlib.GameProfile newProfile = ((ThreadPlayerLookupUUID.NewGameProfileWrapper) b).newProfile; + packetdataserializer.b( newProfile.getProperties().size() ); + for ( String key : newProfile.getProperties().keys() ) + { + for ( Property prop : newProfile.getProperties().get( key ) ) + { + packetdataserializer.a( prop.getName() ); + packetdataserializer.a( prop.getValue() ); + packetdataserializer.a( prop.getSignature() ); + } + } + } else { + packetdataserializer.b( 0 ); + } + packetdataserializer.writeInt( this.c ); + packetdataserializer.writeInt( this.d ); + packetdataserializer.writeInt( this.e ); + packetdataserializer.writeByte( this.f ); + packetdataserializer.writeByte( this.g ); + packetdataserializer.writeShort( this.h ); + this.i.a( packetdataserializer ); + } + + // Spigot end + public void a(PacketPlayOutListener packetplayoutlistener) { packetplayoutlistener.a(this); } diff --git a/src/main/java/net/minecraft/server/PacketPlayOutTileEntityData.java b/src/main/java/net/minecraft/server/PacketPlayOutTileEntityData.java new file mode 100644 index 0000000..005f1fe --- /dev/null +++ b/src/main/java/net/minecraft/server/PacketPlayOutTileEntityData.java @@ -0,0 +1,61 @@ +package net.minecraft.server; + +public class PacketPlayOutTileEntityData extends Packet { + + private int a; + private int b; + private int c; + private int d; + private NBTTagCompound e; + + public PacketPlayOutTileEntityData() {} + + public PacketPlayOutTileEntityData(int i, int j, int k, int l, NBTTagCompound nbttagcompound) { + this.a = i; + this.b = j; + this.c = k; + this.d = l; + this.e = nbttagcompound; + } + + public void a(PacketDataSerializer packetdataserializer) { + this.a = packetdataserializer.readInt(); + this.b = packetdataserializer.readShort(); + this.c = packetdataserializer.readInt(); + this.d = packetdataserializer.readUnsignedByte(); + this.e = packetdataserializer.b(); + } + + public void b(PacketDataSerializer packetdataserializer) { + packetdataserializer.writeInt(this.a); + packetdataserializer.writeShort(this.b); + packetdataserializer.writeInt(this.c); + packetdataserializer.writeByte((byte) this.d); + packetdataserializer.a(this.e); + } + + @Override + public void writeSnapshot(PacketDataSerializer packetdataserializer) + { + packetdataserializer.writeInt(this.a); + packetdataserializer.writeShort(this.b); + packetdataserializer.writeInt(this.c); + packetdataserializer.writeByte((byte) this.d); + if ( this.e.hasKey( "ExtraType" ) ) + { + NBTTagCompound profile = new NBTTagCompound(); + profile.setString( "Name", this.e.getString( "ExtraType" ) ); + profile.setString( "Id", "" ); + this.e.set( "Owner", profile ); + } + packetdataserializer.a(this.e); + } + + public void a(PacketPlayOutListener packetplayoutlistener) { + packetplayoutlistener.a(this); + } + + public void handle(PacketListener packetlistener) { + this.a((PacketPlayOutListener) packetlistener); + } +} diff --git a/src/main/java/net/minecraft/server/PacketStatusListener.java b/src/main/java/net/minecraft/server/PacketStatusListener.java index f9da452..fa493ca 100644 --- a/src/main/java/net/minecraft/server/PacketStatusListener.java +++ b/src/main/java/net/minecraft/server/PacketStatusListener.java @@ -4,6 +4,7 @@ import java.net.InetSocketAddress; // CraftBukkit start import java.util.Iterator; +import java.util.UUID; import org.bukkit.craftbukkit.util.CraftIconCache; import org.bukkit.entity.Player; @@ -117,13 +118,22 @@ public class PacketStatusListener implements PacketStatusInListener { profiles = profiles.subList( 0, Math.min( profiles.size(), org.spigotmc.SpigotConfig.playerSample ) ); // Cap the sample to n (or less) displayed players, ie: Vanilla behaviour } // Spigot End - playerSample.a(profiles.toArray(new GameProfile[profiles.size()])); + // Spigot start + GameProfile[] aProfiles = profiles.toArray( new GameProfile[ profiles.size() ] ); + if ( networkManager.m.attr( HandshakeListener.protocolVersion ).get() == 5 ) + { + for (int i = 0; i < aProfiles.length; i++) { + aProfiles[i] = new GameProfileWrapper( EntityHuman.a( aProfiles[i] ), aProfiles[i].getName() ); + } + } + // Spigot end + playerSample.a(aProfiles); ServerPing ping = new ServerPing(); ping.setFavicon(event.icon.value); ping.setMOTD(new ChatComponentText(event.getMotd())); ping.setPlayerSample(playerSample); - ping.setServerInfo(new ServerPingServerData(minecraftServer.getServerModName() + " " + minecraftServer.getVersion(), 4)); // TODO: Update when protocol changes + ping.setServerInfo(new ServerPingServerData(minecraftServer.getServerModName() + " " + minecraftServer.getVersion(), networkManager.m.attr( HandshakeListener.protocolVersion ).get())); // Spigot // TODO: Update when protocol changes this.networkManager.handle(new PacketStatusOutServerInfo(ping), new GenericFutureListener[0]); // CraftBukkit end @@ -132,4 +142,23 @@ public class PacketStatusListener implements PacketStatusInListener { public void a(PacketStatusInPing packetstatusinping) { this.networkManager.handle(new PacketStatusOutPong(packetstatusinping.c()), new GenericFutureListener[0]); } + + + // Spigot start + private static class GameProfileWrapper extends GameProfile { + + private final UUID uuid; + + public GameProfileWrapper(UUID uuid, String name) { + super("", name); + this.uuid = uuid; + } + + @Override + public String getId() { + return uuid.toString(); + } + } + + // Spigot end } diff --git a/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java b/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java index fe4502a..63101fb 100644 --- a/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java +++ b/src/main/java/net/minecraft/server/ThreadPlayerLookupUUID.java @@ -33,7 +33,9 @@ class ThreadPlayerLookupUUID extends Thread { } // Spigot End String s = (new BigInteger(MinecraftEncryption.a(LoginListener.a(this.a), LoginListener.b(this.a).J().getPublic(), LoginListener.c(this.a)))).toString(16); - LoginListener.a(this.a, LoginListener.b(this.a).at().hasJoinedServer(new GameProfile((String) null, LoginListener.d(this.a).getName()), s)); + //LoginListener.a(this.a, LoginListener.b(this.a).at().hasJoinedServer(new GameProfile((String) null, LoginListener.d(this.a).getName()), s)); + org.spigotmc.authlib.GameProfile profile = LoginListener.b(this.a).newSessionService.hasJoinedServer( new org.spigotmc.authlib.GameProfile( null, LoginListener.d(this.a).getName() ), s ); + LoginListener.a(this.a, new NewGameProfileWrapper( profile ) ); if (LoginListener.d(this.a) != null) { // Spigot Start fireLoginEvents(); @@ -95,4 +97,15 @@ class ThreadPlayerLookupUUID extends Thread { } // CraftBukkit end } + + public static class NewGameProfileWrapper extends GameProfile { + + public org.spigotmc.authlib.GameProfile newProfile; + + public NewGameProfileWrapper(org.spigotmc.authlib.GameProfile newProfile) + { + super( newProfile.getId().toString().replaceAll( "-", "" ), newProfile.getName() ); + this.newProfile = newProfile; + } + } } diff --git a/src/main/java/org/spigotmc/authlib/Agent.java b/src/main/java/org/spigotmc/authlib/Agent.java new file mode 100644 index 0000000..873743d --- /dev/null +++ b/src/main/java/org/spigotmc/authlib/Agent.java @@ -0,0 +1,30 @@ +package org.spigotmc.authlib; + +public class Agent { + public static final Agent MINECRAFT = new Agent("Minecraft", 1); + public static final Agent SCROLLS = new Agent("Scrolls", 1); + + private final String name; + private final int version; + + public Agent(String name, int version) { + this.name = name; + this.version = version; + } + + public String getName() { + return name; + } + + public int getVersion() { + return version; + } + + @Override + public String toString() { + return "Agent{" + + "name='" + name + '\'' + + ", version=" + version + + '}'; + } +} diff --git a/src/main/java/org/spigotmc/authlib/AuthenticationService.java b/src/main/java/org/spigotmc/authlib/AuthenticationService.java new file mode 100644 index 0000000..4110e53 --- /dev/null +++ b/src/main/java/org/spigotmc/authlib/AuthenticationService.java @@ -0,0 +1,33 @@ +package org.spigotmc.authlib; + +import org.spigotmc.authlib.minecraft.MinecraftSessionService; + +public interface AuthenticationService { + /** + * Creates a relevant {@link org.spigotmc.authlib.UserAuthentication} designed for this authentication service. + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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