diff --git a/build.gradle.kts b/build.gradle.kts index 426da767..47ca1fb2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,7 +11,7 @@ repositories { allprojects { group = "pl.spcode.navauth" - version = "0.1.5-SNAPSHOT" + version = "0.1.6-SNAPSHOT" } tasks.register("formatAll") { diff --git a/docs/docs/configuration/multification.md b/docs/docs/configuration/multification.md index cd6b1b58..3651fc0d 100644 --- a/docs/docs/configuration/multification.md +++ b/docs/docs/configuration/multification.md @@ -5,6 +5,23 @@ Multification library was created by [**EternalCode**](https://github.com/Eterna > Multification makes it simple to create customizable notifications and messages within large plugins that require handling a high volume of messages +## With Multification, you have granular control over the notifications and messages sent to players. +Here's a quick example of how easy it is: +![multification-login-example.png](/multification/multification-login-example.png) +Here's the edited property from example above (messages.yml): +```yml +loginPasswordOnlyInstruction: + chat: <#2784F5>ᴇᴋɪᴘᴀ.ɢɢ × Zaloguj się za pomocą + /login + title: <#2784F5>ᴇᴋɪᴘᴀ.ɢɢ + subtitle: Zaloguj się + bossbar: + message: <#2784F5>ᴇᴋɪᴘᴀ.ɢɢ × Oczekiwanie na logowanie... + duration: 3s + color: BLUE + progress: '1.0' +``` + ### 📝 Example usage (copy of original lib docs) ::: tip diff --git a/docs/docs/public/multification/multification-login-example.png b/docs/docs/public/multification/multification-login-example.png new file mode 100644 index 00000000..63a51180 Binary files /dev/null and b/docs/docs/public/multification/multification-login-example.png differ diff --git a/navauth-common/src/main/java/pl/spcode/navauth/common/infra/MethodInvocation.java b/navauth-common/src/main/java/pl/spcode/navauth/common/infra/MethodInvocation.java index 15477e62..7548e744 100644 --- a/navauth-common/src/main/java/pl/spcode/navauth/common/infra/MethodInvocation.java +++ b/navauth-common/src/main/java/pl/spcode/navauth/common/infra/MethodInvocation.java @@ -22,8 +22,13 @@ import java.lang.invoke.MethodHandles; import java.lang.ref.WeakReference; import java.lang.reflect.Method; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class MethodInvocation implements Invocation, Comparable { + + private static final Logger logger = LoggerFactory.getLogger(MethodInvocation.class); + private final MethodHandle methodHandle; private final WeakReference targetRef; private final int priority; @@ -41,7 +46,7 @@ public void invoke(Object event) { try { methodHandle.invoke(target, event); } catch (Throwable t) { - t.printStackTrace(); // Production: Use logger/SLF4J + logger.error("Error occurred while trying to invoke MethodInvocation", t); } } @@ -52,6 +57,6 @@ public Object getTarget() { @Override public int compareTo(MethodInvocation o) { - return Integer.compare(o.priority, priority); // Desc: higher first + return Integer.compare(o.priority, priority); } } diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserActivitySessionService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserActivitySessionService.kt index f241520f..e00541f5 100644 --- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserActivitySessionService.kt +++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserActivitySessionService.kt @@ -25,6 +25,7 @@ import java.util.UUID import java.util.concurrent.ConcurrentHashMap import org.slf4j.Logger import org.slf4j.LoggerFactory +import pl.spcode.navauth.common.config.SessionsConfig import pl.spcode.navauth.common.domain.common.IPAddress import pl.spcode.navauth.common.domain.player.PlayerAdapter import pl.spcode.navauth.common.domain.user.User @@ -36,7 +37,10 @@ import pl.spcode.navauth.common.infra.persistence.Paginator @Singleton class UserActivitySessionService @Inject -constructor(private val userActivitySessionRepository: UserActivitySessionRepository) { +constructor( + private val userActivitySessionRepository: UserActivitySessionRepository, + private val sessionsConfig: SessionsConfig, +) { private val logger: Logger = LoggerFactory.getLogger(UserActivitySessionService::class.java) private val playerJoinedAtMap = ConcurrentHashMap() @@ -45,18 +49,31 @@ constructor(private val userActivitySessionRepository: UserActivitySessionReposi private data class PlayerSessionData(val joinedAt: Date, val ip: IPAddress) fun registerPlayerJoin(playerAdapter: PlayerAdapter) { + if (!sessionsConfig.userActivitySessionsEnabled) { + return + } playerJoinedAtMap[playerAdapter.getUuid().value] = PlayerSessionData(Date(), playerAdapter.getIPAddress()) } fun storePlayerSessionOnLeave(playerAdapter: PlayerAdapter) { - val data = playerJoinedAtMap.get(playerAdapter.getUuid().value) + val data = playerJoinedAtMap[playerAdapter.getUuid().value] if (data == null) { - logger.debug("player with UUID {} session was not registered", playerAdapter.getUuid()) + if (sessionsConfig.userActivitySessionsEnabled) { + logger.debug("player with UUID {} session was not registered", playerAdapter.getUuid()) + } return } val leftAt = Date() + if ((leftAt.time - data.joinedAt.time) < sessionsConfig.sessionMinimumTimeMs) { + logger.debug( + "player with UUID {} session was not registered, session minimum time not fulfilled", + playerAdapter.getUuid(), + ) + return + } + val session = UserActivitySession.create( playerAdapter.getUuid(), diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserService.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserService.kt index 136a5c44..2d2c9711 100644 --- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserService.kt +++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/application/user/UserService.kt @@ -82,13 +82,7 @@ constructor( userRepository.save(user) } - fun deleteUserCredentials(user: User): User { - return txService.inTransaction { - return@inTransaction deleteUserCredentialsNoTx(user) - } - } - - private fun deleteUserCredentialsNoTx(user: User): User { + private fun deleteUserCredentialsUpdateUserNoTx(user: User): User { val user = user.withCredentialsRequired(false) userRepository.save(user) userCredentialsService.deleteUserCredentials(user) @@ -111,12 +105,15 @@ constructor( val requireCredentials = credentials.isTwoFactorEnabled val premiumUser = User.premium(user.uuid, user.username, mojangId, requireCredentials) - userRepository.save(premiumUser) + val status = userRepository.save(premiumUser) + status.isCreated + status.isUpdated + status.numLinesChanged if (requireCredentials) { val newCredentials = credentials.withoutPassword() userCredentialsService.storeUserCredentials(premiumUser, newCredentials) } else { - deleteUserCredentialsNoTx(user) + userCredentialsService.deleteUserCredentials(premiumUser) } return@inTransaction premiumUser @@ -257,7 +254,7 @@ constructor( val newCredentials = credentials.withoutTotpSecret() userCredentialsService.storeUserCredentials(user, newCredentials) } else { - deleteUserCredentialsNoTx(user) + deleteUserCredentialsUpdateUserNoTx(user) } } } diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/GeneralConfig.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/GeneralConfig.kt index 567112c5..a9c70b87 100644 --- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/GeneralConfig.kt +++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/GeneralConfig.kt @@ -52,8 +52,17 @@ open class GeneralConfig : OkaeriConfig() { @Comment("Maximum time of registration.") var maxRegistrationDuration: Duration = Duration.ofSeconds(30) + @Comment("Interval between each register instruction multification.") + var intervalBetweenRegisterMultification: Duration = Duration.ofSeconds(3) + @Comment("Maximum time of login.") var maxLoginDuration: Duration = Duration.ofSeconds(20) + @Comment("Interval between each login instruction multification.") + var intervalBetweenLoginMultification: Duration = Duration.ofSeconds(3) + + @Comment("Delay before sending multification on a successful premium authentication.") + var premiumAuthMultificationDelay: Duration = Duration.ofSeconds(1) + @Comment("Usernames config") var usernamesConfig: UsernamesConfig = UsernamesConfig() @Comment("Two-factor (2FA) TOTP authentication config") @@ -61,6 +70,24 @@ open class GeneralConfig : OkaeriConfig() { @Comment("Passwords config") var passwordsConfig: PasswordsConfig = PasswordsConfig() + @Comment( + "When a non-premium user tries to join with a premium nickname,", + "an 'invalid session' error occurs. This is default Minecraft behavior.", + "Should we cache premium connections and send a more descriptive message", + "when they try to join again after getting disconnected right after the preLogin event?", + "", + "We cache player uuid, IP and protocol version.", + ) + var descriptiveInvalidSessionEnabled = true + + @Comment( + "Max time (milliseconds) to consider a reconnect as an invalid session connection.", + "(You can set this a little higher than the reconnect throttle from other plugins.)", + ) + var descriptiveInvalidSessionCacheTimeMs = 8000 + + var sessionsConfig = SessionsConfig() + @Variable("CONFIG_VERSION") @Comment("Config version. DO NOT CHANGE this property!") var configVersion: Int = 0 diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt index 14f8175e..ea59cda2 100644 --- a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt +++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/MessagesConfig.kt @@ -70,6 +70,14 @@ open class MessagesConfig : OkaeriConfig() { var yourAccountDataHasBeenMigrated = TextComponent("Your account data has been migrated to '%USERNAME%'.") + val possibleInvalidSessionReconnectError = + TextComponent( + "You've tried to log in to a premium account '%USERNAME%' one or more times. " + + "
This might be an invalid session error, caused by one of the following:" + + "
1. An outdated session. If you own the account, restart your client." + + "
2. You don't own the premium account. In that case, use a non-premium username." + ) + @Comment( "Notifications which use multification library.", "Here you can use chat messages, action bars, sounds etc. combined.", @@ -191,6 +199,11 @@ open class MessagesConfig : OkaeriConfig() { ) var adminCmdUserPremiumMigrationSuccess: Notice = Notice.chat("User '%USERNAME%' successfully migrated to premium mode.") + + var adminCmdReloadingConfig: Notice = Notice.chat("Reloading configuration...") + var adminCmdConfigReloadSuccess: Notice = Notice.chat("Config %NAME% has been reloaded.") + var adminCmdConfigReloadError: Notice = + Notice.chat("Failed to reload %NAME% configuration! Error: %CAUSE%") } @Variable("CONFIG_VERSION") diff --git a/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/SessionsConfig.kt b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/SessionsConfig.kt new file mode 100644 index 00000000..370c6ef7 --- /dev/null +++ b/navauth-common/src/main/kotlin/pl/spcode/navauth/common/config/SessionsConfig.kt @@ -0,0 +1,36 @@ +/* + * NavAuth + * Copyright © 2026 Oliwier Fijas (Navio1430) + * + * NavAuth is free software; You can redistribute it and/or modify it under the terms of: + * the GNU Affero General Public License version 3 as published by the Free Software Foundation. + * + * NavAuth is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with NavAuth. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + * + */ + +package pl.spcode.navauth.common.config + +import eu.okaeri.configs.OkaeriConfig +import eu.okaeri.configs.annotation.Comment + +class SessionsConfig : OkaeriConfig() { + + @Comment( + "Should we store player sessions?", + "https://navio1430.github.io/NavAuth/docs/general/user-lookup.html#lookup-user-sessions", + "Make sure you've included information about this function in your Privacy Policy.", + "We store join at, left at times and IP's.", + ) + var userActivitySessionsEnabled = false + + @Comment("Minimum time of milliseconds required to save a player activity session.") + var sessionMinimumTimeMs = 30000 +} diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/InvalidSessionCacheService.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/InvalidSessionCacheService.kt new file mode 100644 index 00000000..43b07348 --- /dev/null +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/InvalidSessionCacheService.kt @@ -0,0 +1,79 @@ +/* + * NavAuth + * Copyright © 2026 Oliwier Fijas (Navio1430) + * + * NavAuth is free software; You can redistribute it and/or modify it under the terms of: + * the GNU Affero General Public License version 3 as published by the Free Software Foundation. + * + * NavAuth is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with NavAuth. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + * + */ + +package pl.spcode.navauth.velocity.application.auth + +import com.google.common.cache.CacheBuilder +import com.google.inject.Inject +import com.google.inject.Singleton +import com.velocitypowered.api.proxy.InboundConnection +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.toJavaDuration +import pl.spcode.navauth.common.config.GeneralConfig +import pl.spcode.navauth.common.domain.common.IPAddress + +/** + * Service used to make the invalid session error more descriptive, which is a significant UX + * improvement. + */ +@Singleton +class InvalidSessionCacheService @Inject constructor(val generalConfig: GeneralConfig) { + + private val cache = + CacheBuilder.newBuilder() + .expireAfterWrite( + generalConfig.descriptiveInvalidSessionCacheTimeMs.milliseconds.toJavaDuration() + ) + .build() + + fun cachePremiumConnection(username: String, connection: InboundConnection) { + val ip = IPAddress.fromInetAddress(connection.remoteAddress.address) + val obj = ConnectionCache(username, ip, connection.protocolVersion.protocol) + cache.put(username, obj) + } + + fun markAsEncryptedPremiumConnection(username: String) { + cache.getIfPresent(username)?.passedEncryption = true + } + + fun isInvalidSessionReconnect(username: String, connection: InboundConnection): Boolean { + val cached = cache.getIfPresent(username) ?: return false + if (!cached.passedEncryption) { + val ip = IPAddress.fromInetAddress(connection.remoteAddress.address) + if ( + cached.ip.compareTo(ip) == 0 && + cached.protocolVersion == connection.protocolVersion.protocol + ) { + return true + } + } + return false + } + + fun invalidate(username: String) { + cache.invalidate(username) + } + + /** @param passedEncryption DO NOT USE THIS FOR SECURITY!!! */ + data class ConnectionCache( + val username: String, + val ip: IPAddress, + val protocolVersion: Int, + var passedEncryption: Boolean = false, + ) +} diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/session/VelocityAuthSessionFactory.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/session/VelocityAuthSessionFactory.kt index f6a77417..18f0cd5e 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/session/VelocityAuthSessionFactory.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/application/auth/session/VelocityAuthSessionFactory.kt @@ -99,7 +99,14 @@ constructor( uniqueSessionId: VelocityUniqueSessionId, ): VelocityAutoLoginAuthSession { val session = - VelocityAutoLoginAuthSession(player, scheduler, multification, messagesConfig, eventBus) + VelocityAutoLoginAuthSession( + player, + scheduler, + multification, + messagesConfig, + generalConfig, + eventBus, + ) return authSessionService.registerSession(uniqueSessionId, session) } } diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/CommandsRegistry.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/CommandsRegistry.kt index 7643784b..875b558f 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/CommandsRegistry.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/CommandsRegistry.kt @@ -24,6 +24,7 @@ import pl.spcode.navauth.velocity.command.admin.ForceCrackedAdminCommand import pl.spcode.navauth.velocity.command.admin.ForcePremiumAdminCommand import pl.spcode.navauth.velocity.command.admin.MigrateUserDataAdminCommand import pl.spcode.navauth.velocity.command.admin.PlayerLookupAdminCommand +import pl.spcode.navauth.velocity.command.admin.ReloadConfigAdminCommand import pl.spcode.navauth.velocity.command.root.MigrationRootCommand import pl.spcode.navauth.velocity.command.user.ChangePasswordCommand import pl.spcode.navauth.velocity.command.user.LoginCommand @@ -50,6 +51,7 @@ class CommandsRegistry { ForcePremiumAdminCommand::class, MigrateUserDataAdminCommand::class, PlayerLookupAdminCommand::class, + ReloadConfigAdminCommand::class, // root (console only) MigrationRootCommand::class, ) diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/Permissions.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/Permissions.kt index c688a716..b81fcd62 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/Permissions.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/Permissions.kt @@ -40,5 +40,6 @@ class Permissions { const val ADMIN_FORCE_CRACKED = "$ADMIN_BASE.forcecracked" const val ADMIN_FORCE_PREMIUM = "$ADMIN_BASE.forcepremium" const val ADMIN_MIGRATE_USER_DATA = "$ADMIN_BASE.migrateuserdata" + const val ADMIN_RELOAD = "$ADMIN_BASE.reload" } } diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceCrackedAdminCommand.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceCrackedAdminCommand.kt index 369c0849..fc43b522 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceCrackedAdminCommand.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ForceCrackedAdminCommand.kt @@ -20,7 +20,6 @@ package pl.spcode.navauth.velocity.command.admin import com.google.inject.Inject import com.velocitypowered.api.command.CommandSource -import com.velocitypowered.api.proxy.ConsoleCommandSource import dev.rollczi.litecommands.annotations.argument.Arg import dev.rollczi.litecommands.annotations.async.Async import dev.rollczi.litecommands.annotations.command.Command @@ -28,7 +27,6 @@ import dev.rollczi.litecommands.annotations.context.Context import dev.rollczi.litecommands.annotations.execute.Execute import dev.rollczi.litecommands.annotations.permission.Permission import java.util.Optional -import me.uniodex.velocityrcon.commandsource.IRconCommandSource import pl.spcode.navauth.common.annotation.Description import pl.spcode.navauth.common.application.credentials.UserCredentialsService import pl.spcode.navauth.common.application.user.UserService @@ -38,6 +36,7 @@ import pl.spcode.navauth.common.extension.StringExtensions.Companion.applyPlaceh import pl.spcode.navauth.common.shared.utils.StringUtils.Companion.generateRandomString import pl.spcode.navauth.velocity.command.Permissions import pl.spcode.navauth.velocity.multification.VelocityMultification +import pl.spcode.navauth.velocity.util.CommandSourceUtils @Command(name = "forcecracked") @Permission(Permissions.ADMIN_FORCE_CRACKED) @@ -77,7 +76,7 @@ constructor( userService.migrateToNonPremium(user, hashedPassword) val passwordText = - if (sender is ConsoleCommandSource || sender is IRconCommandSource) { + if (CommandSourceUtils.isConsoleOrRcon(sender)) { "$newPassword" } else { val placeholders = mapOf(Pair("PASSWORD", newPassword)) diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ReloadConfigAdminCommand.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ReloadConfigAdminCommand.kt new file mode 100644 index 00000000..5e5b1a12 --- /dev/null +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/admin/ReloadConfigAdminCommand.kt @@ -0,0 +1,57 @@ +/* + * NavAuth + * Copyright © 2026 Oliwier Fijas (Navio1430) + * + * NavAuth is free software; You can redistribute it and/or modify it under the terms of: + * the GNU Affero General Public License version 3 as published by the Free Software Foundation. + * + * NavAuth is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with NavAuth. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + * + */ + +package pl.spcode.navauth.velocity.command.admin + +import com.google.inject.Inject +import com.velocitypowered.api.command.CommandSource +import dev.rollczi.litecommands.annotations.command.Command +import dev.rollczi.litecommands.annotations.context.Context +import dev.rollczi.litecommands.annotations.execute.Execute +import dev.rollczi.litecommands.annotations.permission.Permission +import pl.spcode.navauth.common.config.MessagesConfig +import pl.spcode.navauth.velocity.command.Permissions +import pl.spcode.navauth.velocity.multification.VelocityMultification + +@Command(name = "navauth") +class ReloadConfigAdminCommand +@Inject +constructor(val config: MessagesConfig, val multification: VelocityMultification) { + + @Execute(name = "reload") + @Permission(Permissions.ADMIN_RELOAD) + fun reload(@Context sender: CommandSource) { + multification + .create(sender) { it.multification.adminCmdReloadingConfig } + .placeholder("%NAME%", "messages") + .send() + try { + config.load() + } catch (e: Exception) { + multification + .create(sender) { it.multification.adminCmdConfigReloadError } + .placeholder("%NAME%", "messages") + .placeholder("%CAUSE%", e.message) + .send() + } + multification + .create(sender) { it.multification.adminCmdConfigReloadSuccess } + .placeholder("%NAME%", "messages") + .send() + } +} diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/root/MigrationRootCommand.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/root/MigrationRootCommand.kt index 9c5b60e7..5ddea6ad 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/root/MigrationRootCommand.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/command/root/MigrationRootCommand.kt @@ -20,14 +20,13 @@ package pl.spcode.navauth.velocity.command.root import com.google.inject.Inject import com.velocitypowered.api.command.CommandSource -import com.velocitypowered.api.proxy.ConsoleCommandSource import dev.rollczi.litecommands.annotations.command.Command import dev.rollczi.litecommands.annotations.context.Context import dev.rollczi.litecommands.annotations.execute.Execute import dev.rollczi.litecommands.annotations.permission.Permission -import me.uniodex.velocityrcon.commandsource.IRconCommandSource import pl.spcode.navauth.common.annotation.Description import pl.spcode.navauth.common.migrate.MigrationManager +import pl.spcode.navauth.velocity.util.CommandSourceUtils @Command(name = "migration") @Permission("navauth.root") @@ -41,7 +40,7 @@ class MigrationRootCommand @Inject constructor(private val migrationManager: Mig ) fun startMigration(@Context sender: CommandSource) { - if (sender !is ConsoleCommandSource && sender !is IRconCommandSource) { + if (!CommandSourceUtils.isConsoleOrRcon(sender)) { sender.sendPlainMessage("This command can only be executed from console.") return } diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityAutoLoginAuthSession.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityAutoLoginAuthSession.kt index b7d55ae5..8f63faf8 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityAutoLoginAuthSession.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityAutoLoginAuthSession.kt @@ -19,9 +19,9 @@ package pl.spcode.navauth.velocity.infra.auth import com.velocitypowered.api.proxy.Player -import java.time.Duration import pl.spcode.navauth.api.domain.auth.AuthSessionType import pl.spcode.navauth.api.event.NavAuthEventBus +import pl.spcode.navauth.common.config.GeneralConfig import pl.spcode.navauth.common.config.MessagesConfig import pl.spcode.navauth.common.domain.auth.session.AuthSession import pl.spcode.navauth.velocity.infra.player.VelocityPlayerAdapter @@ -33,6 +33,7 @@ class VelocityAutoLoginAuthSession( val scheduler: NavAuthScheduler, val multification: VelocityMultification, val messagesConfig: MessagesConfig, + val generalConfig: GeneralConfig, eventBus: NavAuthEventBus, ) : AuthSession(VelocityPlayerAdapter(player), eventBus) { @@ -55,7 +56,7 @@ class VelocityAutoLoginAuthSession( } } ) - .delay(Duration.ofSeconds(1)) + .delay(generalConfig.premiumAuthMultificationDelay) .schedule() } } diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityLoginAuthSession.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityLoginAuthSession.kt index d444e3bb..5a0893f7 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityLoginAuthSession.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityLoginAuthSession.kt @@ -81,7 +81,7 @@ class VelocityLoginAuthSession( Runnable { multification.create().notice(notification).player(player.uniqueId).send() } ) .delay(Duration.ofSeconds(1)) - .repeat(Duration.ofSeconds(3)) + .repeat(generalConfig.intervalBetweenLoginMultification) .schedule() } diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityRegisterAuthSession.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityRegisterAuthSession.kt index 732795c7..73f77bb0 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityRegisterAuthSession.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/infra/auth/VelocityRegisterAuthSession.kt @@ -67,7 +67,7 @@ class VelocityRegisterAuthSession( } ) .delay(Duration.ofSeconds(1)) - .repeat(Duration.ofSeconds(3)) + .repeat(generalConfig.intervalBetweenRegisterMultification) .schedule() } diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt index 92093fc1..c881c490 100644 --- a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/listener/velocity/LoginListeners.kt @@ -36,6 +36,8 @@ import pl.spcode.navauth.common.application.auth.username.UsernameResolutionServ import pl.spcode.navauth.common.application.user.UserService import pl.spcode.navauth.common.application.validator.UsernameValidator import pl.spcode.navauth.common.component.TextColors +import pl.spcode.navauth.common.component.TextComponent +import pl.spcode.navauth.common.config.GeneralConfig import pl.spcode.navauth.common.config.MessagesConfig import pl.spcode.navauth.common.domain.auth.handshake.AuthHandshakeSession import pl.spcode.navauth.common.domain.auth.handshake.EncryptionType @@ -44,6 +46,7 @@ import pl.spcode.navauth.common.domain.user.MojangId import pl.spcode.navauth.common.domain.user.User import pl.spcode.navauth.common.domain.user.UserUuid import pl.spcode.navauth.common.domain.user.Username +import pl.spcode.navauth.velocity.application.auth.InvalidSessionCacheService import pl.spcode.navauth.velocity.application.auth.session.VelocityAuthSessionFactory import pl.spcode.navauth.velocity.extension.PlayerDisconnectExtension.Companion.disconnectIfActive import pl.spcode.navauth.velocity.infra.auth.VelocityUniqueSessionId @@ -55,8 +58,10 @@ constructor( val usernameValidator: UsernameValidator, val authHandshakeSessionService: AuthHandshakeSessionService, val usernameResolutionService: UsernameResolutionService, + val invalidSessionCacheService: InvalidSessionCacheService, val authSessionFactory: VelocityAuthSessionFactory, val messagesConfig: MessagesConfig, + val generalConfig: GeneralConfig, ) { val logger: Logger = LoggerFactory.getLogger(LoginListeners::class.java) @@ -81,11 +86,21 @@ constructor( is UsernameResResult.Success -> { when (res.requestedEncryption) { EncryptionType.ENFORCE_PREMIUM -> { + if (generalConfig.descriptiveInvalidSessionEnabled) { + if ( + invalidSessionCacheService.isInvalidSessionReconnect( + event.username, + event.connection, + ) + ) { + invalidSessionCacheService.invalidate(event.username) + event.result = invalidSessionReconnectDeniedResult(event.username) + return + } + invalidSessionCacheService.cachePremiumConnection(event.username, event.connection) + } // We force velocity to handle the initiation of the "minecraft encryption protocol". // User won't go any further than this event if not authenticated by velocity. - // todo set session cookie token -> - // if the same player disconnects twice at the same handshake stage - // then display "You're trying to login into a premium account..." event.result = PreLoginEvent.PreLoginComponentResult.forceOnlineMode() } EncryptionType.NONE -> @@ -145,6 +160,10 @@ constructor( return } + if (generalConfig.descriptiveInvalidSessionEnabled && player.isOnlineMode) { + invalidSessionCacheService.markAsEncryptedPremiumConnection(player.username) + } + createAuthSession(player, handshakeSession, username) authHandshakeSessionService.closeSession(sessionId) } @@ -216,6 +235,7 @@ constructor( player.disconnectIfActive(Component.text("NavAuth: Bad auth state", TextColors.RED)) } + // todo move this out of here private fun createAndStorePremiumUser(player: Player) { val premiumUser = User.premium(UserUuid(player.uniqueId), Username(player.username), MojangId(player.uniqueId)) @@ -276,6 +296,19 @@ constructor( return PreLoginEvent.PreLoginComponentResult.denied(comp) } + private fun invalidSessionReconnectDeniedResult( + username: String + ): PreLoginEvent.PreLoginComponentResult { + val comp = + withSupportFooter( + componentWithUsernamePlaceholder( + messagesConfig.possibleInvalidSessionReconnectError, + username, + ) + ) + return PreLoginEvent.PreLoginComponentResult.denied(comp) + } + private fun componentWithUsernamePlaceholder( textComponent: pl.spcode.navauth.common.component.TextComponent, username: String, diff --git a/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/util/CommandSourceUtils.kt b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/util/CommandSourceUtils.kt new file mode 100644 index 00000000..3d516c84 --- /dev/null +++ b/navauth-velocity/src/main/kotlin/pl/spcode/navauth/velocity/util/CommandSourceUtils.kt @@ -0,0 +1,35 @@ +/* + * NavAuth + * Copyright © 2026 Oliwier Fijas (Navio1430) + * + * NavAuth is free software; You can redistribute it and/or modify it under the terms of: + * the GNU Affero General Public License version 3 as published by the Free Software Foundation. + * + * NavAuth is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with NavAuth. If not, see + * and navigate to version 3 of the GNU Affero General Public License. + * + */ + +package pl.spcode.navauth.velocity.util + +import com.velocitypowered.api.proxy.ConsoleCommandSource +import me.uniodex.velocityrcon.commandsource.IRconCommandSource + +object CommandSourceUtils { + fun isConsoleOrRcon(sender: Any): Boolean { + if (sender is ConsoleCommandSource) return true + return try { + sender is IRconCommandSource + } catch (_: ClassNotFoundException) { + false + } catch (_: NoClassDefFoundError) { + false + } + } +}