Move shared Mojang client into independant project

This commit is contained in:
games647
2018-03-02 19:42:20 +01:00
parent 3f9eba69ba
commit a29dd849f9
31 changed files with 169 additions and 893 deletions

View File

@ -1,7 +1,7 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.mojang.SkinProperties;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import java.util.Optional;
@ -20,10 +20,10 @@ public class BukkitLoginSession extends LoginSession {
private boolean verified;
private SkinProperties skinProperty;
private SkinProperty skinProperty;
public BukkitLoginSession(String username, String serverId, byte[] verifyToken, boolean registered
, PlayerProfile profile) {
, StoredProfile profile) {
super(username, registered, profile);
this.serverId = serverId;
@ -36,7 +36,7 @@ public class BukkitLoginSession extends LoginSession {
}
//cracked player
public BukkitLoginSession(String username, PlayerProfile profile) {
public BukkitLoginSession(String username, StoredProfile profile) {
this(username, "", ArrayUtils.EMPTY_BYTE_ARRAY, false, profile);
}
@ -51,14 +51,14 @@ public class BukkitLoginSession extends LoginSession {
return ArrayUtils.clone(verifyToken);
}
public synchronized Optional<SkinProperties> getSkin() {
public synchronized Optional<SkinProperty> getSkin() {
return Optional.ofNullable(skinProperty);
}
/**
* Sets the premium skin property which was retrieved by the session server
*/
public synchronized void setSkinProperty(SkinProperties skinProperty) {
public synchronized void setSkinProperty(SkinProperty skinProperty) {
this.skinProperty = skinProperty;
}

View File

@ -11,15 +11,12 @@ import com.github.games647.fastlogin.bukkit.tasks.DelayedAuthHook;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.messages.ChangePremiumMessage;
import com.github.games647.fastlogin.core.messages.ChannelMessage;
import com.github.games647.fastlogin.core.mojang.MojangApiConnector;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.PlatformPlugin;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.google.common.net.HostAndPort;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentMap;
@ -188,9 +185,4 @@ public class FastLoginBukkit extends JavaPlugin implements PlatformPlugin<Comman
public void sendMessage(CommandSender receiver, String message) {
receiver.sendMessage(message);
}
@Override
public MojangApiConnector makeApiConnector(List<String> addresses, int requests, List<HostAndPort> proxies) {
return new MojangApiBukkit(logger, addresses, requests, proxies);
}
}

View File

@ -1,63 +0,0 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.mojang.MojangApiConnector;
import com.github.games647.fastlogin.core.mojang.SkinProperties;
import com.github.games647.fastlogin.core.mojang.VerificationReply;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.google.common.net.HostAndPort;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import org.slf4j.Logger;
public class MojangApiBukkit extends MojangApiConnector {
//mojang api check to prove a player is logged in Minecraft and made a join server request
private static final String HAS_JOINED_URL = "https://sessionserver.mojang.com/session/minecraft/hasJoined?" +
"username=%s&serverId=%s&ip=%s";
public MojangApiBukkit(Logger logger, Collection<String> localAddresses, int rateLimit
, Iterable<HostAndPort> proxies) {
super(logger, localAddresses, rateLimit, proxies);
}
@Override
public boolean hasJoinedServer(LoginSession session, String serverId, InetSocketAddress ip) {
BukkitLoginSession playerSession = (BukkitLoginSession) session;
try {
String encodedIp = URLEncoder.encode(ip.getAddress().getHostAddress(), "UTF-8");
String url = String.format(HAS_JOINED_URL, playerSession.getUsername(), serverId, encodedIp);
HttpURLConnection conn = getConnection(url);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
//validate parsing
//http://wiki.vg/Protocol_Encryption#Server
VerificationReply verification = gson.fromJson(reader, VerificationReply.class);
playerSession.setUuid(verification.getId());
SkinProperties[] properties = verification.getProperties();
if (properties != null && properties.length > 0) {
SkinProperties skinProperty = properties[0];
playerSession.setSkinProperty(skinProperty);
}
return true;
}
} catch (IOException ex) {
//catch not only io-exceptions also parse and NPE on unexpected json format
logger.warn("Failed to verify session", ex);
}
//this connection doesn't need to be closed. So can make use of keep alive in java
return false;
}
}

View File

@ -1,7 +1,7 @@
package com.github.games647.fastlogin.bukkit.commands;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
@ -31,7 +31,7 @@ public class CrackedCommand implements CommandExecutor {
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {
//todo: load async if
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
StoredProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
plugin.getCore().sendLocaleMessage("remove-premium", sender);
@ -64,7 +64,7 @@ public class CrackedCommand implements CommandExecutor {
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {
//todo: load async
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
StoredProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
if (profile == null) {
sender.sendMessage("Error occurred");
return;

View File

@ -1,7 +1,7 @@
package com.github.games647.fastlogin.bukkit.commands;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import java.util.UUID;
@ -47,7 +47,7 @@ public class PremiumCommand implements CommandExecutor {
plugin.getCore().getPendingConfirms().remove(id);
//todo: load async
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
StoredProfile profile = plugin.getCore().getStorage().loadProfile(sender.getName());
if (profile.isPremium()) {
plugin.getCore().sendLocaleMessage("already-exists", sender);
} else {
@ -80,7 +80,7 @@ public class PremiumCommand implements CommandExecutor {
plugin.getCore().sendLocaleMessage("wait-on-proxy", sender);
} else {
//todo: load async
PlayerProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
StoredProfile profile = plugin.getCore().getStorage().loadProfile(args[0]);
if (profile == null) {
plugin.getCore().sendLocaleMessage("player-unknown", sender);
return;

View File

@ -4,7 +4,7 @@ import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.PacketEvent;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import java.security.PublicKey;
@ -49,8 +49,8 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
//Minecraft server implementation
//https://github.com/bergerkiller/CraftSource/blob/master/net.minecraft.server/LoginListener.java#L161
@Override
public void requestPremiumLogin(ProtocolLibLoginSource source, PlayerProfile profile,
String username, boolean registered) {
public void requestPremiumLogin(ProtocolLibLoginSource source, StoredProfile profile
, String username, boolean registered) {
try {
source.setOnlineMode();
} catch (Exception ex) {
@ -73,7 +73,7 @@ public class NameCheckTask extends JoinManagement<Player, CommandSender, Protoco
}
@Override
public void startCrackedSession(ProtocolLibLoginSource source, PlayerProfile profile, String username) {
public void startCrackedSession(ProtocolLibLoginSource source, StoredProfile profile, String username) {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getLoginSessions().put(player.getAddress().toString(), loginSession);
}

View File

@ -6,9 +6,9 @@ import com.comphenix.protocol.reflect.accessors.MethodAccessor;
import com.comphenix.protocol.utility.MinecraftReflection;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.comphenix.protocol.wrappers.WrappedSignedProperty;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.mojang.SkinProperties;
import java.lang.reflect.InvocationTargetException;
@ -54,14 +54,14 @@ public class SkinApplyListener implements Listener {
private void applySkin(Player player, String skinData, String signature) {
WrappedGameProfile gameProfile = WrappedGameProfile.fromPlayer(player);
WrappedSignedProperty skin = WrappedSignedProperty.fromValues(SkinProperties.TEXTURE_KEY, skinData, signature);
WrappedSignedProperty skin = WrappedSignedProperty.fromValues(SkinProperty.TEXTURE_KEY, skinData, signature);
try {
gameProfile.getProperties().put(SkinProperties.TEXTURE_KEY, skin);
gameProfile.getProperties().put(SkinProperty.TEXTURE_KEY, skin);
} catch (ClassCastException castException) {
//Cauldron, MCPC, Thermos, ...
Object map = GET_PROPERTIES.invoke(gameProfile.getHandle());
try {
MethodUtils.invokeMethod(map, "put", new Object[]{SkinProperties.TEXTURE_KEY, skin.getHandle()});
MethodUtils.invokeMethod(map, "put", new Object[]{SkinProperty.TEXTURE_KEY, skin.getHandle()});
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException ex) {
plugin.getLog().error("Error setting premium skin of: {}", player, ex);
}

View File

@ -8,16 +8,22 @@ import com.comphenix.protocol.reflect.FieldUtils;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.comphenix.protocol.wrappers.WrappedChatComponent;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.github.games647.craftapi.model.auth.Verification;
import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.EncryptionUtil;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Arrays;
import java.util.Optional;
import java.util.UUID;
import javax.crypto.Cipher;
@ -93,17 +99,25 @@ public class VerifyResponseTask implements Runnable {
String serverId = EncryptionUtil.getServerIdHashString("", loginKey, serverKey.getPublic());
String username = session.getUsername();
if (plugin.getCore().getApiConnector().hasJoinedServer(session, serverId, player.getAddress())) {
plugin.getLog().info("GameProfile {} has a verified premium account", username);
InetSocketAddress socketAddress = player.getAddress();
try {
MojangResolver resolver = plugin.getCore().getResolver();
InetAddress address = socketAddress.getAddress();
Optional<Verification> response = resolver.hasJoined(username, serverId, address);
if (response.isPresent()) {
plugin.getLog().info("GameProfile {} has a verified premium account", username);
session.setVerified(true);
setPremiumUUID(session.getUuid());
receiveFakeStartPacket(username);
} else {
//user tried to fake a authentication
disconnect(plugin.getCore().getMessage("invalid-session"), true
, "GameProfile {0} ({1}) tried to log in with an invalid session ServerId: {2}"
, session.getUsername(), player.getAddress(), serverId);
session.setVerified(true);
setPremiumUUID(session.getUuid());
receiveFakeStartPacket(username);
} else {
//user tried to fake a authentication
disconnect(plugin.getCore().getMessage("invalid-session"), true
, "GameProfile {0} ({1}) tried to log in with an invalid session ServerId: {2}"
, session.getUsername(), socketAddress, serverId);
}
} catch (IOException ioEx) {
disconnect("error-kick", false, "Failed to connect to sessionserver", ioEx);
}
}

View File

@ -1,9 +1,9 @@
package com.github.games647.fastlogin.bukkit.listener.protocolsupport;
import com.github.games647.craftapi.model.skin.SkinProperty;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.mojang.SkinProperties;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import java.net.InetSocketAddress;
@ -47,13 +47,13 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
BukkitLoginSession session = plugin.getLoginSessions().get(address.toString());
//skin was resolved -> premium player
if (propertiesResolveEvent.hasProperty(SkinProperties.TEXTURE_KEY) && session != null) {
if (propertiesResolveEvent.hasProperty(SkinProperty.TEXTURE_KEY) && session != null) {
session.setVerified(true);
}
}
@Override
public void requestPremiumLogin(ProtocolLoginSource source, PlayerProfile profile, String username
public void requestPremiumLogin(ProtocolLoginSource source, StoredProfile profile, String username
, boolean registered) {
source.setOnlineMode();
@ -69,7 +69,7 @@ public class ProtocolSupportListener extends JoinManagement<Player, CommandSende
}
@Override
public void startCrackedSession(ProtocolLoginSource source, PlayerProfile profile, String username) {
public void startCrackedSession(ProtocolLoginSource source, StoredProfile profile, String username) {
BukkitLoginSession loginSession = new BukkitLoginSession(username, profile);
plugin.getLoginSessions().put(source.getAddress().toString(), loginSession);
}

View File

@ -1,6 +1,6 @@
package com.github.games647.fastlogin.bungee;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
public class BungeeLoginSession extends LoginSession {
@ -8,7 +8,7 @@ public class BungeeLoginSession extends LoginSession {
private boolean alreadySaved;
private boolean alreadyLogged;
public BungeeLoginSession(String username, boolean registered, PlayerProfile profile) {
public BungeeLoginSession(String username, boolean registered, StoredProfile profile) {
super(username, registered, profile);
}

View File

@ -5,17 +5,14 @@ import com.github.games647.fastlogin.bungee.listener.ConnectListener;
import com.github.games647.fastlogin.bungee.listener.MessageListener;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.messages.ChannelMessage;
import com.github.games647.fastlogin.core.mojang.MojangApiConnector;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.github.games647.fastlogin.core.shared.PlatformPlugin;
import com.google.common.collect.MapMaker;
import com.google.common.io.ByteArrayDataOutput;
import com.google.common.io.ByteStreams;
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ThreadFactory;
@ -122,9 +119,4 @@ public class FastLoginBungee extends Plugin implements PlatformPlugin<CommandSen
.setThreadFactory(new GroupedThreadFactory(this, getName()))
.build();
}
@Override
public MojangApiConnector makeApiConnector(List<String> addresses, int requests, List<HostAndPort> proxies) {
return new MojangApiConnector(logger, addresses, requests, proxies);
}
}

View File

@ -1,10 +1,10 @@
package com.github.games647.fastlogin.bungee.listener;
import com.github.games647.craftapi.UUIDAdapter;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.tasks.AsyncPremiumCheck;
import com.github.games647.fastlogin.bungee.tasks.ForceLoginTask;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.mojang.UUIDTypeAdapter;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.LoginSession;
import java.lang.reflect.Field;
@ -67,13 +67,13 @@ public class ConnectListener implements Listener {
LoginSession session = plugin.getSession().get(connection);
session.setUuid(connection.getUniqueId());
PlayerProfile playerProfile = session.getProfile();
StoredProfile playerProfile = session.getProfile();
playerProfile.setId(connection.getUniqueId());
//bungeecord will do this automatically so override it on disabled option
if (!plugin.getCore().getConfig().get("premiumUuid", true)) {
try {
UUID offlineUUID = UUIDTypeAdapter.getOfflineUUID(username);
UUID offlineUUID = UUIDAdapter.generateOfflineId(username);
//bungeecord doesn't support overriding the premium uuid
//so we have to do it with reflection

View File

@ -3,7 +3,7 @@ package com.github.games647.fastlogin.bungee.listener;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.bungee.tasks.AsyncToggleMessage;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.messages.ChangePremiumMessage;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.google.common.io.ByteArrayDataInput;
@ -86,7 +86,7 @@ public class MessageListener implements Listener {
//bukkit module successfully received and force logged in the user
//update only on success to prevent corrupt data
BungeeLoginSession loginSession = plugin.getSession().get(forPlayer.getPendingConnection());
PlayerProfile playerProfile = loginSession.getProfile();
StoredProfile playerProfile = loginSession.getProfile();
loginSession.setRegistered(true);
if (!loginSession.isAlreadySaved()) {

View File

@ -3,7 +3,7 @@ package com.github.games647.fastlogin.bungee.tasks;
import com.github.games647.fastlogin.bungee.BungeeLoginSession;
import com.github.games647.fastlogin.bungee.BungeeLoginSource;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.JoinManagement;
import net.md_5.bungee.api.CommandSender;
@ -42,7 +42,7 @@ public class AsyncPremiumCheck extends JoinManagement<ProxiedPlayer, CommandSend
}
@Override
public void requestPremiumLogin(BungeeLoginSource source, PlayerProfile profile,
public void requestPremiumLogin(BungeeLoginSource source, StoredProfile profile,
String username, boolean registered) {
source.setOnlineMode();
plugin.getSession().put(source.getConnection(), new BungeeLoginSession(username, registered, profile));
@ -52,7 +52,7 @@ public class AsyncPremiumCheck extends JoinManagement<ProxiedPlayer, CommandSend
}
@Override
public void startCrackedSession(BungeeLoginSource source, PlayerProfile profile, String username) {
public void startCrackedSession(BungeeLoginSource source, StoredProfile profile, String username) {
plugin.getSession().put(source.getConnection(), new BungeeLoginSession(username, false, profile));
}
}

View File

@ -1,7 +1,7 @@
package com.github.games647.fastlogin.bungee.tasks;
import com.github.games647.fastlogin.bungee.FastLoginBungee;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import net.md_5.bungee.api.CommandSender;
@ -36,7 +36,7 @@ public class AsyncToggleMessage implements Runnable {
}
private void turnOffPremium() {
PlayerProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
StoredProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
//existing player is already cracked
if (playerProfile.isSaved() && !playerProfile.isPremium()) {
sendMessage("not-premium");
@ -50,7 +50,7 @@ public class AsyncToggleMessage implements Runnable {
}
private void activatePremium() {
PlayerProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
StoredProfile playerProfile = core.getStorage().loadProfile(targetPlayer);
if (playerProfile.isPremium()) {
sendMessage("already-exists");
return;

View File

@ -19,6 +19,11 @@
<id>luck-repo</id>
<url>https://ci.lucko.me/plugin/repository/everything</url>
</repository>
<repository>
<id>codemc-repo</id>
<url>https://repo.codemc.org/repository/maven-public/</url>
</repository>
</repositories>
<dependencies>
@ -58,6 +63,12 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.github.games647</groupId>
<artifactId>craftapi</artifactId>
<version>0.1</version>
</dependency>
<!-- APIs we can use because they are available in all platforms (Spigot, Bungee, Cauldron) -->
<dependency>
<groupId>com.google.guava</groupId>

View File

@ -1,6 +1,6 @@
package com.github.games647.fastlogin.core;
import com.github.games647.fastlogin.core.mojang.UUIDTypeAdapter;
import com.github.games647.craftapi.UUIDAdapter;
import com.github.games647.fastlogin.core.shared.FastLoginCore;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@ -92,14 +92,14 @@ public class AuthStorage {
}
}
public PlayerProfile loadProfile(String name) {
public StoredProfile loadProfile(String name) {
try (Connection con = dataSource.getConnection();
PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_NAME)
) {
loadStmt.setString(1, name);
try (ResultSet resultSet = loadStmt.executeQuery()) {
return parseResult(resultSet).orElseGet(() -> new PlayerProfile(null, name, false, ""));
return parseResult(resultSet).orElseGet(() -> new StoredProfile(null, name, false, ""));
}
} catch (SQLException sqlEx) {
core.getPlugin().getLog().error("Failed to query profile: {}", name, sqlEx);
@ -108,11 +108,10 @@ public class AuthStorage {
return null;
}
public PlayerProfile loadProfile(UUID uuid) {
public StoredProfile loadProfile(UUID uuid) {
try (Connection con = dataSource.getConnection();
PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_UUID)
) {
loadStmt.setString(1, UUIDTypeAdapter.toMojangId(uuid));
PreparedStatement loadStmt = con.prepareStatement(LOAD_BY_UUID)) {
loadStmt.setString(1, UUIDAdapter.toMojangId(uuid));
try (ResultSet resultSet = loadStmt.executeQuery()) {
return parseResult(resultSet).orElse(null);
@ -124,30 +123,30 @@ public class AuthStorage {
return null;
}
private Optional<PlayerProfile> parseResult(ResultSet resultSet) throws SQLException {
private Optional<StoredProfile> parseResult(ResultSet resultSet) throws SQLException {
if (resultSet.next()) {
long userId = resultSet.getInt(1);
UUID uuid = UUIDTypeAdapter.parseId(resultSet.getString(2));
UUID uuid = UUIDAdapter.parseId(resultSet.getString(2));
String name = resultSet.getString(3);
boolean premium = resultSet.getBoolean(4);
String lastIp = resultSet.getString(5);
Instant lastLogin = resultSet.getTimestamp(6).toInstant();
return Optional.of(new PlayerProfile(userId, uuid, name, premium, lastIp, lastLogin));
return Optional.of(new StoredProfile(userId, uuid, name, premium, lastIp, lastLogin));
}
return Optional.empty();
}
public void save(PlayerProfile playerProfile) {
public void save(StoredProfile playerProfile) {
try (Connection con = dataSource.getConnection()) {
String uuid = playerProfile.getId().map(UUIDTypeAdapter::toMojangId).orElse(null);
String uuid = playerProfile.getOptId().map(UUIDAdapter::toMojangId).orElse(null);
if (playerProfile.isSaved()) {
try (PreparedStatement saveStmt = con.prepareStatement(UPDATE_PROFILE)) {
saveStmt.setString(1, uuid);
saveStmt.setString(2, playerProfile.getPlayerName());
saveStmt.setString(2, playerProfile.getName());
saveStmt.setBoolean(3, playerProfile.isPremium());
saveStmt.setString(4, playerProfile.getLastIp());
@ -158,7 +157,7 @@ public class AuthStorage {
try (PreparedStatement saveStmt = con.prepareStatement(INSERT_PROFILE, RETURN_GENERATED_KEYS)) {
saveStmt.setString(1, uuid);
saveStmt.setString(2, playerProfile.getPlayerName());
saveStmt.setString(2, playerProfile.getName());
saveStmt.setBoolean(3, playerProfile.isPremium());
saveStmt.setString(4, playerProfile.getLastIp());

View File

@ -1,5 +1,6 @@
package com.github.games647.fastlogin.core;
import com.github.games647.craftapi.cache.SafeCacheBuilder;
import com.google.common.cache.CacheLoader;
import java.lang.reflect.Constructor;
@ -17,7 +18,7 @@ public class CommonUtil {
private static final char TRANSLATED_CHAR = '§';
public static <K, V> ConcurrentMap<K, V> buildCache(int expireAfterWrite, int maxSize) {
CompatibleCacheBuilder<Object, Object> builder = CompatibleCacheBuilder.newBuilder();
SafeCacheBuilder<Object, Object> builder = SafeCacheBuilder.newBuilder();
if (expireAfterWrite > 0) {
builder.expireAfterWrite(expireAfterWrite, TimeUnit.MINUTES);

View File

@ -1,312 +0,0 @@
package com.github.games647.fastlogin.core;
import com.google.common.base.Ticker;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.RemovalListener;
import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
/**
* Represents a Guava CacheBuilder that is compatible with both Guava 10 (Minecraft 1.7.X) and 13
*/
public class CompatibleCacheBuilder<K, V> {
private static Method BUILD_METHOD;
private static Method AS_MAP_METHOD;
/**
* Construct a new safe cache builder.
*
* @param <K> Key type
* @param <V> Value type
* @return A new cache builder.
*/
public static <K, V> CompatibleCacheBuilder<K, V> newBuilder() {
return new CompatibleCacheBuilder<>();
}
private final CacheBuilder<K, V> builder;
@SuppressWarnings("unchecked")
private CompatibleCacheBuilder() {
builder = (CacheBuilder<K, V>) CacheBuilder.newBuilder();
}
/**
* Guides the allowed concurrency among update operations. Used as a hint for internal sizing. The table is
* internally partitioned to try to permit the indicated number of concurrent updates without contention. Because
* assignment of entries to these partitions is not necessarily uniform, the actual concurrency observed may vary.
* Ideally, you should choose a value to accommodate as many threads as will ever concurrently modify the table.
* Using a significantly higher value than you need can waste space and time, and a significantly lower value can
* lead to thread contention. But overestimates and underestimates within an order of magnitude do not usually have
* much noticeable impact. A value of one permits only one thread to modify the cache at a time, but since read
* operations can proceed concurrently, this still yields higher concurrency than full synchronization. Defaults to
* 4.
* <p>
* <p>
* <b>Note:</b>The default may change in the future. If you care about this value, you should always choose it
* explicitly.
*
* @param concurrencyLevel New concurrency level
* @return This for chaining
* @throws IllegalArgumentException if {@code concurrencyLevel} is non-positive
* @throws IllegalStateException if a concurrency level was already set
*/
public CompatibleCacheBuilder<K, V> concurrencyLevel(int concurrencyLevel) {
builder.concurrencyLevel(concurrencyLevel);
return this;
}
/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after
* the entry's creation, or last access. Access time is reset by
* {@link com.google.common.cache.Cache#get Cache.get()}, but not by operations on the view returned by
* {@link com.google.common.cache.Cache#asMap() Cache.asMap()}.
* <p>
* <p>
* When {@code duration} is zero, elements will be evicted immediately after being loaded into the cache. This has
* the same effect as invoking {@link #maximumSize maximumSize}{@code (0)}. It can be useful in testing, or to
* disable caching temporarily without a code change.
* <p>
* <p>
* Expired entries may be counted by {@link com.google.common.cache.Cache#size Cache.size()}, but will never be
* visible to read or write operations. Expired entries are currently cleaned up during write operations, or during
* occasional read operations in the absence of writes; though this behavior may change in the future.
*
* @param duration the length of time after an entry is last accessed that it should be automatically removed
* @param unit the unit that {@code duration} is expressed in
* @return This for chaining
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to idle or time to live was already set
*/
public CompatibleCacheBuilder<K, V> expireAfterAccess(long duration, TimeUnit unit) {
builder.expireAfterAccess(duration, unit);
return this;
}
/**
* Specifies that each entry should be automatically removed from the cache once a fixed duration has elapsed after
* the entry's creation, or the most recent replacement of its value.
* <p>
* <p>
* When {@code duration} is zero, elements will be evicted immediately after being loaded into the cache. This has
* the same effect as invoking {@link #maximumSize maximumSize}{@code (0)}. It can be useful in testing, or to
* disable caching temporarily without a code change.
* <p>
* <p>
* Expired entries may be counted by {@link com.google.common.cache.Cache#size Cache.size()}, but will never be
* visible to read or write operations. Expired entries are currently cleaned up during write operations, or during
* occasional read operations in the absence of writes; though this behavior may change in the future.
*
* @param duration the length of time after an entry is created that it should be automatically removed
* @param unit the unit that {@code duration} is expressed in
* @return This for chaining
* @throws IllegalArgumentException if {@code duration} is negative
* @throws IllegalStateException if the time to live or time to idle was already set
*/
public CompatibleCacheBuilder<K, V> expireAfterWrite(long duration, TimeUnit unit) {
builder.expireAfterWrite(duration, unit);
return this;
}
/**
* Sets the minimum total size for the internal hash tables. For example, if the initial capacity is {@code 60}, and
* the concurrency level is {@code 8}, then eight segments are created, each having a hash table of size eight.
* Providing a large enough estimate at construction time avoids the need for expensive resizing operations later,
* but setting this value unnecessarily high wastes memory.
*
* @param initialCapacity - initial capacity
* @return This for chaining
* @throws IllegalArgumentException if {@code initialCapacity} is negative
* @throws IllegalStateException if an initial capacity was already set
*/
public CompatibleCacheBuilder<K, V> initialCapacity(int initialCapacity) {
builder.initialCapacity(initialCapacity);
return this;
}
/**
* Specifies the maximum number of entries the cache may contain. Note that the cache <b>may evict an entry before
* this limit is exceeded</b>. As the cache size grows close to the maximum, the cache evicts entries that are less
* likely to be used again. For example, the cache may evict an entry because it hasn't been used recently or very
* often.
* <p>
* <p>
* When {@code size} is zero, elements will be evicted immediately after being loaded into the cache. This has the
* same effect as invoking {@link #expireAfterWrite expireAfterWrite}{@code (0, unit)} or
* {@link #expireAfterAccess expireAfterAccess}{@code (0,unit)}.
* It can be useful in testing, or to disable caching temporarily without a code change.
*
* @param size the maximum size of the cache
* @return This for chaining
* @throws IllegalArgumentException if {@code size} is negative
* @throws IllegalStateException if a maximum size was already set
*/
public CompatibleCacheBuilder<K, V> maximumSize(int size) {
builder.maximumSize(size);
return this;
}
/**
* Specifies a listener instance, which all caches built using this {@code CacheBuilder} will notify each time an
* entry is removed from the cache by any means.
* <p>
* <p>
* Each cache built by this {@code CacheBuilder} after this method is called invokes the supplied listener after
* removing an element for any reason (see removal causes in
* {@link com.google.common.cache.RemovalCause RemovalCause}). It will invoke the listener during invocations of any
* of that cache's public methods (even read-only methods).
* <p>
* <p>
* <b>Important note:</b> Instead of returning <em>this</em> as a {@code CacheBuilder} instance, this method returns
* {@code CacheBuilder<K1, V1>}. From this point on, either the original reference or the returned reference may be
* used to complete configuration and build the cache, but only the "generic" one is type-safe. That is, it will
* properly prevent you from building caches whose key or value types are incompatible with the types accepted by
* the listener already provided; the {@code CacheBuilder} type cannot do this. For best results, simply use the
* standard method-chaining idiom, as illustrated in the documentation at top, configuring a {@code CacheBuilder}
* and building your {@link com.google.common.cache.Cache Cache} all in a single statement.
* <p>
* <p>
* <b>Warning:</b> if you ignore the above advice, and use this {@code CacheBuilder} to build a cache whose key or
* value type is incompatible with the listener, you will likely experience a {@link ClassCastException} at some
* <i>undefined</i> point in the future.
*
* @param <K1> Key type
* @param <V1> Value type
* @param listener - removal listener
* @return This for chaining
* @throws IllegalStateException if a removal listener was already set
*/
@SuppressWarnings("unchecked")
public <K1 extends K, V1 extends V> CompatibleCacheBuilder<K1, V1> removalListener(
RemovalListener<? super K1, ? super V1> listener) {
builder.removalListener(listener);
return (CompatibleCacheBuilder<K1, V1>) this;
}
/**
* Specifies a nanosecond-precision time source for use in determining when entries should be expired. By default,
* {@link System#nanoTime} is used.
* <p>
* <p>
* The primary intent of this method is to facilitate testing of caches which have been configured with
* {@link #expireAfterWrite} or {@link #expireAfterAccess}.
*
* @param ticker - ticker
* @return This for chaining
* @throws IllegalStateException if a ticker was already set
*/
public CompatibleCacheBuilder<K, V> ticker(Ticker ticker) {
builder.ticker(ticker);
return this;
}
/**
* Specifies that each value (not key) stored in the cache should be wrapped in a
* {@link java.lang.ref.SoftReference SoftReference} (by default, strong references are used). Softly-referenced
* objects will be garbage-collected in a <i>globally</i>
* least-recently-used manner, in response to memory demand.
* <p>
* <p>
* <b>Warning:</b> in most circumstances it is better to set a per-cache {@linkplain #maximumSize maximum size}
* instead of using soft references. You should only use this method if you are well familiar with the practical
* consequences of soft references.
* <p>
* <p>
* <b>Note:</b> when this method is used, the resulting cache will use identity ({@code ==}) comparison to determine
* equality of values.
*
* @return This for chaining
* @throws IllegalStateException if the value strength was already set
*/
public CompatibleCacheBuilder<K, V> softValues() {
builder.softValues();
return this;
}
/**
* Specifies that each key (not value) stored in the cache should be wrapped in a
* {@link java.lang.ref.WeakReference WeakReference} (by default, strong references are used).
* <p>
* <p>
* <b>Warning:</b> when this method is used, the resulting cache will use identity ({@code ==}) comparison to
* determine equality of keys.
*
* @return This for chaining
* @throws IllegalStateException if the key strength was already set
*/
public CompatibleCacheBuilder<K, V> weakKeys() {
builder.weakKeys();
return this;
}
/**
* Specifies that each value (not key) stored in the cache should be wrapped in a
* {@link java.lang.ref.WeakReference WeakReference} (by default, strong references are used).
* <p>
* <p>
* Weak values will be garbage collected once they are weakly reachable. This makes them a poor candidate for
* caching; consider {@link #softValues} instead.
* <p>
* <p>
* <b>Note:</b> when this method is used, the resulting cache will use identity ({@code ==}) comparison to determine
* equality of values.
*
* @return This for chaining
* @throws IllegalStateException if the value strength was already set
*/
public CompatibleCacheBuilder<K, V> weakValues() {
builder.weakValues();
return this;
}
/**
* Returns the cache wrapped as a ConcurrentMap.
* <p>
* We can't return the direct Cache instance as it changed in Guava 13.
*
* @param <K1> Key type
* @param <V1> Value type
* @param loader - cache loader
* @return The cache as a a map.
*/
@SuppressWarnings("unchecked")
public <K1 extends K, V1 extends V> ConcurrentMap<K1, V1> build(CacheLoader<? super K1, V1> loader) {
Object cache;
if (BUILD_METHOD == null) {
try {
BUILD_METHOD = builder.getClass().getDeclaredMethod("build", CacheLoader.class);
BUILD_METHOD.setAccessible(true);
} catch (Exception e) {
throw new IllegalStateException("Unable to find CacheBuilder.build(CacheLoader)", e);
}
}
// Attempt to build the Cache
try {
cache = BUILD_METHOD.invoke(builder, loader);
} catch (Exception e) {
throw new IllegalStateException("Unable to invoke " + BUILD_METHOD + " on " + builder, e);
}
if (AS_MAP_METHOD == null) {
try {
AS_MAP_METHOD = cache.getClass().getMethod("asMap");
AS_MAP_METHOD.setAccessible(true);
} catch (Exception e) {
throw new IllegalStateException("Unable to find Cache.asMap() in " + cache, e);
}
}
// Retrieve it as a map
try {
return (ConcurrentMap<K1, V1>) AS_MAP_METHOD.invoke(cache);
} catch (Exception e) {
throw new IllegalStateException("Unable to invoke " + AS_MAP_METHOD + " on " + cache, e);
}
}
}

View File

@ -1,29 +1,31 @@
package com.github.games647.fastlogin.core;
import com.github.games647.craftapi.model.Profile;
import java.time.Instant;
import java.util.Optional;
import java.util.UUID;
public class PlayerProfile {
import javax.annotation.Nullable;
public class StoredProfile extends Profile {
private String playerName;
private long rowId;
private UUID uuid;
private boolean premium;
private String lastIp;
private Instant lastLogin;
public PlayerProfile(long rowId, UUID uuid, String playerName, boolean premium, String lastIp, Instant lastLogin) {
public StoredProfile(long rowId, UUID uuid, String playerName, boolean premium, String lastIp, Instant lastLogin) {
super(uuid, playerName);
this.rowId = rowId;
this.uuid = uuid;
this.playerName = playerName;
this.premium = premium;
this.lastIp = lastIp;
this.lastLogin = lastLogin;
}
public PlayerProfile(UUID uuid, String playerName, boolean premium, String lastIp) {
public StoredProfile(UUID uuid, String playerName, boolean premium, String lastIp) {
this(-1, uuid, playerName, premium, lastIp, Instant.now());
}
@ -31,12 +33,8 @@ public class PlayerProfile {
return rowId >= 0;
}
public synchronized String getPlayerName() {
return playerName;
}
public synchronized void setPlayerName(String playerName) {
this.playerName = playerName;
this.name = playerName;
}
public synchronized long getRowId() {
@ -47,12 +45,17 @@ public class PlayerProfile {
this.rowId = generatedId;
}
public synchronized Optional<UUID> getId() {
return Optional.ofNullable(uuid);
@Nullable
public synchronized UUID getId() {
return id;
}
public synchronized void setId(UUID uuid) {
this.uuid = uuid;
public synchronized Optional<UUID> getOptId() {
return Optional.ofNullable(id);
}
public synchronized void setId(UUID uniqueId) {
this.id = uniqueId;
}
public synchronized boolean isPremium() {
@ -82,12 +85,10 @@ public class PlayerProfile {
@Override
public synchronized String toString() {
return this.getClass().getSimpleName() + '{' +
"playerName='" + playerName + '\'' +
", rowId=" + rowId +
", uuid=" + uuid +
"rowId=" + rowId +
", premium=" + premium +
", lastIp='" + lastIp + '\'' +
", lastLogin=" + lastLogin +
'}';
"} " + super.toString();
}
}

View File

@ -1,71 +0,0 @@
package com.github.games647.fastlogin.core.mojang;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Iterator;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
public class BalancedSSLFactory extends SSLSocketFactory {
//in order to be thread-safe
private final Iterator<InetAddress> iterator;
private final SSLSocketFactory oldFactory;
public BalancedSSLFactory(SSLSocketFactory oldFactory, Iterable<InetAddress> localAddresses) {
this.oldFactory = oldFactory;
this.iterator = Iterables.cycle(localAddresses).iterator();
}
public BalancedSSLFactory(Iterable<InetAddress> iterator) {
this(HttpsURLConnection.getDefaultSSLSocketFactory(), iterator);
}
@Override
public String[] getDefaultCipherSuites() {
return oldFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return oldFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
return oldFactory.createSocket(host, port, getNextLocalAddress(), 0);
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return oldFactory.createSocket(host, port, getNextLocalAddress(), 0);
}
@Override
public Socket createSocket(String host, int port, InetAddress localAddress, int localPort)
throws IOException {
//default
return oldFactory.createSocket(host, port, localAddress, localPort);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return oldFactory.createSocket(host, port, getNextLocalAddress(), 0);
}
@Override
public Socket createSocket(InetAddress host, int port, InetAddress local, int localPort) throws IOException {
//Default
return oldFactory.createSocket(host, port, local, localPort);
}
private InetAddress getNextLocalAddress() {
synchronized (iterator) {
return iterator.next();
}
}
}

View File

@ -1,34 +0,0 @@
package com.github.games647.fastlogin.core.mojang;
import java.util.UUID;
public class GameProfile {
private UUID id;
private String name;
public GameProfile(UUID id, String name) {
this.id = id;
this.name = name;
}
public GameProfile() {
//gson
}
public UUID getId() {
return id;
}
public String getName() {
return name;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

View File

@ -1,176 +0,0 @@
package com.github.games647.fastlogin.core.mojang;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.shared.LoginSession;
import com.google.common.collect.Iterables;
import com.google.common.net.HostAndPort;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.URL;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
import org.slf4j.Logger;
public class MojangApiConnector {
//http connection, read timeout and user agent for a connection to mojang api servers
private static final int TIMEOUT = 3 * 1_000;
private static final String USER_AGENT = "Premium-Checker";
private static final int RATE_LIMIT_CODE = 429;
//only premium (paid account) users have a uuid from here
private static final String UUID_LINK = "https://api.mojang.com/users/profiles/minecraft/";
//this includes a-zA-Z1-9_
//compile the pattern only on plugin enable -> and this have to be thread-safe
private final Pattern validNameMatcher = Pattern.compile("^\\w{2,16}$");
private final Iterator<Proxy> proxies;
private final SSLSocketFactory sslFactory;
private final Map<Object, Object> requests = CommonUtil.buildCache(10, -1);
private final int rateLimit;
private Instant lastRateLimit = Instant.now().minus(10, ChronoUnit.MINUTES);
protected final Logger logger;
protected final Gson gson = new GsonBuilder()
.registerTypeAdapter(UUID.class, new UUIDTypeAdapter()).create();
public MojangApiConnector(Logger logger, Iterable<String> localAddresses, int rateLimit
, Iterable<HostAndPort> proxies) {
this.logger = logger;
this.rateLimit = Math.max(rateLimit, 600);
this.sslFactory = buildAddresses(logger, localAddresses);
Collection<Proxy> proxyBuilder = new ArrayList<>();
for (HostAndPort proxy : proxies) {
proxyBuilder.add(new Proxy(Type.HTTP, new InetSocketAddress(proxy.getHostText(), proxy.getPort())));
}
this.proxies = Iterables.cycle(proxyBuilder).iterator();
}
public Optional<UUID> getPremiumUUID(String playerName) {
if (!validNameMatcher.matcher(playerName).matches()) {
//check if it's a valid player name
return Optional.empty();
}
try {
HttpsURLConnection connection;
if (requests.size() >= rateLimit || Duration.between(lastRateLimit, Instant.now()).getSeconds() < 60 * 10) {
synchronized (proxies) {
if (proxies.hasNext()) {
connection = getConnection(UUID_LINK + playerName, proxies.next());
} else {
return Optional.empty();
}
}
} else {
requests.put(new Object(), new Object());
connection = getConnection(UUID_LINK + playerName);
}
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
String content = reader.lines().collect(Collectors.joining());
return Optional.of(getUUIDFromJson(content));
}
} else if (connection.getResponseCode() == RATE_LIMIT_CODE) {
logger.info("Mojang's rate-limit reached. The public IPv4 address of this server issued more than 600" +
" Name -> UUID requests within 10 minutes. Once those 10 minutes ended we could make requests" +
" again. In the meanwhile new skins can only be downloaded using the UUID directly." +
" If you are using BungeeCord, consider adding a caching server in order to prevent multiple" +
" Spigot servers creating the same requests against Mojang's servers.");
lastRateLimit = Instant.now();
if (!connection.usingProxy()) {
return getPremiumUUID(playerName);
}
}
//204 - no content for not found
} catch (Exception ex) {
logger.error("Failed to check if player has a paid account", ex);
}
return Optional.empty();
}
public boolean hasJoinedServer(LoginSession session, String serverId, InetSocketAddress ip) {
//only available in Spigot and not in BungeeCord
return false;
}
private UUID getUUIDFromJson(String json) {
boolean isArray = json.startsWith("[");
GameProfile mojangPlayer;
if (isArray) {
mojangPlayer = gson.fromJson(json, GameProfile[].class)[0];
} else {
mojangPlayer = gson.fromJson(json, GameProfile.class);
}
return mojangPlayer.getId();
}
protected HttpsURLConnection getConnection(String url, Proxy proxy) throws IOException {
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(proxy);
connection.setConnectTimeout(TIMEOUT);
connection.setReadTimeout(2 * TIMEOUT);
//the new Mojang API just uses json as response
connection.setRequestProperty("Content-Type", "application/json");
connection.setRequestProperty("User-Agent", USER_AGENT);
connection.setSSLSocketFactory(sslFactory);
//this connection doesn't need to be closed. So can make use of keep alive in java
return connection;
}
protected HttpsURLConnection getConnection(String url) throws IOException {
return getConnection(url, Proxy.NO_PROXY);
}
private SSLSocketFactory buildAddresses(Logger logger, Iterable<String> localAddresses) {
Set<InetAddress> addresses = new HashSet<>();
for (String localAddress : localAddresses) {
try {
InetAddress address = InetAddress.getByName(localAddress.replace('-', '.'));
addresses.add(address);
} catch (UnknownHostException ex) {
logger.error("IP-Address is unknown to us", ex);
}
}
if (addresses.isEmpty()) {
return HttpsURLConnection.getDefaultSSLSocketFactory();
}
return new BalancedSSLFactory(HttpsURLConnection.getDefaultSSLSocketFactory(), addresses);
}
}

View File

@ -1,28 +0,0 @@
package com.github.games647.fastlogin.core.mojang;
public class SkinProperties {
public static final String TEXTURE_KEY = "textures";
private final String name = TEXTURE_KEY;
private String value;
private String signature;
public String getValue() {
return value;
}
public String getSignature() {
return signature;
}
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"name='" + name + '\'' +
", value='" + value + '\'' +
", signature='" + signature + '\'' +
'}';
}
}

View File

@ -1,36 +0,0 @@
package com.github.games647.fastlogin.core.mojang;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.bind.TypeAdapters;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.regex.Pattern;
public class UUIDTypeAdapter extends TypeAdapter<UUID> {
private static final Pattern UUID_PATTERN = Pattern.compile("(\\w{8})(\\w{4})(\\w{4})(\\w{4})(\\w{12})");
public static UUID parseId(CharSequence withoutDashes) {
return UUID.fromString(UUID_PATTERN.matcher(withoutDashes).replaceAll("$1-$2-$3-$4-$5"));
}
public static String toMojangId(UUID uuid) {
return uuid.toString().replace("-", "");
}
public static UUID getOfflineUUID(String playerName) {
return UUID.nameUUIDFromBytes(("OfflinePlayer:" + playerName).getBytes(StandardCharsets.UTF_8));
}
public void write(JsonWriter out, UUID value) throws IOException {
TypeAdapters.STRING.write(out, toMojangId(value));
}
public UUID read(JsonReader in) throws IOException {
return parseId(TypeAdapters.STRING.read(in));
}
}

View File

@ -1,32 +0,0 @@
package com.github.games647.fastlogin.core.mojang;
import java.util.Arrays;
import java.util.UUID;
public class VerificationReply {
private UUID id;
private String name;
private SkinProperties[] properties;
public UUID getId() {
return id;
}
public String getName() {
return name;
}
public SkinProperties[] getProperties() {
return Arrays.copyOf(properties, properties.length);
}
@Override
public String toString() {
return this.getClass().getSimpleName() + '{' +
"id=" + id +
", name='" + name + '\'' +
", properties=" + Arrays.toString(properties) +
'}';
}
}

View File

@ -1,22 +1,27 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.craftapi.resolver.MojangResolver;
import com.github.games647.craftapi.resolver.http.RotatingProxySelector;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.CommonUtil;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import com.github.games647.fastlogin.core.hooks.DefaultPasswordGenerator;
import com.github.games647.fastlogin.core.hooks.PasswordGenerator;
import com.github.games647.fastlogin.core.mojang.MojangApiConnector;
import com.google.common.net.HostAndPort;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Proxy.Type;
import java.net.UnknownHostException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@ -28,8 +33,8 @@ import net.md_5.bungee.config.YamlConfiguration;
import org.slf4j.Logger;
import static java.util.function.Function.identity;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static java.util.stream.Collectors.toSet;
/**
* @param <P> GameProfile class
@ -38,14 +43,14 @@ import static java.util.stream.Collectors.toMap;
*/
public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
protected final Map<String, String> localeMessages = new ConcurrentHashMap<>();
private final Map<String, String> localeMessages = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Object> pendingLogin = CommonUtil.buildCache(5, -1);
private final Collection<UUID> pendingConfirms = new HashSet<>();
private final T plugin;
private final MojangResolver resolver = new MojangResolver();
private Configuration config;
private MojangApiConnector apiConnector;
private AuthStorage storage;
private PasswordGenerator<P> passwordGenerator = new DefaultPasswordGenerator<>();
private AuthPlugin<P> authPlugin;
@ -76,12 +81,25 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
plugin.getLog().error("Failed to load yaml files", ioEx);
}
List<String> ipAddresses = config.getStringList("ip-addresses");
int requestLimit = config.getInt("mojang-request-limit");
List<String> proxyList = config.get("proxies", new ArrayList<>());
List<HostAndPort> proxies = proxyList.stream().map(HostAndPort::fromString).collect(toList());
Set<Proxy> proxies = config.getStringList("proxies")
.stream()
.map(HostAndPort::fromString)
.map(proxy -> new InetSocketAddress(proxy.getHostText(), proxy.getPort()))
.map(sa -> new Proxy(Type.HTTP, sa))
.collect(toSet());
this.apiConnector = plugin.makeApiConnector(ipAddresses, requestLimit, proxies);
Collection<InetAddress> addresses = new HashSet<>();
for (String localAddress : config.getStringList("ip-addresses")) {
try {
addresses.add(InetAddress.getByName(localAddress.replace('-', '.')));
} catch (UnknownHostException ex) {
plugin.getLog().error("IP-Address is unknown to us", ex);
}
}
resolver.setMaxNameRequests(config.getInt("mojang-request-limit"));
resolver.setProxySelector(new RotatingProxySelector(proxies));
resolver.setOutgoingAddresses(addresses);
}
private Configuration loadFile(String fileName) throws IOException {
@ -96,8 +114,8 @@ public class FastLoginCore<P extends C, C, T extends PlatformPlugin<C>> {
return configProvider.load(Files.newBufferedReader(file), defaults);
}
public MojangApiConnector getApiConnector() {
return apiConnector;
public MojangResolver getResolver() {
return resolver;
}
public AuthStorage getStorage() {

View File

@ -1,7 +1,7 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.AuthStorage;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
public abstract class ForceLoginManagement<P extends C, C, L extends LoginSession, T extends PlatformPlugin<C>>
@ -25,7 +25,7 @@ public abstract class ForceLoginManagement<P extends C, C, L extends LoginSessio
AuthStorage storage = core.getStorage();
PlayerProfile playerProfile = session.getProfile();
StoredProfile playerProfile = session.getProfile();
try {
if (isOnlineMode()) {
//premium player

View File

@ -1,10 +1,11 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.craftapi.model.Profile;
import com.github.games647.craftapi.resolver.RateLimitException;
import com.github.games647.fastlogin.core.StoredProfile;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import java.util.Optional;
import java.util.UUID;
import net.md_5.bungee.config.Configuration;
@ -19,7 +20,7 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
}
public void onLogin(String username, S source) {
PlayerProfile profile = core.getStorage().loadProfile(username);
StoredProfile profile = core.getStorage().loadProfile(username);
if (profile == null) {
return;
}
@ -44,9 +45,9 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
return;
}
Optional<UUID> premiumUUID = Optional.empty();
Optional<Profile> premiumUUID = Optional.empty();
if (config.get("nameChangeCheck", false) || config.get("autoRegister", false)) {
premiumUUID = core.getApiConnector().getPremiumUUID(username);
premiumUUID = core.getResolver().findProfile(username);
}
if (!premiumUUID.isPresent()
@ -61,12 +62,17 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
startCrackedSession(source, profile, username);
}
}
} catch (RateLimitException rateLimitEx) {
core.getPlugin().getLog().error("Mojang's rate limit reached for {}. The public IPv4 address of this" +
" server issued more than 600 Name -> UUID requests within 10 minutes. After those 10" +
" minutes we can make requests again.", username);
} catch (Exception ex) {
core.getPlugin().getLog().error("Failed to check premium state for {}", username, ex);
core.getPlugin().getLog().error("Failed to check premium state of {}", username, ex);
}
}
private boolean checkPremiumName(S source, String username, PlayerProfile profile) throws Exception {
private boolean checkPremiumName(S source, String username, StoredProfile profile) throws Exception {
core.getPlugin().getLog().info("GameProfile {} uses a premium username", username);
if (core.getConfig().get("autoRegister", false) && (authHook == null || !authHook.isRegistered(username))) {
requestPremiumLogin(source, profile, username, false);
@ -76,18 +82,18 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
return false;
}
private boolean checkNameChange(S source, String username, UUID premiumUUID) {
private boolean checkNameChange(S source, String username, Profile profile) {
//user not exists in the db
if (core.getConfig().get("nameChangeCheck", false)) {
PlayerProfile profile = core.getStorage().loadProfile(premiumUUID);
if (profile != null) {
StoredProfile storedProfile = core.getStorage().loadProfile(profile.getId());
if (storedProfile != null) {
//uuid exists in the database
core.getPlugin().getLog().info("GameProfile {} changed it's username", premiumUUID);
core.getPlugin().getLog().info("GameProfile {} changed it's username", profile);
//update the username to the new one in the database
profile.setPlayerName(username);
storedProfile.setPlayerName(username);
requestPremiumLogin(source, profile, username, false);
requestPremiumLogin(source, storedProfile, username, false);
return true;
}
}
@ -95,7 +101,7 @@ public abstract class JoinManagement<P extends C, C, S extends LoginSource> {
return false;
}
public abstract void requestPremiumLogin(S source, PlayerProfile profile, String username, boolean registered);
public abstract void requestPremiumLogin(S source, StoredProfile profile, String username, boolean registered);
public abstract void startCrackedSession(S source, PlayerProfile profile, String username);
public abstract void startCrackedSession(S source, StoredProfile profile, String username);
}

View File

@ -1,19 +1,19 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.PlayerProfile;
import com.github.games647.fastlogin.core.StoredProfile;
import java.util.UUID;
public abstract class LoginSession {
private final String username;
private final PlayerProfile profile;
private final StoredProfile profile;
private UUID uuid;
protected boolean registered;
public LoginSession(String username, boolean registered, PlayerProfile profile) {
public LoginSession(String username, boolean registered, StoredProfile profile) {
this.username = username;
this.registered = registered;
this.profile = profile;
@ -32,7 +32,7 @@ public abstract class LoginSession {
return !registered;
}
public PlayerProfile getProfile() {
public StoredProfile getProfile() {
return profile;
}

View File

@ -1,10 +1,6 @@
package com.github.games647.fastlogin.core.shared;
import com.github.games647.fastlogin.core.mojang.MojangApiConnector;
import com.google.common.net.HostAndPort;
import java.nio.file.Path;
import java.util.List;
import java.util.concurrent.ThreadFactory;
import org.slf4j.Logger;
@ -22,6 +18,4 @@ public interface PlatformPlugin<C> {
default ThreadFactory getThreadFactory() {
return null;
}
MojangApiConnector makeApiConnector(List<String> addresses, int requests, List<HostAndPort> proxies);
}