diff --git a/src/main/java/org/samo_lego/simpleauth/SimpleAuth.java b/src/main/java/org/samo_lego/simpleauth/SimpleAuth.java index 37a0e2f..e77ad29 100644 --- a/src/main/java/org/samo_lego/simpleauth/SimpleAuth.java +++ b/src/main/java/org/samo_lego/simpleauth/SimpleAuth.java @@ -22,6 +22,7 @@ import java.io.FileReader; import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; +import java.util.HashSet; import java.util.Properties; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -46,6 +47,14 @@ public class SimpleAuth implements DedicatedServerModInitializer { */ public static HashMap playerCacheMap = new HashMap<>(); + /** + * HashSet of player names that have Mojang accounts. + * If player is saved in here, they will be treated as online-mode ones. + */ + public static HashSet mojangAccountNamesCache = new HashSet<>(); + + public static HashSet accountStatusCache = new HashSet<>(); + // Getting game directory public static final Path gameDirectory = FabricLoader.getInstance().getGameDir(); @@ -62,6 +71,12 @@ public class SimpleAuth implements DedicatedServerModInitializer { // The support on discord was great! I really appreciate your help. logInfo("This mod wouldn't exist without the awesome Fabric Community. TYSM guys!"); + try { + serverProp.load(new FileReader(gameDirectory + "/server.properties")); + } catch (IOException e) { + logError("Error while reading server properties: " + e.getMessage()); + } + // Creating data directory (database and config files are stored there) File file = new File(gameDirectory + "/mods/SimpleAuth/leveldbStore"); if (!file.exists() && !file.mkdirs()) @@ -71,12 +86,6 @@ public class SimpleAuth implements DedicatedServerModInitializer { // Connecting to db DB.openConnection(); - try { - serverProp.load(new FileReader(gameDirectory + "/server.properties")); - } catch (IOException e) { - logError("Error while reading server properties: " + e.getMessage()); - } - // Registering the commands CommandRegistrationCallback.EVENT.register((dispatcher, dedicated) -> { diff --git a/src/main/java/org/samo_lego/simpleauth/mixin/MixinPlayerEntity.java b/src/main/java/org/samo_lego/simpleauth/mixin/MixinPlayerEntity.java index 9553c4d..2b46435 100644 --- a/src/main/java/org/samo_lego/simpleauth/mixin/MixinPlayerEntity.java +++ b/src/main/java/org/samo_lego/simpleauth/mixin/MixinPlayerEntity.java @@ -1,5 +1,6 @@ package org.samo_lego.simpleauth.mixin; +import com.mojang.authlib.GameProfile; import net.fabricmc.loader.api.FabricLoader; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; @@ -17,7 +18,9 @@ import org.samo_lego.simpleauth.SimpleAuth; import org.samo_lego.simpleauth.event.item.DropItemCallback; import org.samo_lego.simpleauth.storage.PlayerCache; import org.samo_lego.simpleauth.utils.PlayerAuth; +import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @@ -26,10 +29,12 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import static org.samo_lego.simpleauth.SimpleAuth.config; import static org.samo_lego.simpleauth.SimpleAuth.playerCacheMap; import static org.samo_lego.simpleauth.utils.CarpetHelper.isPlayerCarpetFake; +import static org.samo_lego.simpleauth.utils.SimpleLogger.logInfo; @Mixin(PlayerEntity.class) public abstract class MixinPlayerEntity implements PlayerAuth { + @Shadow @Final private GameProfile gameProfile; private final ServerPlayerEntity player = (ServerPlayerEntity) (Object) this; // * 20 for 20 ticks in second @@ -50,6 +55,8 @@ public abstract class MixinPlayerEntity implements PlayerAuth { assert server != null; PlayerCache cache = playerCacheMap.get(this.getFakeUuid()); + if(config.main.spawnOnJoin) + logInfo("Teleporting " + player.getName().asString() + (hide ? " to spawn." : " to original position.")); if (hide) { // Saving position cache.lastLocation.dimension = player.getServerWorld(); @@ -90,14 +97,15 @@ public abstract class MixinPlayerEntity implements PlayerAuth { public String getFakeUuid() { // If server is in online mode online-mode UUIDs should be used assert server != null; - if(server.isOnlineMode()) + if(server.isOnlineMode() && this.isUsingMojangAccount()) return player.getUuidAsString(); /* Lower case is used for Player and PlAyEr to get same UUID (for password storing) Mimicking Mojang behaviour, where players cannot set their name to ExAmple if Example is already taken. */ - String playername = player.getName().asString().toLowerCase(); + // Getting player+s name via GameProfile, in order to be compatible with Drogtor mod + String playername = player.getGameProfile().getName().toLowerCase(); return PlayerEntity.getOfflinePlayerUuid(playername).toString(); } @@ -172,14 +180,23 @@ public abstract class MixinPlayerEntity implements PlayerAuth { } /** - * Checks whether player is a fake player (from CarpetMod). + * Checks whether player can skip authentication process. * - * @return true if player is fake (can skip authentication process), otherwise false + * @return true if can skip authentication process, otherwise false */ @Override public boolean canSkipAuth() { // We ask CarpetHelper class since it has the imports needed - return this.isRunningCarpet && isPlayerCarpetFake(this.player); + return (this.isRunningCarpet && isPlayerCarpetFake(this.player)) || (isUsingMojangAccount() && config.experimental.premiumAutologin); + } + + /** + * Whether the player is using the mojang account. + * @return true if paid, otherwise false + */ + @Override + public boolean isUsingMojangAccount() { + return !gameProfile.getId().equals(PlayerEntity.getOfflinePlayerUuid(gameProfile.getName())); } /** @@ -190,7 +207,7 @@ public abstract class MixinPlayerEntity implements PlayerAuth { @Override public boolean isAuthenticated() { String uuid = ((PlayerAuth) player).getFakeUuid(); - return playerCacheMap.containsKey(uuid) && playerCacheMap.get(uuid).isAuthenticated; + return this.canSkipAuth() || (playerCacheMap.containsKey(uuid) && playerCacheMap.get(uuid).isAuthenticated); } @Inject(method = "tick()V", at = @At("HEAD"), cancellable = true) diff --git a/src/main/java/org/samo_lego/simpleauth/mixin/MixinServerLoginNetworkHandler.java b/src/main/java/org/samo_lego/simpleauth/mixin/MixinServerLoginNetworkHandler.java new file mode 100644 index 0000000..0d183c2 --- /dev/null +++ b/src/main/java/org/samo_lego/simpleauth/mixin/MixinServerLoginNetworkHandler.java @@ -0,0 +1,108 @@ +package org.samo_lego.simpleauth.mixin; + +import com.mojang.authlib.GameProfile; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.packet.c2s.login.LoginHelloC2SPacket; +import net.minecraft.server.network.ServerLoginNetworkHandler; +import net.minecraft.text.TranslatableText; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.net.ssl.HttpsURLConnection; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; + +import static org.samo_lego.simpleauth.SimpleAuth.*; +import static org.samo_lego.simpleauth.utils.SimpleLogger.logError; + +@Mixin(ServerLoginNetworkHandler.class) +public class MixinServerLoginNetworkHandler { + + @Shadow + private GameProfile profile; + @Shadow + private int loginTicks; + + /** + * Fake state of current player. + */ + private boolean acceptCrackedPlayer = false; + + /** + * Mimicks the ticking if autologin is enabled. + * @param ci + */ + @Inject(method = "tick()V", at = @At("HEAD"), cancellable = true) + private void preTick(CallbackInfo ci) { + if (this.acceptCrackedPlayer && config.experimental.premiumAutologin) { + ((ServerLoginNetworkHandler) (Object) this).acceptPlayer(); + + if (this.loginTicks++ == 600) + ((ServerLoginNetworkHandler) (Object) this).disconnect(new TranslatableText("multiplayer.disconnect.slow_login")); + ci.cancel(); + } + } + + /** + * Checks whether the player has purchased an account. + * If so, server is presented as online, and continues as in normal-online mode. + * Otherwise, player is marked as ready to be accepted into the game. + * @param packet + * @param ci + */ + @Inject( + method = "onHello(Lnet/minecraft/network/packet/c2s/login/LoginHelloC2SPacket;)V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/network/packet/c2s/login/LoginHelloC2SPacket;getProfile()Lcom/mojang/authlib/GameProfile;", + shift = At.Shift.AFTER + ), + cancellable = true + ) + private void checkPremium(LoginHelloC2SPacket packet, CallbackInfo ci) { + if(config.experimental.premiumAutologin) { + try { + String playername = packet.getProfile().getName().toLowerCase(); + if(playerCacheMap.containsKey(PlayerEntity.getOfflinePlayerUuid(playername).toString())) { + // Player definitely doesn't have a mojang account + this.acceptCrackedPlayer = true; + + this.profile = packet.getProfile(); + ci.cancel(); + } + else if(!mojangAccountNamesCache.contains(playername)) { + // Checking account status from API + HttpsURLConnection httpsURLConnection = (HttpsURLConnection) new URL("https://api.mojang.com/users/profiles/minecraft/" + playername).openConnection(); + httpsURLConnection.setRequestMethod("GET"); + httpsURLConnection.setConnectTimeout(5000); + httpsURLConnection.setReadTimeout(5000); + + int response = httpsURLConnection.getResponseCode(); + if (response == HttpURLConnection.HTTP_OK) { + // Player has a Mojang account + httpsURLConnection.disconnect(); + + + // Caches the request + mojangAccountNamesCache.add(playername); + // Authentication continues in original method + } + else if(response == HttpURLConnection.HTTP_NO_CONTENT) { + // Player doesn't have a Mojang account + httpsURLConnection.disconnect(); + this.acceptCrackedPlayer = true; + + this.profile = packet.getProfile(); + ci.cancel(); + } + } + } catch (IOException e) { + logError(e.getMessage()); + } + } + } +} diff --git a/src/main/java/org/samo_lego/simpleauth/storage/AuthConfig.java b/src/main/java/org/samo_lego/simpleauth/storage/AuthConfig.java index 7b58941..34af65a 100644 --- a/src/main/java/org/samo_lego/simpleauth/storage/AuthConfig.java +++ b/src/main/java/org/samo_lego/simpleauth/storage/AuthConfig.java @@ -23,6 +23,7 @@ import com.google.gson.GsonBuilder; import java.io.*; import java.nio.charset.StandardCharsets; +import static org.samo_lego.simpleauth.SimpleAuth.serverProp; import static org.samo_lego.simpleauth.utils.SimpleLogger.logError; public class AuthConfig { @@ -191,6 +192,12 @@ public class AuthConfig { * @see wiki */ public boolean useBCryptLibrary = false; + /** + * Whether players who have a valid session should skip the authentication process. + * You have to set online-mode to true in server.properties! + * (cracked players will still be able to enter, but they'll need to login) + */ + public boolean premiumAutologin = false; } public MainConfig main = new MainConfig(); @@ -212,6 +219,10 @@ public class AuthConfig { new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8) )) { config = gson.fromJson(fileReader, AuthConfig.class); + if(config.experimental.premiumAutologin && !Boolean.parseBoolean(serverProp.getProperty("online-mode"))) { + logError("You cannot use server in offline mode and premiumAutologin! Disabling the latter."); + config.experimental.premiumAutologin = false; + } } catch (IOException e) { throw new RuntimeException("[SimpleAuth] Problem occurred when trying to load config: ", e); } diff --git a/src/main/java/org/samo_lego/simpleauth/utils/PlayerAuth.java b/src/main/java/org/samo_lego/simpleauth/utils/PlayerAuth.java index 6aebecc..35abc37 100644 --- a/src/main/java/org/samo_lego/simpleauth/utils/PlayerAuth.java +++ b/src/main/java/org/samo_lego/simpleauth/utils/PlayerAuth.java @@ -58,4 +58,10 @@ public interface PlayerAuth { * @see See implementation */ boolean canSkipAuth(); + + /** + * Whether the player is using the mojang account + * @return true if paid, false if cracked + */ + boolean isUsingMojangAccount(); } diff --git a/src/main/resources/mixins.simpleauth.json b/src/main/resources/mixins.simpleauth.json index 4da9d2d..c069d4a 100644 --- a/src/main/resources/mixins.simpleauth.json +++ b/src/main/resources/mixins.simpleauth.json @@ -6,6 +6,7 @@ "server": [ "MixinPlayerEntity", "MixinPlayerManager", + "MixinServerLoginNetworkHandler", "MixinServerPlayNetworkHandler", "MixinSlot" ],