diff --git a/src/main/java/ua/mei/minekord/mixin/ServerLoginNetworkHandlerMixin.java b/src/main/java/ua/mei/minekord/mixin/ServerLoginNetworkHandlerMixin.java index 76795ae..a626a7f 100644 --- a/src/main/java/ua/mei/minekord/mixin/ServerLoginNetworkHandlerMixin.java +++ b/src/main/java/ua/mei/minekord/mixin/ServerLoginNetworkHandlerMixin.java @@ -15,15 +15,18 @@ 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.callback.CallbackInfo; import ua.mei.minekord.cache.IPCache; import ua.mei.minekord.config.MinekordConfig; +import ua.mei.minekord.event.IPCheckEvent; import ua.mei.minekord.utils.AuthUtils; import java.util.concurrent.atomic.AtomicInteger; +// TODO: improve this fuckin mixin @Mixin(ServerLoginNetworkHandler.class) public abstract class ServerLoginNetworkHandlerMixin { @Shadow @@ -49,6 +52,9 @@ public abstract class ServerLoginNetworkHandlerMixin { @Final ClientConnection connection; + @Unique + Member member = null; + @Shadow public abstract void disconnect(Text text); @@ -62,9 +68,11 @@ public abstract class ServerLoginNetworkHandlerMixin { @Inject(method = "onHello", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;isOnlineMode()Z"), cancellable = true) public void minekord$replaceUuid(LoginHelloC2SPacket loginHelloC2SPacket, CallbackInfo ci) { - if (MinekordConfig.Auth.INSTANCE.getSnowflakeBasedUuid() && !server.isOnlineMode()) { - Member member = AuthUtils.INSTANCE.findMember(loginHelloC2SPacket.comp_765()); + if (MinekordConfig.Auth.INSTANCE.getSnowflakeBasedUuid() || MinekordConfig.Auth.INSTANCE.getIpBasedLogin()) { + member = AuthUtils.INSTANCE.findMember(loginHelloC2SPacket.comp_765()); + } + if (MinekordConfig.Auth.INSTANCE.getSnowflakeBasedUuid() && !server.isOnlineMode()) { if (member == null) { this.disconnect(Text.translatable("multiplayer.disconnect.unverified_username")); ci.cancel(); @@ -73,16 +81,33 @@ public abstract class ServerLoginNetworkHandlerMixin { LOGGER.info("Snowflake based UUID of player {} is {}", this.profile.getName(), this.profile.getId()); } } + + if (MinekordConfig.Auth.INSTANCE.getIpBasedLogin() && this.profile != null) { + if (member == null) { + this.disconnect(Text.translatable("multiplayer.disconnect.unverified_username")); + ci.cancel(); + } else { + if (!IPCache.INSTANCE.containsInCache(this.connection.getAddress(), this.profile)) { + if (!IPCache.INSTANCE.isRequested(this.connection.getAddress(), this.profile)) { + IPCheckEvent.Companion.getEVENT().invoker().check(this.connection.getAddress(), this.profile); + } + this.disconnect(Text.literal(MinekordConfig.Messages.INSTANCE.getIpKickMessage())); + ci.cancel(); + } + } + } } @Inject(method = "onKey", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;setUncaughtExceptionHandler(Ljava/lang/Thread$UncaughtExceptionHandler;)V"), cancellable = true) public void minekord$replaceThread(LoginKeyC2SPacket loginKeyC2SPacket, CallbackInfo ci, @Local String string) { if (MinekordConfig.Auth.INSTANCE.getSnowflakeBasedUuid()) { - Thread thread = new Thread("User Authenticator #" + NEXT_AUTHENTICATOR_THREAD_ID.incrementAndGet()) { + Thread thread = new Thread("Minekord User Authenticator #" + NEXT_AUTHENTICATOR_THREAD_ID.incrementAndGet()) { public void run() { GameProfile gameProfile = ServerLoginNetworkHandlerMixin.this.profile; - Member member = AuthUtils.INSTANCE.findMember(gameProfile.getName()); + if (member == null) { + member = AuthUtils.INSTANCE.findMember(gameProfile.getName()); + } if (member != null) { ServerLoginNetworkHandlerMixin.this.profile = new GameProfile(AuthUtils.INSTANCE.uuidFromMember(member), gameProfile.getName()); diff --git a/src/main/kotlin/ua/mei/minekord/Minekord.kt b/src/main/kotlin/ua/mei/minekord/Minekord.kt index 3b7f02c..44a02fd 100644 --- a/src/main/kotlin/ua/mei/minekord/Minekord.kt +++ b/src/main/kotlin/ua/mei/minekord/Minekord.kt @@ -8,6 +8,7 @@ import net.fabricmc.loader.api.FabricLoader import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import ua.mei.minekord.bot.MinekordBot +import ua.mei.minekord.bot.extension.IPCheckExtension import ua.mei.minekord.bot.extension.MessagesExtension import ua.mei.minekord.bot.extension.PlayerListExtension import ua.mei.minekord.cache.IPCache @@ -43,6 +44,7 @@ object Minekord : ModInitializer { MinekordConfig.load() MinekordBot.registerExtension(::MessagesExtension) + MinekordBot.registerExtension(::IPCheckExtension) if (Commands.PlayerList.enabled) { MinekordBot.registerExtension(::PlayerListExtension) } diff --git a/src/main/kotlin/ua/mei/minekord/bot/extension/IPCheckExtensions.kt b/src/main/kotlin/ua/mei/minekord/bot/extension/IPCheckExtensions.kt new file mode 100644 index 0000000..4b6cd59 --- /dev/null +++ b/src/main/kotlin/ua/mei/minekord/bot/extension/IPCheckExtensions.kt @@ -0,0 +1,141 @@ +package ua.mei.minekord.bot.extension + +import com.mojang.authlib.GameProfile +import dev.kord.common.entity.ButtonStyle +import dev.kord.core.behavior.channel.createMessage +import dev.kord.core.entity.Member +import dev.kord.rest.builder.message.EmbedBuilder +import dev.kord.rest.builder.message.embed +import dev.kordex.core.components.ComponentContainer +import dev.kordex.core.components.components +import dev.kordex.core.components.disabledButton +import dev.kordex.core.components.publicButton +import dev.kordex.core.extensions.Extension +import dev.kordex.core.i18n.toKey +import dev.kordex.core.time.TimestampType +import dev.kordex.core.time.toDiscord +import io.ktor.util.network.address +import kotlinx.coroutines.launch +import kotlinx.datetime.Clock +import ua.mei.minekord.bot.MinekordBot +import ua.mei.minekord.cache.IPCache +import ua.mei.minekord.config.MinekordConfig.Auth +import ua.mei.minekord.config.MinekordConfig.Messages +import ua.mei.minekord.config.spec.MessagesSpec +import ua.mei.minekord.event.IPCheckEvent +import ua.mei.minekord.utils.AuthUtils +import java.net.SocketAddress + +class IPCheckExtension : Extension() { + override val name: String = "minekord.ipcheck" + + override suspend fun setup() { + IPCheckEvent.EVENT.register { socketAddress, profile -> + if (!Auth.ipBasedLogin) return@register + + MinekordBot.launch { + try { + val member: Member? = AuthUtils.findMember(profile.name) + + member?.getDmChannelOrNull()?.createMessage { + embed { + title = Messages.embedTitle + addIpField(socketAddress) + addTimeField() + } + components { + addYesButton(socketAddress, profile) + addNoButton(socketAddress, profile) + } + } + } catch (e: Exception) { + println("Error handling IP check: ${e.message}") + } + } + } + } + + private fun EmbedBuilder.addIpField(socketAddress: SocketAddress) { + field { + name = "> IP" + value = "> ${socketAddress.address}" + inline = true + } + } + + private fun EmbedBuilder.addTimeField() { + field { + name = "> ${MessagesSpec.timeLabel}" + value = "> ${Clock.System.now().toDiscord(TimestampType.Default)}" + inline = true + } + } + + private suspend fun ComponentContainer.addYesButton(socketAddress: SocketAddress, profile: GameProfile) { + publicButton { + label = Messages.yesButton.toKey() + style = ButtonStyle.Success + + action { + IPCache.ipCache[profile.name] = socketAddress.address + + edit { + components { + disabledButton { + label = Messages.yesButton.toKey() + style = ButtonStyle.Success + } + } + } + } + } + } + + private suspend fun ComponentContainer.addNoButton(socketAddress: SocketAddress, profile: GameProfile) { + publicButton { + label = Messages.noButton.toKey() + style = ButtonStyle.Danger + + action { + IPCache.blockedIps += socketAddress.address + + edit { + embed { + title = Messages.ipBlockedTitle + addIpField(socketAddress) + addTimeField() + } + components { + addUnblockButton(socketAddress, profile) + } + } + } + } + } + + private suspend fun ComponentContainer.addUnblockButton(socketAddress: SocketAddress, profile: GameProfile) { + publicButton { + label = Messages.unblockButton.toKey() + style = ButtonStyle.Danger + + action { + IPCache.blockedIps.removeAll { it == socketAddress.address } + IPCache.alreadyRequestedIps[profile.name]?.removeAll { it == socketAddress.address } + + edit { + embed { + title = Messages.ipUnblockedTitle + addIpField(socketAddress) + addTimeField() + } + components { + disabledButton { + label = Messages.unblockButton.toKey() + style = ButtonStyle.Danger + } + } + } + } + } + } +} diff --git a/src/main/kotlin/ua/mei/minekord/cache/IPCache.kt b/src/main/kotlin/ua/mei/minekord/cache/IPCache.kt index f0af8fe..8f48f08 100644 --- a/src/main/kotlin/ua/mei/minekord/cache/IPCache.kt +++ b/src/main/kotlin/ua/mei/minekord/cache/IPCache.kt @@ -3,6 +3,7 @@ package ua.mei.minekord.cache import com.google.gson.Gson import com.google.gson.GsonBuilder import com.google.gson.reflect.TypeToken +import com.mojang.authlib.GameProfile import io.ktor.util.network.address import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents import net.fabricmc.loader.api.FabricLoader @@ -15,7 +16,7 @@ import java.nio.file.Path object IPCache : ServerLifecycleEvents.ServerStarting, ServerLifecycleEvents.ServerStopped { var ipCache: MutableMap = mutableMapOf() - val alreadyRequestedIps: MutableMap = mutableMapOf() + val alreadyRequestedIps: MutableMap> = mutableMapOf() val blockedIps: MutableList = mutableListOf() val path: Path = FabricLoader.getInstance().gameDir.resolve("minekord/ip-cache.json") val type: TypeToken> = object : TypeToken>() {} @@ -43,4 +44,16 @@ object IPCache : ServerLifecycleEvents.ServerStarting, ServerLifecycleEvents.Ser fun isBlocked(socketAddress: SocketAddress): Boolean { return blockedIps.contains(socketAddress.address) } + + fun isRequested(address: SocketAddress, profile: GameProfile): Boolean { + if (alreadyRequestedIps[profile.name]?.contains(address.address) == true) { + return true + } + alreadyRequestedIps.getOrPut(profile.name) { mutableListOf() }.add(address.address) + return false + } + + fun containsInCache(address: SocketAddress, profile: GameProfile): Boolean { + return ipCache[profile.name] == address.address + } } \ No newline at end of file diff --git a/src/main/kotlin/ua/mei/minekord/config/MinekordConfig.kt b/src/main/kotlin/ua/mei/minekord/config/MinekordConfig.kt index 1f9ec2c..c0a7f23 100644 --- a/src/main/kotlin/ua/mei/minekord/config/MinekordConfig.kt +++ b/src/main/kotlin/ua/mei/minekord/config/MinekordConfig.kt @@ -19,6 +19,7 @@ import ua.mei.minekord.config.spec.ChatSpec import ua.mei.minekord.config.spec.ColorsSpec import ua.mei.minekord.config.spec.CommandsSpec import ua.mei.minekord.config.spec.MainSpec +import ua.mei.minekord.config.spec.MessagesSpec import ua.mei.minekord.config.spec.PresenceSpec import ua.mei.minekord.utils.MinekordActivityType import ua.mei.minekord.utils.toColor @@ -42,6 +43,7 @@ object MinekordConfig { addSpec(CommandsSpec) addSpec(ColorsSpec) addSpec(AuthSpec) + addSpec(MessagesSpec) }.from.toml.file(FabricLoader.getInstance().configDir.resolve(CONFIG_PATH).toFile()) config.validateRequired() @@ -52,6 +54,7 @@ object MinekordConfig { Commands.load() Colors.load() Auth.load() + Messages.load() } private fun parseNode(text: String): TextNode { @@ -244,6 +247,36 @@ object MinekordConfig { } } + object Messages { + lateinit var ipKickMessage: String + private set + lateinit var embedTitle: String + private set + lateinit var timeLabel: String + private set + lateinit var yesButton: String + private set + lateinit var noButton: String + private set + lateinit var unblockButton: String + private set + lateinit var ipBlockedTitle: String + private set + lateinit var ipUnblockedTitle: String + private set + + fun load() { + ipKickMessage = config[MessagesSpec.ipKickMessage] + embedTitle = config[MessagesSpec.embedTitle] + timeLabel = config[MessagesSpec.timeLabel] + yesButton = config[MessagesSpec.yesButton] + noButton = config[MessagesSpec.noButton] + unblockButton = config[MessagesSpec.unblockButton] + ipBlockedTitle = config[MessagesSpec.ipBlockedTitle] + ipUnblockedTitle = config[MessagesSpec.ipUnblockedTitle] + } + } + data class DynamicNode(val key: String, val text: Text) : TextNode { companion object { fun of(key: String): DynamicNode { diff --git a/src/main/kotlin/ua/mei/minekord/config/spec/MessagesSpec.kt b/src/main/kotlin/ua/mei/minekord/config/spec/MessagesSpec.kt new file mode 100644 index 0000000..6066389 --- /dev/null +++ b/src/main/kotlin/ua/mei/minekord/config/spec/MessagesSpec.kt @@ -0,0 +1,14 @@ +package ua.mei.minekord.config.spec + +import com.uchuhimo.konf.ConfigSpec + +object MessagesSpec : ConfigSpec() { + val ipKickMessage by required() + val embedTitle by required() + val timeLabel by required() + val yesButton by required() + val noButton by required() + val unblockButton by required() + val ipBlockedTitle by required() + val ipUnblockedTitle by required() +} diff --git a/src/main/resources/minekord.toml b/src/main/resources/minekord.toml index accf51a..8b503c0 100644 --- a/src/main/resources/minekord.toml +++ b/src/main/resources/minekord.toml @@ -67,3 +67,14 @@ link = "#5865F2" snowflakeBasedUuid = false requiredRoles = [] ipBasedLogin = false + +[Messages] + +ipKickMessage = "Joined with new IP! Check your DM." +embedTitle = "This is your IP?" +timeLabel = "Time" +yesButton = "Yes" +noButton = "No" +unblockButton = "Unblock" +ipBlockedTitle = "IP was blocked!" +ipUnblockedTitle = "IP was unblocked!"