Merge branch 'fabric' into mongodb

# Conflicts:
#	src/main/java/org/samo_lego/simpleauth/SimpleAuth.java
#	src/main/java/org/samo_lego/simpleauth/mixin/MixinPlayerEntity.java
#	src/main/java/org/samo_lego/simpleauth/storage/AuthConfig.java
#	src/main/java/org/samo_lego/simpleauth/storage/PlayerCache.java
#	src/main/java/org/samo_lego/simpleauth/storage/database/LevelDB.java
This commit is contained in:
samo_lego 2020-11-06 12:01:50 +01:00
commit 2f7d4d5568
20 changed files with 641 additions and 282 deletions

View File

@ -21,7 +21,7 @@ echo "MC_VERSION=$mcVersion" >> $GITHUB_ENV
# Checks if build is stable (I always bump version when I release stable, uploadable version)
latestRelease=$(curl -s "https://api.github.com/repos/$GITHUB_REPOSITORY/releases/latest" | grep -oP '"tag_name": "\K(.*)(?=")')
latestRelease=$(curl -s "https://api.github.com/repos/$GITHUB_REPOSITORY/releases" | grep -oP '(?<="tag_name": ")[^"]*' | head -n 1)
echo "Latest release is: $latestRelease"

View File

@ -2,15 +2,15 @@
org.gradle.jvmargs=-Xmx1G
# Fabric properties
minecraft_version=1.16.3
yarn_mappings=1.16.3+build.1
loader_version=0.9.3+build.207
minecraft_version=1.16.4
yarn_mappings=1.16.4+build.1
loader_version=0.10.6+build.214
#Fabric api
fabric_version=0.20.2+build.402-1.16
fabric_version=0.25.1+build.416-1.16
# Mod Properties
mod_version = 1.6.0
mod_version = 1.6.5
maven_group = org.samo_lego
archives_base_name = simpleauth

Binary file not shown.

View File

@ -1,5 +1,5 @@
#Thu Jun 04 11:39:28 CEST 2020
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists

View File

@ -41,15 +41,23 @@ public class SimpleAuth implements DedicatedServerModInitializer {
* It's cleared on server stop in order to save some interactions with database during runtime.
* Stores their data as {@link org.samo_lego.simpleauth.storage.PlayerCache PlayerCache} object.
*/
public static HashMap<String, PlayerCache> playerCacheMap = new HashMap<>();
public static final HashMap<String, PlayerCache> 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 final HashSet<String> mojangAccountNamesCache = new HashSet<>();
// Getting game directory
public static final Path gameDirectory = FabricLoader.getInstance().getGameDir();
// Server properties
public static Properties serverProp = new Properties();
public static final Properties serverProp = new Properties();
// Mod config
/**
* Config of the SimpleAuth mod.
*/
public static AuthConfig config;
@Override
@ -59,6 +67,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())
@ -68,12 +82,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) -> {
@ -113,7 +121,7 @@ public class SimpleAuth implements DedicatedServerModInitializer {
// Closing threads
try {
THREADPOOL.shutdownNow();
if (!THREADPOOL.awaitTermination(100, TimeUnit.MICROSECONDS)) {
if (!THREADPOOL.awaitTermination(500, TimeUnit.MILLISECONDS)) {
Thread.currentThread().interrupt();
}
} catch (InterruptedException e) {

View File

@ -9,6 +9,7 @@ import org.samo_lego.simpleauth.utils.PlayerAuth;
import static net.minecraft.server.command.CommandManager.literal;
import static org.samo_lego.simpleauth.SimpleAuth.config;
import static org.samo_lego.simpleauth.SimpleAuth.mojangAccountNamesCache;
public class LogoutCommand {
@ -22,8 +23,12 @@ public class LogoutCommand {
private static int logout(ServerCommandSource serverCommandSource) throws CommandSyntaxException {
ServerPlayerEntity player = serverCommandSource.getPlayer();
((PlayerAuth) player).setAuthenticated(false);
player.sendMessage(new LiteralText(config.lang.successfulLogout), false);
if(!mojangAccountNamesCache.contains(player.getGameProfile().getName().toLowerCase())) {
((PlayerAuth) player).setAuthenticated(false);
player.sendMessage(new LiteralText(config.lang.successfulLogout), false);
}
else
player.sendMessage(new LiteralText(config.lang.cannotLogout), false);
return 1;
}
}

View File

@ -63,7 +63,6 @@ public class AuthEventHandler {
// Player joining the server
public static void onPlayerJoin(ServerPlayerEntity player) {
// If player is fake auth is not needed
if (((PlayerAuth) player).canSkipAuth())
return;
// Checking if session is still valid
@ -76,22 +75,12 @@ public class AuthEventHandler {
playerCache.validUntil >= System.currentTimeMillis() &&
player.getIp().equals(playerCache.lastIp)
) {
// Valid session
((PlayerAuth) player).setAuthenticated(true);
return;
}
player.setInvulnerable(config.experimental.playerInvulnerable);
player.setInvisible(config.experimental.playerInvisible);
// Invalidating session
playerCache.isAuthenticated = false;
if(config.main.spawnOnJoin)
((PlayerAuth) player).hidePosition(true);
}
else {
((PlayerAuth) player).setAuthenticated(false);
playerCache = playerCacheMap.get(uuid);
playerCache.wasOnFire = false;
}
((PlayerAuth) player).setAuthenticated(false);
// Tries to rescue player from nether portal
@ -116,10 +105,8 @@ public class AuthEventHandler {
String uuid = ((PlayerAuth) player).getFakeUuid();
PlayerCache playerCache = playerCacheMap.get(uuid);
if(((PlayerAuth) player).isAuthenticated()) {
if(playerCache.isAuthenticated) {
playerCache.lastIp = player.getIp();
playerCache.lastAir = player.getAir();
playerCache.wasOnFire = player.isOnFire();
playerCache.wasInPortal = player.getBlockState().getBlock().equals(Blocks.NETHER_PORTAL);
// Setting the session expire time
@ -151,13 +138,8 @@ public class AuthEventHandler {
public static ActionResult onPlayerMove(PlayerEntity player) {
// Player will fall if enabled (prevent fly kick)
boolean auth = ((PlayerAuth) player).isAuthenticated();
if(!auth && config.main.allowFalling && !player.isOnGround() && !player.isInsideWaterOrBubbleColumn()) {
if(player.isInvulnerable())
player.setInvulnerable(false);
return ActionResult.PASS;
}
// Otherwise movement should be disabled
else if(!auth && !config.experimental.allowMovement) {
if(!auth && !config.experimental.allowMovement) {
if(!player.isInvulnerable())
player.setInvulnerable(true);
return ActionResult.FAIL;

View File

@ -0,0 +1,46 @@
package org.samo_lego.simpleauth.mixin;
import net.minecraft.advancement.PlayerAdvancementTracker;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.ServerAdvancementLoader;
import net.minecraft.server.network.ServerPlayerEntity;
import org.samo_lego.simpleauth.utils.PlayerAuth;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
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 java.io.File;
import static org.samo_lego.simpleauth.SimpleAuth.config;
@Mixin(PlayerAdvancementTracker.class)
public class MixinPlayerAdvancementTracker {
@Mutable
@Shadow
@Final
private File advancementFile;
@Shadow
private ServerPlayerEntity owner;
@Inject(method = "load(Lnet/minecraft/server/ServerAdvancementLoader;)V", at = @At("HEAD"))
private void startMigratingOfflineAdvancements(ServerAdvancementLoader advancementLoader, CallbackInfo ci) {
if(config.experimental.premiumAutologin && !config.experimental.forceoOfflineUuids && ((PlayerAuth) this.owner).isUsingMojangAccount() && !this.advancementFile.isFile()) {
// Migrate
String playername = owner.getGameProfile().getName();
this.advancementFile = new File(this.advancementFile.getParent(), PlayerEntity.getOfflinePlayerUuid(playername).toString() + ".json");
}
}
@Inject(method = "load(Lnet/minecraft/server/ServerAdvancementLoader;)V", at = @At("TAIL"))
private void endMigratingOfflineAdvancements(ServerAdvancementLoader advancementLoader, CallbackInfo ci) {
if(config.experimental.premiumAutologin && !config.experimental.forceoOfflineUuids && ((PlayerAuth) this.owner).isUsingMojangAccount()) {
this.advancementFile = new File(this.advancementFile.getParent(), owner.getUuid() + ".json");
}
}
}

View File

@ -1,220 +1,17 @@
package org.samo_lego.simpleauth.mixin;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.registry.Registry;
import net.minecraft.util.registry.RegistryKey;
import net.minecraft.world.World;
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.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import static org.samo_lego.simpleauth.SimpleAuth.*;
import static org.samo_lego.simpleauth.utils.CarpetHelper.isPlayerCarpetFake;
@Mixin(PlayerEntity.class)
public abstract class MixinPlayerEntity implements PlayerAuth {
public class MixinPlayerEntity {
private final ServerPlayerEntity player = (ServerPlayerEntity) (Object) this;
// * 20 for 20 ticks in second
private int kickTimer = config.main.kickTime * 20;
private final boolean isRunningCarpet = FabricLoader.getInstance().isModLoaded("carpet");
private final MinecraftServer server = player.getServer();
/**
* Teleports player to spawn or last location that is recorded.
* Last location means the location before de-authentication.
*
* @param hide whether to teleport player to spawn (provided in config) or last recorded position
*/
@Override
public void hidePosition(boolean hide) {
assert server != null;
PlayerCache cache = playerCacheMap.get(this.getFakeUuid());
System.out.println("Teleporting Player. hide:" + hide);
if (hide) {
// Saving position
cache.lastLocation.dimension = player.getServerWorld();
cache.lastLocation.position = player.getPos();
cache.lastLocation.yaw = player.yaw;
cache.lastLocation.pitch = player.pitch;
// Teleports player to spawn
player.teleport(
server.getWorld(RegistryKey.of(Registry.DIMENSION, new Identifier(config.worldSpawn.dimension))),
config.worldSpawn.x,
config.worldSpawn.y,
config.worldSpawn.z,
config.worldSpawn.yaw,
config.worldSpawn.pitch
);
return;
}
// Puts player to last cached position
player.teleport(
cache.lastLocation.dimension,
cache.lastLocation.position.getX(),
cache.lastLocation.position.getY(),
cache.lastLocation.position.getZ(),
cache.lastLocation.yaw,
cache.lastLocation.pitch
);
}
/**
* Converts player uuid, to ensure player with "nAmE" and "NamE" get same uuid.
* Both players are not allowed to play, since mod mimics Mojang behaviour.
* of not allowing accounts with same names but different capitalization.
*
* @return converted UUID as string
*/
@Override
public String getFakeUuid() {
// If server is in online mode online-mode UUIDs should be used
assert server != null;
if(server.isOnlineMode())
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();
return PlayerEntity.getOfflinePlayerUuid(playername).toString();
}
/**
* Sets the authentication status of the player.
*
* @param authenticated whether player should be authenticated
*/
@Override
public void setAuthenticated(boolean authenticated) {
PlayerCache playerCache;
if(!playerCacheMap.containsKey(this.getFakeUuid())) {
// First join
String jsonString = DB.getUserData(this.getFakeUuid());
if(jsonString != null && !jsonString.isEmpty())
playerCache = PlayerCache.fromJson(player, jsonString);
else
playerCache = new PlayerCache(player);
// Saving to hashmap of player caches
playerCacheMap.put(this.getFakeUuid(), playerCache);
}
else {
playerCache = playerCacheMap.get(this.getFakeUuid());
if(this.isAuthenticated() == authenticated)
return;
playerCache.isAuthenticated = authenticated;
}
player.setInvulnerable(!authenticated && config.experimental.playerInvulnerable);
player.setInvisible(!authenticated && config.experimental.playerInvisible);
// Teleporting player (hiding / restoring position)
if(config.main.spawnOnJoin)
this.hidePosition(!authenticated);
if(authenticated) {
kickTimer = config.main.kickTime * 20;
// Updating blocks if needed (if portal rescue action happened)
if(playerCache.wasInPortal) {
World world = player.getEntityWorld();
BlockPos pos = player.getBlockPos();
// Sending updates to portal blocks
// This is technically not needed, but it cleans the "messed portal" on the client
world.updateListeners(pos, world.getBlockState(pos), world.getBlockState(pos), 3);
world.updateListeners(pos.up(), world.getBlockState(pos.up()), world.getBlockState(pos.up()), 3);
}
// Setting last air to player
if(player.isSubmergedInWater())
player.setAir(playerCache.lastAir);
// In case player is in lava during authentication proccess
if(!playerCache.wasOnFire)
player.setFireTicks(0);
}
}
/**
* Gets the text which tells the player
* to login or register, depending on account status.
*
* @return LiteralText with appropriate string (login or register)
*/
@Override
public Text getAuthMessage() {
final PlayerCache cache = playerCacheMap.get(((PlayerAuth) player).getFakeUuid());
if(SimpleAuth.config.main.enableGlobalPassword || cache.isRegistered)
return new LiteralText(
SimpleAuth.config.lang.notAuthenticated + "\n" + SimpleAuth.config.lang.loginRequired
);
return new LiteralText(
SimpleAuth.config.lang.notAuthenticated+ "\n" + SimpleAuth.config.lang.registerRequired
);
}
/**
* Checks whether player is a fake player (from CarpetMod).
*
* @return true if player is fake (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);
}
/**
* Checks whether player is authenticated.
*
* @return false if player is not authenticated, otherwise true.
*/
@Override
public boolean isAuthenticated() {
String uuid = ((PlayerAuth) player).getFakeUuid();
return playerCacheMap.containsKey(uuid) && playerCacheMap.get(uuid).isAuthenticated;
}
@Inject(method = "tick()V", at = @At("HEAD"), cancellable = true)
private void tick(CallbackInfo ci) {
if(!this.isAuthenticated()) {
// Checking player timer
if(kickTimer <= 0 && player.networkHandler.getConnection().isOpen()) {
player.networkHandler.disconnect(new LiteralText(config.lang.timeExpired));
}
else {
// Sending authentication prompt every 10 seconds
if(kickTimer % 200 == 0)
player.sendMessage(this.getAuthMessage(), false);
kickTimer--;
}
ci.cancel();
}
}
// Player item dropping
@Inject(method = "dropSelectedItem(Z)Z", at = @At("HEAD"), cancellable = true)
@ -223,14 +20,6 @@ public abstract class MixinPlayerEntity implements PlayerAuth {
ActionResult result = DropItemCallback.EVENT.invoker().onDropItem(player);
if (result == ActionResult.FAIL) {
// Canceling the item drop, as well as giving the items back to player (and updating inv with packet)
player.networkHandler.sendPacket(
new ScreenHandlerSlotUpdateS2CPacket(
-2,
player.inventory.selectedSlot,
player.inventory.getStack(player.inventory.selectedSlot))
);
player.networkHandler.sendPacket(new ScreenHandlerSlotUpdateS2CPacket(-1, -1, player.inventory.getCursorStack()));
cir.setReturnValue(false);
}
}

View File

@ -1,25 +1,39 @@
package org.samo_lego.simpleauth.mixin;
import com.mojang.authlib.GameProfile;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.network.ClientConnection;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.PlayerManager;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.stat.ServerStatHandler;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
import org.samo_lego.simpleauth.event.entity.player.PlayerJoinServerCallback;
import org.samo_lego.simpleauth.event.entity.player.PlayerLeaveServerCallback;
import org.samo_lego.simpleauth.event.entity.player.PrePlayerJoinCallback;
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.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import java.io.File;
import java.net.SocketAddress;
import java.util.UUID;
import static org.samo_lego.simpleauth.SimpleAuth.config;
@Mixin(PlayerManager.class)
public abstract class MixinPlayerManager {
@Shadow @Final private MinecraftServer server;
@Inject(method = "onPlayerConnect(Lnet/minecraft/network/ClientConnection;Lnet/minecraft/server/network/ServerPlayerEntity;)V", at = @At("RETURN"))
private void onPlayerConnect(ClientConnection clientConnection, ServerPlayerEntity serverPlayerEntity, CallbackInfo ci) {
PlayerJoinServerCallback.EVENT.invoker().onPlayerJoin(serverPlayerEntity);
@ -30,7 +44,6 @@ public abstract class MixinPlayerManager {
PlayerLeaveServerCallback.EVENT.invoker().onPlayerLeave(serverPlayerEntity);
}
// Method for kicking player for
@Inject(method = "checkCanJoin(Ljava/net/SocketAddress;Lcom/mojang/authlib/GameProfile;)Lnet/minecraft/text/Text;", at = @At("HEAD"), cancellable = true)
private void checkCanJoin(SocketAddress socketAddress, GameProfile profile, CallbackInfoReturnable<Text> cir) {
// Getting the player that is trying to join the server
@ -43,4 +56,35 @@ public abstract class MixinPlayerManager {
cir.setReturnValue(returnText);
}
}
@ModifyVariable(
method = "createStatHandler(Lnet/minecraft/entity/player/PlayerEntity;)Lnet/minecraft/stat/ServerStatHandler;",
at = @At(
value = "INVOKE",
target = "Lnet/minecraft/entity/player/PlayerEntity;getName()Lnet/minecraft/text/Text;"
),
ordinal = 1
)
private File migrateOfflineStats(File file, PlayerEntity player) {
if(config.experimental.premiumAutologin && !config.experimental.forceoOfflineUuids && ((PlayerAuth) player).isUsingMojangAccount()) {
String playername = player.getGameProfile().getName();
file = new File(file.getParent(), PlayerEntity.getOfflinePlayerUuid(playername) + ".json");
}
return file;
}
@Inject(
method = "createStatHandler(Lnet/minecraft/entity/player/PlayerEntity;)Lnet/minecraft/stat/ServerStatHandler;",
at = @At(
value = "INVOKE",
target = "Ljava/util/Map;put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;"
),
locals = LocalCapture.CAPTURE_FAILHARD
)
private void migrateOfflineStats(PlayerEntity player, CallbackInfoReturnable<ServerStatHandler> cir, UUID uUID, ServerStatHandler serverStatHandler, File serverStatsDir, File playerStatFile) {
File onlineFile = new File(serverStatsDir, uUID + ".json");
if(config.experimental.premiumAutologin && !config.experimental.forceoOfflineUuids && ((PlayerAuth) player).isUsingMojangAccount() && !onlineFile.exists()) {
((ServerStatHandlerAccessor) serverStatHandler).setFile(onlineFile);
}
}
}

View File

@ -0,0 +1,123 @@
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.Unique;
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 java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.samo_lego.simpleauth.SimpleAuth.*;
import static org.samo_lego.simpleauth.utils.SimpleLogger.logError;
@Mixin(ServerLoginNetworkHandler.class)
public abstract class MixinServerLoginNetworkHandler {
@Shadow
private GameProfile profile;
@Shadow
private int loginTicks;
@Shadow protected abstract GameProfile toOfflineProfile(GameProfile profile);
/**
* Fake state of current player.
*/
@Unique
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();
}
}
@Inject(method = "acceptPlayer()V", at = @At("HEAD"))
private void acceptPlayer(CallbackInfo ci) {
if(config.experimental.forceoOfflineUuids) {
this.profile = this.toOfflineProfile(this.profile);
}
}
/**
* 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();
Pattern pattern = Pattern.compile("^[a-z0-9_]{3,16}$");
Matcher matcher = pattern.matcher(playername);
if(playerCacheMap.containsKey(PlayerEntity.getOfflinePlayerUuid(playername).toString()) || !matcher.matches()) {
// 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());
}
}
}
}

View File

@ -0,0 +1,216 @@
package org.samo_lego.simpleauth.mixin;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.registry.Registry;
import net.minecraft.util.registry.RegistryKey;
import net.minecraft.world.World;
import org.samo_lego.simpleauth.SimpleAuth;
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;
import static org.samo_lego.simpleauth.SimpleAuth.*;
import static org.samo_lego.simpleauth.utils.CarpetHelper.isPlayerCarpetFake;
import static org.samo_lego.simpleauth.utils.SimpleLogger.logInfo;
@Mixin(ServerPlayerEntity.class)
public class MixinServerPlayerEntity implements PlayerAuth {
private final ServerPlayerEntity player = (ServerPlayerEntity) (Object) this;
// * 20 for 20 ticks in second
private int kickTimer = config.main.kickTime * 20;
private final boolean isRunningCarpet = FabricLoader.getInstance().isModLoaded("carpet");
@Final
@Shadow
public MinecraftServer server;
/**
* Teleports player to spawn or last location that is recorded.
* Last location means the location before de-authentication.
*
* @param hide whether to teleport player to spawn (provided in config) or last recorded position
*/
@Override
public void hidePosition(boolean hide) {
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();
cache.lastLocation.position = player.getPos();
cache.lastLocation.yaw = player.yaw;
cache.lastLocation.pitch = player.pitch;
// Teleports player to spawn
player.teleport(
server.getWorld(RegistryKey.of(Registry.DIMENSION, new Identifier(config.worldSpawn.dimension))),
config.worldSpawn.x,
config.worldSpawn.y,
config.worldSpawn.z,
config.worldSpawn.yaw,
config.worldSpawn.pitch
);
return;
}
// Puts player to last cached position
player.teleport(
cache.lastLocation.dimension,
cache.lastLocation.position.getX(),
cache.lastLocation.position.getY(),
cache.lastLocation.position.getZ(),
cache.lastLocation.yaw,
cache.lastLocation.pitch
);
}
/**
* Converts player uuid, to ensure player with "nAmE" and "NamE" get same uuid.
* Both players are not allowed to play, since mod mimics Mojang behaviour.
* of not allowing accounts with same names but different capitalization.
*
* @return converted UUID as string
*/
@Override
public String getFakeUuid() {
// If server is in online mode online-mode UUIDs should be used
assert server != null;
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.
*/
// 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();
}
/**
* Sets the authentication status of the player.
*
* @param authenticated whether player should be authenticated
*/
@Override
public void setAuthenticated(boolean authenticated) {
PlayerCache playerCache;
if(!playerCacheMap.containsKey(this.getFakeUuid())) {
// First join
playerCache = new PlayerCache(this.getFakeUuid(), player);
playerCacheMap.put(this.getFakeUuid(), playerCache);
}
else {
playerCache = playerCacheMap.get(this.getFakeUuid());
if(this.isAuthenticated() == authenticated)
return;
playerCache.isAuthenticated = authenticated;
}
player.setInvulnerable(!authenticated && config.experimental.playerInvulnerable);
player.setInvisible(!authenticated && config.experimental.playerInvisible);
// Teleporting player (hiding / restoring position)
if(config.main.spawnOnJoin)
this.hidePosition(!authenticated);
if(authenticated) {
kickTimer = config.main.kickTime * 20;
// Updating blocks if needed (if portal rescue action happened)
if(playerCache.wasInPortal) {
World world = player.getEntityWorld();
BlockPos pos = player.getBlockPos();
// Sending updates to portal blocks
// This is technically not needed, but it cleans the "messed portal" on the client
world.updateListeners(pos, world.getBlockState(pos), world.getBlockState(pos), 3);
world.updateListeners(pos.up(), world.getBlockState(pos.up()), world.getBlockState(pos.up()), 3);
}
}
}
/**
* Gets the text which tells the player
* to login or register, depending on account status.
*
* @return LiteralText with appropriate string (login or register)
*/
@Override
public Text getAuthMessage() {
final PlayerCache cache = playerCacheMap.get(((PlayerAuth) player).getFakeUuid());
if(SimpleAuth.config.main.enableGlobalPassword || cache.isRegistered)
return new LiteralText(
SimpleAuth.config.lang.notAuthenticated + "\n" + SimpleAuth.config.lang.loginRequired
);
return new LiteralText(
SimpleAuth.config.lang.notAuthenticated+ "\n" + SimpleAuth.config.lang.registerRequired
);
}
/**
* Checks whether player can skip authentication process.
*
* @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)) || (isUsingMojangAccount() && config.experimental.premiumAutologin);
}
/**
* Whether the player is using the mojang account.
* @return true if paid, otherwise false
*/
@Override
public boolean isUsingMojangAccount() {
return mojangAccountNamesCache.contains(player.getGameProfile().getName().toLowerCase());
}
/**
* Checks whether player is authenticated.
*
* @return false if player is not authenticated, otherwise true.
*/
@Override
public boolean isAuthenticated() {
String uuid = ((PlayerAuth) player).getFakeUuid();
return this.canSkipAuth() || (playerCacheMap.containsKey(uuid) && playerCacheMap.get(uuid).isAuthenticated);
}
@Inject(method = "playerTick()V", at = @At("HEAD"), cancellable = true)
private void playerTick(CallbackInfo ci) {
if(!this.isAuthenticated()) {
// Checking player timer
if(kickTimer <= 0 && player.networkHandler.getConnection().isOpen()) {
player.networkHandler.disconnect(new LiteralText(config.lang.timeExpired));
}
else {
// Sending authentication prompt every 10 seconds
if(kickTimer % 200 == 0)
player.sendMessage(this.getAuthMessage(), false);
--kickTimer;
}
ci.cancel();
}
}
}

View File

@ -0,0 +1,100 @@
package org.samo_lego.simpleauth.mixin;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.NbtIo;
import net.minecraft.world.WorldSaveHandler;
import org.apache.logging.log4j.Logger;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import static org.samo_lego.simpleauth.SimpleAuth.config;
import static org.samo_lego.simpleauth.SimpleAuth.mojangAccountNamesCache;
import static org.samo_lego.simpleauth.utils.SimpleLogger.logInfo;
@Mixin(WorldSaveHandler.class)
public class MixinWorldSaveHandler {
@Final
@Shadow
private File playerDataDir;
@Unique
private boolean fileExists;
@Final
@Shadow
private static Logger LOGGER;
/**
* Saves whether player save file exists.
*
* @param playerEntity
* @param cir
* @param compoundTag
* @param file
*/
@Inject(
method = "loadPlayerData(Lnet/minecraft/entity/player/PlayerEntity;)Lnet/minecraft/nbt/CompoundTag;",
at = @At(
value = "INVOKE",
target = "Ljava/io/File;exists()Z"
),
locals = LocalCapture.CAPTURE_FAILHARD
)
private void fileExists(PlayerEntity playerEntity, CallbackInfoReturnable<CompoundTag> cir, CompoundTag compoundTag, File file) {
// @ModifyVariable cannot capture locals
this.fileExists = file.exists();
}
/**
* Loads offline-uuid player data to compoundTag in order to migrate from offline to online.
*
* @param compoundTag null compound tag.
* @param player player who might need migration of datd.
* @return compoundTag containing migrated data.
*/
@ModifyVariable(
method = "loadPlayerData(Lnet/minecraft/entity/player/PlayerEntity;)Lnet/minecraft/nbt/CompoundTag;",
at = @At(
value = "INVOKE",
target = "Ljava/io/File;exists()Z"
)
)
private CompoundTag migratePlayerData(CompoundTag compoundTag, PlayerEntity player) {
// Checking for offline player data only if online doesn't exist yet
String playername = player.getGameProfile().getName().toLowerCase();
if(config.experimental.premiumAutologin && mojangAccountNamesCache.contains(playername) && !this.fileExists) {
if(config.experimental.debugMode)
logInfo("Migrating data for " + playername);
File file = new File(this.playerDataDir, PlayerEntity.getOfflinePlayerUuid(player.getGameProfile().getName()) + ".dat");
if (file.exists() && file.isFile())
try {
compoundTag = NbtIo.readCompressed(new FileInputStream(file));
}
catch (IOException e) {
LOGGER.warn("Failed to load player data for {}", playername);
}
}
else if(config.experimental.debugMode)
logInfo("Not migrating " +
playername +
", as premium status is: " +
mojangAccountNamesCache.contains(playername) +
" and data file is " + (this.fileExists ? "" : "not") +
" present."
);
return compoundTag;
}
}

View File

@ -0,0 +1,17 @@
package org.samo_lego.simpleauth.mixin;
import net.minecraft.stat.ServerStatHandler;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.io.File;
@Mixin(ServerStatHandler.class)
public interface ServerStatHandlerAccessor {
@Accessor("file")
File getFile();
@Accessor("file")
void setFile(File file);
}

View File

@ -23,7 +23,9 @@ 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;
import static org.samo_lego.simpleauth.utils.SimpleLogger.logInfo;
public class AuthConfig {
private static final Gson gson = new GsonBuilder()
@ -80,11 +82,6 @@ public class AuthConfig {
*/
public int sessionTimeoutTime = 60;
/**
* Should deauthenticated players fall if the login mid-air?
*/
public boolean allowFalling = false;
/**
* Whether to tp player to spawn when joining (to hide original player coordinates).
*/
@ -145,7 +142,7 @@ public class AuthConfig {
public String timeExpired = "§cTime for authentication has expired.";
public String registerRequired = "§6Type /register <password> <password> to claim this account.";
public String alreadyRegistered = "§6This account name is already registered!";
public String registerSuccess = "§aYou are now authenticated.";
public String registerSuccess = "§aAccount was created.";
public String userdataDeleted = "§aUserdata deleted.";
public String userdataUpdated = "§aUserdata updated.";
public String accountDeleted = "§aYour account was successfully deleted!";
@ -157,6 +154,7 @@ public class AuthConfig {
public String worldSpawnSet = "§aSpawn for logging in was set successfully.";
public String corruptedPlayerData = "§cYour data is probably corrupted. Please contact admin.";
public String userNotRegistered = "§cThis player is not registered!";
public String cannotLogout = "§cYou cannot logout!";
}
public static class ExperimentalConfig {
/**
@ -212,6 +210,27 @@ public class AuthConfig {
* @see <a href="https://github.com/samolego/SimpleAuth/wiki/GLIBC-problems" target="_blank">wiki</a>
*/
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)
*
* This protects premium usernames from being stolen, since cracked players
* with name that is found in Mojang database, are kicked.
*/
public boolean premiumAutologin = false;
/**
* Whether to modify player uuids to offline style.
* Note: this should be used only if you had your server
* running in offline mode and you made the switch to use
* AuthConfig#premiumAutoLogin AND your players already
* have e.g. villager discounts, which are based on uuid.
* Other things (advancements, playerdata) are migrated
* automatically, so think before enabling this. In case
* an online-mode player changes username, they'll loose all
* their stuff, unless you migrate it manually.
*/
public boolean forceoOfflineUuids = false;
}
public MainConfig main = new MainConfig();
@ -234,6 +253,16 @@ public class AuthConfig {
new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)
)) {
config = gson.fromJson(fileReader, AuthConfig.class);
if(!Boolean.parseBoolean(serverProp.getProperty("online-mode"))) {
if(config.experimental.forceoOfflineUuids) {
logInfo("Server is in offline mode, forceoOfflineUuids option is irrelevant. Setting it to false.");
config.experimental.forceoOfflineUuids = false;
}
if(config.experimental.premiumAutologin) {
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);
}

View File

@ -1,11 +1,14 @@
package org.samo_lego.simpleauth.storage;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.*;
import net.minecraft.block.Blocks;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.text.LiteralText;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.Vec3d;
import net.minecraft.util.registry.Registry;
import net.minecraft.util.registry.RegistryKey;
import java.util.Objects;
@ -46,8 +49,6 @@ public class PlayerCache {
/**
* Player stats before de-authentication.
*/
public int lastAir;
public boolean wasOnFire;
public boolean wasInPortal;
/**
@ -60,7 +61,7 @@ public class PlayerCache {
public float pitch;
}
public PlayerCache.LastLocation lastLocation = new PlayerCache.LastLocation();
public final PlayerCache.LastLocation lastLocation = new PlayerCache.LastLocation();
private static final Gson gson = new Gson();
@ -79,10 +80,6 @@ public class PlayerCache {
if(player != null) {
this.lastIp = player.getIp();
this.wasOnFire = player.isOnFire();
this.wasInPortal = player.getBlockState().getBlock().equals(Blocks.NETHER_PORTAL);
this.lastAir = player.getAir();
// Setting position cache
this.lastLocation.dimension = player.getServerWorld();
this.lastLocation.position = player.getPos();
@ -92,9 +89,7 @@ public class PlayerCache {
this.wasInPortal = player.getBlockState().getBlock().equals(Blocks.NETHER_PORTAL);
}
else {
this.wasOnFire = false;
this.wasInPortal = false;
this.lastAir = 300;
}
this.isRegistered = false;

View File

@ -63,7 +63,8 @@ public class LevelDB {
* @param data data to put inside database
* @return true if operation was successful, otherwise false
*/
public static boolean registerUser(String uuid, String data) {
@Deprecated
public boolean registerUser(String uuid, String data) {
try {
if(!isUserRegistered(uuid)) {
levelDBStore.put(bytes("UUID:" + uuid), bytes("data:" + data));
@ -110,7 +111,8 @@ public class LevelDB {
* @param uuid uuid of the player to update data for
* @param data data to put inside database
*/
public static void updateUserData(String uuid, String data) {
@Deprecated
public void updateUserData(String uuid, String data) {
try {
levelDBStore.put(bytes("UUID:" + uuid), bytes("data:" + data));
} catch (Error e) {

View File

@ -1,10 +1,5 @@
package org.samo_lego.simpleauth.utils;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.samo_lego.simpleauth.SimpleAuth;
import org.samo_lego.simpleauth.utils.hashing.HasherArgon2;
import org.samo_lego.simpleauth.utils.hashing.HasherBCrypt;
@ -12,9 +7,6 @@ import static org.samo_lego.simpleauth.SimpleAuth.config;
import static org.samo_lego.simpleauth.SimpleAuth.playerCacheMap;
public class AuthHelper {
// Json parser
private static final JsonParser parser = new JsonParser();
/**
* Checks password of user
*

View File

@ -58,4 +58,10 @@ public interface PlayerAuth {
* @see <a href="https://samolego.github.io/SimpleAuth/org/samo_lego/simpleauth/mixin/MixinPlayerEntity.html">See implementation</a>
*/
boolean canSkipAuth();
/**
* Whether the player is using the mojang account
* @return true if paid, false if cracked
*/
boolean isUsingMojangAccount();
}

View File

@ -4,10 +4,15 @@
"compatibilityLevel": "JAVA_8",
"mixins": [],
"server": [
"MixinPlayerAdvancementTracker",
"MixinPlayerEntity",
"MixinPlayerManager",
"MixinServerLoginNetworkHandler",
"MixinServerPlayerEntity",
"MixinServerPlayNetworkHandler",
"MixinSlot"
"MixinSlot",
"MixinWorldSaveHandler",
"ServerStatHandlerAccessor"
],
"injectors": {
"defaultRequire": 1