Compare commits

..

7 Commits

Author SHA1 Message Date
TuxCoding
568ad7a621 Add configuration option to kick players when toggling premium state
Fixes #1285
2025-04-23 17:12:32 +02:00
TuxCoding
1464ef2952 Add command invoker for change command events
Related #1285
2025-04-23 16:46:37 +02:00
TuxCoding
46239c9ffc Use InboundConnection as session keys for Velocity
Related #1286
2025-04-23 12:22:10 +02:00
TuxCoding
083068b856 Update Mockito for newer Java versions 2025-04-23 12:21:24 +02:00
TuxCoding
dd2aa922d7 [ci-skip] Add project logo 2025-03-18 17:22:48 +01:00
games647
b77ea285e5 Be more precise about why using legacy scheduler 2025-03-18 17:22:48 +01:00
games647
f8f0e7ac7a Drop lombok tool 2025-03-18 17:22:48 +01:00
26 changed files with 176 additions and 113 deletions

View File

@@ -1,5 +1,7 @@
# FastLogin
![A shield-shaped emblem with a bold lightning bolt on the left, resembling Minecraft blocks. To the right, "FastLogin" is written in teal, with the tagline: "Automatically detect and login premium Minecraft players"](https://github.com/user-attachments/assets/0788ef69-029b-465e-83a2-b8e7bccc6295 "FastLogin project logo.avif")
Checks if a Minecraft player has a paid account (premium). If so, they can skip offline authentication (auth plugins).
So they don't need to enter passwords. This is also called auto login (auto-login).

View File

@@ -30,6 +30,7 @@ import com.github.games647.fastlogin.bukkit.event.BukkitFastLoginPremiumToggleEv
import com.github.games647.fastlogin.core.storage.StoredProfile;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import static com.github.games647.fastlogin.core.shared.event.FastLoginPremiumToggleEvent.PremiumToggleReason;
@@ -57,6 +58,7 @@ public class CrackedCommand extends ToggleCommand {
return;
}
Player player = (Player) sender;
if (forwardCrackedCommand(sender, sender.getName())) {
return;
}
@@ -71,7 +73,16 @@ public class CrackedCommand extends ToggleCommand {
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
plugin.getServer().getPluginManager().callEvent(
new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_OTHER));
new BukkitFastLoginPremiumToggleEvent(sender, profile, PremiumToggleReason.COMMAND_OTHER)
);
plugin.getScheduler().getSyncExecutor().execute(() -> {
if (plugin.getCore().getConfig().getBoolean("kick-toggle", true)) {
player.kickPlayer(plugin.getCore().getMessage("remove-premium"));
} else {
plugin.getCore().sendLocaleMessage("add-premium", sender);
}
});
});
} else {
plugin.getCore().sendLocaleMessage("not-premium", sender);
@@ -104,7 +115,7 @@ public class CrackedCommand extends ToggleCommand {
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
plugin.getServer().getPluginManager().callEvent(
new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_OTHER));
new BukkitFastLoginPremiumToggleEvent(sender, profile, PremiumToggleReason.COMMAND_OTHER));
});
}
}

View File

@@ -68,7 +68,8 @@ public class PremiumCommand extends ToggleCommand {
return;
}
UUID id = ((Player) sender).getUniqueId();
Player player = (Player) sender;
UUID id = player.getUniqueId();
if (plugin.getConfig().getBoolean("premium-warning") && !plugin.getCore().getPendingConfirms().contains(id)) {
sender.sendMessage(plugin.getCore().getMessage("premium-warning"));
plugin.getCore().getPendingConfirms().add(id);
@@ -86,10 +87,17 @@ public class PremiumCommand extends ToggleCommand {
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
plugin.getServer().getPluginManager().callEvent(
new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_SELF));
});
new BukkitFastLoginPremiumToggleEvent(sender, profile, PremiumToggleReason.COMMAND_SELF)
);
plugin.getCore().sendLocaleMessage("add-premium", sender);
plugin.getScheduler().getSyncExecutor().execute(() -> {
if (plugin.getCore().getConfig().getBoolean("kick-toggle", true)) {
player.kickPlayer(plugin.getCore().getMessage("remove-premium"));
} else {
plugin.getCore().sendLocaleMessage("add-premium", sender);
}
});
});
}
}
@@ -117,7 +125,8 @@ public class PremiumCommand extends ToggleCommand {
plugin.getScheduler().runAsync(() -> {
plugin.getCore().getStorage().save(profile);
plugin.getServer().getPluginManager().callEvent(
new BukkitFastLoginPremiumToggleEvent(profile, PremiumToggleReason.COMMAND_OTHER));
new BukkitFastLoginPremiumToggleEvent(sender, profile, PremiumToggleReason.COMMAND_OTHER)
);
});
plugin.getCore().sendLocaleMessage("add-premium-other", sender);

View File

@@ -27,6 +27,7 @@ package com.github.games647.fastlogin.bukkit.event;
import com.github.games647.fastlogin.core.shared.event.FastLoginPremiumToggleEvent;
import com.github.games647.fastlogin.core.storage.StoredProfile;
import org.bukkit.command.CommandSender;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.NotNull;
@@ -34,11 +35,15 @@ import org.jetbrains.annotations.NotNull;
public class BukkitFastLoginPremiumToggleEvent extends Event implements FastLoginPremiumToggleEvent {
private static final HandlerList HANDLERS = new HandlerList();
private final CommandSender invoker;
private final StoredProfile profile;
private final PremiumToggleReason reason;
public BukkitFastLoginPremiumToggleEvent(StoredProfile profile, PremiumToggleReason reason) {
public BukkitFastLoginPremiumToggleEvent(CommandSender invoker, StoredProfile profile, PremiumToggleReason reason) {
super(true);
this.invoker = invoker;
this.profile = profile;
this.reason = reason;
}
@@ -48,6 +53,13 @@ public class BukkitFastLoginPremiumToggleEvent extends Event implements FastLogi
return profile;
}
/**
* @return who triggered this change. This could be a Player for itself or others (Admin) or the console.
*/
public CommandSender getInvoker() {
return invoker;
}
@Override
public PremiumToggleReason getReason() {
return reason;

View File

@@ -31,7 +31,6 @@ import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import com.google.common.io.Resources;
import com.google.common.primitives.Longs;
import lombok.val;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
@@ -41,6 +40,7 @@ import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URL;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
@@ -53,6 +53,7 @@ import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Instant;
import java.util.Arrays;
@@ -198,9 +199,9 @@ final class EncryptionUtil {
private static PublicKey loadMojangSessionKey()
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
val keyUrl = FastLoginBukkit.class.getClassLoader().getResource("yggdrasil_session_pubkey.der");
val keyData = Resources.toByteArray(keyUrl);
val keySpec = new X509EncodedKeySpec(keyData);
URL keyUrl = FastLoginBukkit.class.getClassLoader().getResource("yggdrasil_session_pubkey.der");
byte[] keyData = Resources.toByteArray(keyUrl);
KeySpec keySpec = new X509EncodedKeySpec(keyData);
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
}

View File

@@ -39,6 +39,7 @@ import com.comphenix.protocol.utility.MinecraftVersion;
import com.comphenix.protocol.wrappers.BukkitConverters;
import com.comphenix.protocol.wrappers.Converters;
import com.comphenix.protocol.wrappers.WrappedGameProfile;
import com.comphenix.protocol.wrappers.WrappedProfilePublicKey.WrappedProfileKeyData;
import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
@@ -48,7 +49,6 @@ import com.mojang.datafixers.util.Either;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.util.AttributeKey;
import lombok.val;
import org.bukkit.entity.Player;
import org.geysermc.floodgate.api.player.FloodgatePlayer;
import org.jetbrains.annotations.NotNull;
@@ -245,8 +245,9 @@ public class ProtocolLibListener extends PacketAdapter {
// public key is sent separate
clientKey = Optional.empty();
} else {
val profileKey = packet.getOptionals(BukkitConverters.getWrappedPublicKeyDataConverter())
.optionRead(0);
Optional<Optional<WrappedProfileKeyData>> profileKey = packet.getOptionals(
BukkitConverters.getWrappedPublicKeyDataConverter()
).optionRead(0);
clientKey = profileKey.flatMap(Function.identity()).flatMap(data -> {
Instant expires = data.getExpireTime();

View File

@@ -50,7 +50,6 @@ import com.github.games647.fastlogin.bukkit.BukkitLoginSession;
import com.github.games647.fastlogin.bukkit.FastLoginBukkit;
import com.github.games647.fastlogin.bukkit.InetUtils;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import lombok.val;
import org.bukkit.entity.Player;
import javax.crypto.Cipher;
@@ -307,7 +306,7 @@ public class VerifyResponseTask implements Runnable {
startPacket.getStrings().write(0, username);
EquivalentConverter<WrappedProfileKeyData> converter = BukkitConverters.getWrappedPublicKeyDataConverter();
val wrappedKey = Optional.ofNullable(clientKey).map(key ->
Optional<WrappedProfileKeyData> wrappedKey = Optional.ofNullable(clientKey).map(key ->
new WrappedProfileKeyData(clientKey.expiry(), clientKey.key(), clientKey.signature())
);

View File

@@ -25,20 +25,38 @@
*/
package com.github.games647.fastlogin.bukkit.listener.protocollib.packet;
import lombok.Value;
import lombok.experimental.Accessors;
import java.security.PublicKey;
import java.time.Instant;
import java.util.Base64;
import java.util.StringJoiner;
@Accessors(fluent = true)
@Value(staticConstructor = "of")
public class ClientPublicKey {
Instant expiry;
PublicKey key;
byte[] signature;
private final Instant expiry;
private final PublicKey key;
private final byte[] signature;
public Instant expiry() {
return expiry;
}
public PublicKey key() {
return key;
}
public byte[] signature() {
return signature;
}
public ClientPublicKey(Instant expiry, PublicKey key, byte[] signature) {
this.expiry = expiry;
this.key = key;
this.signature = signature;
}
public static ClientPublicKey of(Instant expiry, PublicKey key, byte[] signature) {
return new ClientPublicKey(expiry, key, signature);
}
public boolean isExpired(Instant verifyTimestamp) {
return !verifyTimestamp.isBefore(expiry);

View File

@@ -26,7 +26,7 @@
package com.github.games647.fastlogin.bukkit;
import com.github.games647.fastlogin.core.CommonUtil;
import lombok.val;
import net.md_5.bungee.api.chat.BaseComponent;
import net.md_5.bungee.api.chat.TextComponent;
import net.md_5.bungee.chat.ComponentSerializer;
import org.junit.jupiter.api.Test;
@@ -37,13 +37,13 @@ class FastLoginBukkitTest {
@Test
void testRGB() {
val message = "&x00002a00002b&lText";
val msg = CommonUtil.translateColorCodes(message);
String message = "&x00002a00002b&lText";
String msg = CommonUtil.translateColorCodes(message);
assertEquals(msg, "§x00002a00002b§lText");
@SuppressWarnings("deprecation")
val components = TextComponent.fromLegacyText(msg);
val expected = "{\"bold\":true,\"color\":\"#00a00b\",\"text\":\"Text\"}";
BaseComponent[] components = TextComponent.fromLegacyText(msg);
String expected = "{\"bold\":true,\"color\":\"#00a00b\",\"text\":\"Text\"}";
//noinspection deprecation
assertEquals(ComponentSerializer.toString(components), expected);
}

View File

@@ -28,7 +28,6 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import lombok.val;
import java.io.IOException;
import java.util.Base64;
@@ -37,7 +36,7 @@ public class Base64Adapter extends TypeAdapter<byte[]> {
@Override
public void write(JsonWriter out, byte[] value) throws IOException {
val encoded = Base64.getEncoder().encodeToString(value);
String encoded = Base64.getEncoder().encodeToString(value);
out.value(encoded);
}

View File

@@ -27,8 +27,8 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.github.games647.fastlogin.bukkit.listener.protocollib.SignatureTestData.SignatureData;
import com.github.games647.fastlogin.bukkit.listener.protocollib.packet.ClientPublicKey;
import com.google.common.hash.Hasher;
import com.google.common.hash.Hashing;
import lombok.val;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -51,6 +51,7 @@ import java.security.SignatureException;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.ThreadLocalRandom;
@@ -60,7 +61,7 @@ class EncryptionUtilTest {
@Test
void testVerifyToken() {
val random = ThreadLocalRandom.current();
Random random = ThreadLocalRandom.current();
byte[] token = EncryptionUtil.generateVerifyToken(random);
assertAll(
@@ -88,10 +89,10 @@ class EncryptionUtilTest {
@Test
void testExpiredClientKey() throws Exception {
val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
// Client expires at the exact second mentioned, so use it for verification
val expiredTimestamp = clientKey.expiry();
Instant expiredTimestamp = clientKey.expiry();
assertFalse(EncryptionUtil.verifyClientKey(clientKey, expiredTimestamp, null));
}
@@ -105,7 +106,7 @@ class EncryptionUtilTest {
"client_keys/invalid_wrong_signature.json"
})
void testInvalidClientKey(String clientKeySource) throws Exception {
val clientKey = ResourceLoader.loadClientKey(clientKeySource);
ClientPublicKey clientKey = ResourceLoader.loadClientKey(clientKeySource);
Instant expireTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
assertFalse(EncryptionUtil.verifyClientKey(clientKey, expireTimestamp, null));
@@ -113,34 +114,34 @@ class EncryptionUtilTest {
@Test
void testValidClientKey() throws Exception {
val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
val verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key.json");
Instant verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
assertTrue(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp, null));
}
@Test
void testValid191ClientKey() throws Exception {
val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key_19_1.json");
val verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key_19_1.json");
Instant verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
val ownerPremiumId = UUID.fromString("0aaa2c13-922a-411b-b655-9b8c08404695");
UUID ownerPremiumId = UUID.fromString("0aaa2c13-922a-411b-b655-9b8c08404695");
assertTrue(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp, ownerPremiumId));
}
@Test
void testIncorrect191ClientOwner() throws Exception {
val clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key_19_1.json");
val verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
ClientPublicKey clientKey = ResourceLoader.loadClientKey("client_keys/valid_public_key_19_1.json");
Instant verificationTimestamp = clientKey.expiry().minus(5, ChronoUnit.HOURS);
val ownerPremiumId = UUID.fromString("61699b2e-d327-4a01-9f1e-0ea8c3f06bc6");
UUID ownerPremiumId = UUID.fromString("61699b2e-d327-4a01-9f1e-0ea8c3f06bc6");
assertFalse(EncryptionUtil.verifyClientKey(clientKey, verificationTimestamp, ownerPremiumId));
}
@Test
void testDecryptSharedSecret() throws Exception {
KeyPair serverPair = EncryptionUtil.generateKeyPair();
val serverPK = serverPair.getPublic();
PublicKey serverPK = serverPair.getPublic();
SecretKey secretKey = generateSharedKey();
byte[] encryptedSecret = encrypt(serverPK, secretKey.getEncoded());
@@ -152,7 +153,7 @@ class EncryptionUtilTest {
private static byte[] encrypt(PublicKey receiverKey, byte... message)
throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException,
IllegalBlockSizeException, BadPaddingException {
val encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm());
Cipher encryptCipher = Cipher.getInstance(receiverKey.getAlgorithm());
encryptCipher.init(Cipher.ENCRYPT_MODE, receiverKey);
return encryptCipher.doFinal(message);
}
@@ -168,9 +169,9 @@ class EncryptionUtilTest {
@Test
void testServerIdHash() throws Exception {
val serverId = "";
val sharedSecret = generateSharedKey();
val serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
String serverId = "";
SecretKeySpec sharedSecret = generateSharedKey();
PublicKey serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
assertEquals(EncryptionUtil.getServerIdHashString(serverId, sharedSecret, serverPK), sessionHash);
@@ -185,7 +186,7 @@ class EncryptionUtilTest {
// sha1.update(server's encoded public key from Encryption Request)
// hash := sha1.hexdigest() # String of hex characters
@SuppressWarnings("deprecation")
val hasher = Hashing.sha1().newHasher();
Hasher hasher = Hashing.sha1().newHasher();
hasher.putString(serverId, StandardCharsets.US_ASCII);
hasher.putBytes(sharedSecret.getEncoded());
hasher.putBytes(serverPK.getEncoded());
@@ -198,9 +199,9 @@ class EncryptionUtilTest {
@Test
void testServerIdHashWrongSecret() throws Exception {
val serverId = "";
val sharedSecret = generateSharedKey();
val serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
String serverId = "";
SecretKeySpec sharedSecret = generateSharedKey();
PublicKey serverPK = ResourceLoader.loadClientKey("client_keys/valid_public_key.json").key();
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
assertNotEquals(EncryptionUtil.getServerIdHashString("", generateSharedKey(), serverPK), sessionHash);
@@ -208,12 +209,12 @@ class EncryptionUtilTest {
@Test
void testServerIdHashWrongServerKey() {
val serverId = "";
val sharedSecret = generateSharedKey();
val serverPK = EncryptionUtil.generateKeyPair().getPublic();
String serverId = "";
SecretKeySpec sharedSecret = generateSharedKey();
PublicKey serverPK = EncryptionUtil.generateKeyPair().getPublic();
String sessionHash = getServerHash(serverId, sharedSecret, serverPK);
val wrongPK = EncryptionUtil.generateKeyPair().getPublic();
PublicKey wrongPK = EncryptionUtil.generateKeyPair().getPublic();
assertNotEquals(EncryptionUtil.getServerIdHashString("", sharedSecret, wrongPK), sessionHash);
}
@@ -257,8 +258,8 @@ class EncryptionUtilTest {
@Test
void testNonce() throws Exception {
byte[] expected = {1, 2, 3, 4};
val serverKey = EncryptionUtil.generateKeyPair();
val encryptedNonce = encrypt(serverKey.getPublic(), expected);
KeyPair serverKey = EncryptionUtil.generateKeyPair();
byte[] encryptedNonce = encrypt(serverKey.getPublic(), expected);
assertTrue(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce));
}
@@ -266,19 +267,19 @@ class EncryptionUtilTest {
@Test
void testNonceIncorrect() throws Exception {
byte[] expected = {1, 2, 3, 4};
val serverKey = EncryptionUtil.generateKeyPair();
KeyPair serverKey = EncryptionUtil.generateKeyPair();
// flipped first character
val encryptedNonce = encrypt(serverKey.getPublic(), new byte[]{0, 2, 3, 4});
byte[] encryptedNonce = encrypt(serverKey.getPublic(), new byte[]{0, 2, 3, 4});
assertFalse(EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce));
}
@Test
void testNonceFailedDecryption() throws Exception {
byte[] expected = {1, 2, 3, 4};
val serverKey = EncryptionUtil.generateKeyPair();
KeyPair serverKey = EncryptionUtil.generateKeyPair();
// generate a new keypair that is different
val encryptedNonce = encrypt(EncryptionUtil.generateKeyPair().getPublic(), expected);
byte[] encryptedNonce = encrypt(EncryptionUtil.generateKeyPair().getPublic(), expected);
assertThrows(GeneralSecurityException.class,
() -> EncryptionUtil.verifyNonce(expected, serverKey.getPrivate(), encryptedNonce)
@@ -288,7 +289,7 @@ class EncryptionUtilTest {
@Test
void testNonceIncorrectEmpty() {
byte[] expected = {1, 2, 3, 4};
val serverKey = EncryptionUtil.generateKeyPair();
KeyPair serverKey = EncryptionUtil.generateKeyPair();
byte[] encryptedNonce = {};
assertThrows(GeneralSecurityException.class,

View File

@@ -28,16 +28,16 @@ package com.github.games647.fastlogin.bukkit.listener.protocollib;
import com.google.common.io.Resources;
import com.google.gson.Gson;
import com.google.gson.annotations.JsonAdapter;
import lombok.val;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class SignatureTestData {
public static SignatureTestData fromResource(String resourceName) throws IOException {
val keyUrl = Resources.getResource(resourceName);
val encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
URL keyUrl = Resources.getResource(resourceName);
String encodedSignature = Resources.toString(keyUrl, StandardCharsets.US_ASCII);
return new Gson().fromJson(encodedSignature, SignatureTestData.class);
}

View File

@@ -26,7 +26,6 @@
package com.github.games647.fastlogin.bukkit.task;
import com.github.games647.fastlogin.core.hooks.AuthPlugin;
import lombok.val;
import org.bukkit.entity.Player;
import org.junit.jupiter.api.Test;
@@ -36,7 +35,7 @@ class DelayedAuthHookTest {
@Test
void createNewReflectiveInstance() throws ReflectiveOperationException {
val authHook = new DelayedAuthHook(null);
DelayedAuthHook authHook = new DelayedAuthHook(null);
assertNotNull(authHook.newInstance(DummyHook.class));
}

View File

@@ -76,7 +76,12 @@ public class AsyncToggleMessage implements Runnable {
? PremiumToggleReason.COMMAND_OTHER : PremiumToggleReason.COMMAND_SELF;
core.getPlugin().getProxy().getPluginManager().callEvent(
new BungeeFastLoginPremiumToggleEvent(playerProfile, reason));
sendMessage("remove-premium");
if (isPlayerSender && core.getConfig().getBoolean("kick-toggle", true)) {
sender.disconnect(TextComponent.fromLegacyText(core.getMessage("remove-premium")));
} else {
sendMessage("remove-premium");
}
}
private void activatePremium() {

View File

@@ -147,7 +147,7 @@
<dependency>
<groupId>net.md-5</groupId>
<artifactId>bungeecord-config</artifactId>
<version>1.20-R0.2-SNAPSHOT</version>
<version>1.20-R0.2</version>
</dependency>
<!-- This is optional in BungeeCord-config, so we include it here manually -->

View File

@@ -63,7 +63,7 @@ public class LoginActionMessage implements ChannelMessage {
@Override
public void readFrom(ByteArrayDataInput input) {
this.type = Type.values()[input.readInt()];
this.type = Type.values()[input.readByte()];
this.playerName = input.readUTF();
@@ -75,7 +75,7 @@ public class LoginActionMessage implements ChannelMessage {
@Override
public void writeTo(ByteArrayDataOutput output) {
output.writeInt(type.ordinal());
output.writeByte(type.ordinal());
//Data is sent through a random player. We have to tell the Bukkit version of this plugin the target
output.writeUTF(playerName);

View File

@@ -42,7 +42,8 @@ public class AsyncScheduler extends AbstractAsyncScheduler {
public AsyncScheduler(Logger logger, Executor processingPool) {
super(logger, processingPool);
logger.info("Using legacy scheduler");
logger.info("Using legacy platform scheduler for using an older Java version. "
+ "Upgrade Java to 21+ for improved performance");
}
@Override

View File

@@ -150,11 +150,14 @@ nameChangeCheck: false
forwardSkin: true
# Displays a warning message that this message SHOULD only be invoked by
# users who actually are the owner of this account. So not by cracked players
# users who actually are the owner of this account (and not cracked players)
#
# If they still want to invoke the command, they have to invoke /premium again
premium-warning: true
# Kick players after they confirmed the command from above.
kick-toggle: true
# ======[[ Spigot+ProtocolLib users only ]]======
# When set to true, enables the use of alternative session resolver which does not send the server IP
# to mojang session servers. This setting might be useful when you are trying to run the server via a

10
pom.xml
View File

@@ -218,14 +218,6 @@
</build>
<dependencies>
<!-- Use lombok to use some newer Java syntax features in Java 8 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombook.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
@@ -237,7 +229,7 @@
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.12.0</version>
<version>5.17.0</version>
<scope>test</scope>
</dependency>
</dependencies>

View File

@@ -46,6 +46,7 @@ import com.velocitypowered.api.event.proxy.ProxyInitializeEvent;
import com.velocitypowered.api.event.proxy.ProxyShutdownEvent;
import com.velocitypowered.api.plugin.Plugin;
import com.velocitypowered.api.plugin.annotation.DataDirectory;
import com.velocitypowered.api.proxy.InboundConnection;
import com.velocitypowered.api.proxy.Player;
import com.velocitypowered.api.proxy.ProxyServer;
import com.velocitypowered.api.proxy.messages.ChannelMessageSink;
@@ -57,7 +58,6 @@ import org.geysermc.geyser.GeyserImpl;
import org.slf4j.Logger;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -75,7 +75,7 @@ public class FastLoginVelocity implements PlatformPlugin<CommandSource> {
private final ProxyServer server;
private final Path dataDirectory;
private final Logger logger;
private final ConcurrentMap<InetSocketAddress, VelocityLoginSession> session = new MapMaker().weakKeys().makeMap();
private final ConcurrentMap<InboundConnection, VelocityLoginSession> session = new MapMaker().weakKeys().makeMap();
private static final String PROXY_ID_FILE = "proxyId.txt";
private FastLoginCore<Player, CommandSource, FastLoginVelocity> core;
@@ -175,7 +175,7 @@ public class FastLoginVelocity implements PlatformPlugin<CommandSource> {
return core;
}
public ConcurrentMap<InetSocketAddress, VelocityLoginSession> getSession() {
public ConcurrentMap<InboundConnection, VelocityLoginSession> getSession() {
return session;
}

View File

@@ -66,4 +66,8 @@ public class VelocityLoginSource implements LoginSource {
public InetSocketAddress getAddress() {
return connection.getRemoteAddress();
}
public InboundConnection getConnection() {
return connection;
}
}

View File

@@ -119,9 +119,9 @@ public class ConnectListener {
@Subscribe
public void onGameProfileRequest(GameProfileRequestEvent event) {
if (event.isOnlineMode()) {
LoginSession session = plugin.getSession().get(event.getConnection().getRemoteAddress());
LoginSession session = plugin.getSession().get(event.getConnection());
if (session == null) {
plugin.getLog().warn("No active login session found for player {}", event.getUsername());
plugin.getLog().error("No active login session found for onlinemode player {}", event.getUsername());
return;
}
@@ -173,7 +173,7 @@ public class ConnectListener {
}
}
VelocityLoginSession session = plugin.getSession().get(player.getRemoteAddress());
VelocityLoginSession session = plugin.getSession().get(player);
if (session == null) {
plugin.getLog().info("No active login session found on server connect for {}", player);
return;
@@ -192,6 +192,8 @@ public class ConnectListener {
public void onDisconnect(DisconnectEvent disconnectEvent) {
Player player = disconnectEvent.getPlayer();
plugin.getCore().getPendingConfirms().remove(player.getUniqueId());
plugin.getSession().remove(player);
}
/**

View File

@@ -83,12 +83,12 @@ public class PluginMessageListener {
plugin.getScheduler().runAsync(() -> readMessage(forPlayer, channel, data));
}
private void readMessage(Player forPlayer, String channel, byte[] data) {
private void readMessage(Player sender, String channel, byte[] data) {
FastLoginCore<Player, CommandSource, FastLoginVelocity> core = plugin.getCore();
ByteArrayDataInput dataInput = ByteStreams.newDataInput(data);
if (successChannel.equals(channel)) {
onSuccessMessage(forPlayer);
onSuccessMessage(sender);
} else if (changeChannel.equals(channel)) {
ChangePremiumMessage changeMessage = new ChangePremiumMessage();
changeMessage.readFrom(dataInput);
@@ -97,19 +97,19 @@ public class PluginMessageListener {
boolean isSourceInvoker = changeMessage.isSourceInvoker();
if (changeMessage.shouldEnable()) {
Boolean premiumWarning = plugin.getCore().getConfig().get("premium-warning", true);
if (playerName.equals(forPlayer.getUsername()) && premiumWarning
&& !core.getPendingConfirms().contains(forPlayer.getUniqueId())) {
if (playerName.equals(sender.getUsername()) && premiumWarning
&& !core.getPendingConfirms().contains(sender.getUniqueId())) {
String message = core.getMessage("premium-warning");
forPlayer.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(message));
core.getPendingConfirms().add(forPlayer.getUniqueId());
sender.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(message));
core.getPendingConfirms().add(sender.getUniqueId());
return;
}
core.getPendingConfirms().remove(forPlayer.getUniqueId());
Runnable task = new AsyncToggleMessage(core, forPlayer, playerName, true, isSourceInvoker);
core.getPendingConfirms().remove(sender.getUniqueId());
Runnable task = new AsyncToggleMessage(core, sender, playerName, true, isSourceInvoker);
plugin.getScheduler().runAsync(task);
} else {
Runnable task = new AsyncToggleMessage(core, forPlayer, playerName, false, isSourceInvoker);
Runnable task = new AsyncToggleMessage(core, sender, playerName, false, isSourceInvoker);
plugin.getScheduler().runAsync(task);
}
}
@@ -127,7 +127,7 @@ public class PluginMessageListener {
if (shouldPersist) {
//bukkit module successfully received and force logged in the user
//update only on success to prevent corrupt data
VelocityLoginSession loginSession = plugin.getSession().get(forPlayer.getRemoteAddress());
VelocityLoginSession loginSession = plugin.getSession().get(forPlayer);
StoredProfile playerProfile = loginSession.getProfile();
loginSession.setRegistered(true);
if (!loginSession.isAlreadySaved()) {

View File

@@ -58,7 +58,7 @@ public class AsyncPremiumCheck extends JoinManagement<Player, CommandSource, Vel
@Override
public void run() {
plugin.getSession().remove(connection.getRemoteAddress());
plugin.getSession().remove(connection);
super.onLogin(username, new VelocityLoginSource(connection, preLoginEvent));
}
@@ -82,7 +82,7 @@ public class AsyncPremiumCheck extends JoinManagement<Player, CommandSource, Vel
String username, boolean registered) {
source.enableOnlinemode();
VelocityLoginSession session = new VelocityLoginSession(username, registered, profile);
plugin.getSession().put(source.getAddress(), session);
plugin.getSession().put(source.getConnection(), session);
String ip = source.getAddress().getAddress().getHostAddress();
plugin.getCore().addLoginAttempt(ip, username);
@@ -91,6 +91,6 @@ public class AsyncPremiumCheck extends JoinManagement<Player, CommandSource, Vel
@Override
public void startCrackedSession(VelocityLoginSource source, StoredProfile profile, String username) {
VelocityLoginSession session = new VelocityLoginSession(username, false, profile);
plugin.getSession().put(source.getAddress(), session);
plugin.getSession().put(source.getConnection(), session);
}
}

View File

@@ -33,29 +33,26 @@ import com.github.games647.fastlogin.velocity.event.VelocityFastLoginPremiumTogg
import com.velocitypowered.api.command.CommandSource;
import com.velocitypowered.api.proxy.ConsoleCommandSource;
import com.velocitypowered.api.proxy.Player;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
public class AsyncToggleMessage implements Runnable {
private final FastLoginCore<Player, CommandSource, FastLoginVelocity> core;
private final CommandSource sender;
private final Player sender;
private final String senderName;
private final String targetPlayer;
private final boolean toPremium;
private final boolean isPlayerSender;
public AsyncToggleMessage(FastLoginCore<Player, CommandSource, FastLoginVelocity> core,
CommandSource sender, String playerName, boolean toPremium, boolean playerSender) {
Player sender, String playerName, boolean toPremium, boolean playerSender) {
this.core = core;
this.sender = sender;
this.targetPlayer = playerName;
this.toPremium = toPremium;
this.isPlayerSender = playerSender;
if (sender instanceof Player playSender) {
senderName = playSender.getUsername();
} else {
senderName = "";
}
this.senderName = sender.getUsername();
}
@Override
@@ -82,7 +79,14 @@ public class AsyncToggleMessage implements Runnable {
? PremiumToggleReason.COMMAND_OTHER : PremiumToggleReason.COMMAND_SELF;
core.getPlugin().getProxy().getEventManager().fire(
new VelocityFastLoginPremiumToggleEvent(playerProfile, reason));
sendMessage("remove-premium");
if (isPlayerSender && core.getConfig().getBoolean("kick-toggle", true)) {
TextComponent msg = LegacyComponentSerializer.legacyAmpersand()
.deserialize(core.getMessage("remove-premium"));
sender.disconnect(msg);
} else {
sendMessage("remove-premium");
}
}
private void activatePremium() {

View File

@@ -52,7 +52,7 @@ public class FloodgateAuthTask
@Override
protected void startLogin() {
VelocityLoginSession session = new VelocityLoginSession(player.getUsername(), isRegistered, profile);
core.getPlugin().getSession().put(player.getRemoteAddress(), session);
core.getPlugin().getSession().put(player, session);
// enable auto login based on the value of 'autoLoginFloodgate' in config.yml
boolean forcedOnlineMode = autoLoginFloodgate.equals("true")