diff --git a/mirai-core-api/compatibility-validation/android/api/android.api b/mirai-core-api/compatibility-validation/android/api/android.api index eec8413e7..59e2dd499 100644 --- a/mirai-core-api/compatibility-validation/android/api/android.api +++ b/mirai-core-api/compatibility-validation/android/api/android.api @@ -5370,6 +5370,10 @@ public final class net/mamoe/mirai/message/data/XmlMessageBuilder$ItemBuilder { public final class net/mamoe/mirai/message/data/visitor/MessageVisitorKt { } +public final class net/mamoe/mirai/network/BotAuthorizationException : net/mamoe/mirai/network/LoginFailedException { + public final fun getAuthorization ()Lnet/mamoe/mirai/auth/BotAuthorization; +} + public abstract class net/mamoe/mirai/network/CustomLoginFailedException : net/mamoe/mirai/network/LoginFailedException { public fun (Z)V public fun (ZLjava/lang/String;)V diff --git a/mirai-core-api/compatibility-validation/jvm/api/jvm.api b/mirai-core-api/compatibility-validation/jvm/api/jvm.api index ebd6187cc..3ff7aaa04 100644 --- a/mirai-core-api/compatibility-validation/jvm/api/jvm.api +++ b/mirai-core-api/compatibility-validation/jvm/api/jvm.api @@ -5370,6 +5370,10 @@ public final class net/mamoe/mirai/message/data/XmlMessageBuilder$ItemBuilder { public final class net/mamoe/mirai/message/data/visitor/MessageVisitorKt { } +public final class net/mamoe/mirai/network/BotAuthorizationException : net/mamoe/mirai/network/LoginFailedException { + public final fun getAuthorization ()Lnet/mamoe/mirai/auth/BotAuthorization; +} + public abstract class net/mamoe/mirai/network/CustomLoginFailedException : net/mamoe/mirai/network/LoginFailedException { public fun (Z)V public fun (ZLjava/lang/String;)V diff --git a/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt b/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt index a8107c562..a77c83ab7 100644 --- a/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt +++ b/mirai-core-api/src/commonMain/kotlin/network/LoginFailedException.kt @@ -12,6 +12,7 @@ package net.mamoe.mirai.network import net.mamoe.mirai.Bot +import net.mamoe.mirai.auth.BotAuthorization import net.mamoe.mirai.utils.LoginSolver import net.mamoe.mirai.utils.MiraiInternalApi @@ -75,6 +76,19 @@ public class NoStandardInputForCaptchaException @MiraiInternalApi constructor( public override val cause: Throwable? = null ) : LoginFailedException(true, "no standard input for captcha") +/** + * 表示在登录过程中, [BotAuthorization] 抛出的异常. + * @since 2.15 + */ +public class BotAuthorizationException @MiraiInternalApi constructor( + public val authorization: BotAuthorization, + cause: Throwable?, +) : LoginFailedException( + killBot = true, + "BotAuthorization(${authorization}) threw an exception during authorization process. See cause below.", + cause +) + /** * 当前 [LoginSolver] 不支持此验证方式 * diff --git a/mirai-core-utils/src/commonMain/kotlin/channels/ProducerFailureException.kt b/mirai-core-utils/src/commonMain/kotlin/channels/ProducerFailureException.kt index c3f044b26..c7ce333c8 100644 --- a/mirai-core-utils/src/commonMain/kotlin/channels/ProducerFailureException.kt +++ b/mirai-core-utils/src/commonMain/kotlin/channels/ProducerFailureException.kt @@ -11,5 +11,13 @@ package net.mamoe.mirai.utils.channels public class ProducerFailureException( override val message: String? = "Producer failed to produce a value, see cause", - override val cause: Throwable? -) : Exception() \ No newline at end of file + override var cause: Throwable? +) : Exception() { + private val unwrapped: Throwable by lazy { + val cause = cause ?: return@lazy this + this.cause = null + cause.also { addSuppressed(this) } + } + + public fun unwrap(): Throwable = unwrapped +} \ No newline at end of file diff --git a/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/channels/OnDemandChannelTest.kt b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/channels/OnDemandChannelTest.kt index 5ef5dbff1..0264db6a9 100644 --- a/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/channels/OnDemandChannelTest.kt +++ b/mirai-core-utils/src/commonTest/kotlin/net/mamoe/mirai/utils/channels/OnDemandChannelTest.kt @@ -285,7 +285,8 @@ class OnDemandChannelTest { } assertTrue { channel.isClosed } - // The exception looks like this, though I don't know why there are two causes. + // The exception looks like this. + // The first cause is stacktrace-recovered by coroutines, and the second is the original one. //net.mamoe.mirai.utils.channels.ProducerFailureException: Producer failed to produce a value, see cause // at net.mamoe.mirai.utils.channels.CoroutineOnDemandReceiveChannel.receiveOrNull(OnDemandChannelImpl.kt:164) diff --git a/mirai-core/src/commonMain/kotlin/BotAccount.kt b/mirai-core/src/commonMain/kotlin/BotAccount.kt index c05e1e959..11a53ab83 100644 --- a/mirai-core/src/commonMain/kotlin/BotAccount.kt +++ b/mirai-core/src/commonMain/kotlin/BotAccount.kt @@ -17,8 +17,13 @@ import net.mamoe.mirai.utils.TestOnly internal class BotAccount( internal val id: Long, - val authorization: BotAuthorization, + authorization: BotAuthorization, ) { + var authorization: BotAuthorization = authorization + // FIXME: Making this mutable is very bad. + // But I had to do this because the current test framework is bad, and I don't have time to do a major rewrite. + @TestOnly set + @TestOnly // to be compatible with your local tests :) constructor( id: Long, pwd: String diff --git a/mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt b/mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt index 8d9266c16..cbf26213c 100644 --- a/mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt +++ b/mirai-core/src/commonMain/kotlin/network/auth/AuthControl.kt @@ -58,7 +58,7 @@ internal class AuthControl( val rsp = try { userDecisions.receiveOrNull() ?: SsoProcessorImpl.AuthMethod.NotAvailable } catch (e: ProducerFailureException) { - SsoProcessorImpl.AuthMethod.Error(e) + SsoProcessorImpl.AuthMethod.Error(e.unwrap()) } logger.debug { "[AuthControl/acquire] Authorization responded: $rsp" } diff --git a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt index da7d3053b..3079085c0 100644 --- a/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt +++ b/mirai-core/src/commonMain/kotlin/network/components/SsoProcessor.kt @@ -32,10 +32,7 @@ import net.mamoe.mirai.internal.network.protocol.packet.login.UrlDeviceVerificat import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin.Login.LoginPacketResponse.Captcha import net.mamoe.mirai.internal.network.protocol.packet.login.wtlogin.* -import net.mamoe.mirai.network.LoginFailedException -import net.mamoe.mirai.network.RetryLaterException -import net.mamoe.mirai.network.UnsupportedSliderCaptchaException -import net.mamoe.mirai.network.WrongPasswordException +import net.mamoe.mirai.network.* import net.mamoe.mirai.utils.* import net.mamoe.mirai.utils.BotConfiguration.MiraiProtocol import kotlin.coroutines.cancellation.CancellationException @@ -117,7 +114,7 @@ internal interface SsoSession { * * Used by `NettyNetworkHandler.StateConnecting`. */ -internal class SsoProcessorImpl( +internal open class SsoProcessorImpl( val ssoContext: SsoProcessorContext, ) : SsoProcessor { @@ -155,10 +152,22 @@ internal class SsoProcessorImpl( get() = ssoContext.bot.configuration } + protected open suspend fun doSlowLogin( + handler: NetworkHandler, + loginType: LoginType + ) { + SlowLoginImpl(handler, loginType).doLogin() + } + + protected open suspend fun doFastLogin(handler: NetworkHandler) { + FastLoginImpl(handler).doLogin() + } + + /** - * Do login. Throws [LoginFailedException] if failed + * Throws [LoginFailedException] if failed. Any other exceptions are considered as internal error. */ - override suspend fun login(handler: NetworkHandler) { + final override suspend fun login(handler: NetworkHandler) { fun initAndStartAuthControl() { authControl = AuthControl( @@ -194,7 +203,7 @@ internal class SsoProcessorImpl( if (client.wLoginSigInfoInitialized) { ssoContext.bot.components[EcdhInitialPublicKeyUpdater].refreshInitialPublicKeyAndApplyEcdh() kotlin.runCatching { - FastLoginImpl(handler).doLogin() + doFastLogin(handler) }.onFailure { e -> initAndStartAuthControl() authControl!!.exceptionCollector.collect(e) @@ -220,7 +229,7 @@ internal class SsoProcessorImpl( when (val authw = authControl0.acquireAuth().also { nextAuthMethod = it }) { is AuthMethod.Error -> { authControl = null - throw authw.exception + throw BotAuthorizationException(ssoContext.account.authorization, authw.exception) } AuthMethod.NotAvailable -> { @@ -229,7 +238,8 @@ internal class SsoProcessorImpl( } is AuthMethod.Pwd -> { - SlowLoginImpl(handler, LoginType.Password(authw.passwordMd5)).doLogin() + val loginType = LoginType.Password(authw.passwordMd5) + doSlowLogin(handler, loginType) } AuthMethod.QRCode -> { @@ -237,7 +247,8 @@ internal class SsoProcessorImpl( handler, client ).process(handler, client) - SlowLoginImpl(handler, LoginType.QRCode(rsp)).doLogin() + val loginType = LoginType.QRCode(rsp) + doSlowLogin(handler, loginType) } } @@ -271,7 +282,6 @@ internal class SsoProcessorImpl( } - sealed class AuthMethod { object NotAvailable : AuthMethod() { override fun toString(): String = "NotAvailable" @@ -288,7 +298,9 @@ internal class SsoProcessorImpl( /** * Exception in [BotAuthorization] */ - class Error(val exception: Throwable) : AuthMethod() { + class Error( + val exception: Throwable // unwrapped + ) : AuthMethod() { override fun toString(): String = "Error[$exception]@${hashCode()}" } } @@ -296,10 +308,6 @@ internal class SsoProcessorImpl( private var authControl: AuthControl? = null override suspend fun sendRegister(handler: NetworkHandler): StatSvc.Register.Response { - return registerClientOnline(handler).also { registerResp = it } - } - - private suspend fun registerClientOnline(handler: NetworkHandler): StatSvc.Register.Response { return handler.sendAndExpect(StatSvc.Register.online(client)).also { registerResp = it } @@ -317,7 +325,7 @@ internal class SsoProcessorImpl( // we have exactly two methods----slow and fast. - private abstract inner class LoginStrategy( + protected abstract inner class LoginStrategy( val handler: NetworkHandler, ) { protected val context get() = handler.context @@ -478,12 +486,12 @@ internal class SsoProcessorImpl( } } - private sealed class LoginType { + protected sealed class LoginType { class Password(val passwordMd5: SecretsProtection.EscapedByteBuffer) : LoginType() class QRCode(val qrCodeLoginData: QRCodeLoginData) : LoginType() } - private inner class FastLoginImpl(handler: NetworkHandler) : LoginStrategy(handler) { + protected inner class FastLoginImpl(handler: NetworkHandler) : LoginStrategy(handler) { override suspend fun doLogin() { val login10 = handler.sendAndExpect(WtLogin10(client)) check(login10 is LoginPacketResponse.Success) { "Fast login failed: $login10" } diff --git a/mirai-core/src/commonTest/kotlin/network/auth/AbstractBotAuthTest.kt b/mirai-core/src/commonTest/kotlin/network/auth/AbstractBotAuthTest.kt new file mode 100644 index 000000000..a26a747c6 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/network/auth/AbstractBotAuthTest.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2019-2023 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.auth + +import net.mamoe.mirai.auth.BotAuthInfo +import net.mamoe.mirai.auth.BotAuthResult +import net.mamoe.mirai.auth.BotAuthSession +import net.mamoe.mirai.auth.BotAuthorization +import net.mamoe.mirai.internal.network.components.SsoProcessor +import net.mamoe.mirai.internal.network.components.SsoProcessorContext +import net.mamoe.mirai.internal.network.components.SsoProcessorImpl +import net.mamoe.mirai.internal.network.framework.AbstractCommonNHTestWithSelector +import net.mamoe.mirai.internal.network.framework.PacketReplierDslBuilder +import net.mamoe.mirai.internal.network.framework.buildPacketReplier + +internal abstract class AbstractBotAuthTest : AbstractCommonNHTestWithSelector() { + init { + // Use real processor to test login + overrideComponents[SsoProcessor] = SsoProcessorImpl(overrideComponents[SsoProcessorContext]) + } + + protected fun setAuthorization(authorize: (session: BotAuthSession, info: BotAuthInfo) -> BotAuthResult) { + // Run a real SsoProcessor, just without sending packets + bot.account.authorization = object : BotAuthorization { + override suspend fun authorize(session: BotAuthSession, info: BotAuthInfo): BotAuthResult { + return authorize(session, info) + } + } + } + + // Use the same replier even after reconnection + protected fun usePacketReplierThroughout(builderAction: PacketReplierDslBuilder.() -> Unit) { + val replier = buildPacketReplier { + builderAction() + } + onEachNetworkInstance { + addPacketReplier(replier) // share the decisions + } + } +} \ No newline at end of file diff --git a/mirai-core/src/commonTest/kotlin/network/auth/BotAuthorizationTest.kt b/mirai-core/src/commonTest/kotlin/network/auth/BotAuthorizationTest.kt new file mode 100644 index 000000000..cd0866406 --- /dev/null +++ b/mirai-core/src/commonTest/kotlin/network/auth/BotAuthorizationTest.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019-2023 Mamoe Technologies and contributors. + * + * 此源代码的使用受 GNU AFFERO GENERAL PUBLIC LICENSE version 3 许可证的约束, 可以在以下链接找到该许可证. + * Use of this source code is governed by the GNU AGPLv3 license that can be found through the following link. + * + * https://github.com/mamoe/mirai/blob/dev/LICENSE + */ + +package net.mamoe.mirai.internal.network.auth + +import kotlinx.coroutines.test.runTest +import net.mamoe.mirai.internal.network.protocol.data.jce.SvcRespRegister +import net.mamoe.mirai.internal.network.protocol.packet.login.StatSvc +import net.mamoe.mirai.internal.network.protocol.packet.login.WtLogin +import net.mamoe.mirai.network.BotAuthorizationException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs + +internal class BotAuthorizationTest : AbstractBotAuthTest() { + @Test + fun `authorization failure throws BotAuthorizationException`() = runTest { + // Run a real SsoProcessor, just without sending packets + setAuthorization { _, _ -> + throw IllegalStateException("Oops") + } + + usePacketReplierThroughout { + expect(WtLogin.ExchangeEmp) reply { WtLogin.Login.LoginPacketResponse.Error(bot, 1, "", "", "") } + expect(WtLogin.Login) reply { WtLogin.Login.LoginPacketResponse.Success(bot) } + expect(StatSvc.Register) reply { StatSvc.Register.Response(SvcRespRegister()) } + } + + assertFailsWith { + bot.login() + }.run { + val cause = cause + assertIs(cause) + assertEquals("Oops", cause.message) + } + + // Stacktrace like this: + + //net.mamoe.mirai.network.BotAuthorizationException: BotAuthorization(net.mamoe.mirai.internal.network.auth.AbstractBotAuthTest$authorization failure throws BotAuthorizationException$1$2@157c6b0f) threw an exception during authorization process. See cause below. + // at net.mamoe.mirai.internal.network.components.SsoProcessorImpl.login(SsoProcessor.kt:232) + //Caused by: java.lang.IllegalStateException: Oops + // at net.mamoe.mirai.internal.network.auth.AbstractBotAuthTest$authorization failure throws BotAuthorizationException$1$2.authorize(AbstractBotAuthTest.kt:44) + // (Coroutine boundary) + // at net.mamoe.mirai.utils.channels.CoroutineOnDemandReceiveChannel.receiveOrNull(OnDemandChannelImpl.kt:237) + // (Coroutine creation stacktrace) + // at net.mamoe.mirai.internal.network.handler.CommonNetworkHandler$StateConnecting.startState(CommonNetworkHandler.kt:244) + //Caused by: java.lang.IllegalStateException: Oops + // at net.mamoe.mirai.internal.network.auth.AbstractBotAuthTest$authorization failure throws BotAuthorizationException$1$2.authorize(AbstractBotAuthTest.kt:44) + } + +} \ No newline at end of file